diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f57f403115..2a0610cf03 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,8 +13,9 @@ env: CARGO_PROFILE_TEST_DEBUG: 0 CARGO_PROFILE_DEV_DEBUG: 0 # If nightly is breaking CI, modify this variable to target a specific nightly version. - NIGHTLY_TOOLCHAIN: nightly + NIGHTLY_TOOLCHAIN: nightly-2025-05-16 # pinned until a fix for https://github.com/rust-lang/miri/issues/4323 is released RUSTFLAGS: "-D warnings" + BINSTALL_VERSION: "v1.12.3" concurrency: group: ${{github.workflow}}-${{github.ref}} @@ -271,9 +272,9 @@ jobs: timeout-minutes: 30 steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable + - uses: cargo-bins/cargo-binstall@v1.12.3 - name: Install taplo - run: cargo install taplo-cli --locked + run: cargo binstall taplo-cli@0.9.3 --locked - name: Run Taplo id: taplo run: taplo fmt --check --diff @@ -292,7 +293,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Check for typos - uses: crate-ci/typos@v1.31.1 + uses: crate-ci/typos@v1.32.0 - name: Typos info if: failure() run: | diff --git a/Cargo.toml b/Cargo.toml index 8bb16b741d..7cf1334e81 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -291,9 +291,6 @@ bevy_log = ["bevy_internal/bevy_log"] # Enable input focus subsystem bevy_input_focus = ["bevy_internal/bevy_input_focus"] -# Use the configurable global error handler as the default error handler. -configurable_error_handler = ["bevy_internal/configurable_error_handler"] - # Enable passthrough loading for SPIR-V shaders (Only supported on Vulkan, shader capabilities and extensions must agree with the platform implementation) spirv_shader_passthrough = ["bevy_internal/spirv_shader_passthrough"] @@ -2215,7 +2212,6 @@ wasm = false name = "fallible_params" path = "examples/ecs/fallible_params.rs" doc-scrape-examples = true -required-features = ["configurable_error_handler"] [package.metadata.example.fallible_params] name = "Fallible System Parameters" @@ -2227,7 +2223,7 @@ wasm = false name = "error_handling" path = "examples/ecs/error_handling.rs" doc-scrape-examples = true -required-features = ["bevy_mesh_picking_backend", "configurable_error_handler"] +required-features = ["bevy_mesh_picking_backend"] [package.metadata.example.error_handling] name = "Error handling" @@ -2305,6 +2301,17 @@ description = "Pipe the output of one system into a second, allowing you to hand category = "ECS (Entity Component System)" wasm = false +[[example]] +name = "state_scoped" +path = "examples/ecs/state_scoped.rs" +doc-scrape-examples = true + +[package.metadata.example.state_scoped] +name = "State Scoped" +description = "Shows how to spawn entities that are automatically despawned either when entering or exiting specific game states." +category = "ECS (Entity Component System)" +wasm = false + [[example]] name = "system_closure" path = "examples/ecs/system_closure.rs" @@ -3344,6 +3351,17 @@ description = "Illustrates creating and updating text" category = "UI (User Interface)" wasm = true +[[example]] +name = "text_background_colors" +path = "examples/ui/text_background_colors.rs" +doc-scrape-examples = true + +[package.metadata.example.text_background_colors] +name = "Text Background Colors" +description = "Demonstrates text background colors" +category = "UI (User Interface)" +wasm = true + [[example]] name = "text_debug" path = "examples/ui/text_debug.rs" @@ -3499,6 +3517,17 @@ description = "An example for debugging viewport coordinates" category = "UI (User Interface)" wasm = true +[[example]] +name = "viewport_node" +path = "examples/ui/viewport_node.rs" +doc-scrape-examples = true + +[package.metadata.example.viewport_node] +name = "Viewport Node" +description = "Demonstrates how to create a viewport node with picking support" +category = "UI (User Interface)" +wasm = true + # Window [[example]] name = "clear_color" diff --git a/assets/scenes/load_scene_example.scn.ron b/assets/scenes/load_scene_example.scn.ron index e768a7b149..477bee3054 100644 --- a/assets/scenes/load_scene_example.scn.ron +++ b/assets/scenes/load_scene_example.scn.ron @@ -5,7 +5,7 @@ ), }, entities: { - 4294967296: ( + 4294967297: ( components: { "bevy_ecs::name::Name": "joe", "bevy_transform::components::global_transform::GlobalTransform": ((1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0)), @@ -23,7 +23,7 @@ ), }, ), - 4294967297: ( + 4294967298: ( components: { "scene::ComponentA": ( x: 3.0, @@ -32,4 +32,4 @@ }, ), }, -) \ No newline at end of file +) diff --git a/benches/Cargo.toml b/benches/Cargo.toml index 3f547d80d9..a299a6526c 100644 --- a/benches/Cargo.toml +++ b/benches/Cargo.toml @@ -32,6 +32,7 @@ bevy_platform = { path = "../crates/bevy_platform", default-features = false, fe glam = "0.29" rand = "0.8" rand_chacha = "0.3" +nonmax = { version = "0.5", default-features = false } # Make `bevy_render` compile on Linux with x11 windowing. x11 vs. Wayland does not matter here # because the benches do not actually open any windows. diff --git a/benches/benches/bevy_ecs/components/archetype_updates.rs b/benches/benches/bevy_ecs/components/archetype_updates.rs index 2908332ea5..4a4ddda9aa 100644 --- a/benches/benches/bevy_ecs/components/archetype_updates.rs +++ b/benches/benches/bevy_ecs/components/archetype_updates.rs @@ -51,8 +51,7 @@ fn add_archetypes(world: &mut World, count: u16) { pub fn no_archetypes(criterion: &mut Criterion) { let mut group = criterion.benchmark_group("no_archetypes"); - for i in 0..=5 { - let system_count = i * 20; + for system_count in [0, 10, 100] { let (mut world, mut schedule) = setup(system_count); group.bench_with_input( BenchmarkId::new("system_count", system_count), @@ -69,7 +68,7 @@ pub fn no_archetypes(criterion: &mut Criterion) { pub fn added_archetypes(criterion: &mut Criterion) { const SYSTEM_COUNT: usize = 100; let mut group = criterion.benchmark_group("added_archetypes"); - for archetype_count in [100, 200, 500, 1000, 2000, 5000, 10000] { + for archetype_count in [100, 1_000, 10_000] { group.bench_with_input( BenchmarkId::new("archetype_count", archetype_count), &archetype_count, diff --git a/benches/benches/bevy_ecs/empty_archetypes.rs b/benches/benches/bevy_ecs/empty_archetypes.rs index e5e7639066..b131f1c9c3 100644 --- a/benches/benches/bevy_ecs/empty_archetypes.rs +++ b/benches/benches/bevy_ecs/empty_archetypes.rs @@ -155,7 +155,7 @@ fn add_archetypes(world: &mut World, count: u16) { fn empty_archetypes(criterion: &mut Criterion) { let mut group = criterion.benchmark_group("empty_archetypes"); - for archetype_count in [10, 100, 500, 1000, 2000, 5000, 10000] { + for archetype_count in [10, 100, 1_000, 10_000] { let (mut world, mut schedule) = setup(true, |schedule| { schedule.add_systems(iter); }); @@ -186,7 +186,7 @@ fn empty_archetypes(criterion: &mut Criterion) { }, ); } - for archetype_count in [10, 100, 500, 1000, 2000, 5000, 10000] { + for archetype_count in [10, 100, 1_000, 10_000] { let (mut world, mut schedule) = setup(true, |schedule| { schedule.add_systems(for_each); }); @@ -217,7 +217,7 @@ fn empty_archetypes(criterion: &mut Criterion) { }, ); } - for archetype_count in [10, 100, 500, 1000, 2000, 5000, 10000] { + for archetype_count in [10, 100, 1_000, 10_000] { let (mut world, mut schedule) = setup(true, |schedule| { schedule.add_systems(par_for_each); }); diff --git a/benches/benches/bevy_ecs/events/mod.rs b/benches/benches/bevy_ecs/events/mod.rs index 4367c45c3e..b87a138e06 100644 --- a/benches/benches/bevy_ecs/events/mod.rs +++ b/benches/benches/bevy_ecs/events/mod.rs @@ -9,19 +9,19 @@ fn send(c: &mut Criterion) { let mut group = c.benchmark_group("events_send"); group.warm_up_time(core::time::Duration::from_millis(500)); group.measurement_time(core::time::Duration::from_secs(4)); - for count in [100, 1000, 10000, 50000] { + for count in [100, 1_000, 10_000] { group.bench_function(format!("size_4_events_{}", count), |b| { let mut bench = send::Benchmark::<4>::new(count); b.iter(move || bench.run()); }); } - for count in [100, 1000, 10000, 50000] { + for count in [100, 1_000, 10_000] { group.bench_function(format!("size_16_events_{}", count), |b| { let mut bench = send::Benchmark::<16>::new(count); b.iter(move || bench.run()); }); } - for count in [100, 1000, 10000, 50000] { + for count in [100, 1_000, 10_000] { group.bench_function(format!("size_512_events_{}", count), |b| { let mut bench = send::Benchmark::<512>::new(count); b.iter(move || bench.run()); @@ -34,19 +34,19 @@ fn iter(c: &mut Criterion) { let mut group = c.benchmark_group("events_iter"); group.warm_up_time(core::time::Duration::from_millis(500)); group.measurement_time(core::time::Duration::from_secs(4)); - for count in [100, 1000, 10000, 50000] { + for count in [100, 1_000, 10_000] { group.bench_function(format!("size_4_events_{}", count), |b| { let mut bench = iter::Benchmark::<4>::new(count); b.iter(move || bench.run()); }); } - for count in [100, 1000, 10000, 50000] { + for count in [100, 1_000, 10_000] { group.bench_function(format!("size_16_events_{}", count), |b| { let mut bench = iter::Benchmark::<4>::new(count); b.iter(move || bench.run()); }); } - for count in [100, 1000, 10000, 50000] { + for count in [100, 1_000, 10_000] { group.bench_function(format!("size_512_events_{}", count), |b| { let mut bench = iter::Benchmark::<512>::new(count); b.iter(move || bench.run()); diff --git a/benches/benches/bevy_ecs/scheduling/run_condition.rs b/benches/benches/bevy_ecs/scheduling/run_condition.rs index 0d6e4107c6..7b9cf418f4 100644 --- a/benches/benches/bevy_ecs/scheduling/run_condition.rs +++ b/benches/benches/bevy_ecs/scheduling/run_condition.rs @@ -17,15 +17,14 @@ pub fn run_condition_yes(criterion: &mut Criterion) { group.warm_up_time(core::time::Duration::from_millis(500)); group.measurement_time(core::time::Duration::from_secs(3)); fn empty() {} - for amount in 0..21 { + for amount in [10, 100, 1_000] { let mut schedule = Schedule::default(); - schedule.add_systems(empty.run_if(yes)); - for _ in 0..amount { + for _ in 0..(amount / 5) { schedule.add_systems((empty, empty, empty, empty, empty).distributive_run_if(yes)); } // run once to initialize systems schedule.run(&mut world); - group.bench_function(format!("{:03}_systems", 5 * amount + 1), |bencher| { + group.bench_function(format!("{}_systems", amount), |bencher| { bencher.iter(|| { schedule.run(&mut world); }); @@ -40,15 +39,14 @@ pub fn run_condition_no(criterion: &mut Criterion) { group.warm_up_time(core::time::Duration::from_millis(500)); group.measurement_time(core::time::Duration::from_secs(3)); fn empty() {} - for amount in 0..21 { + for amount in [10, 100, 1_000] { let mut schedule = Schedule::default(); - schedule.add_systems(empty.run_if(no)); - for _ in 0..amount { + for _ in 0..(amount / 5) { schedule.add_systems((empty, empty, empty, empty, empty).distributive_run_if(no)); } // run once to initialize systems schedule.run(&mut world); - group.bench_function(format!("{:03}_systems", 5 * amount + 1), |bencher| { + group.bench_function(format!("{}_systems", amount), |bencher| { bencher.iter(|| { schedule.run(&mut world); }); @@ -70,17 +68,16 @@ pub fn run_condition_yes_with_query(criterion: &mut Criterion) { fn yes_with_query(query: Single<&TestBool>) -> bool { query.0 } - for amount in 0..21 { + for amount in [10, 100, 1_000] { let mut schedule = Schedule::default(); - schedule.add_systems(empty.run_if(yes_with_query)); - for _ in 0..amount { + for _ in 0..(amount / 5) { schedule.add_systems( (empty, empty, empty, empty, empty).distributive_run_if(yes_with_query), ); } // run once to initialize systems schedule.run(&mut world); - group.bench_function(format!("{:03}_systems", 5 * amount + 1), |bencher| { + group.bench_function(format!("{}_systems", amount), |bencher| { bencher.iter(|| { schedule.run(&mut world); }); @@ -99,17 +96,16 @@ pub fn run_condition_yes_with_resource(criterion: &mut Criterion) { fn yes_with_resource(res: Res) -> bool { res.0 } - for amount in 0..21 { + for amount in [10, 100, 1_000] { let mut schedule = Schedule::default(); - schedule.add_systems(empty.run_if(yes_with_resource)); - for _ in 0..amount { + for _ in 0..(amount / 5) { schedule.add_systems( (empty, empty, empty, empty, empty).distributive_run_if(yes_with_resource), ); } // run once to initialize systems schedule.run(&mut world); - group.bench_function(format!("{:03}_systems", 5 * amount + 1), |bencher| { + group.bench_function(format!("{}_systems", amount), |bencher| { bencher.iter(|| { schedule.run(&mut world); }); diff --git a/benches/benches/bevy_ecs/scheduling/running_systems.rs b/benches/benches/bevy_ecs/scheduling/running_systems.rs index 4a14553885..2fc1da1710 100644 --- a/benches/benches/bevy_ecs/scheduling/running_systems.rs +++ b/benches/benches/bevy_ecs/scheduling/running_systems.rs @@ -20,25 +20,25 @@ pub fn empty_systems(criterion: &mut Criterion) { group.warm_up_time(core::time::Duration::from_millis(500)); group.measurement_time(core::time::Duration::from_secs(3)); fn empty() {} - for amount in 0..5 { + for amount in [0, 2, 4] { let mut schedule = Schedule::default(); for _ in 0..amount { schedule.add_systems(empty); } schedule.run(&mut world); - group.bench_function(format!("{:03}_systems", amount), |bencher| { + group.bench_function(format!("{}_systems", amount), |bencher| { bencher.iter(|| { schedule.run(&mut world); }); }); } - for amount in 1..21 { + for amount in [10, 100, 1_000] { let mut schedule = Schedule::default(); - for _ in 0..amount { + for _ in 0..(amount / 5) { schedule.add_systems((empty, empty, empty, empty, empty)); } schedule.run(&mut world); - group.bench_function(format!("{:03}_systems", 5 * amount), |bencher| { + group.bench_function(format!("{}_systems", amount), |bencher| { bencher.iter(|| { schedule.run(&mut world); }); @@ -67,23 +67,21 @@ pub fn busy_systems(criterion: &mut Criterion) { let mut group = criterion.benchmark_group("busy_systems"); group.warm_up_time(core::time::Duration::from_millis(500)); group.measurement_time(core::time::Duration::from_secs(3)); - for entity_bunches in 1..6 { + for entity_bunches in [1, 3, 5] { world.spawn_batch((0..4 * ENTITY_BUNCH).map(|_| (A(0.0), B(0.0)))); world.spawn_batch((0..4 * ENTITY_BUNCH).map(|_| (A(0.0), B(0.0), C(0.0)))); world.spawn_batch((0..ENTITY_BUNCH).map(|_| (A(0.0), B(0.0), C(0.0), D(0.0)))); world.spawn_batch((0..ENTITY_BUNCH).map(|_| (A(0.0), B(0.0), C(0.0), E(0.0)))); - for system_amount in 0..5 { + for system_amount in [3, 9, 15] { let mut schedule = Schedule::default(); - schedule.add_systems((ab, cd, ce)); - for _ in 0..system_amount { + for _ in 0..(system_amount / 3) { schedule.add_systems((ab, cd, ce)); } schedule.run(&mut world); group.bench_function( format!( "{:02}x_entities_{:02}_systems", - entity_bunches, - 3 * system_amount + 3 + entity_bunches, system_amount ), |bencher| { bencher.iter(|| { @@ -119,22 +117,20 @@ pub fn contrived(criterion: &mut Criterion) { let mut group = criterion.benchmark_group("contrived"); group.warm_up_time(core::time::Duration::from_millis(500)); group.measurement_time(core::time::Duration::from_secs(3)); - for entity_bunches in 1..6 { + for entity_bunches in [1, 3, 5] { world.spawn_batch((0..ENTITY_BUNCH).map(|_| (A(0.0), B(0.0), C(0.0), D(0.0)))); world.spawn_batch((0..ENTITY_BUNCH).map(|_| (A(0.0), B(0.0)))); world.spawn_batch((0..ENTITY_BUNCH).map(|_| (C(0.0), D(0.0)))); - for system_amount in 0..5 { + for system_amount in [3, 9, 15] { let mut schedule = Schedule::default(); - schedule.add_systems((s_0, s_1, s_2)); - for _ in 0..system_amount { + for _ in 0..(system_amount / 3) { schedule.add_systems((s_0, s_1, s_2)); } schedule.run(&mut world); group.bench_function( format!( "{:02}x_entities_{:02}_systems", - entity_bunches, - 3 * system_amount + 3 + entity_bunches, system_amount ), |bencher| { bencher.iter(|| { diff --git a/benches/benches/bevy_ecs/scheduling/schedule.rs b/benches/benches/bevy_ecs/scheduling/schedule.rs index 9844461d39..d7d1243f7f 100644 --- a/benches/benches/bevy_ecs/scheduling/schedule.rs +++ b/benches/benches/bevy_ecs/scheduling/schedule.rs @@ -137,6 +137,7 @@ pub fn empty_schedule_run(criterion: &mut Criterion) { }); let mut schedule = Schedule::default(); + #[expect(deprecated, reason = "We still need to test/bench this.")] schedule.set_executor_kind(bevy_ecs::schedule::ExecutorKind::Simple); group.bench_function("Simple", |bencher| { bencher.iter(|| schedule.run(app.world_mut())); diff --git a/benches/benches/bevy_ecs/world/commands.rs b/benches/benches/bevy_ecs/world/commands.rs index 8ad87862eb..7b1cc29457 100644 --- a/benches/benches/bevy_ecs/world/commands.rs +++ b/benches/benches/bevy_ecs/world/commands.rs @@ -36,7 +36,7 @@ pub fn spawn_commands(criterion: &mut Criterion) { group.warm_up_time(core::time::Duration::from_millis(500)); group.measurement_time(core::time::Duration::from_secs(4)); - for entity_count in (1..5).map(|i| i * 2 * 1000) { + for entity_count in [100, 1_000, 10_000] { group.bench_function(format!("{}_entities", entity_count), |bencher| { let mut world = World::default(); let mut command_queue = CommandQueue::default(); @@ -92,28 +92,6 @@ pub fn insert_commands(criterion: &mut Criterion) { command_queue.apply(&mut world); }); }); - group.bench_function("insert_or_spawn_batch", |bencher| { - let mut world = World::default(); - let mut command_queue = CommandQueue::default(); - let mut entities = Vec::new(); - for _ in 0..entity_count { - entities.push(world.spawn_empty().id()); - } - - bencher.iter(|| { - let mut commands = Commands::new(&mut command_queue, &world); - let mut values = Vec::with_capacity(entity_count); - for entity in &entities { - values.push((*entity, (Matrix::default(), Vec3::default()))); - } - #[expect( - deprecated, - reason = "This needs to be supported for now, and therefore still needs the benchmark." - )] - commands.insert_or_spawn_batch(values); - command_queue.apply(&mut world); - }); - }); group.bench_function("insert_batch", |bencher| { let mut world = World::default(); let mut command_queue = CommandQueue::default(); @@ -158,7 +136,7 @@ pub fn fake_commands(criterion: &mut Criterion) { group.warm_up_time(core::time::Duration::from_millis(500)); group.measurement_time(core::time::Duration::from_secs(4)); - for command_count in (1..5).map(|i| i * 2 * 1000) { + for command_count in [100, 1_000, 10_000] { group.bench_function(format!("{}_commands", command_count), |bencher| { let mut world = World::default(); let mut command_queue = CommandQueue::default(); @@ -203,7 +181,7 @@ pub fn sized_commands_impl(criterion: &mut Criterion) { group.warm_up_time(core::time::Duration::from_millis(500)); group.measurement_time(core::time::Duration::from_secs(4)); - for command_count in (1..5).map(|i| i * 2 * 1000) { + for command_count in [100, 1_000, 10_000] { group.bench_function(format!("{}_commands", command_count), |bencher| { let mut world = World::default(); let mut command_queue = CommandQueue::default(); diff --git a/benches/benches/bevy_ecs/world/despawn.rs b/benches/benches/bevy_ecs/world/despawn.rs index 5419867a9e..cd693fc15c 100644 --- a/benches/benches/bevy_ecs/world/despawn.rs +++ b/benches/benches/bevy_ecs/world/despawn.rs @@ -12,7 +12,7 @@ pub fn world_despawn(criterion: &mut Criterion) { group.warm_up_time(core::time::Duration::from_millis(500)); group.measurement_time(core::time::Duration::from_secs(4)); - for entity_count in (0..5).map(|i| 10_u32.pow(i)) { + for entity_count in [1, 100, 10_000] { group.bench_function(format!("{}_entities", entity_count), |bencher| { bencher.iter_batched_ref( || { diff --git a/benches/benches/bevy_ecs/world/despawn_recursive.rs b/benches/benches/bevy_ecs/world/despawn_recursive.rs index 6ae59b10a5..78c644174b 100644 --- a/benches/benches/bevy_ecs/world/despawn_recursive.rs +++ b/benches/benches/bevy_ecs/world/despawn_recursive.rs @@ -12,7 +12,7 @@ pub fn world_despawn_recursive(criterion: &mut Criterion) { group.warm_up_time(core::time::Duration::from_millis(500)); group.measurement_time(core::time::Duration::from_secs(4)); - for entity_count in (0..5).map(|i| 10_u32.pow(i)) { + for entity_count in [1, 100, 10_000] { group.bench_function(format!("{}_entities", entity_count), |bencher| { bencher.iter_batched_ref( || { diff --git a/benches/benches/bevy_ecs/world/entity_hash.rs b/benches/benches/bevy_ecs/world/entity_hash.rs index 7e8dfb4a21..2f92a715b4 100644 --- a/benches/benches/bevy_ecs/world/entity_hash.rs +++ b/benches/benches/bevy_ecs/world/entity_hash.rs @@ -1,4 +1,4 @@ -use bevy_ecs::entity::{Entity, EntityHashSet}; +use bevy_ecs::entity::{Entity, EntityGeneration, EntityHashSet}; use criterion::{BenchmarkId, Criterion, Throughput}; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha8Rng; @@ -17,10 +17,14 @@ fn make_entity(rng: &mut impl Rng, size: usize) -> Entity { let generation = 1.0 + -(1.0 - x).log2() * 2.0; // this is not reliable, but we're internal so a hack is ok - let bits = ((generation as u64) << 32) | (id as u64); + let id = id as u64 + 1; + let bits = ((generation as u64) << 32) | id; let e = Entity::from_bits(bits); - assert_eq!(e.index(), id as u32); - assert_eq!(e.generation(), generation as u32); + assert_eq!(e.index(), !(id as u32)); + assert_eq!( + e.generation(), + EntityGeneration::FIRST.after_versions(generation as u32) + ); e } diff --git a/benches/benches/bevy_ecs/world/spawn.rs b/benches/benches/bevy_ecs/world/spawn.rs index 0777a20cb9..502d10ceb3 100644 --- a/benches/benches/bevy_ecs/world/spawn.rs +++ b/benches/benches/bevy_ecs/world/spawn.rs @@ -12,7 +12,7 @@ pub fn world_spawn(criterion: &mut Criterion) { group.warm_up_time(core::time::Duration::from_millis(500)); group.measurement_time(core::time::Duration::from_secs(4)); - for entity_count in (0..5).map(|i| 10_u32.pow(i)) { + for entity_count in [1, 100, 10_000] { group.bench_function(format!("{}_entities", entity_count), |bencher| { let mut world = World::default(); bencher.iter(|| { diff --git a/benches/benches/bevy_ecs/world/world_get.rs b/benches/benches/bevy_ecs/world/world_get.rs index 283b984186..e6e2a0bb90 100644 --- a/benches/benches/bevy_ecs/world/world_get.rs +++ b/benches/benches/bevy_ecs/world/world_get.rs @@ -1,9 +1,10 @@ use core::hint::black_box; +use nonmax::NonMaxU32; use bevy_ecs::{ bundle::{Bundle, NoBundleEffect}, component::Component, - entity::Entity, + entity::{Entity, EntityRow}, system::{Query, SystemState}, world::World, }; @@ -53,7 +54,9 @@ pub fn world_entity(criterion: &mut Criterion) { bencher.iter(|| { for i in 0..entity_count { - let entity = Entity::from_raw(i); + let entity = + // SAFETY: Range is exclusive. + Entity::from_raw(EntityRow::new(unsafe { NonMaxU32::new_unchecked(i) })); black_box(world.entity(entity)); } }); @@ -74,7 +77,9 @@ pub fn world_get(criterion: &mut Criterion) { bencher.iter(|| { for i in 0..entity_count { - let entity = Entity::from_raw(i); + let entity = + // SAFETY: Range is exclusive. + Entity::from_raw(EntityRow::new(unsafe { NonMaxU32::new_unchecked(i) })); assert!(world.get::(entity).is_some()); } }); @@ -84,7 +89,9 @@ pub fn world_get(criterion: &mut Criterion) { bencher.iter(|| { for i in 0..entity_count { - let entity = Entity::from_raw(i); + let entity = + // SAFETY: Range is exclusive. + Entity::from_raw(EntityRow::new(unsafe { NonMaxU32::new_unchecked(i) })); assert!(world.get::(entity).is_some()); } }); @@ -106,7 +113,9 @@ pub fn world_query_get(criterion: &mut Criterion) { bencher.iter(|| { for i in 0..entity_count { - let entity = Entity::from_raw(i); + let entity = + // SAFETY: Range is exclusive. + Entity::from_raw(EntityRow::new(unsafe { NonMaxU32::new_unchecked(i) })); assert!(query.get(&world, entity).is_ok()); } }); @@ -131,7 +140,9 @@ pub fn world_query_get(criterion: &mut Criterion) { bencher.iter(|| { for i in 0..entity_count { - let entity = Entity::from_raw(i); + let entity = + // SAFETY: Range is exclusive. + Entity::from_raw(EntityRow::new(unsafe { NonMaxU32::new_unchecked(i) })); assert!(query.get(&world, entity).is_ok()); } }); @@ -142,7 +153,9 @@ pub fn world_query_get(criterion: &mut Criterion) { bencher.iter(|| { for i in 0..entity_count { - let entity = Entity::from_raw(i); + let entity = + // SAFETY: Range is exclusive. + Entity::from_raw(EntityRow::new(unsafe { NonMaxU32::new_unchecked(i) })); assert!(query.get(&world, entity).is_ok()); } }); @@ -169,7 +182,10 @@ pub fn world_query_get(criterion: &mut Criterion) { bencher.iter(|| { for i in 0..entity_count { - let entity = Entity::from_raw(i); + // SAFETY: Range is exclusive. + let entity = Entity::from_raw(EntityRow::new(unsafe { + NonMaxU32::new_unchecked(i) + })); assert!(query.get(&world, entity).is_ok()); } }); diff --git a/benches/benches/bevy_picking/ray_mesh_intersection.rs b/benches/benches/bevy_picking/ray_mesh_intersection.rs index ee81f1ac7f..871a6d1062 100644 --- a/benches/benches/bevy_picking/ray_mesh_intersection.rs +++ b/benches/benches/bevy_picking/ray_mesh_intersection.rs @@ -37,7 +37,7 @@ fn create_mesh(vertices_per_side: u32) -> SimpleMesh { for p in 0..vertices_per_side.pow(2) { let (x, z) = p_to_xz_norm(p, vertices_per_side); - // Push a new vertice to the mesh. We translate all vertices so the final square is + // Push a new vertex to the mesh. We translate all vertices so the final square is // centered at (0, 0), instead of (0.5, 0.5). positions.push([x - 0.5, 0.0, z - 0.5]); diff --git a/clippy.toml b/clippy.toml index 2c98e8ed02..372ffbaf0b 100644 --- a/clippy.toml +++ b/clippy.toml @@ -41,7 +41,6 @@ disallowed-methods = [ { path = "f32::asinh", reason = "use bevy_math::ops::asinh instead for libm determinism" }, { path = "f32::acosh", reason = "use bevy_math::ops::acosh instead for libm determinism" }, { path = "f32::atanh", reason = "use bevy_math::ops::atanh instead for libm determinism" }, - { path = "criterion::black_box", reason = "use core::hint::black_box instead" }, ] # Require `bevy_ecs::children!` to use `[]` braces, instead of `()` or `{}`. diff --git a/crates/bevy_a11y/src/lib.rs b/crates/bevy_a11y/src/lib.rs index 910ec3ca35..94468c148c 100644 --- a/crates/bevy_a11y/src/lib.rs +++ b/crates/bevy_a11y/src/lib.rs @@ -137,11 +137,15 @@ impl From for AccessibilityNode { all(feature = "bevy_reflect", feature = "serialize"), reflect(Serialize, Deserialize, Clone) )] -pub enum AccessibilitySystem { +pub enum AccessibilitySystems { /// Update the accessibility tree Update, } +/// Deprecated alias for [`AccessibilitySystems`]. +#[deprecated(since = "0.17.0", note = "Renamed to `AccessibilitySystems`.")] +pub type AccessibilitySystem = AccessibilitySystems; + /// Plugin managing non-GUI aspects of integrating with accessibility APIs. #[derive(Default)] pub struct AccessibilityPlugin; diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 43ea343aa3..21ea15f96f 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -31,14 +31,14 @@ use crate::{ prelude::EvaluatorId, }; -use bevy_app::{Animation, App, Plugin, PostUpdate}; -use bevy_asset::{Asset, AssetApp, AssetEvents, Assets}; +use bevy_app::{AnimationSystems, App, Plugin, PostUpdate}; +use bevy_asset::{Asset, AssetApp, AssetEventSystems, Assets}; use bevy_ecs::{prelude::*, world::EntityMutExcept}; use bevy_math::FloatOrd; use bevy_platform::{collections::HashMap, hash::NoOpHash}; use bevy_reflect::{prelude::ReflectDefault, Reflect, TypePath}; use bevy_time::Time; -use bevy_transform::TransformSystem; +use bevy_transform::TransformSystems; use bevy_utils::{PreHashMap, PreHashMapExt, TypeIdMap}; use petgraph::graph::NodeIndex; use serde::{Deserialize, Serialize}; @@ -1244,7 +1244,7 @@ impl Plugin for AnimationPlugin { .add_systems( PostUpdate, ( - graph::thread_animation_graphs.before(AssetEvents), + graph::thread_animation_graphs.before(AssetEventSystems), advance_transitions, advance_animations, // TODO: `animate_targets` can animate anything, so @@ -1260,8 +1260,8 @@ impl Plugin for AnimationPlugin { expire_completed_transitions, ) .chain() - .in_set(Animation) - .before(TransformSystem::TransformPropagate), + .in_set(AnimationSystems) + .before(TransformSystems::Propagate), ); } } diff --git a/crates/bevy_anti_aliasing/src/contrast_adaptive_sharpening/mod.rs b/crates/bevy_anti_aliasing/src/contrast_adaptive_sharpening/mod.rs index a07b5e2239..707d75819d 100644 --- a/crates/bevy_anti_aliasing/src/contrast_adaptive_sharpening/mod.rs +++ b/crates/bevy_anti_aliasing/src/contrast_adaptive_sharpening/mod.rs @@ -18,7 +18,7 @@ use bevy_render::{ }, renderer::RenderDevice, view::{ExtractedView, ViewTarget}, - Render, RenderApp, RenderSet, + Render, RenderApp, RenderSystems, }; mod node; @@ -121,7 +121,7 @@ impl Plugin for CasPlugin { }; render_app .init_resource::>() - .add_systems(Render, prepare_cas_pipelines.in_set(RenderSet::Prepare)); + .add_systems(Render, prepare_cas_pipelines.in_set(RenderSystems::Prepare)); { render_app diff --git a/crates/bevy_anti_aliasing/src/fxaa/mod.rs b/crates/bevy_anti_aliasing/src/fxaa/mod.rs index 6d7824cf21..4848d3d268 100644 --- a/crates/bevy_anti_aliasing/src/fxaa/mod.rs +++ b/crates/bevy_anti_aliasing/src/fxaa/mod.rs @@ -18,7 +18,7 @@ use bevy_render::{ }, renderer::RenderDevice, view::{ExtractedView, ViewTarget}, - Render, RenderApp, RenderSet, + Render, RenderApp, RenderSystems, }; use bevy_utils::default; @@ -96,7 +96,10 @@ impl Plugin for FxaaPlugin { }; render_app .init_resource::>() - .add_systems(Render, prepare_fxaa_pipelines.in_set(RenderSet::Prepare)) + .add_systems( + Render, + prepare_fxaa_pipelines.in_set(RenderSystems::Prepare), + ) .add_render_graph_node::>(Core3d, Node3d::Fxaa) .add_render_graph_edges( Core3d, diff --git a/crates/bevy_anti_aliasing/src/smaa/mod.rs b/crates/bevy_anti_aliasing/src/smaa/mod.rs index f1e4d28678..4259b5e33d 100644 --- a/crates/bevy_anti_aliasing/src/smaa/mod.rs +++ b/crates/bevy_anti_aliasing/src/smaa/mod.rs @@ -76,7 +76,7 @@ use bevy_render::{ renderer::{RenderContext, RenderDevice, RenderQueue}, texture::{CachedTexture, GpuImage, TextureCache}, view::{ExtractedView, ViewTarget}, - Render, RenderApp, RenderSet, + Render, RenderApp, RenderSystems, }; use bevy_utils::prelude::default; @@ -346,10 +346,10 @@ impl Plugin for SmaaPlugin { .add_systems( Render, ( - prepare_smaa_pipelines.in_set(RenderSet::Prepare), - prepare_smaa_uniforms.in_set(RenderSet::PrepareResources), - prepare_smaa_textures.in_set(RenderSet::PrepareResources), - prepare_smaa_bind_groups.in_set(RenderSet::PrepareBindGroups), + prepare_smaa_pipelines.in_set(RenderSystems::Prepare), + prepare_smaa_uniforms.in_set(RenderSystems::PrepareResources), + prepare_smaa_textures.in_set(RenderSystems::PrepareResources), + prepare_smaa_bind_groups.in_set(RenderSystems::PrepareBindGroups), ), ) .add_render_graph_node::>(Core3d, Node3d::Smaa) diff --git a/crates/bevy_anti_aliasing/src/taa/mod.rs b/crates/bevy_anti_aliasing/src/taa/mod.rs index cf5ac269e2..dc12d34423 100644 --- a/crates/bevy_anti_aliasing/src/taa/mod.rs +++ b/crates/bevy_anti_aliasing/src/taa/mod.rs @@ -36,7 +36,7 @@ use bevy_render::{ sync_world::RenderEntity, texture::{CachedTexture, TextureCache}, view::{ExtractedView, Msaa, ViewTarget}, - ExtractSchedule, MainWorld, Render, RenderApp, RenderSet, + ExtractSchedule, MainWorld, Render, RenderApp, RenderSystems, }; use tracing::warn; @@ -64,9 +64,9 @@ impl Plugin for TemporalAntiAliasPlugin { .add_systems( Render, ( - prepare_taa_jitter_and_mip_bias.in_set(RenderSet::ManageViews), - prepare_taa_pipelines.in_set(RenderSet::Prepare), - prepare_taa_history_textures.in_set(RenderSet::PrepareResources), + prepare_taa_jitter_and_mip_bias.in_set(RenderSystems::ManageViews), + prepare_taa_pipelines.in_set(RenderSystems::Prepare), + prepare_taa_history_textures.in_set(RenderSystems::PrepareResources), ), ) .add_render_graph_node::>(Core3d, Node3d::Taa) diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs index 654a7098b9..81d7baeb4b 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -10,6 +10,7 @@ use alloc::{ pub use bevy_derive::AppLabel; use bevy_ecs::{ component::RequiredComponentsError, + error::{DefaultErrorHandler, ErrorHandler}, event::{event_update_system, EventCursor}, intern::Interned, prelude::*, @@ -85,6 +86,7 @@ pub struct App { /// [`WinitPlugin`]: https://docs.rs/bevy/latest/bevy/winit/struct.WinitPlugin.html /// [`ScheduleRunnerPlugin`]: https://docs.rs/bevy/latest/bevy/app/struct.ScheduleRunnerPlugin.html pub(crate) runner: RunnerFn, + default_error_handler: Option, } impl Debug for App { @@ -117,7 +119,7 @@ impl Default for App { app.add_systems( First, event_update_system - .in_set(bevy_ecs::event::EventUpdates) + .in_set(bevy_ecs::event::EventUpdateSystems) .run_if(bevy_ecs::event::event_update_condition), ); app.add_event::(); @@ -143,6 +145,7 @@ impl App { sub_apps: HashMap::default(), }, runner: Box::new(run_once), + default_error_handler: None, } } @@ -1115,7 +1118,12 @@ impl App { } /// Inserts a [`SubApp`] with the given label. - pub fn insert_sub_app(&mut self, label: impl AppLabel, sub_app: SubApp) { + pub fn insert_sub_app(&mut self, label: impl AppLabel, mut sub_app: SubApp) { + if let Some(handler) = self.default_error_handler { + sub_app + .world_mut() + .get_resource_or_insert_with(|| DefaultErrorHandler(handler)); + } self.sub_apps.sub_apps.insert(label.intern(), sub_app); } @@ -1334,6 +1342,49 @@ impl App { self.world_mut().add_observer(observer); self } + + /// Gets the error handler to set for new supapps. + /// + /// Note that the error handler of existing subapps may differ. + pub fn get_error_handler(&self) -> Option { + self.default_error_handler + } + + /// Set the [default error handler] for the all subapps (including the main one and future ones) + /// that do not have one. + /// + /// May only be called once and should be set by the application, not by libraries. + /// + /// The handler will be called when an error is produced and not otherwise handled. + /// + /// # Panics + /// Panics if called multiple times. + /// + /// # Example + /// ``` + /// # use bevy_app::*; + /// # use bevy_ecs::error::warn; + /// # fn MyPlugins(_: &mut App) {} + /// App::new() + /// .set_error_handler(warn) + /// .add_plugins(MyPlugins) + /// .run(); + /// ``` + /// + /// [default error handler]: bevy_ecs::error::DefaultErrorHandler + pub fn set_error_handler(&mut self, handler: ErrorHandler) -> &mut Self { + assert!( + self.default_error_handler.is_none(), + "`set_error_handler` called multiple times on same `App`" + ); + self.default_error_handler = Some(handler); + for sub_app in self.sub_apps.iter_mut() { + sub_app + .world_mut() + .get_resource_or_insert_with(|| DefaultErrorHandler(handler)); + } + self + } } type RunnerFn = Box AppExit>; diff --git a/crates/bevy_app/src/lib.rs b/crates/bevy_app/src/lib.rs index 6772136414..743806df71 100644 --- a/crates/bevy_app/src/lib.rs +++ b/crates/bevy_app/src/lib.rs @@ -55,7 +55,7 @@ pub mod prelude { main_schedule::{ First, FixedFirst, FixedLast, FixedPostUpdate, FixedPreUpdate, FixedUpdate, Last, Main, PostStartup, PostUpdate, PreStartup, PreUpdate, RunFixedMainLoop, - RunFixedMainLoopSystem, SpawnScene, Startup, Update, + RunFixedMainLoopSystems, SpawnScene, Startup, Update, }, sub_app::SubApp, Plugin, PluginGroup, TaskPoolOptions, TaskPoolPlugin, diff --git a/crates/bevy_app/src/main_schedule.rs b/crates/bevy_app/src/main_schedule.rs index 23e8ca0c33..7e0c759d47 100644 --- a/crates/bevy_app/src/main_schedule.rs +++ b/crates/bevy_app/src/main_schedule.rs @@ -92,8 +92,8 @@ pub struct PreUpdate; /// Runs the [`FixedMain`] schedule in a loop according until all relevant elapsed time has been "consumed". /// -/// If you need to order your variable timestep systems -/// before or after the fixed update logic, use the [`RunFixedMainLoopSystem`] system set. +/// If you need to order your variable timestep systems before or after +/// the fixed update logic, use the [`RunFixedMainLoopSystems`] system set. /// /// Note that in contrast to most other Bevy schedules, systems added directly to /// [`RunFixedMainLoop`] will *not* be parallelized between each other. @@ -149,7 +149,7 @@ pub struct FixedLast; /// The schedule that contains systems which only run after a fixed period of time has elapsed. /// /// This is run by the [`RunFixedMainLoop`] schedule. If you need to order your variable timestep systems -/// before or after the fixed update logic, use the [`RunFixedMainLoopSystem`] system set. +/// before or after the fixed update logic, use the [`RunFixedMainLoopSystems`] system set. /// /// Frequency of execution is configured by inserting `Time` resource, 64 Hz by default. /// See [this example](https://github.com/bevyengine/bevy/blob/latest/examples/time/time.rs). @@ -196,7 +196,11 @@ pub struct Last; /// Animation system set. This exists in [`PostUpdate`]. #[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)] -pub struct Animation; +pub struct AnimationSystems; + +/// Deprecated alias for [`AnimationSystems`]. +#[deprecated(since = "0.17.0", note = "Renamed to `AnimationSystems`.")] +pub type Animation = AnimationSystems; /// Defines the schedules to be run for the [`Main`] schedule, including /// their order. @@ -318,9 +322,9 @@ impl Plugin for MainSchedulePlugin { .configure_sets( RunFixedMainLoop, ( - RunFixedMainLoopSystem::BeforeFixedMainLoop, - RunFixedMainLoopSystem::FixedMainLoop, - RunFixedMainLoopSystem::AfterFixedMainLoop, + RunFixedMainLoopSystems::BeforeFixedMainLoop, + RunFixedMainLoopSystems::FixedMainLoop, + RunFixedMainLoopSystems::AfterFixedMainLoop, ) .chain(), ); @@ -400,7 +404,7 @@ impl FixedMain { /// Note that in contrast to most other Bevy schedules, systems added directly to /// [`RunFixedMainLoop`] will *not* be parallelized between each other. #[derive(Debug, Hash, PartialEq, Eq, Copy, Clone, SystemSet)] -pub enum RunFixedMainLoopSystem { +pub enum RunFixedMainLoopSystems { /// Runs before the fixed update logic. /// /// A good example of a system that fits here @@ -419,7 +423,7 @@ pub enum RunFixedMainLoopSystem { /// App::new() /// .add_systems( /// RunFixedMainLoop, - /// update_camera_rotation.in_set(RunFixedMainLoopSystem::BeforeFixedMainLoop)) + /// update_camera_rotation.in_set(RunFixedMainLoopSystems::BeforeFixedMainLoop)) /// .add_systems(FixedUpdate, update_physics); /// /// # fn update_camera_rotation() {} @@ -432,7 +436,7 @@ pub enum RunFixedMainLoopSystem { /// /// Don't place systems here, use [`FixedUpdate`] and friends instead. /// Use this system instead to order your systems to run specifically inbetween the fixed update logic and all - /// other systems that run in [`RunFixedMainLoopSystem::BeforeFixedMainLoop`] or [`RunFixedMainLoopSystem::AfterFixedMainLoop`]. + /// other systems that run in [`RunFixedMainLoopSystems::BeforeFixedMainLoop`] or [`RunFixedMainLoopSystems::AfterFixedMainLoop`]. /// /// [`Time`]: https://docs.rs/bevy/latest/bevy/prelude/struct.Virtual.html /// [`Time::overstep`]: https://docs.rs/bevy/latest/bevy/time/struct.Time.html#method.overstep @@ -448,8 +452,8 @@ pub enum RunFixedMainLoopSystem { /// // This system will be called before all interpolation systems /// // that third-party plugins might add. /// prepare_for_interpolation - /// .after(RunFixedMainLoopSystem::FixedMainLoop) - /// .before(RunFixedMainLoopSystem::AfterFixedMainLoop), + /// .after(RunFixedMainLoopSystems::FixedMainLoop) + /// .before(RunFixedMainLoopSystems::AfterFixedMainLoop), /// ) /// ); /// @@ -473,10 +477,14 @@ pub enum RunFixedMainLoopSystem { /// .add_systems(FixedUpdate, update_physics) /// .add_systems( /// RunFixedMainLoop, - /// interpolate_transforms.in_set(RunFixedMainLoopSystem::AfterFixedMainLoop)); + /// interpolate_transforms.in_set(RunFixedMainLoopSystems::AfterFixedMainLoop)); /// /// # fn interpolate_transforms() {} /// # fn update_physics() {} /// ``` AfterFixedMainLoop, } + +/// Deprecated alias for [`RunFixedMainLoopSystems`]. +#[deprecated(since = "0.17.0", note = "Renamed to `RunFixedMainLoopSystems`.")] +pub type RunFixedMainLoopSystem = RunFixedMainLoopSystems; diff --git a/crates/bevy_asset/Cargo.toml b/crates/bevy_asset/Cargo.toml index 2c7c918300..07a45a3f6d 100644 --- a/crates/bevy_asset/Cargo.toml +++ b/crates/bevy_asset/Cargo.toml @@ -11,7 +11,7 @@ keywords = ["bevy"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -file_watcher = ["notify-debouncer-full", "watch"] +file_watcher = ["notify-debouncer-full", "watch", "multi_threaded"] embedded_watcher = ["file_watcher"] multi_threaded = ["bevy_tasks/multi_threaded"] asset_processor = [] @@ -19,38 +19,50 @@ watch = [] trace = [] [dependencies] -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } +bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false, features = [ + "bevy_reflect", +] } bevy_asset_macros = { path = "macros", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", features = [ +bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev", default-features = false } +bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, features = [ "uuid", ] } -bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } +bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev", default-features = false, features = [ + "async_executor", +] } +bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev", default-features = false } bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ "std", ] } -stackfuture = "0.3" -atomicow = "1.0" -async-broadcast = "0.7.2" -async-fs = "2.0" -async-lock = "3.0" -bitflags = { version = "2.3", features = ["serde"] } -crossbeam-channel = "0.5" -downcast-rs = { version = "2", default-features = false, features = ["std"] } -disqualified = "1.0" -either = "1.13" -futures-io = "0.3" -futures-lite = "2.0.1" -blake3 = "1.5" -parking_lot = { version = "0.12", features = ["arc_lock", "send_guard"] } -ron = "0.8" -serde = { version = "1", features = ["derive"] } +stackfuture = { version = "0.3", default-features = false } +atomicow = { version = "1.1", default-features = false, features = ["std"] } +async-broadcast = { version = "0.7.2", default-features = false } +async-fs = { version = "2.0", default-features = false } +async-lock = { version = "3.0", default-features = false } +bitflags = { version = "2.3", default-features = false } +crossbeam-channel = { version = "0.5", default-features = false, features = [ + "std", +] } +downcast-rs = { version = "2", default-features = false } +disqualified = { version = "1.0", default-features = false } +either = { version = "1.13", default-features = false } +futures-io = { version = "0.3", default-features = false } +futures-lite = { version = "2.0.1", default-features = false } +blake3 = { version = "1.5", default-features = false } +parking_lot = { version = "0.12", default-features = false, features = [ + "arc_lock", + "send_guard", +] } +ron = { version = "0.8", default-features = false } +serde = { version = "1", default-features = false, features = ["derive"] } thiserror = { version = "2", default-features = false } derive_more = { version = "1", default-features = false, features = ["from"] } -uuid = { version = "1.13.1", features = ["v4"] } -tracing = { version = "0.1", default-features = false, features = ["std"] } +uuid = { version = "1.13.1", default-features = false, features = [ + "v4", + "serde", +] } +tracing = { version = "0.1", default-features = false } [target.'cfg(target_os = "android")'.dependencies] bevy_window = { path = "../bevy_window", version = "0.16.0-dev" } @@ -77,7 +89,7 @@ bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-featu ] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -notify-debouncer-full = { version = "0.5.0", optional = true } +notify-debouncer-full = { version = "0.5.0", default-features = false, optional = true } [lints] workspace = true diff --git a/crates/bevy_asset/src/asset_changed.rs b/crates/bevy_asset/src/asset_changed.rs index 40723d7b08..10f298c968 100644 --- a/crates/bevy_asset/src/asset_changed.rs +++ b/crates/bevy_asset/src/asset_changed.rs @@ -22,10 +22,10 @@ use tracing::error; /// the [`AssetChanged`] filter to determine if an asset has changed since the last time /// a query ran. /// -/// This resource is automatically managed by the [`AssetEvents`](crate::AssetEvents) schedule and -/// should not be exposed to the user in order to maintain safety guarantees. Any additional uses of -/// this resource should be carefully audited to ensure that they do not introduce any safety -/// issues. +/// This resource is automatically managed by the [`AssetEventSystems`](crate::AssetEventSystems) +/// system set and should not be exposed to the user in order to maintain safety guarantees. +/// Any additional uses of this resource should be carefully audited to ensure that they do not +/// introduce any safety issues. #[derive(Resource)] pub(crate) struct AssetChanges { change_ticks: HashMap, Tick>, @@ -102,14 +102,13 @@ impl<'w, A: AsAssetId> AssetChangeCheck<'w, A> { /// /// # Quirks /// -/// - Asset changes are registered in the [`AssetEvents`] schedule. +/// - Asset changes are registered in the [`AssetEventSystems`] system set. /// - Removed assets are not detected. /// -/// The list of changed assets only gets updated in the -/// [`AssetEvents`] schedule which runs in `Last`. Therefore, `AssetChanged` -/// will only pick up asset changes in schedules following `AssetEvents` or the -/// next frame. Consider adding the system in the `Last` schedule after [`AssetEvents`] if you need -/// to react without frame delay to asset changes. +/// The list of changed assets only gets updated in the [`AssetEventSystems`] system set, +/// which runs in `Last`. Therefore, `AssetChanged` will only pick up asset changes in schedules +/// following [`AssetEventSystems`] or the next frame. Consider adding the system in the `Last` schedule +/// after [`AssetEventSystems`] if you need to react without frame delay to asset changes. /// /// # Performance /// @@ -120,7 +119,7 @@ impl<'w, A: AsAssetId> AssetChangeCheck<'w, A> { /// /// If no `A` asset updated since the last time the system ran, then no lookups occur. /// -/// [`AssetEvents`]: crate::AssetEvents +/// [`AssetEventSystems`]: crate::AssetEventSystems /// [`Assets::get_mut`]: crate::Assets::get_mut pub struct AssetChanged(PhantomData); @@ -166,7 +165,7 @@ unsafe impl WorldQuery for AssetChanged { this_run: Tick, ) -> Self::Fetch<'w> { // SAFETY: - // - `AssetChanges` is private and only accessed mutably in the `AssetEvents` schedule + // - `AssetChanges` is private and only accessed mutably in the `AssetEventSystems` system set. // - `resource_id` was obtained from the type ID of `AssetChanges`. let Some(changes) = (unsafe { world @@ -283,7 +282,7 @@ unsafe impl QueryFilter for AssetChanged { #[cfg(test)] #[expect(clippy::print_stdout, reason = "Allowed in tests.")] mod tests { - use crate::{AssetEvents, AssetPlugin, Handle}; + use crate::{AssetEventSystems, AssetPlugin, Handle}; use alloc::{vec, vec::Vec}; use core::num::NonZero; use std::println; @@ -406,7 +405,7 @@ mod tests { .init_asset::() .insert_resource(Counter(vec![0, 0, 0, 0])) .add_systems(Update, add_some) - .add_systems(PostUpdate, count_update.after(AssetEvents)); + .add_systems(PostUpdate, count_update.after(AssetEventSystems)); // First run of the app, `add_systems(Startup…)` runs. app.update(); // run_count == 0 @@ -441,7 +440,7 @@ mod tests { }, ) .add_systems(Update, update_some) - .add_systems(PostUpdate, count_update.after(AssetEvents)); + .add_systems(PostUpdate, count_update.after(AssetEventSystems)); // First run of the app, `add_systems(Startup…)` runs. app.update(); // run_count == 0 diff --git a/crates/bevy_asset/src/handle.rs b/crates/bevy_asset/src/handle.rs index a78c43ffdd..e6f00c7da5 100644 --- a/crates/bevy_asset/src/handle.rs +++ b/crates/bevy_asset/src/handle.rs @@ -11,7 +11,6 @@ use core::{ use crossbeam_channel::{Receiver, Sender}; use disqualified::ShortName; use thiserror::Error; -use uuid::Uuid; /// Provides [`Handle`] and [`UntypedHandle`] _for a specific asset type_. /// This should _only_ be used for one specific asset type. @@ -149,17 +148,6 @@ impl Clone for Handle { } impl Handle { - /// Create a new [`Handle::Weak`] with the given [`u128`] encoding of a [`Uuid`]. - #[deprecated( - since = "0.16.0", - note = "use the `weak_handle!` macro with a UUID string instead" - )] - pub const fn weak_from_u128(value: u128) -> Self { - Handle::Weak(AssetId::Uuid { - uuid: Uuid::from_u128(value), - }) - } - /// Returns the [`AssetId`] of this [`Asset`]. #[inline] pub fn id(&self) -> AssetId { @@ -554,6 +542,7 @@ mod tests { use bevy_platform::hash::FixedHasher; use bevy_reflect::PartialReflect; use core::hash::BuildHasher; + use uuid::Uuid; use super::*; diff --git a/crates/bevy_asset/src/io/embedded/mod.rs b/crates/bevy_asset/src/io/embedded/mod.rs index 13610531e2..f6c44397fc 100644 --- a/crates/bevy_asset/src/io/embedded/mod.rs +++ b/crates/bevy_asset/src/io/embedded/mod.rs @@ -8,8 +8,10 @@ use crate::io::{ memory::{Dir, MemoryAssetReader, Value}, AssetSource, AssetSourceBuilders, }; +use crate::AssetServer; use alloc::boxed::Box; -use bevy_ecs::resource::Resource; +use bevy_app::App; +use bevy_ecs::{resource::Resource, world::World}; use std::path::{Path, PathBuf}; #[cfg(feature = "embedded_watcher")] @@ -132,6 +134,71 @@ impl EmbeddedAssetRegistry { } } +/// Trait for the [`load_embedded_asset!`] macro, to access [`AssetServer`] +/// from arbitrary things. +/// +/// [`load_embedded_asset!`]: crate::load_embedded_asset +pub trait GetAssetServer { + fn get_asset_server(&self) -> &AssetServer; +} +impl GetAssetServer for App { + fn get_asset_server(&self) -> &AssetServer { + self.world().get_asset_server() + } +} +impl GetAssetServer for World { + fn get_asset_server(&self) -> &AssetServer { + self.resource() + } +} +impl GetAssetServer for AssetServer { + fn get_asset_server(&self) -> &AssetServer { + self + } +} + +/// Load an [embedded asset](crate::embedded_asset). +/// +/// This is useful if the embedded asset in question is not publicly exposed, but +/// you need to use it internally. +/// +/// # Syntax +/// +/// This macro takes two arguments and an optional third one: +/// 1. The asset source. It may be `AssetServer`, `World` or `App`. +/// 2. The path to the asset to embed, as a string literal. +/// 3. Optionally, a closure of the same type as in [`AssetServer::load_with_settings`]. +/// Consider explicitly typing the closure argument in case of type error. +/// +/// # Usage +/// +/// The advantage compared to using directly [`AssetServer::load`] is: +/// - This also accepts [`World`] and [`App`] arguments. +/// - This uses the exact same path as `embedded_asset!`, so you can keep it +/// consistent. +/// +/// As a rule of thumb: +/// - If the asset in used in the same module as it is declared using `embedded_asset!`, +/// use this macro. +/// - Otherwise, use `AssetServer::load`. +#[macro_export] +macro_rules! load_embedded_asset { + (@get: $path: literal, $provider: expr) => {{ + let path = $crate::embedded_path!($path); + let path = $crate::AssetPath::from_path_buf(path).with_source("embedded"); + let asset_server = $crate::io::embedded::GetAssetServer::get_asset_server($provider); + (path, asset_server) + }}; + ($provider: expr, $path: literal, $settings: expr) => {{ + let (path, asset_server) = $crate::load_embedded_asset!(@get: $path, $provider); + asset_server.load_with_settings(path, $settings) + }}; + ($provider: expr, $path: literal) => {{ + let (path, asset_server) = $crate::load_embedded_asset!(@get: $path, $provider); + asset_server.load(path) + }}; +} + /// Returns the [`Path`] for a given `embedded` asset. /// This is used internally by [`embedded_asset`] and can be used to get a [`Path`] /// that matches the [`AssetPath`](crate::AssetPath) used by that asset. @@ -140,7 +207,7 @@ impl EmbeddedAssetRegistry { #[macro_export] macro_rules! embedded_path { ($path_str: expr) => {{ - embedded_path!("src", $path_str) + $crate::embedded_path!("src", $path_str) }}; ($source_path: expr, $path_str: expr) => {{ @@ -168,6 +235,13 @@ pub fn _embedded_asset_path( file_path: &Path, asset_path: &Path, ) -> PathBuf { + let file_path = if cfg!(not(target_family = "windows")) { + // Work around bug: https://github.com/bevyengine/bevy/issues/14246 + // Note, this will break any paths on Linux/Mac containing "\" + PathBuf::from(file_path.to_str().unwrap().replace("\\", "/")) + } else { + PathBuf::from(file_path) + }; let mut maybe_parent = file_path.parent(); let after_src = loop { let Some(parent) = maybe_parent else { @@ -185,7 +259,7 @@ pub fn _embedded_asset_path( /// Creates a new `embedded` asset by embedding the bytes of the given path into the current binary /// and registering those bytes with the `embedded` [`AssetSource`]. /// -/// This accepts the current [`App`](bevy_app::App) as the first parameter and a path `&str` (relative to the current file) as the second. +/// This accepts the current [`App`] as the first parameter and a path `&str` (relative to the current file) as the second. /// /// By default this will generate an [`AssetPath`] using the following rules: /// @@ -210,14 +284,19 @@ pub fn _embedded_asset_path( /// /// `embedded_asset!(app, "rock.wgsl")` /// -/// `rock.wgsl` can now be loaded by the [`AssetServer`](crate::AssetServer) with the following path: +/// `rock.wgsl` can now be loaded by the [`AssetServer`] as follows: /// /// ```no_run -/// # use bevy_asset::{Asset, AssetServer}; +/// # use bevy_asset::{Asset, AssetServer, load_embedded_asset}; /// # use bevy_reflect::TypePath; /// # let asset_server: AssetServer = panic!(); /// # #[derive(Asset, TypePath)] /// # struct Shader; +/// // If we are loading the shader in the same module we used `embedded_asset!`: +/// let shader = load_embedded_asset!(&asset_server, "rock.wgsl"); +/// # let _: bevy_asset::Handle = shader; +/// +/// // If the goal is to expose the asset **to the end user**: /// let shader = asset_server.load::("embedded://bevy_rock/render/rock.wgsl"); /// ``` /// @@ -251,11 +330,11 @@ pub fn _embedded_asset_path( /// [`embedded_path`]: crate::embedded_path #[macro_export] macro_rules! embedded_asset { - ($app: ident, $path: expr) => {{ + ($app: expr, $path: expr) => {{ $crate::embedded_asset!($app, "src", $path) }}; - ($app: ident, $source_path: expr, $path: expr) => {{ + ($app: expr, $source_path: expr, $path: expr) => {{ let mut embedded = $app .world_mut() .resource_mut::<$crate::io::embedded::EmbeddedAssetRegistry>(); diff --git a/crates/bevy_asset/src/io/file/file_asset.rs b/crates/bevy_asset/src/io/file/file_asset.rs index f65680217b..58ae546e7f 100644 --- a/crates/bevy_asset/src/io/file/file_asset.rs +++ b/crates/bevy_asset/src/io/file/file_asset.rs @@ -74,6 +74,15 @@ impl AssetReader for FileAssetReader { return None; } } + // filter out hidden files. they are not listed by default but are directly targetable + if path + .file_name() + .and_then(|file_name| file_name.to_str()) + .map(|file_name| file_name.starts_with('.')) + .unwrap_or_default() + { + return None; + } let relative_path = path.strip_prefix(&root_path).unwrap(); Some(relative_path.to_owned()) }) diff --git a/crates/bevy_asset/src/io/file/sync_file_asset.rs b/crates/bevy_asset/src/io/file/sync_file_asset.rs index 7533256204..9281d323c8 100644 --- a/crates/bevy_asset/src/io/file/sync_file_asset.rs +++ b/crates/bevy_asset/src/io/file/sync_file_asset.rs @@ -145,6 +145,16 @@ impl AssetReader for FileAssetReader { return None; } } + // filter out hidden files. they are not listed by default but are directly targetable + if path + .file_name() + .and_then(|file_name| file_name.to_str()) + .map(|file_name| file_name.starts_with('.')) + .unwrap_or_default() + { + return None; + } + let relative_path = path.strip_prefix(&root_path).unwrap(); Some(relative_path.to_owned()) }) diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index 5d98d2b21e..5b680eb191 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -228,13 +228,6 @@ use bevy_reflect::{FromReflect, GetTypeRegistration, Reflect, TypePath}; use core::any::TypeId; use tracing::error; -#[cfg(all(feature = "file_watcher", not(feature = "multi_threaded")))] -compile_error!( - "The \"file_watcher\" feature for hot reloading requires the \ - \"multi_threaded\" feature to be functional.\n\ - Consider either disabling the \"file_watcher\" feature or enabling \"multi_threaded\"" -); - /// Provides "asset" loading and processing functionality. An [`Asset`] is a "runtime value" that is loaded from an [`AssetSource`], /// which can be something like a filesystem, a network, etc. /// @@ -421,7 +414,10 @@ impl Plugin for AssetPlugin { .init_asset::() .init_asset::<()>() .add_event::() - .configure_sets(PreUpdate, TrackAssets.after(handle_internal_asset_events)) + .configure_sets( + PreUpdate, + AssetTrackingSystems.after(handle_internal_asset_events), + ) // `handle_internal_asset_events` requires the use of `&mut World`, // and as a result has ambiguous system ordering with all other systems in `PreUpdate`. // This is virtually never a real problem: asset loading is async and so anything that interacts directly with it @@ -618,9 +614,12 @@ impl AssetApp for App { PostUpdate, Assets::::asset_events .run_if(Assets::::asset_events_condition) - .in_set(AssetEvents), + .in_set(AssetEventSystems), + ) + .add_systems( + PreUpdate, + Assets::::track_assets.in_set(AssetTrackingSystems), ) - .add_systems(PreUpdate, Assets::::track_assets.in_set(TrackAssets)) } fn register_asset_reflect(&mut self) -> &mut Self @@ -650,13 +649,21 @@ impl AssetApp for App { /// A system set that holds all "track asset" operations. #[derive(SystemSet, Hash, Debug, PartialEq, Eq, Clone)] -pub struct TrackAssets; +pub struct AssetTrackingSystems; + +/// Deprecated alias for [`AssetTrackingSystems`]. +#[deprecated(since = "0.17.0", note = "Renamed to `AssetTrackingSystems`.")] +pub type TrackAssets = AssetTrackingSystems; /// A system set where events accumulated in [`Assets`] are applied to the [`AssetEvent`] [`Events`] resource. /// /// [`Events`]: bevy_ecs::event::Events #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] -pub struct AssetEvents; +pub struct AssetEventSystems; + +/// Deprecated alias for [`AssetEventSystems`]. +#[deprecated(since = "0.17.0", note = "Renamed to `AssetEventSystems`.")] +pub type AssetEvents = AssetEventSystems; #[cfg(test)] mod tests { diff --git a/crates/bevy_asset/src/path.rs b/crates/bevy_asset/src/path.rs index ad127812dc..97e6c6499d 100644 --- a/crates/bevy_asset/src/path.rs +++ b/crates/bevy_asset/src/path.rs @@ -223,6 +223,16 @@ impl<'a> AssetPath<'a> { Ok((source, path, label)) } + /// Creates a new [`AssetPath`] from a [`PathBuf`]. + #[inline] + pub fn from_path_buf(path_buf: PathBuf) -> AssetPath<'a> { + AssetPath { + path: CowArc::Owned(path_buf.into()), + source: AssetSourceId::Default, + label: None, + } + } + /// Creates a new [`AssetPath`] from a [`Path`]. #[inline] pub fn from_path(path: &'a Path) -> AssetPath<'a> { diff --git a/crates/bevy_audio/src/lib.rs b/crates/bevy_audio/src/lib.rs index babae2f8a9..becbf5d1da 100644 --- a/crates/bevy_audio/src/lib.rs +++ b/crates/bevy_audio/src/lib.rs @@ -58,13 +58,13 @@ pub use sinks::*; use bevy_app::prelude::*; use bevy_asset::{Asset, AssetApp}; use bevy_ecs::prelude::*; -use bevy_transform::TransformSystem; +use bevy_transform::TransformSystems; use audio_output::*; /// Set for the audio playback systems, so they can share a run condition #[derive(SystemSet, Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] -struct AudioPlaySet; +struct AudioPlaybackSystems; /// Adds support for audio playback to a Bevy Application /// @@ -90,13 +90,13 @@ impl Plugin for AudioPlugin { .insert_resource(DefaultSpatialScale(self.default_spatial_scale)) .configure_sets( PostUpdate, - AudioPlaySet + AudioPlaybackSystems .run_if(audio_output_available) - .after(TransformSystem::TransformPropagate), // For spatial audio transforms + .after(TransformSystems::Propagate), // For spatial audio transforms ) .add_systems( PostUpdate, - (update_emitter_positions, update_listener_positions).in_set(AudioPlaySet), + (update_emitter_positions, update_listener_positions).in_set(AudioPlaybackSystems), ) .init_resource::(); @@ -118,7 +118,8 @@ impl AddAudioSource for App { { self.init_asset::().add_systems( PostUpdate, - (play_queued_audio_system::, cleanup_finished_audio::).in_set(AudioPlaySet), + (play_queued_audio_system::, cleanup_finished_audio::) + .in_set(AudioPlaybackSystems), ); self } diff --git a/crates/bevy_audio/src/sinks.rs b/crates/bevy_audio/src/sinks.rs index b0c77456e1..ed51754f86 100644 --- a/crates/bevy_audio/src/sinks.rs +++ b/crates/bevy_audio/src/sinks.rs @@ -1,10 +1,11 @@ +use crate::Volume; use bevy_ecs::component::Component; use bevy_math::Vec3; use bevy_transform::prelude::Transform; +use core::time::Duration; +pub use rodio::source::SeekError; use rodio::{Sink, SpatialSink}; -use crate::Volume; - /// Common interactions with an audio sink. pub trait AudioSinkPlayback { /// Gets the volume of the sound as a [`Volume`]. @@ -41,6 +42,26 @@ pub trait AudioSinkPlayback { /// No effect if not paused. fn play(&self); + /// Attempts to seek to a given position in the current source. + /// + /// This blocks between 0 and ~5 milliseconds. + /// + /// As long as the duration of the source is known, seek is guaranteed to saturate + /// at the end of the source. For example given a source that reports a total duration + /// of 42 seconds calling `try_seek()` with 60 seconds as argument will seek to + /// 42 seconds. + /// + /// # Errors + /// This function will return [`SeekError::NotSupported`] if one of the underlying + /// sources does not support seeking. + /// + /// It will return an error if an implementation ran + /// into one during the seek. + /// + /// When seeking beyond the end of a source, this + /// function might return an error if the duration of the source is not known. + fn try_seek(&self, pos: Duration) -> Result<(), SeekError>; + /// Pauses playback of this sink. /// /// No effect if already paused. @@ -160,6 +181,10 @@ impl AudioSinkPlayback for AudioSink { self.sink.play(); } + fn try_seek(&self, pos: Duration) -> Result<(), SeekError> { + self.sink.try_seek(pos) + } + fn pause(&self) { self.sink.pause(); } @@ -256,6 +281,10 @@ impl AudioSinkPlayback for SpatialAudioSink { self.sink.play(); } + fn try_seek(&self, pos: Duration) -> Result<(), SeekError> { + self.sink.try_seek(pos) + } + fn pause(&self) { self.sink.pause(); } diff --git a/crates/bevy_color/src/color_ops.rs b/crates/bevy_color/src/color_ops.rs index 776ee906f9..a3266ba2d6 100644 --- a/crates/bevy_color/src/color_ops.rs +++ b/crates/bevy_color/src/color_ops.rs @@ -77,6 +77,20 @@ pub trait Alpha: Sized { } } +impl Alpha for f32 { + fn with_alpha(&self, alpha: f32) -> Self { + alpha + } + + fn alpha(&self) -> f32 { + *self + } + + fn set_alpha(&mut self, alpha: f32) { + *self = alpha; + } +} + /// Trait for manipulating the hue of a color. pub trait Hue: Sized { /// Return a new version of this color with the hue channel set to the given value. diff --git a/crates/bevy_color/src/interpolate.rs b/crates/bevy_color/src/interpolate.rs new file mode 100644 index 0000000000..75d1717d5a --- /dev/null +++ b/crates/bevy_color/src/interpolate.rs @@ -0,0 +1,37 @@ +//! TODO: Implement for non-linear colors. + +#[cfg(test)] +mod test { + use bevy_math::StableInterpolate; + + use crate::{Gray, Laba, LinearRgba, Oklaba, Srgba, Xyza}; + + #[test] + pub fn test_color_stable_interpolate() { + let b = Srgba::BLACK; + let w = Srgba::WHITE; + assert_eq!( + b.interpolate_stable(&w, 0.5), + Srgba::new(0.5, 0.5, 0.5, 1.0), + ); + + let b = LinearRgba::BLACK; + let w = LinearRgba::WHITE; + assert_eq!( + b.interpolate_stable(&w, 0.5), + LinearRgba::new(0.5, 0.5, 0.5, 1.0), + ); + + let b = Xyza::BLACK; + let w = Xyza::WHITE; + assert_eq!(b.interpolate_stable(&w, 0.5), Xyza::gray(0.5),); + + let b = Laba::BLACK; + let w = Laba::WHITE; + assert_eq!(b.interpolate_stable(&w, 0.5), Laba::new(0.5, 0.0, 0.0, 1.0),); + + let b = Oklaba::BLACK; + let w = Oklaba::WHITE; + assert_eq!(b.interpolate_stable(&w, 0.5), Oklaba::gray(0.5),); + } +} diff --git a/crates/bevy_color/src/lib.rs b/crates/bevy_color/src/lib.rs index e1ee1fbe38..712da5d7ec 100644 --- a/crates/bevy_color/src/lib.rs +++ b/crates/bevy_color/src/lib.rs @@ -105,6 +105,7 @@ mod color_range; mod hsla; mod hsva; mod hwba; +mod interpolate; mod laba; mod lcha; mod linear_rgba; @@ -265,6 +266,12 @@ macro_rules! impl_componentwise_vector_space { $($element: 0.0,)+ }; } + + impl bevy_math::StableInterpolate for $ty { + fn interpolate_stable(&self, other: &Self, t: f32) -> Self { + bevy_math::VectorSpace::lerp(*self, *other, t) + } + } }; } diff --git a/crates/bevy_core_pipeline/src/auto_exposure/mod.rs b/crates/bevy_core_pipeline/src/auto_exposure/mod.rs index f94a61d09b..7e7e6c1af7 100644 --- a/crates/bevy_core_pipeline/src/auto_exposure/mod.rs +++ b/crates/bevy_core_pipeline/src/auto_exposure/mod.rs @@ -9,7 +9,7 @@ use bevy_render::{ Buffer, BufferDescriptor, BufferUsages, PipelineCache, Shader, SpecializedComputePipelines, }, renderer::RenderDevice, - ExtractSchedule, Render, RenderApp, RenderSet, + ExtractSchedule, Render, RenderApp, RenderSystems, }; mod buffers; @@ -72,8 +72,8 @@ impl Plugin for AutoExposurePlugin { .add_systems( Render, ( - prepare_buffers.in_set(RenderSet::Prepare), - queue_view_auto_exposure_pipelines.in_set(RenderSet::Queue), + prepare_buffers.in_set(RenderSystems::Prepare), + queue_view_auto_exposure_pipelines.in_set(RenderSystems::Queue), ), ) .add_render_graph_node::(Core3d, node::AutoExposure) diff --git a/crates/bevy_core_pipeline/src/bloom/mod.rs b/crates/bevy_core_pipeline/src/bloom/mod.rs index 8717b9096e..cbd87d11bd 100644 --- a/crates/bevy_core_pipeline/src/bloom/mod.rs +++ b/crates/bevy_core_pipeline/src/bloom/mod.rs @@ -24,7 +24,7 @@ use bevy_render::{ renderer::{RenderContext, RenderDevice}, texture::{CachedTexture, TextureCache}, view::ViewTarget, - Render, RenderApp, RenderSet, + Render, RenderApp, RenderSystems, }; use downsampling_pipeline::{ prepare_downsampling_pipeline, BloomDownsamplingPipeline, BloomDownsamplingPipelineIds, @@ -63,10 +63,10 @@ impl Plugin for BloomPlugin { .add_systems( Render, ( - prepare_downsampling_pipeline.in_set(RenderSet::Prepare), - prepare_upsampling_pipeline.in_set(RenderSet::Prepare), - prepare_bloom_textures.in_set(RenderSet::PrepareResources), - prepare_bloom_bind_groups.in_set(RenderSet::PrepareBindGroups), + prepare_downsampling_pipeline.in_set(RenderSystems::Prepare), + prepare_upsampling_pipeline.in_set(RenderSystems::Prepare), + prepare_bloom_textures.in_set(RenderSystems::PrepareResources), + prepare_bloom_bind_groups.in_set(RenderSystems::PrepareBindGroups), ), ) // Add bloom to the 3d render graph diff --git a/crates/bevy_core_pipeline/src/core_2d/mod.rs b/crates/bevy_core_pipeline/src/core_2d/mod.rs index 0a8ed17f8e..725ac38ed9 100644 --- a/crates/bevy_core_pipeline/src/core_2d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_2d/mod.rs @@ -65,7 +65,7 @@ use bevy_render::{ sync_world::MainEntity, texture::TextureCache, view::{Msaa, ViewDepthTexture}, - Extract, ExtractSchedule, Render, RenderApp, RenderSet, + Extract, ExtractSchedule, Render, RenderApp, RenderSystems, }; use self::graph::{Core2d, Node2d}; @@ -93,8 +93,8 @@ impl Plugin for Core2dPlugin { .add_systems( Render, ( - sort_phase_system::.in_set(RenderSet::PhaseSort), - prepare_core_2d_depth_textures.in_set(RenderSet::PrepareResources), + sort_phase_system::.in_set(RenderSystems::PhaseSort), + prepare_core_2d_depth_textures.in_set(RenderSystems::PrepareResources), ), ); diff --git a/crates/bevy_core_pipeline/src/core_3d/mod.rs b/crates/bevy_core_pipeline/src/core_3d/mod.rs index b9f6955499..0ff61db842 100644 --- a/crates/bevy_core_pipeline/src/core_3d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_3d/mod.rs @@ -106,7 +106,7 @@ use bevy_render::{ sync_world::{MainEntity, RenderEntity}, texture::{ColorAttachment, TextureCache}, view::{ExtractedView, ViewDepthTexture, ViewTarget}, - Extract, ExtractSchedule, Render, RenderApp, RenderSet, + Extract, ExtractSchedule, Render, RenderApp, RenderSystems, }; use nonmax::NonMaxU32; use tracing::warn; @@ -167,14 +167,14 @@ impl Plugin for Core3dPlugin { .add_systems( Render, ( - sort_phase_system::.in_set(RenderSet::PhaseSort), - sort_phase_system::.in_set(RenderSet::PhaseSort), + sort_phase_system::.in_set(RenderSystems::PhaseSort), + sort_phase_system::.in_set(RenderSystems::PhaseSort), configure_occlusion_culling_view_targets .after(prepare_view_targets) - .in_set(RenderSet::ManageViews), - prepare_core_3d_depth_textures.in_set(RenderSet::PrepareResources), - prepare_core_3d_transmission_textures.in_set(RenderSet::PrepareResources), - prepare_prepass_textures.in_set(RenderSet::PrepareResources), + .in_set(RenderSystems::ManageViews), + prepare_core_3d_depth_textures.in_set(RenderSystems::PrepareResources), + prepare_core_3d_transmission_textures.in_set(RenderSystems::PrepareResources), + prepare_prepass_textures.in_set(RenderSystems::PrepareResources), ), ); diff --git a/crates/bevy_core_pipeline/src/deferred/copy_lighting_id.rs b/crates/bevy_core_pipeline/src/deferred/copy_lighting_id.rs index 966be880c2..77430e0291 100644 --- a/crates/bevy_core_pipeline/src/deferred/copy_lighting_id.rs +++ b/crates/bevy_core_pipeline/src/deferred/copy_lighting_id.rs @@ -12,7 +12,7 @@ use bevy_render::{ renderer::RenderDevice, texture::{CachedTexture, TextureCache}, view::ViewTarget, - Render, RenderApp, RenderSet, + Render, RenderApp, RenderSystems, }; use bevy_ecs::query::QueryItem; @@ -40,7 +40,7 @@ impl Plugin for CopyDeferredLightingIdPlugin { }; render_app.add_systems( Render, - (prepare_deferred_lighting_id_textures.in_set(RenderSet::PrepareResources),), + (prepare_deferred_lighting_id_textures.in_set(RenderSystems::PrepareResources),), ); } diff --git a/crates/bevy_core_pipeline/src/dof/mod.rs b/crates/bevy_core_pipeline/src/dof/mod.rs index 87a10313f1..5eee57b8bb 100644 --- a/crates/bevy_core_pipeline/src/dof/mod.rs +++ b/crates/bevy_core_pipeline/src/dof/mod.rs @@ -55,7 +55,7 @@ use bevy_render::{ prepare_view_targets, ExtractedView, Msaa, ViewDepthTexture, ViewTarget, ViewUniform, ViewUniformOffset, ViewUniforms, }, - Extract, ExtractSchedule, Render, RenderApp, RenderSet, + Extract, ExtractSchedule, Render, RenderApp, RenderSystems, }; use bevy_utils::{default, once}; use smallvec::SmallVec; @@ -229,7 +229,7 @@ impl Plugin for DepthOfFieldPlugin { prepare_auxiliary_depth_of_field_textures, ) .after(prepare_view_targets) - .in_set(RenderSet::ManageViews), + .in_set(RenderSystems::ManageViews), ) .add_systems( Render, @@ -238,11 +238,11 @@ impl Plugin for DepthOfFieldPlugin { prepare_depth_of_field_pipelines, ) .chain() - .in_set(RenderSet::Prepare), + .in_set(RenderSystems::Prepare), ) .add_systems( Render, - prepare_depth_of_field_global_bind_group.in_set(RenderSet::PrepareBindGroups), + prepare_depth_of_field_global_bind_group.in_set(RenderSystems::PrepareBindGroups), ) .add_render_graph_node::>(Core3d, Node3d::DepthOfField) .add_render_graph_edges( diff --git a/crates/bevy_core_pipeline/src/experimental/mip_generation/mod.rs b/crates/bevy_core_pipeline/src/experimental/mip_generation/mod.rs index cd2099e49e..1223ed35ec 100644 --- a/crates/bevy_core_pipeline/src/experimental/mip_generation/mod.rs +++ b/crates/bevy_core_pipeline/src/experimental/mip_generation/mod.rs @@ -44,7 +44,7 @@ use bevy_render::{ renderer::{RenderContext, RenderDevice}, texture::TextureCache, view::{ExtractedView, NoIndirectDrawing, ViewDepthTexture}, - Render, RenderApp, RenderSet, + Render, RenderApp, RenderSystems, }; use bitflags::bitflags; use tracing::debug; @@ -103,7 +103,7 @@ impl Plugin for MipGenerationPlugin { ) .add_systems( Render, - create_downsample_depth_pipelines.in_set(RenderSet::Prepare), + create_downsample_depth_pipelines.in_set(RenderSystems::Prepare), ) .add_systems( Render, @@ -112,7 +112,7 @@ impl Plugin for MipGenerationPlugin { prepare_downsample_depth_view_bind_groups, ) .chain() - .in_set(RenderSet::PrepareResources) + .in_set(RenderSystems::PrepareResources) .run_if(resource_exists::) .after(prepare_core_3d_depth_textures), ); diff --git a/crates/bevy_core_pipeline/src/motion_blur/mod.rs b/crates/bevy_core_pipeline/src/motion_blur/mod.rs index 5898f1a8c5..331dd2408d 100644 --- a/crates/bevy_core_pipeline/src/motion_blur/mod.rs +++ b/crates/bevy_core_pipeline/src/motion_blur/mod.rs @@ -20,7 +20,7 @@ use bevy_render::{ extract_component::{ExtractComponent, ExtractComponentPlugin, UniformComponentPlugin}, render_graph::{RenderGraphApp, ViewNodeRunner}, render_resource::{Shader, ShaderType, SpecializedRenderPipelines}, - Render, RenderApp, RenderSet, + Render, RenderApp, RenderSystems, }; pub mod node; @@ -152,7 +152,7 @@ impl Plugin for MotionBlurPlugin { .init_resource::>() .add_systems( Render, - pipeline::prepare_motion_blur_pipelines.in_set(RenderSet::Prepare), + pipeline::prepare_motion_blur_pipelines.in_set(RenderSystems::Prepare), ); render_app diff --git a/crates/bevy_core_pipeline/src/msaa_writeback.rs b/crates/bevy_core_pipeline/src/msaa_writeback.rs index f9c543aeff..8dc51e4ed5 100644 --- a/crates/bevy_core_pipeline/src/msaa_writeback.rs +++ b/crates/bevy_core_pipeline/src/msaa_writeback.rs @@ -12,7 +12,7 @@ use bevy_render::{ render_resource::*, renderer::RenderContext, view::{Msaa, ViewTarget}, - Render, RenderApp, RenderSet, + Render, RenderApp, RenderSystems, }; /// This enables "msaa writeback" support for the `core_2d` and `core_3d` pipelines, which can be enabled on cameras @@ -26,7 +26,7 @@ impl Plugin for MsaaWritebackPlugin { }; render_app.add_systems( Render, - prepare_msaa_writeback_pipelines.in_set(RenderSet::Prepare), + prepare_msaa_writeback_pipelines.in_set(RenderSystems::Prepare), ); { render_app diff --git a/crates/bevy_core_pipeline/src/oit/mod.rs b/crates/bevy_core_pipeline/src/oit/mod.rs index 6a15fd126c..673bbc5a8b 100644 --- a/crates/bevy_core_pipeline/src/oit/mod.rs +++ b/crates/bevy_core_pipeline/src/oit/mod.rs @@ -16,7 +16,7 @@ use bevy_render::{ }, renderer::{RenderDevice, RenderQueue}, view::Msaa, - Render, RenderApp, RenderSet, + Render, RenderApp, RenderSystems, }; use bevy_window::PrimaryWindow; use resolve::{ @@ -126,7 +126,7 @@ impl Plugin for OrderIndependentTransparencyPlugin { render_app.add_systems( Render, - prepare_oit_buffers.in_set(RenderSet::PrepareResources), + prepare_oit_buffers.in_set(RenderSystems::PrepareResources), ); render_app diff --git a/crates/bevy_core_pipeline/src/oit/resolve/mod.rs b/crates/bevy_core_pipeline/src/oit/resolve/mod.rs index 7db98650fd..0e5102c954 100644 --- a/crates/bevy_core_pipeline/src/oit/resolve/mod.rs +++ b/crates/bevy_core_pipeline/src/oit/resolve/mod.rs @@ -20,7 +20,7 @@ use bevy_render::{ }, renderer::{RenderAdapter, RenderDevice}, view::{ExtractedView, ViewTarget, ViewUniform, ViewUniforms}, - Render, RenderApp, RenderSet, + Render, RenderApp, RenderSystems, }; use tracing::warn; @@ -65,8 +65,8 @@ impl Plugin for OitResolvePlugin { .add_systems( Render, ( - queue_oit_resolve_pipeline.in_set(RenderSet::Queue), - prepare_oit_resolve_bind_group.in_set(RenderSet::PrepareBindGroups), + queue_oit_resolve_pipeline.in_set(RenderSystems::Queue), + prepare_oit_resolve_bind_group.in_set(RenderSystems::PrepareBindGroups), ), ) .init_resource::(); diff --git a/crates/bevy_core_pipeline/src/post_process/mod.rs b/crates/bevy_core_pipeline/src/post_process/mod.rs index 2ac03c08c8..fddac95066 100644 --- a/crates/bevy_core_pipeline/src/post_process/mod.rs +++ b/crates/bevy_core_pipeline/src/post_process/mod.rs @@ -36,7 +36,7 @@ use bevy_render::{ renderer::{RenderContext, RenderDevice, RenderQueue}, texture::GpuImage, view::{ExtractedView, ViewTarget}, - Render, RenderApp, RenderSet, + Render, RenderApp, RenderSystems, }; use bevy_utils::prelude::default; @@ -234,7 +234,7 @@ impl Plugin for PostProcessingPlugin { prepare_post_processing_pipelines, prepare_post_processing_uniforms, ) - .in_set(RenderSet::Prepare), + .in_set(RenderSystems::Prepare), ) .add_render_graph_node::>( Core3d, diff --git a/crates/bevy_core_pipeline/src/skybox/mod.rs b/crates/bevy_core_pipeline/src/skybox/mod.rs index 7e2dba466c..ede50d6d8f 100644 --- a/crates/bevy_core_pipeline/src/skybox/mod.rs +++ b/crates/bevy_core_pipeline/src/skybox/mod.rs @@ -25,7 +25,7 @@ use bevy_render::{ renderer::RenderDevice, texture::GpuImage, view::{ExtractedView, Msaa, ViewTarget, ViewUniform, ViewUniforms}, - Render, RenderApp, RenderSet, + Render, RenderApp, RenderSystems, }; use bevy_transform::components::Transform; use prepass::{SkyboxPrepassPipeline, SKYBOX_PREPASS_SHADER_HANDLE}; @@ -63,11 +63,11 @@ impl Plugin for SkyboxPlugin { .add_systems( Render, ( - prepare_skybox_pipelines.in_set(RenderSet::Prepare), - prepass::prepare_skybox_prepass_pipelines.in_set(RenderSet::Prepare), - prepare_skybox_bind_groups.in_set(RenderSet::PrepareBindGroups), + prepare_skybox_pipelines.in_set(RenderSystems::Prepare), + prepass::prepare_skybox_prepass_pipelines.in_set(RenderSystems::Prepare), + prepare_skybox_bind_groups.in_set(RenderSystems::PrepareBindGroups), prepass::prepare_skybox_prepass_bind_groups - .in_set(RenderSet::PrepareBindGroups), + .in_set(RenderSystems::PrepareBindGroups), ), ); } diff --git a/crates/bevy_core_pipeline/src/tonemapping/mod.rs b/crates/bevy_core_pipeline/src/tonemapping/mod.rs index 9f3964ad17..f546ef54d3 100644 --- a/crates/bevy_core_pipeline/src/tonemapping/mod.rs +++ b/crates/bevy_core_pipeline/src/tonemapping/mod.rs @@ -16,7 +16,7 @@ use bevy_render::{ renderer::RenderDevice, texture::{FallbackImage, GpuImage}, view::{ExtractedView, ViewTarget, ViewUniform}, - Render, RenderApp, RenderSet, + Render, RenderApp, RenderSystems, }; use bitflags::bitflags; #[cfg(not(feature = "tonemapping_luts"))] @@ -118,7 +118,7 @@ impl Plugin for TonemappingPlugin { .init_resource::>() .add_systems( Render, - prepare_view_tonemapping_pipelines.in_set(RenderSet::Prepare), + prepare_view_tonemapping_pipelines.in_set(RenderSystems::Prepare), ); } diff --git a/crates/bevy_core_pipeline/src/upscaling/mod.rs b/crates/bevy_core_pipeline/src/upscaling/mod.rs index 20dd19f4ce..4ce91de393 100644 --- a/crates/bevy_core_pipeline/src/upscaling/mod.rs +++ b/crates/bevy_core_pipeline/src/upscaling/mod.rs @@ -6,7 +6,7 @@ use bevy_render::{ camera::{CameraOutputMode, ExtractedCamera}, render_resource::*, view::ViewTarget, - Render, RenderApp, RenderSet, + Render, RenderApp, RenderSystems, }; mod node; @@ -26,7 +26,7 @@ impl Plugin for UpscalingPlugin { // and aversion to extensive and intrusive system ordering. // See https://github.com/bevyengine/bevy/issues/14770 for more context. prepare_view_upscaling_pipelines - .in_set(RenderSet::Prepare) + .in_set(RenderSystems::Prepare) .ambiguous_with_all(), ); } diff --git a/crates/bevy_dev_tools/src/ci_testing/mod.rs b/crates/bevy_dev_tools/src/ci_testing/mod.rs index 09f16d71a7..faf99b1a71 100644 --- a/crates/bevy_dev_tools/src/ci_testing/mod.rs +++ b/crates/bevy_dev_tools/src/ci_testing/mod.rs @@ -57,7 +57,7 @@ impl Plugin for CiTestingPlugin { systems::send_events .before(trigger_screenshots) .before(bevy_window::close_when_requested) - .in_set(SendEvents) + .in_set(EventSenderSystems) .ambiguous_with_all(), ); @@ -66,10 +66,10 @@ impl Plugin for CiTestingPlugin { #[cfg(any(unix, windows))] app.configure_sets( Update, - SendEvents.before(bevy_app::TerminalCtrlCHandlerPlugin::exit_on_flag), + EventSenderSystems.before(bevy_app::TerminalCtrlCHandlerPlugin::exit_on_flag), ); } } #[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)] -struct SendEvents; +struct EventSenderSystems; diff --git a/crates/bevy_dev_tools/src/picking_debug.rs b/crates/bevy_dev_tools/src/picking_debug.rs index f72b70fc88..8e4ee7ae86 100644 --- a/crates/bevy_dev_tools/src/picking_debug.rs +++ b/crates/bevy_dev_tools/src/picking_debug.rs @@ -8,7 +8,7 @@ use bevy_picking::backend::HitData; use bevy_picking::hover::HoverMap; use bevy_picking::pointer::{Location, PointerId, PointerPress}; use bevy_picking::prelude::*; -use bevy_picking::{pointer, PickSet}; +use bevy_picking::{pointer, PickingSystems}; use bevy_reflect::prelude::*; use bevy_render::prelude::*; use bevy_text::prelude::*; @@ -85,7 +85,7 @@ impl Plugin for DebugPickingPlugin { app.init_resource::() .add_systems( PreUpdate, - pointer_debug_visibility.in_set(PickSet::PostHover), + pointer_debug_visibility.in_set(PickingSystems::PostHover), ) .add_systems( PreUpdate, @@ -108,7 +108,7 @@ impl Plugin for DebugPickingPlugin { log_pointer_event_debug::, ) .distributive_run_if(DebugPickingMode::is_enabled) - .in_set(PickSet::Last), + .in_set(PickingSystems::Last), ); app.add_systems( @@ -116,7 +116,7 @@ impl Plugin for DebugPickingPlugin { (add_pointer_debug, update_debug_data, debug_draw) .chain() .distributive_run_if(DebugPickingMode::is_enabled) - .in_set(PickSet::Last), + .in_set(PickingSystems::Last), ); } } diff --git a/crates/bevy_diagnostic/Cargo.toml b/crates/bevy_diagnostic/Cargo.toml index 92e64b7e6c..eff1710438 100644 --- a/crates/bevy_diagnostic/Cargo.toml +++ b/crates/bevy_diagnostic/Cargo.toml @@ -77,14 +77,14 @@ log = { version = "0.4", default-features = false } # macOS [target.'cfg(all(target_os="macos"))'.dependencies] # Some features of sysinfo are not supported by apple. This will disable those features on apple devices -sysinfo = { version = "0.34.0", optional = true, default-features = false, features = [ +sysinfo = { version = "0.35.0", optional = true, default-features = false, features = [ "apple-app-store", "system", ] } # Only include when on linux/windows/android/freebsd [target.'cfg(any(target_os = "linux", target_os = "windows", target_os = "android", target_os = "freebsd"))'.dependencies] -sysinfo = { version = "0.34.0", optional = true, default-features = false, features = [ +sysinfo = { version = "0.35.0", optional = true, default-features = false, features = [ "system", ] } diff --git a/crates/bevy_ecs/Cargo.toml b/crates/bevy_ecs/Cargo.toml index 97cdcee082..28987f1413 100644 --- a/crates/bevy_ecs/Cargo.toml +++ b/crates/bevy_ecs/Cargo.toml @@ -33,13 +33,6 @@ bevy_reflect = ["dep:bevy_reflect"] ## Extends reflection support to functions. reflect_functions = ["bevy_reflect", "bevy_reflect/functions"] -## Use the configurable global error handler as the default error handler. -## -## This is typically used to turn panics from the ECS into loggable errors. -## This may be useful for production builds, -## but can result in a measurable performance impact, especially for commands. -configurable_error_handler = [] - ## Enables automatic backtrace capturing in BevyError backtrace = ["std"] diff --git a/crates/bevy_ecs/README.md b/crates/bevy_ecs/README.md index 8614dbc5e3..c2fdc53d05 100644 --- a/crates/bevy_ecs/README.md +++ b/crates/bevy_ecs/README.md @@ -290,7 +290,7 @@ struct MyEvent { } fn writer(mut writer: EventWriter) { - writer.send(MyEvent { + writer.write(MyEvent { message: "hello!".to_string(), }); } diff --git a/crates/bevy_ecs/examples/change_detection.rs b/crates/bevy_ecs/examples/change_detection.rs index 1b101b4033..820860070c 100644 --- a/crates/bevy_ecs/examples/change_detection.rs +++ b/crates/bevy_ecs/examples/change_detection.rs @@ -29,11 +29,11 @@ fn main() { // Add systems to the Schedule to execute our app logic // We can label our systems to force a specific run-order between some of them schedule.add_systems(( - spawn_entities.in_set(SimulationSet::Spawn), - print_counter_when_changed.after(SimulationSet::Spawn), - age_all_entities.in_set(SimulationSet::Age), - remove_old_entities.after(SimulationSet::Age), - print_changed_entities.after(SimulationSet::Age), + spawn_entities.in_set(SimulationSystems::Spawn), + print_counter_when_changed.after(SimulationSystems::Spawn), + age_all_entities.in_set(SimulationSystems::Age), + remove_old_entities.after(SimulationSystems::Age), + print_changed_entities.after(SimulationSystems::Age), )); // Simulate 10 frames in our world @@ -57,7 +57,7 @@ struct Age { // System sets can be used to group systems and configured to control relative ordering #[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)] -enum SimulationSet { +enum SimulationSystems { Spawn, Age, } diff --git a/crates/bevy_ecs/examples/events.rs b/crates/bevy_ecs/examples/events.rs index 4c0059ed2d..fb01184048 100644 --- a/crates/bevy_ecs/examples/events.rs +++ b/crates/bevy_ecs/examples/events.rs @@ -19,13 +19,13 @@ fn main() { // This update should happen before we use the events. // Here, we use system sets to control the ordering. #[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)] - pub struct FlushEvents; + pub struct EventFlusherSystems; - schedule.add_systems(bevy_ecs::event::event_update_system.in_set(FlushEvents)); + schedule.add_systems(bevy_ecs::event::event_update_system.in_set(EventFlusherSystems)); // Add systems sending and receiving events after the events are flushed. schedule.add_systems(( - sending_system.after(FlushEvents), + sending_system.after(EventFlusherSystems), receiving_system.after(sending_system), )); diff --git a/crates/bevy_ecs/macros/src/component.rs b/crates/bevy_ecs/macros/src/component.rs index d3199c0909..e87a4b650b 100644 --- a/crates/bevy_ecs/macros/src/component.rs +++ b/crates/bevy_ecs/macros/src/component.rs @@ -251,6 +251,8 @@ pub fn derive_component(input: TokenStream) -> TokenStream { let clone_behavior = if relationship_target.is_some() { quote!(#bevy_ecs_path::component::ComponentCloneBehavior::Custom(#bevy_ecs_path::relationship::clone_relationship_target::)) + } else if let Some(behavior) = attrs.clone_behavior { + quote!(#bevy_ecs_path::component::ComponentCloneBehavior::#behavior) } else { quote!( use #bevy_ecs_path::component::{DefaultCloneBehaviorBase, DefaultCloneBehaviorViaClone}; @@ -396,6 +398,7 @@ pub const ON_REMOVE: &str = "on_remove"; pub const ON_DESPAWN: &str = "on_despawn"; pub const IMMUTABLE: &str = "immutable"; +pub const CLONE_BEHAVIOR: &str = "clone_behavior"; /// All allowed attribute value expression kinds for component hooks #[derive(Debug)] @@ -458,6 +461,7 @@ struct Attrs { relationship: Option, relationship_target: Option, immutable: bool, + clone_behavior: Option, } #[derive(Clone, Copy)] @@ -496,6 +500,7 @@ fn parse_component_attr(ast: &DeriveInput) -> Result { relationship: None, relationship_target: None, immutable: false, + clone_behavior: None, }; let mut require_paths = HashSet::new(); @@ -531,6 +536,9 @@ fn parse_component_attr(ast: &DeriveInput) -> Result { } else if nested.path.is_ident(IMMUTABLE) { attrs.immutable = true; Ok(()) + } else if nested.path.is_ident(CLONE_BEHAVIOR) { + attrs.clone_behavior = Some(nested.value()?.parse()?); + Ok(()) } else { Err(nested.error("Unsupported attribute")) } @@ -560,6 +568,13 @@ fn parse_component_attr(ast: &DeriveInput) -> Result { } } + if attrs.relationship_target.is_some() && attrs.clone_behavior.is_some() { + return Err(syn::Error::new( + attrs.clone_behavior.span(), + "A Relationship Target already has its own clone behavior, please remove `clone_behavior = ...`", + )); + } + Ok(attrs) } diff --git a/crates/bevy_ecs/macros/src/lib.rs b/crates/bevy_ecs/macros/src/lib.rs index a657765ac2..a1898b1328 100644 --- a/crates/bevy_ecs/macros/src/lib.rs +++ b/crates/bevy_ecs/macros/src/lib.rs @@ -6,7 +6,6 @@ extern crate proc_macro; mod component; mod query_data; mod query_filter; -mod states; mod world_query; use crate::{ @@ -455,7 +454,7 @@ pub fn derive_system_param(input: TokenStream) -> TokenStream { <#field_types as #path::system::SystemParam>::validate_param(#field_locals, _system_meta, _world) .map_err(|err| #path::system::SystemParamValidationError::new::(err.skipped, #field_messages, #field_names))?; )* - Ok(()) + Result::Ok(()) } #[inline] @@ -548,16 +547,6 @@ pub fn derive_component(input: TokenStream) -> TokenStream { component::derive_component(input) } -#[proc_macro_derive(States)] -pub fn derive_states(input: TokenStream) -> TokenStream { - states::derive_states(input) -} - -#[proc_macro_derive(SubStates, attributes(source))] -pub fn derive_substates(input: TokenStream) -> TokenStream { - states::derive_substates(input) -} - #[proc_macro_derive(FromWorld, attributes(from_world))] pub fn derive_from_world(input: TokenStream) -> TokenStream { let bevy_ecs_path = bevy_ecs_path(); diff --git a/crates/bevy_ecs/macros/src/query_data.rs b/crates/bevy_ecs/macros/src/query_data.rs index d919d0b05e..4e4529e631 100644 --- a/crates/bevy_ecs/macros/src/query_data.rs +++ b/crates/bevy_ecs/macros/src/query_data.rs @@ -268,6 +268,14 @@ pub fn derive_query_data_impl(input: TokenStream) -> TokenStream { } } + fn provide_extra_access( + state: &mut Self::State, + access: &mut #path::query::Access<#path::component::ComponentId>, + available_access: &#path::query::Access<#path::component::ComponentId>, + ) { + #(<#field_types>::provide_extra_access(&mut state.#named_field_idents, access, available_access);)* + } + /// SAFETY: we call `fetch` for each member that implements `Fetch`. #[inline(always)] unsafe fn fetch<'__w>( @@ -305,6 +313,14 @@ pub fn derive_query_data_impl(input: TokenStream) -> TokenStream { } } + fn provide_extra_access( + state: &mut Self::State, + access: &mut #path::query::Access<#path::component::ComponentId>, + available_access: &#path::query::Access<#path::component::ComponentId>, + ) { + #(<#field_types>::provide_extra_access(&mut state.#named_field_idents, access, available_access);)* + } + /// SAFETY: we call `fetch` for each member that implements `Fetch`. #[inline(always)] unsafe fn fetch<'__w>( diff --git a/crates/bevy_ecs/macros/src/states.rs b/crates/bevy_ecs/macros/src/states.rs deleted file mode 100644 index ff69812aea..0000000000 --- a/crates/bevy_ecs/macros/src/states.rs +++ /dev/null @@ -1,144 +0,0 @@ -use proc_macro::TokenStream; -use quote::{format_ident, quote}; -use syn::{parse_macro_input, spanned::Spanned, DeriveInput, Pat, Path, Result}; - -use crate::bevy_ecs_path; - -pub fn derive_states(input: TokenStream) -> TokenStream { - let ast = parse_macro_input!(input as DeriveInput); - - let generics = ast.generics; - let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - - let mut base_trait_path = bevy_ecs_path(); - base_trait_path - .segments - .push(format_ident!("schedule").into()); - - let mut trait_path = base_trait_path.clone(); - trait_path.segments.push(format_ident!("States").into()); - - let mut state_mutation_trait_path = base_trait_path.clone(); - state_mutation_trait_path - .segments - .push(format_ident!("FreelyMutableState").into()); - - let struct_name = &ast.ident; - - quote! { - impl #impl_generics #trait_path for #struct_name #ty_generics #where_clause {} - - impl #impl_generics #state_mutation_trait_path for #struct_name #ty_generics #where_clause { - } - } - .into() -} - -struct Source { - source_type: Path, - source_value: Pat, -} - -fn parse_sources_attr(ast: &DeriveInput) -> Result { - let mut result = ast - .attrs - .iter() - .filter(|a| a.path().is_ident("source")) - .map(|meta| { - let mut source = None; - let value = meta.parse_nested_meta(|nested| { - let source_type = nested.path.clone(); - let source_value = Pat::parse_multi(nested.value()?)?; - source = Some(Source { - source_type, - source_value, - }); - Ok(()) - }); - match source { - Some(value) => Ok(value), - None => match value { - Ok(_) => Err(syn::Error::new( - ast.span(), - "Couldn't parse SubStates source", - )), - Err(e) => Err(e), - }, - } - }) - .collect::>>()?; - - if result.len() > 1 { - return Err(syn::Error::new( - ast.span(), - "Only one source is allowed for SubStates", - )); - } - - let Some(result) = result.pop() else { - return Err(syn::Error::new(ast.span(), "SubStates require a source")); - }; - - Ok(result) -} - -pub fn derive_substates(input: TokenStream) -> TokenStream { - let ast = parse_macro_input!(input as DeriveInput); - let sources = parse_sources_attr(&ast).expect("Failed to parse substate sources"); - - let generics = ast.generics; - let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - - let mut base_trait_path = bevy_ecs_path(); - base_trait_path - .segments - .push(format_ident!("schedule").into()); - - let mut trait_path = base_trait_path.clone(); - trait_path.segments.push(format_ident!("SubStates").into()); - - let mut state_set_trait_path = base_trait_path.clone(); - state_set_trait_path - .segments - .push(format_ident!("StateSet").into()); - - let mut state_trait_path = base_trait_path.clone(); - state_trait_path - .segments - .push(format_ident!("States").into()); - - let mut state_mutation_trait_path = base_trait_path.clone(); - state_mutation_trait_path - .segments - .push(format_ident!("FreelyMutableState").into()); - - let struct_name = &ast.ident; - - let source_state_type = sources.source_type; - let source_state_value = sources.source_value; - - let result = quote! { - impl #impl_generics #trait_path for #struct_name #ty_generics #where_clause { - type SourceStates = #source_state_type; - - fn should_exist(sources: #source_state_type) -> Option { - if matches!(sources, #source_state_value) { - Some(Self::default()) - } else { - None - } - } - } - - impl #impl_generics #state_trait_path for #struct_name #ty_generics #where_clause { - const DEPENDENCY_DEPTH : usize = ::SourceStates::SET_DEPENDENCY_DEPTH + 1; - } - - impl #impl_generics #state_mutation_trait_path for #struct_name #ty_generics #where_clause { - } - }; - - // panic!("Got Result\n{}", result.to_string()); - - result.into() -} diff --git a/crates/bevy_ecs/src/bundle.rs b/crates/bevy_ecs/src/bundle.rs index 5666d90c53..a7fb4f6fd4 100644 --- a/crates/bevy_ecs/src/bundle.rs +++ b/crates/bevy_ecs/src/bundle.rs @@ -20,7 +20,10 @@ use crate::{ query::DebugCheckedUnwrap, relationship::RelationshipHookMode, storage::{SparseSetIndex, SparseSets, Storages, Table, TableRow}, - world::{unsafe_world_cell::UnsafeWorldCell, EntityWorldMut, ON_ADD, ON_INSERT, ON_REPLACE}, + world::{ + unsafe_world_cell::UnsafeWorldCell, EntityWorldMut, ON_ADD, ON_INSERT, ON_REMOVE, + ON_REPLACE, + }, }; use alloc::{boxed::Box, vec, vec::Vec}; use bevy_platform::collections::{HashMap, HashSet}; @@ -630,13 +633,14 @@ impl BundleInfo { let mut bundle_component = 0; let after_effect = bundle.get_components(&mut |storage_type, component_ptr| { let component_id = *self.component_ids.get_unchecked(bundle_component); + // SAFETY: bundle_component is a valid index for this bundle + let status = unsafe { bundle_component_status.get_status(bundle_component) }; match storage_type { StorageType::Table => { - // SAFETY: bundle_component is a valid index for this bundle - let status = unsafe { bundle_component_status.get_status(bundle_component) }; - // SAFETY: If component_id is in self.component_ids, BundleInfo::new ensures that - // the target table contains the component. - let column = table.get_column_mut(component_id).debug_checked_unwrap(); + let column = + // SAFETY: If component_id is in self.component_ids, BundleInfo::new ensures that + // the target table contains the component. + unsafe { table.get_column_mut(component_id).debug_checked_unwrap() }; match (status, insert_mode) { (ComponentStatus::Added, _) => { column.initialize(table_row, component_ptr, change_tick, caller); @@ -656,7 +660,16 @@ impl BundleInfo { // SAFETY: If component_id is in self.component_ids, BundleInfo::new ensures that // a sparse set exists for the component. unsafe { sparse_sets.get_mut(component_id).debug_checked_unwrap() }; - sparse_set.insert(entity, component_ptr, change_tick, caller); + match (status, insert_mode) { + (ComponentStatus::Added, _) | (_, InsertMode::Replace) => { + sparse_set.insert(entity, component_ptr, change_tick, caller); + } + (ComponentStatus::Existing, InsertMode::Keep) => { + if let Some(drop_fn) = sparse_set.get_drop() { + drop_fn(component_ptr); + } + } + } } } bundle_component += 1; @@ -1361,6 +1374,274 @@ impl<'w> BundleInserter<'w> { } } +// SAFETY: We have exclusive world access so our pointers can't be invalidated externally +pub(crate) struct BundleRemover<'w> { + world: UnsafeWorldCell<'w>, + bundle_info: ConstNonNull, + old_and_new_table: Option<(NonNull
, NonNull
)>, + old_archetype: NonNull, + new_archetype: NonNull, +} + +impl<'w> BundleRemover<'w> { + /// Creates a new [`BundleRemover`], if such a remover would do anything. + /// + /// If `require_all` is true, the [`BundleRemover`] is only created if the entire bundle is present on the archetype. + /// + /// # Safety + /// Caller must ensure that `archetype_id` is valid + #[inline] + pub(crate) unsafe fn new( + world: &'w mut World, + archetype_id: ArchetypeId, + require_all: bool, + ) -> Option { + // SAFETY: These come from the same world. `world.components_registrator` can't be used since we borrow other fields too. + let mut registrator = + unsafe { ComponentsRegistrator::new(&mut world.components, &mut world.component_ids) }; + let bundle_id = world + .bundles + .register_info::(&mut registrator, &mut world.storages); + // SAFETY: we initialized this bundle_id in `init_info`, and caller ensures archetype is valid. + unsafe { Self::new_with_id(world, archetype_id, bundle_id, require_all) } + } + + /// Creates a new [`BundleRemover`], if such a remover would do anything. + /// + /// If `require_all` is true, the [`BundleRemover`] is only created if the entire bundle is present on the archetype. + /// + /// # Safety + /// Caller must ensure that `bundle_id` exists in `world.bundles` and `archetype_id` is valid. + #[inline] + pub(crate) unsafe fn new_with_id( + world: &'w mut World, + archetype_id: ArchetypeId, + bundle_id: BundleId, + require_all: bool, + ) -> Option { + let bundle_info = world.bundles.get_unchecked(bundle_id); + // SAFETY: Caller ensures archetype and bundle ids are correct. + let new_archetype_id = unsafe { + bundle_info.remove_bundle_from_archetype( + &mut world.archetypes, + &mut world.storages, + &world.components, + &world.observers, + archetype_id, + !require_all, + )? + }; + if new_archetype_id == archetype_id { + return None; + } + let (old_archetype, new_archetype) = + world.archetypes.get_2_mut(archetype_id, new_archetype_id); + + let tables = if old_archetype.table_id() == new_archetype.table_id() { + None + } else { + let (old, new) = world + .storages + .tables + .get_2_mut(old_archetype.table_id(), new_archetype.table_id()); + Some((old.into(), new.into())) + }; + + Some(Self { + bundle_info: bundle_info.into(), + new_archetype: new_archetype.into(), + old_archetype: old_archetype.into(), + old_and_new_table: tables, + world: world.as_unsafe_world_cell(), + }) + } + + /// This can be passed to [`remove`](Self::remove) as the `pre_remove` function if you don't want to do anything before removing. + pub fn empty_pre_remove( + _: &mut SparseSets, + _: Option<&mut Table>, + _: &Components, + _: &[ComponentId], + ) -> (bool, ()) { + (true, ()) + } + + /// Performs the removal. + /// + /// `pre_remove` should return a bool for if the components still need to be dropped. + /// + /// # Safety + /// The `location` must have the same archetype as the remover. + #[inline] + pub(crate) unsafe fn remove( + &mut self, + entity: Entity, + location: EntityLocation, + caller: MaybeLocation, + pre_remove: impl FnOnce( + &mut SparseSets, + Option<&mut Table>, + &Components, + &[ComponentId], + ) -> (bool, T), + ) -> (EntityLocation, T) { + // Hooks + // SAFETY: all bundle components exist in World + unsafe { + // SAFETY: We only keep access to archetype/bundle data. + let mut deferred_world = self.world.into_deferred(); + let bundle_components_in_archetype = || { + self.bundle_info + .as_ref() + .iter_explicit_components() + .filter(|component_id| self.old_archetype.as_ref().contains(*component_id)) + }; + if self.old_archetype.as_ref().has_replace_observer() { + deferred_world.trigger_observers( + ON_REPLACE, + entity, + bundle_components_in_archetype(), + caller, + ); + } + deferred_world.trigger_on_replace( + self.old_archetype.as_ref(), + entity, + bundle_components_in_archetype(), + caller, + RelationshipHookMode::Run, + ); + if self.old_archetype.as_ref().has_remove_observer() { + deferred_world.trigger_observers( + ON_REMOVE, + entity, + bundle_components_in_archetype(), + caller, + ); + } + deferred_world.trigger_on_remove( + self.old_archetype.as_ref(), + entity, + bundle_components_in_archetype(), + caller, + ); + } + + // SAFETY: We still have the cell, so this is unique, it doesn't conflict with other references, and we drop it shortly. + let world = unsafe { self.world.world_mut() }; + + let (needs_drop, pre_remove_result) = pre_remove( + &mut world.storages.sparse_sets, + self.old_and_new_table + .as_ref() + // SAFETY: There is no conflicting access for this scope. + .map(|(old, _)| unsafe { &mut *old.as_ptr() }), + &world.components, + self.bundle_info.as_ref().explicit_components(), + ); + + // Handle sparse set removes + for component_id in self.bundle_info.as_ref().iter_explicit_components() { + if self.old_archetype.as_ref().contains(component_id) { + world.removed_components.send(component_id, entity); + + // Make sure to drop components stored in sparse sets. + // Dense components are dropped later in `move_to_and_drop_missing_unchecked`. + if let Some(StorageType::SparseSet) = + self.old_archetype.as_ref().get_storage_type(component_id) + { + world + .storages + .sparse_sets + .get_mut(component_id) + // Set exists because the component existed on the entity + .unwrap() + // If it was already forgotten, it would not be in the set. + .remove(entity); + } + } + } + + // Handle archetype change + let remove_result = self + .old_archetype + .as_mut() + .swap_remove(location.archetype_row); + // if an entity was moved into this entity's archetype row, update its archetype row + if let Some(swapped_entity) = remove_result.swapped_entity { + let swapped_location = world.entities.get(swapped_entity).unwrap(); + + world.entities.set( + swapped_entity.index(), + EntityLocation { + archetype_id: swapped_location.archetype_id, + archetype_row: location.archetype_row, + table_id: swapped_location.table_id, + table_row: swapped_location.table_row, + }, + ); + } + + // Handle table change + let new_location = if let Some((mut old_table, mut new_table)) = self.old_and_new_table { + let move_result = if needs_drop { + // SAFETY: old_table_row exists + unsafe { + old_table + .as_mut() + .move_to_and_drop_missing_unchecked(location.table_row, new_table.as_mut()) + } + } else { + // SAFETY: old_table_row exists + unsafe { + old_table.as_mut().move_to_and_forget_missing_unchecked( + location.table_row, + new_table.as_mut(), + ) + } + }; + + // SAFETY: move_result.new_row is a valid position in new_archetype's table + let new_location = unsafe { + self.new_archetype + .as_mut() + .allocate(entity, move_result.new_row) + }; + + // if an entity was moved into this entity's table row, update its table row + if let Some(swapped_entity) = move_result.swapped_entity { + let swapped_location = world.entities.get(swapped_entity).unwrap(); + + world.entities.set( + swapped_entity.index(), + EntityLocation { + archetype_id: swapped_location.archetype_id, + archetype_row: swapped_location.archetype_row, + table_id: swapped_location.table_id, + table_row: location.table_row, + }, + ); + world.archetypes[swapped_location.archetype_id] + .set_entity_table_row(swapped_location.archetype_row, location.table_row); + } + + new_location + } else { + // The tables are the same + self.new_archetype + .as_mut() + .allocate(entity, location.table_row) + }; + + // SAFETY: The entity is valid and has been moved to the new location already. + unsafe { + world.entities.set(entity.index(), new_location); + } + + (new_location, pre_remove_result) + } +} + // SAFETY: We have exclusive world access so our pointers can't be invalidated externally pub(crate) struct BundleSpawner<'w> { world: UnsafeWorldCell<'w>, @@ -1455,7 +1736,7 @@ impl<'w> BundleSpawner<'w> { InsertMode::Replace, caller, ); - entities.set(entity.index(), location); + entities.set_spawn_despawn(entity.index(), location, caller, self.change_tick); (location, after_effect) }; diff --git a/crates/bevy_ecs/src/change_detection.rs b/crates/bevy_ecs/src/change_detection.rs index 767134fdc6..fdb5d496c5 100644 --- a/crates/bevy_ecs/src/change_detection.rs +++ b/crates/bevy_ecs/src/change_detection.rs @@ -1847,8 +1847,7 @@ mod tests { let mut new = value.map_unchanged(|ptr| { // SAFETY: The underlying type of `ptr` matches `reflect_from_ptr`. - let value = unsafe { reflect_from_ptr.as_reflect_mut(ptr) }; - value + unsafe { reflect_from_ptr.as_reflect_mut(ptr) } }); assert!(!new.is_changed()); diff --git a/crates/bevy_ecs/src/component.rs b/crates/bevy_ecs/src/component.rs index bfa55804b6..e7c14d1c3e 100644 --- a/crates/bevy_ecs/src/component.rs +++ b/crates/bevy_ecs/src/component.rs @@ -418,6 +418,21 @@ use thiserror::Error; /// println!("{message}"); /// } /// } +/// +/// ``` +/// # Setting the clone behavior +/// +/// You can specify how the [`Component`] is cloned when deriving it. +/// +/// Your options are the functions and variants of [`ComponentCloneBehavior`] +/// See [Handlers section of `EntityClonerBuilder`](crate::entity::EntityClonerBuilder#handlers) to understand how this affects handler priority. +/// ``` +/// # use bevy_ecs::prelude::*; +/// +/// #[derive(Component)] +/// #[component(clone_behavior = Ignore)] +/// struct MyComponent; +/// /// ``` /// /// # Implementing the trait for foreign types @@ -494,15 +509,6 @@ pub trait Component: Send + Sync + 'static { /// * For a component to be immutable, this type must be [`Immutable`]. type Mutability: ComponentMutability; - /// Called when registering this component, allowing mutable access to its [`ComponentHooks`]. - #[deprecated( - since = "0.16.0", - note = "Use the individual hook methods instead (e.g., `Component::on_add`, etc.)" - )] - fn register_component_hooks(hooks: &mut ComponentHooks) { - hooks.update_from_component::(); - } - /// Gets the `on_add` [`ComponentHook`] for this [`Component`] if one is defined. fn on_add() -> Option { None @@ -559,6 +565,17 @@ pub trait Component: Send + Sync + 'static { /// ``` /// /// Fields with `#[entities]` must implement [`MapEntities`](crate::entity::MapEntities). + /// + /// Bevy provides various implementations of [`MapEntities`](crate::entity::MapEntities), so that arbitrary combinations like these are supported with `#[entities]`: + /// + /// ```rust + /// # use bevy_ecs::{component::Component, entity::Entity}; + /// #[derive(Component)] + /// struct Inventory { + /// #[entities] + /// items: Vec> + /// } + /// ``` #[inline] fn map_entities(_this: &mut Self, _mapper: &mut E) {} } @@ -668,7 +685,7 @@ pub struct HookContext { /// This information is stored in the [`ComponentInfo`] of the associated component. /// /// There is two ways of configuring hooks for a component: -/// 1. Defining the [`Component::register_component_hooks`] method (see [`Component`]) +/// 1. Defining the relevant hooks on the [`Component`] implementation /// 2. Using the [`World::register_component_hooks`] method /// /// # Example 2 @@ -1784,12 +1801,7 @@ impl<'w> ComponentsRegistrator<'w> { .debug_checked_unwrap() }; - #[expect( - deprecated, - reason = "need to use this method until it is removed to ensure user defined components register hooks correctly" - )] - // TODO: Replace with `info.hooks.update_from_component::();` once `Component::register_component_hooks` is removed - T::register_component_hooks(&mut info.hooks); + info.hooks.update_from_component::(); info.required_components = required_components; } @@ -2049,7 +2061,7 @@ impl Components { } /// Gets the [`ComponentDescriptor`] of the component with this [`ComponentId`] if it is present. - /// This will return `None` only if the id is neither regisered nor queued to be registered. + /// This will return `None` only if the id is neither registered nor queued to be registered. /// /// Currently, the [`Cow`] will be [`Cow::Owned`] if and only if the component is queued. It will be [`Cow::Borrowed`] otherwise. /// @@ -2073,7 +2085,7 @@ impl Components { } /// Gets the name of the component with this [`ComponentId`] if it is present. - /// This will return `None` only if the id is neither regisered nor queued to be registered. + /// This will return `None` only if the id is neither registered nor queued to be registered. /// /// This will return an incorrect result if `id` did not come from the same world as `self`. It may return `None` or a garbage value. #[inline] diff --git a/crates/bevy_ecs/src/entity/clone_entities.rs b/crates/bevy_ecs/src/entity/clone_entities.rs index bd8eb2b4bd..5328eb1d3a 100644 --- a/crates/bevy_ecs/src/entity/clone_entities.rs +++ b/crates/bevy_ecs/src/entity/clone_entities.rs @@ -324,16 +324,10 @@ impl<'a, 'b> ComponentCloneCtx<'a, 'b> { /// ``` /// # use bevy_ecs::prelude::*; /// # use bevy_ecs::component::{StorageType, ComponentCloneBehavior, Mutable}; -/// #[derive(Clone)] +/// #[derive(Clone, Component)] +/// #[component(clone_behavior = clone::())] /// struct SomeComponent; /// -/// impl Component for SomeComponent { -/// const STORAGE_TYPE: StorageType = StorageType::Table; -/// type Mutability = Mutable; -/// fn clone_behavior() -> ComponentCloneBehavior { -/// ComponentCloneBehavior::clone::() -/// } -/// } /// ``` /// /// # Clone Behaviors diff --git a/crates/bevy_ecs/src/entity/map_entities.rs b/crates/bevy_ecs/src/entity/map_entities.rs index c79853f979..3dac2fa749 100644 --- a/crates/bevy_ecs/src/entity/map_entities.rs +++ b/crates/bevy_ecs/src/entity/map_entities.rs @@ -1,16 +1,21 @@ pub use bevy_ecs_macros::MapEntities; +use indexmap::IndexSet; use crate::{ entity::{hash_map::EntityHashMap, Entity}, - identifier::masks::{IdentifierMask, HIGH_MASK}, world::World, }; -use alloc::{collections::VecDeque, vec::Vec}; +use alloc::{ + collections::{BTreeSet, VecDeque}, + vec::Vec, +}; use bevy_platform::collections::HashSet; -use core::hash::BuildHasher; +use core::{hash::BuildHasher, mem}; use smallvec::SmallVec; +use super::EntityIndexSet; + /// Operation to map all contained [`Entity`] fields in a type to new values. /// /// As entity IDs are valid only for the [`World`] they're sourced from, using [`Entity`] @@ -59,42 +64,87 @@ impl MapEntities for Entity { } } -impl MapEntities for Option { +impl MapEntities for Option { fn map_entities(&mut self, entity_mapper: &mut E) { - if let Some(entity) = self { - *entity = entity_mapper.get_mapped(*entity); + if let Some(entities) = self { + entities.map_entities(entity_mapper); } } } -impl MapEntities for HashSet { +impl MapEntities + for HashSet +{ fn map_entities(&mut self, entity_mapper: &mut E) { - *self = self.drain().map(|e| entity_mapper.get_mapped(e)).collect(); + *self = self + .drain() + .map(|mut entities| { + entities.map_entities(entity_mapper); + entities + }) + .collect(); } } -impl MapEntities for Vec { + +impl MapEntities + for IndexSet +{ fn map_entities(&mut self, entity_mapper: &mut E) { - for entity in self.iter_mut() { - *entity = entity_mapper.get_mapped(*entity); + *self = self + .drain(..) + .map(|mut entities| { + entities.map_entities(entity_mapper); + entities + }) + .collect(); + } +} + +impl MapEntities for EntityIndexSet { + fn map_entities(&mut self, entity_mapper: &mut E) { + *self = self + .drain(..) + .map(|e| entity_mapper.get_mapped(e)) + .collect(); + } +} + +impl MapEntities for BTreeSet { + fn map_entities(&mut self, entity_mapper: &mut E) { + *self = mem::take(self) + .into_iter() + .map(|mut entities| { + entities.map_entities(entity_mapper); + entities + }) + .collect(); + } +} + +impl MapEntities for Vec { + fn map_entities(&mut self, entity_mapper: &mut E) { + for entities in self.iter_mut() { + entities.map_entities(entity_mapper); } } } -impl MapEntities for VecDeque { +impl MapEntities for VecDeque { fn map_entities(&mut self, entity_mapper: &mut E) { - for entity in self.iter_mut() { - *entity = entity_mapper.get_mapped(*entity); + for entities in self.iter_mut() { + entities.map_entities(entity_mapper); } } } -impl> MapEntities for SmallVec { +impl> MapEntities for SmallVec { fn map_entities(&mut self, entity_mapper: &mut E) { - for entity in self.iter_mut() { - *entity = entity_mapper.get_mapped(*entity); + for entities in self.iter_mut() { + entities.map_entities(entity_mapper); } } } + /// An implementor of this trait knows how to map an [`Entity`] into another [`Entity`]. /// /// Usually this is done by using an [`EntityHashMap`] to map source entities @@ -120,7 +170,7 @@ impl> MapEntities for SmallVec { /// fn get_mapped(&mut self, entity: Entity) -> Entity { /// self.map.get(&entity).copied().unwrap_or(entity) /// } -/// +/// /// fn set_mapped(&mut self, source: Entity, target: Entity) { /// self.map.insert(source, target); /// } @@ -177,12 +227,10 @@ impl EntityMapper for SceneEntityMapper<'_> { // this new entity reference is specifically designed to never represent any living entity let new = Entity::from_raw_and_generation( - self.dead_start.index(), - IdentifierMask::inc_masked_high_by(self.dead_start.generation, self.generations), + self.dead_start.row(), + self.dead_start.generation.after_versions(self.generations), ); - - // Prevent generations counter from being a greater value than HIGH_MASK. - self.generations = (self.generations + 1) & HIGH_MASK; + self.generations = self.generations.wrapping_add(1); self.map.insert(source, new); @@ -280,6 +328,7 @@ impl<'m> SceneEntityMapper<'m> { #[cfg(test)] mod tests { + use crate::{ entity::{Entity, EntityHashMap, EntityMapper, SceneEntityMapper}, world::World, @@ -287,14 +336,11 @@ mod tests { #[test] fn entity_mapper() { - const FIRST_IDX: u32 = 1; - const SECOND_IDX: u32 = 2; - let mut map = EntityHashMap::default(); let mut world = World::new(); let mut mapper = SceneEntityMapper::new(&mut map, &mut world); - let mapped_ent = Entity::from_raw(FIRST_IDX); + let mapped_ent = Entity::from_raw_u32(1).unwrap(); let dead_ref = mapper.get_mapped(mapped_ent); assert_eq!( @@ -303,7 +349,7 @@ mod tests { "should persist the allocated mapping from the previous line" ); assert_eq!( - mapper.get_mapped(Entity::from_raw(SECOND_IDX)).index(), + mapper.get_mapped(Entity::from_raw_u32(2).unwrap()).index(), dead_ref.index(), "should re-use the same index for further dead refs" ); @@ -321,7 +367,7 @@ mod tests { let mut world = World::new(); let dead_ref = SceneEntityMapper::world_scope(&mut map, &mut world, |_, mapper| { - mapper.get_mapped(Entity::from_raw(0)) + mapper.get_mapped(Entity::from_raw_u32(0).unwrap()) }); // Next allocated entity should be a further generation on the same index diff --git a/crates/bevy_ecs/src/entity/mod.rs b/crates/bevy_ecs/src/entity/mod.rs index 7bba07aac6..e4d4b26d97 100644 --- a/crates/bevy_ecs/src/entity/mod.rs +++ b/crates/bevy_ecs/src/entity/mod.rs @@ -45,6 +45,7 @@ use bevy_reflect::Reflect; use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; pub use clone_entities::*; +use derive_more::derive::Display; pub use entity_set::*; pub use map_entities::*; @@ -67,6 +68,7 @@ pub mod unique_array; pub mod unique_slice; pub mod unique_vec; +use nonmax::NonMaxU32; pub use unique_array::{UniqueEntityArray, UniqueEntityEquivalentArray}; pub use unique_slice::{UniqueEntityEquivalentSlice, UniqueEntitySlice}; pub use unique_vec::{UniqueEntityEquivalentVec, UniqueEntityVec}; @@ -74,17 +76,18 @@ pub use unique_vec::{UniqueEntityEquivalentVec, UniqueEntityVec}; use crate::{ archetype::{ArchetypeId, ArchetypeRow}, change_detection::MaybeLocation, - identifier::{ - error::IdentifierError, - kinds::IdKind, - masks::{IdentifierMask, HIGH_MASK}, - Identifier, - }, + component::Tick, storage::{SparseSetIndex, TableId, TableRow}, }; use alloc::vec::Vec; use bevy_platform::sync::atomic::Ordering; -use core::{fmt, hash::Hash, mem, num::NonZero, panic::Location}; +use core::{ + fmt, + hash::Hash, + mem::{self, MaybeUninit}, + num::NonZero, + panic::Location, +}; use log::warn; #[cfg(feature = "serialize")] @@ -103,6 +106,134 @@ use bevy_platform::sync::atomic::AtomicIsize as AtomicIdCursor; #[cfg(not(target_has_atomic = "64"))] type IdCursor = isize; +/// This represents the row or "index" of an [`Entity`] within the [`Entities`] table. +/// This is a lighter weight version of [`Entity`]. +/// +/// This is a unique identifier for an entity in the world. +/// This differs from [`Entity`] in that [`Entity`] is unique for all entities total (unless the [`Entity::generation`] wraps), +/// but this is only unique for entities that are active. +/// +/// This can be used over [`Entity`] to improve performance in some cases, +/// but improper use can cause this to identify a different entity than intended. +/// Use with caution. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Display)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +#[cfg_attr(feature = "bevy_reflect", reflect(opaque))] +#[cfg_attr(feature = "bevy_reflect", reflect(Hash, PartialEq, Debug, Clone))] +#[repr(transparent)] +pub struct EntityRow(NonMaxU32); + +impl EntityRow { + const PLACEHOLDER: Self = Self(NonMaxU32::MAX); + + /// Constructs a new [`EntityRow`] from its index. + pub const fn new(index: NonMaxU32) -> Self { + Self(index) + } + + /// Gets the index of the entity. + #[inline(always)] + pub const fn index(self) -> u32 { + self.0.get() + } + + /// Gets some bits that represent this value. + /// The bits are opaque and should not be regarded as meaningful. + #[inline(always)] + const fn to_bits(self) -> u32 { + // SAFETY: NonMax is repr transparent. + unsafe { mem::transmute::(self.0) } + } + + /// Reconstruct an [`EntityRow`] previously destructured with [`EntityRow::to_bits`]. + /// + /// Only useful when applied to results from `to_bits` in the same instance of an application. + /// + /// # Panics + /// + /// This method will likely panic if given `u32` values that did not come from [`EntityRow::to_bits`]. + #[inline] + const fn from_bits(bits: u32) -> Self { + Self::try_from_bits(bits).expect("Attempted to initialize invalid bits as an entity row") + } + + /// Reconstruct an [`EntityRow`] previously destructured with [`EntityRow::to_bits`]. + /// + /// Only useful when applied to results from `to_bits` in the same instance of an application. + /// + /// This method is the fallible counterpart to [`EntityRow::from_bits`]. + #[inline(always)] + const fn try_from_bits(bits: u32) -> Option { + match NonZero::::new(bits) { + // SAFETY: NonMax and NonZero are repr transparent. + Some(underlying) => Some(Self(unsafe { + mem::transmute::, NonMaxU32>(underlying) + })), + None => None, + } + } +} + +impl SparseSetIndex for EntityRow { + #[inline] + fn sparse_set_index(&self) -> usize { + self.index() as usize + } + + #[inline] + fn get_sparse_set_index(value: usize) -> Self { + Self::from_bits(value as u32) + } +} + +/// This tracks different versions or generations of an [`EntityRow`]. +/// Importantly, this can wrap, meaning each generation is not necessarily unique per [`EntityRow`]. +/// +/// This should be treated as a opaque identifier, and it's internal representation may be subject to change. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Display)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +#[cfg_attr(feature = "bevy_reflect", reflect(opaque))] +#[cfg_attr(feature = "bevy_reflect", reflect(Hash, PartialEq, Debug, Clone))] +#[repr(transparent)] +pub struct EntityGeneration(u32); + +impl EntityGeneration { + /// Represents the first generation of an [`EntityRow`]. + pub const FIRST: Self = Self(0); + + /// Gets some bits that represent this value. + /// The bits are opaque and should not be regarded as meaningful. + #[inline(always)] + const fn to_bits(self) -> u32 { + self.0 + } + + /// Reconstruct an [`EntityGeneration`] previously destructured with [`EntityGeneration::to_bits`]. + /// + /// Only useful when applied to results from `to_bits` in the same instance of an application. + #[inline] + const fn from_bits(bits: u32) -> Self { + Self(bits) + } + + /// Returns the [`EntityGeneration`] that would result from this many more `versions` of the corresponding [`EntityRow`] from passing. + #[inline] + pub const fn after_versions(self, versions: u32) -> Self { + Self(self.0.wrapping_add(versions)) + } + + /// Identical to [`after_versions`](Self::after_versions) but also returns a `bool` indicating if, + /// after these `versions`, one such version could conflict with a previous one. + /// + /// If this happens, this will no longer uniquely identify a version of an [`EntityRow`]. + /// This is called entity aliasing. + #[inline] + pub const fn after_versions_and_could_alias(self, versions: u32) -> (Self, bool) { + let raw = self.0.overflowing_add(versions); + (Self(raw.0), raw.1) + } +} + /// Lightweight identifier of an [entity](crate::entity). /// /// The identifier is implemented using a [generational index]: a combination of an index and a generation. @@ -186,10 +317,10 @@ pub struct Entity { // Do not reorder the fields here. The ordering is explicitly used by repr(C) // to make this struct equivalent to a u64. #[cfg(target_endian = "little")] - index: u32, - generation: NonZero, + row: EntityRow, + generation: EntityGeneration, #[cfg(target_endian = "big")] - index: u32, + row: EntityRow, } // By not short-circuiting in comparisons, we get better codegen. @@ -243,24 +374,15 @@ impl Hash for Entity { } } -#[deprecated( - since = "0.16.0", - note = "This is exclusively used with the now deprecated `Entities::alloc_at_without_replacement`." -)] -pub(crate) enum AllocAtWithoutReplacement { - Exists(EntityLocation), - DidNotExist, - ExistsWithWrongGeneration, -} - impl Entity { - /// Construct an [`Entity`] from a raw `index` value and a non-zero `generation` value. + /// Construct an [`Entity`] from a raw `row` value and a non-zero `generation` value. /// Ensure that the generation value is never greater than `0x7FFF_FFFF`. #[inline(always)] - pub(crate) const fn from_raw_and_generation(index: u32, generation: NonZero) -> Entity { - debug_assert!(generation.get() <= HIGH_MASK); - - Self { index, generation } + pub(crate) const fn from_raw_and_generation( + row: EntityRow, + generation: EntityGeneration, + ) -> Entity { + Self { row, generation } } /// An entity ID with a placeholder value. This may or may not correspond to an actual entity, @@ -297,9 +419,9 @@ impl Entity { /// } /// } /// ``` - pub const PLACEHOLDER: Self = Self::from_raw(u32::MAX); + pub const PLACEHOLDER: Self = Self::from_raw(EntityRow::PLACEHOLDER); - /// Creates a new entity ID with the specified `index` and a generation of 1. + /// Creates a new entity ID with the specified `row` and a generation of 1. /// /// # Note /// @@ -312,8 +434,19 @@ impl Entity { /// `Entity` lines up between instances, but instead insert a secondary identifier as /// a component. #[inline(always)] - pub const fn from_raw(index: u32) -> Entity { - Self::from_raw_and_generation(index, NonZero::::MIN) + pub const fn from_raw(row: EntityRow) -> Entity { + Self::from_raw_and_generation(row, EntityGeneration::FIRST) + } + + /// This is equivalent to [`from_raw`](Self::from_raw) except that it takes a `u32` instead of an [`EntityRow`]. + /// + /// Returns `None` if the row is `u32::MAX`. + #[inline(always)] + pub const fn from_raw_u32(row: u32) -> Option { + match NonMaxU32::new(row) { + Some(row) => Some(Self::from_raw(EntityRow::new(row))), + None => None, + } } /// Convert to a form convenient for passing outside of rust. @@ -324,7 +457,7 @@ impl Entity { /// No particular structure is guaranteed for the returned bits. #[inline(always)] pub const fn to_bits(self) -> u64 { - IdentifierMask::pack_into_u64(self.index, self.generation.get()) + self.row.to_bits() as u64 | ((self.generation.to_bits() as u64) << 32) } /// Reconstruct an `Entity` previously destructured with [`Entity::to_bits`]. @@ -336,12 +469,10 @@ impl Entity { /// This method will likely panic if given `u64` values that did not come from [`Entity::to_bits`]. #[inline] pub const fn from_bits(bits: u64) -> Self { - // Construct an Identifier initially to extract the kind from. - let id = Self::try_from_bits(bits); - - match id { - Ok(entity) => entity, - Err(_) => panic!("Attempted to initialize invalid bits as an entity"), + if let Some(id) = Self::try_from_bits(bits) { + id + } else { + panic!("Attempted to initialize invalid bits as an entity") } } @@ -351,54 +482,43 @@ impl Entity { /// /// This method is the fallible counterpart to [`Entity::from_bits`]. #[inline(always)] - pub const fn try_from_bits(bits: u64) -> Result { - if let Ok(id) = Identifier::try_from_bits(bits) { - let kind = id.kind() as u8; + pub const fn try_from_bits(bits: u64) -> Option { + let raw_row = bits as u32; + let raw_gen = (bits >> 32) as u32; - if kind == (IdKind::Entity as u8) { - return Ok(Self { - index: id.low(), - generation: id.high(), - }); - } + if let Some(row) = EntityRow::try_from_bits(raw_row) { + Some(Self { + row, + generation: EntityGeneration::from_bits(raw_gen), + }) + } else { + None } - - Err(IdentifierError::InvalidEntityId(bits)) } /// Return a transiently unique identifier. + /// See also [`EntityRow`]. /// - /// No two simultaneously-live entities share the same index, but dead entities' indices may collide + /// No two simultaneously-live entities share the same row, but dead entities' indices may collide /// with both live and dead entities. Useful for compactly representing entities within a /// specific snapshot of the world, such as when serializing. #[inline] + pub const fn row(self) -> EntityRow { + self.row + } + + /// Equivalent to `self.row().index()`. See [`Self::row`] for details. + #[inline] pub const fn index(self) -> u32 { - self.index + self.row.index() } - /// Returns the generation of this Entity's index. The generation is incremented each time an - /// entity with a given index is despawned. This serves as a "count" of the number of times a - /// given index has been reused (index, generation) pairs uniquely identify a given Entity. + /// Returns the generation of this Entity's row. The generation is incremented each time an + /// entity with a given row is despawned. This serves as a "count" of the number of times a + /// given row has been reused (row, generation) pairs uniquely identify a given Entity. #[inline] - pub const fn generation(self) -> u32 { - // Mask so not to expose any flags - IdentifierMask::extract_value_from_high(self.generation.get()) - } -} - -impl TryFrom for Entity { - type Error = IdentifierError; - - #[inline] - fn try_from(value: Identifier) -> Result { - Self::try_from_bits(value.to_bits()) - } -} - -impl From for Identifier { - #[inline] - fn from(value: Entity) -> Self { - Identifier::from_bits(value.to_bits()) + pub const fn generation(self) -> EntityGeneration { + self.generation } } @@ -420,7 +540,8 @@ impl<'de> Deserialize<'de> for Entity { { use serde::de::Error; let id: u64 = Deserialize::deserialize(deserializer)?; - Entity::try_from_bits(id).map_err(D::Error::custom) + Entity::try_from_bits(id) + .ok_or_else(|| D::Error::custom("Attempting to deserialize an invalid entity.")) } } @@ -481,12 +602,12 @@ impl fmt::Display for Entity { impl SparseSetIndex for Entity { #[inline] fn sparse_set_index(&self) -> usize { - self.index() as usize + self.row().sparse_set_index() } #[inline] fn get_sparse_set_index(value: usize) -> Self { - Entity::from_raw(value as u32) + Entity::from_raw(EntityRow::get_sparse_set_index(value)) } } @@ -496,7 +617,7 @@ pub struct ReserveEntitiesIterator<'a> { meta: &'a [EntityMeta], // Reserved indices formerly in the freelist to hand out. - freelist_indices: core::slice::Iter<'a, u32>, + freelist_indices: core::slice::Iter<'a, EntityRow>, // New Entity indices to hand out, outside the range of meta.len(). new_indices: core::ops::Range, @@ -508,10 +629,16 @@ impl<'a> Iterator for ReserveEntitiesIterator<'a> { fn next(&mut self) -> Option { self.freelist_indices .next() - .map(|&index| { - Entity::from_raw_and_generation(index, self.meta[index as usize].generation) + .map(|&row| { + Entity::from_raw_and_generation(row, self.meta[row.index() as usize].generation) + }) + .or_else(|| { + self.new_indices.next().map(|index| { + // SAFETY: This came from an exclusive range so the max can't be hit. + let row = unsafe { EntityRow::new(NonMaxU32::new_unchecked(index)) }; + Entity::from_raw(row) + }) }) - .or_else(|| self.new_indices.next().map(Entity::from_raw)) } fn size_hint(&self) -> (usize, Option) { @@ -578,7 +705,7 @@ pub struct Entities { /// [`reserve_entity`]: Entities::reserve_entity /// [`reserve_entities`]: Entities::reserve_entities /// [`flush`]: Entities::flush - pending: Vec, + pending: Vec, free_cursor: AtomicIdCursor, } @@ -654,17 +781,21 @@ impl Entities { let n = self.free_cursor.fetch_sub(1, Ordering::Relaxed); if n > 0 { // Allocate from the freelist. - let index = self.pending[(n - 1) as usize]; - Entity::from_raw_and_generation(index, self.meta[index as usize].generation) + let row = self.pending[(n - 1) as usize]; + Entity::from_raw_and_generation(row, self.meta[row.index() as usize].generation) } else { // Grab a new ID, outside the range of `meta.len()`. `flush()` must // eventually be called to make it valid. // // As `self.free_cursor` goes more and more negative, we return IDs farther // and farther beyond `meta.len()`. - Entity::from_raw( - u32::try_from(self.meta.len() as IdCursor - n).expect("too many entities"), - ) + let raw = self.meta.len() as IdCursor - n; + if raw >= u32::MAX as IdCursor { + panic!("too many entities"); + } + // SAFETY: We just checked the bounds + let row = unsafe { EntityRow::new(NonMaxU32::new_unchecked(raw as u32)) }; + Entity::from_raw(row) } } @@ -679,98 +810,20 @@ impl Entities { /// Allocate an entity ID directly. pub fn alloc(&mut self) -> Entity { self.verify_flushed(); - if let Some(index) = self.pending.pop() { + if let Some(row) = self.pending.pop() { let new_free_cursor = self.pending.len() as IdCursor; *self.free_cursor.get_mut() = new_free_cursor; - Entity::from_raw_and_generation(index, self.meta[index as usize].generation) + Entity::from_raw_and_generation(row, self.meta[row.index() as usize].generation) } else { - let index = u32::try_from(self.meta.len()).expect("too many entities"); + let index = u32::try_from(self.meta.len()) + .ok() + .and_then(NonMaxU32::new) + .expect("too many entities"); self.meta.push(EntityMeta::EMPTY); - Entity::from_raw(index) + Entity::from_raw(EntityRow::new(index)) } } - /// Allocate a specific entity ID, overwriting its generation. - /// - /// Returns the location of the entity currently using the given ID, if any. Location should be - /// written immediately. - #[deprecated( - since = "0.16.0", - note = "This can cause extreme performance problems when used after freeing a large number of entities and requesting an arbitrary entity. See #18054 on GitHub." - )] - pub fn alloc_at(&mut self, entity: Entity) -> Option { - self.verify_flushed(); - - let loc = if entity.index() as usize >= self.meta.len() { - self.pending - .extend((self.meta.len() as u32)..entity.index()); - let new_free_cursor = self.pending.len() as IdCursor; - *self.free_cursor.get_mut() = new_free_cursor; - self.meta - .resize(entity.index() as usize + 1, EntityMeta::EMPTY); - None - } else if let Some(index) = self.pending.iter().position(|item| *item == entity.index()) { - self.pending.swap_remove(index); - let new_free_cursor = self.pending.len() as IdCursor; - *self.free_cursor.get_mut() = new_free_cursor; - None - } else { - Some(mem::replace( - &mut self.meta[entity.index() as usize].location, - EntityMeta::EMPTY.location, - )) - }; - - self.meta[entity.index() as usize].generation = entity.generation; - - loc - } - - /// Allocate a specific entity ID, overwriting its generation. - /// - /// Returns the location of the entity currently using the given ID, if any. - #[deprecated( - since = "0.16.0", - note = "This can cause extreme performance problems when used after freeing a large number of entities and requesting an arbitrary entity. See #18054 on GitHub." - )] - #[expect( - deprecated, - reason = "We need to support `AllocAtWithoutReplacement` for now." - )] - pub(crate) fn alloc_at_without_replacement( - &mut self, - entity: Entity, - ) -> AllocAtWithoutReplacement { - self.verify_flushed(); - - let result = if entity.index() as usize >= self.meta.len() { - self.pending - .extend((self.meta.len() as u32)..entity.index()); - let new_free_cursor = self.pending.len() as IdCursor; - *self.free_cursor.get_mut() = new_free_cursor; - self.meta - .resize(entity.index() as usize + 1, EntityMeta::EMPTY); - AllocAtWithoutReplacement::DidNotExist - } else if let Some(index) = self.pending.iter().position(|item| *item == entity.index()) { - self.pending.swap_remove(index); - let new_free_cursor = self.pending.len() as IdCursor; - *self.free_cursor.get_mut() = new_free_cursor; - AllocAtWithoutReplacement::DidNotExist - } else { - let current_meta = &self.meta[entity.index() as usize]; - if current_meta.location.archetype_id == ArchetypeId::INVALID { - AllocAtWithoutReplacement::DidNotExist - } else if current_meta.generation == entity.generation { - AllocAtWithoutReplacement::Exists(current_meta.location) - } else { - return AllocAtWithoutReplacement::ExistsWithWrongGeneration; - } - }; - - self.meta[entity.index() as usize].generation = entity.generation; - result - } - /// Destroy an entity, allowing it to be reused. /// /// Must not be called while reserved entities are awaiting `flush()`. @@ -782,18 +835,18 @@ impl Entities { return None; } - meta.generation = IdentifierMask::inc_masked_high_by(meta.generation, 1); - - if meta.generation == NonZero::::MIN { + let (new_generation, aliased) = meta.generation.after_versions_and_could_alias(1); + meta.generation = new_generation; + if aliased { warn!( "Entity({}) generation wrapped on Entities::free, aliasing may occur", - entity.index + entity.row() ); } let loc = mem::replace(&mut meta.location, EntityMeta::EMPTY.location); - self.pending.push(entity.index()); + self.pending.push(entity.row()); let new_free_cursor = self.pending.len() as IdCursor; *self.free_cursor.get_mut() = new_free_cursor; @@ -825,7 +878,7 @@ impl Entities { // This will return false for entities which have been freed, even if // not reallocated since the generation is incremented in `free` pub fn contains(&self, entity: Entity) -> bool { - self.resolve_from_id(entity.index()) + self.resolve_from_id(entity.row()) .is_some_and(|e| e.generation() == entity.generation()) } @@ -853,7 +906,10 @@ impl Entities { } /// Updates the location of an [`Entity`]. This must be called when moving the components of - /// the entity around in storage. + /// the existing entity around in storage. + /// + /// For spawning and despawning entities, [`set_spawn_despawn`](Self::set_spawn_despawn) must + /// be used instead. /// /// # Safety /// - `index` must be a valid entity index. @@ -866,6 +922,27 @@ impl Entities { meta.location = location; } + /// Updates the location of an [`Entity`]. This must be called when moving the components of + /// the spawned or despawned entity around in storage. + /// + /// # Safety + /// - `index` must be a valid entity index. + /// - `location` must be valid for the entity at `index` or immediately made valid afterwards + /// before handing control to unknown code. + #[inline] + pub(crate) unsafe fn set_spawn_despawn( + &mut self, + index: u32, + location: EntityLocation, + by: MaybeLocation, + at: Tick, + ) { + // SAFETY: Caller guarantees that `index` a valid entity index + let meta = unsafe { self.meta.get_unchecked_mut(index as usize) }; + meta.location = location; + meta.spawned_or_despawned = MaybeUninit::new(SpawnedOrDespawned { by, at }); + } + /// Increments the `generation` of a freed [`Entity`]. The next entity ID allocated with this /// `index` will count `generation` starting from the prior `generation` + the specified /// value + 1. @@ -878,7 +955,7 @@ impl Entities { let meta = &mut self.meta[index as usize]; if meta.location.archetype_id == ArchetypeId::INVALID { - meta.generation = IdentifierMask::inc_masked_high_by(meta.generation, generations); + meta.generation = meta.generation.after_versions(generations); true } else { false @@ -891,17 +968,17 @@ impl Entities { /// Note: This method may return [`Entities`](Entity) which are currently free /// Note that [`contains`](Entities::contains) will correctly return false for freed /// entities, since it checks the generation - pub fn resolve_from_id(&self, index: u32) -> Option { - let idu = index as usize; + pub fn resolve_from_id(&self, row: EntityRow) -> Option { + let idu = row.index() as usize; if let Some(&EntityMeta { generation, .. }) = self.meta.get(idu) { - Some(Entity::from_raw_and_generation(index, generation)) + Some(Entity::from_raw_and_generation(row, generation)) } else { // `id` is outside of the meta list - check whether it is reserved but not yet flushed. let free_cursor = self.free_cursor.load(Ordering::Relaxed); // If this entity was manually created, then free_cursor might be positive // Returning None handles that case correctly let num_pending = usize::try_from(-free_cursor).ok()?; - (idu < self.meta.len() + num_pending).then_some(Entity::from_raw(index)) + (idu < self.meta.len() + num_pending).then_some(Entity::from_raw(row)) } } @@ -930,8 +1007,10 @@ impl Entities { let new_meta_len = old_meta_len + -current_free_cursor as usize; self.meta.resize(new_meta_len, EntityMeta::EMPTY); for (index, meta) in self.meta.iter_mut().enumerate().skip(old_meta_len) { + // SAFETY: the index is less than the meta length, which can not exceeded u32::MAX + let row = EntityRow::new(unsafe { NonMaxU32::new_unchecked(index as u32) }); init( - Entity::from_raw_and_generation(index as u32, meta.generation), + Entity::from_raw_and_generation(row, meta.generation), &mut meta.location, ); } @@ -940,10 +1019,10 @@ impl Entities { 0 }; - for index in self.pending.drain(new_free_cursor..) { - let meta = &mut self.meta[index as usize]; + for row in self.pending.drain(new_free_cursor..) { + let meta = &mut self.meta[row.index() as usize]; init( - Entity::from_raw_and_generation(index, meta.generation), + Entity::from_raw_and_generation(row, meta.generation), &mut meta.location, ); } @@ -1004,19 +1083,6 @@ impl Entities { self.len() == 0 } - /// Sets the source code location from which this entity has last been spawned - /// or despawned. - #[inline] - pub(crate) fn set_spawned_or_despawned_by(&mut self, index: u32, caller: MaybeLocation) { - caller.map(|caller| { - let meta = self - .meta - .get_mut(index as usize) - .expect("Entity index invalid"); - meta.spawned_or_despawned_by = MaybeLocation::new(Some(caller)); - }); - } - /// Returns the source code location from which this entity has last been spawned /// or despawned. Returns `None` if its index has been reused by another entity /// or if this entity has never existed. @@ -1025,16 +1091,67 @@ impl Entities { entity: Entity, ) -> MaybeLocation>> { MaybeLocation::new_with_flattened(|| { - self.meta - .get(entity.index() as usize) - .filter(|meta| - // Generation is incremented immediately upon despawn - (meta.generation == entity.generation) - || (meta.location.archetype_id == ArchetypeId::INVALID) - && (meta.generation == IdentifierMask::inc_masked_high_by(entity.generation, 1))) - .map(|meta| meta.spawned_or_despawned_by) + self.entity_get_spawned_or_despawned(entity) + .map(|spawned_or_despawned| spawned_or_despawned.by) }) - .map(Option::flatten) + } + + /// Returns the [`Tick`] at which this entity has last been spawned or despawned. + /// Returns `None` if its index has been reused by another entity or if this entity + /// has never existed. + pub fn entity_get_spawned_or_despawned_at(&self, entity: Entity) -> Option { + self.entity_get_spawned_or_despawned(entity) + .map(|spawned_or_despawned| spawned_or_despawned.at) + } + + /// Returns the [`SpawnedOrDespawned`] related to the entity's last spawn or + /// respawn. Returns `None` if its index has been reused by another entity or if + /// this entity has never existed. + #[inline] + fn entity_get_spawned_or_despawned(&self, entity: Entity) -> Option { + self.meta + .get(entity.index() as usize) + .filter(|meta| + // Generation is incremented immediately upon despawn + (meta.generation == entity.generation) + || (meta.location.archetype_id == ArchetypeId::INVALID) + && (meta.generation == entity.generation.after_versions(1))) + .map(|meta| { + // SAFETY: valid archetype or non-min generation is proof this is init + unsafe { meta.spawned_or_despawned.assume_init() } + }) + } + + /// Returns the source code location from which this entity has last been spawned + /// or despawned and the Tick of when that happened. + /// + /// # Safety + /// + /// The entity index must belong to an entity that is currently alive or, if it + /// despawned, was not overwritten by a new entity of the same index. + #[inline] + pub(crate) unsafe fn entity_get_spawned_or_despawned_unchecked( + &self, + entity: Entity, + ) -> (MaybeLocation, Tick) { + // SAFETY: caller ensures entity is allocated + let meta = unsafe { self.meta.get_unchecked(entity.index() as usize) }; + // SAFETY: caller ensures entities of this index were at least spawned + let spawned_or_despawned = unsafe { meta.spawned_or_despawned.assume_init() }; + (spawned_or_despawned.by, spawned_or_despawned.at) + } + + #[inline] + pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { + for meta in &mut self.meta { + if meta.generation != EntityGeneration::FIRST + || meta.location.archetype_id != ArchetypeId::INVALID + { + // SAFETY: non-min generation or valid archetype is proof this is init + let spawned_or_despawned = unsafe { meta.spawned_or_despawned.assume_init_mut() }; + spawned_or_despawned.at.check_tick(change_tick); + } + } } /// Constructs a message explaining why an entity does not exist, if known. @@ -1092,20 +1209,26 @@ impl fmt::Display for EntityDoesNotExistDetails { #[derive(Copy, Clone, Debug)] struct EntityMeta { - /// The current generation of the [`Entity`]. - pub generation: NonZero, - /// The current location of the [`Entity`] + /// The current [`EntityGeneration`] of the [`EntityRow`]. + pub generation: EntityGeneration, + /// The current location of the [`EntityRow`] pub location: EntityLocation, /// Location of the last spawn or despawn of this entity - spawned_or_despawned_by: MaybeLocation>>, + spawned_or_despawned: MaybeUninit, +} + +#[derive(Copy, Clone, Debug)] +struct SpawnedOrDespawned { + by: MaybeLocation, + at: Tick, } impl EntityMeta { /// meta for **pending entity** const EMPTY: EntityMeta = EntityMeta { - generation: NonZero::::MIN, + generation: EntityGeneration::FIRST, location: EntityLocation::INVALID, - spawned_or_despawned_by: MaybeLocation::new(None), + spawned_or_despawned: MaybeUninit::uninit(), }; } @@ -1155,9 +1278,14 @@ mod tests { #[test] fn entity_bits_roundtrip() { + let r = EntityRow::new(NonMaxU32::new(0xDEADBEEF).unwrap()); + assert_eq!(EntityRow::from_bits(r.to_bits()), r); + // Generation cannot be greater than 0x7FFF_FFFF else it will be an invalid Entity id - let e = - Entity::from_raw_and_generation(0xDEADBEEF, NonZero::::new(0x5AADF00D).unwrap()); + let e = Entity::from_raw_and_generation( + EntityRow::new(NonMaxU32::new(0xDEADBEEF).unwrap()), + EntityGeneration::from_bits(0x5AADF00D), + ); assert_eq!(Entity::from_bits(e.to_bits()), e); } @@ -1190,18 +1318,20 @@ mod tests { #[test] fn entity_const() { - const C1: Entity = Entity::from_raw(42); + const C1: Entity = Entity::from_raw(EntityRow::new(NonMaxU32::new(42).unwrap())); assert_eq!(42, C1.index()); - assert_eq!(1, C1.generation()); + assert_eq!(0, C1.generation().to_bits()); const C2: Entity = Entity::from_bits(0x0000_00ff_0000_00cc); - assert_eq!(0x0000_00cc, C2.index()); - assert_eq!(0x0000_00ff, C2.generation()); + assert_eq!(!0x0000_00cc, C2.index()); + assert_eq!(0x0000_00ff, C2.generation().to_bits()); - const C3: u32 = Entity::from_raw(33).index(); + const C3: u32 = Entity::from_raw(EntityRow::new(NonMaxU32::new(33).unwrap())).index(); assert_eq!(33, C3); - const C4: u32 = Entity::from_bits(0x00dd_00ff_0000_0000).generation(); + const C4: u32 = Entity::from_bits(0x00dd_00ff_1111_1111) + .generation() + .to_bits(); assert_eq!(0x00dd_00ff, C4); } @@ -1227,7 +1357,7 @@ mod tests { // The very next entity allocated should be a further generation on the same index let next_entity = entities.alloc(); assert_eq!(next_entity.index(), entity.index()); - assert!(next_entity.generation() > entity.generation() + GENERATIONS); + assert!(next_entity.generation() > entity.generation().after_versions(GENERATIONS)); } #[test] @@ -1237,65 +1367,139 @@ mod tests { )] fn entity_comparison() { assert_eq!( - Entity::from_raw_and_generation(123, NonZero::::new(456).unwrap()), - Entity::from_raw_and_generation(123, NonZero::::new(456).unwrap()) + Entity::from_raw_and_generation( + EntityRow::new(NonMaxU32::new(123).unwrap()), + EntityGeneration::from_bits(456) + ), + Entity::from_raw_and_generation( + EntityRow::new(NonMaxU32::new(123).unwrap()), + EntityGeneration::from_bits(456) + ) ); assert_ne!( - Entity::from_raw_and_generation(123, NonZero::::new(789).unwrap()), - Entity::from_raw_and_generation(123, NonZero::::new(456).unwrap()) + Entity::from_raw_and_generation( + EntityRow::new(NonMaxU32::new(123).unwrap()), + EntityGeneration::from_bits(789) + ), + Entity::from_raw_and_generation( + EntityRow::new(NonMaxU32::new(123).unwrap()), + EntityGeneration::from_bits(456) + ) ); assert_ne!( - Entity::from_raw_and_generation(123, NonZero::::new(456).unwrap()), - Entity::from_raw_and_generation(123, NonZero::::new(789).unwrap()) + Entity::from_raw_and_generation( + EntityRow::new(NonMaxU32::new(123).unwrap()), + EntityGeneration::from_bits(456) + ), + Entity::from_raw_and_generation( + EntityRow::new(NonMaxU32::new(123).unwrap()), + EntityGeneration::from_bits(789) + ) ); assert_ne!( - Entity::from_raw_and_generation(123, NonZero::::new(456).unwrap()), - Entity::from_raw_and_generation(456, NonZero::::new(123).unwrap()) + Entity::from_raw_and_generation( + EntityRow::new(NonMaxU32::new(123).unwrap()), + EntityGeneration::from_bits(456) + ), + Entity::from_raw_and_generation( + EntityRow::new(NonMaxU32::new(456).unwrap()), + EntityGeneration::from_bits(123) + ) ); // ordering is by generation then by index assert!( - Entity::from_raw_and_generation(123, NonZero::::new(456).unwrap()) - >= Entity::from_raw_and_generation(123, NonZero::::new(456).unwrap()) + Entity::from_raw_and_generation( + EntityRow::new(NonMaxU32::new(123).unwrap()), + EntityGeneration::from_bits(456) + ) >= Entity::from_raw_and_generation( + EntityRow::new(NonMaxU32::new(123).unwrap()), + EntityGeneration::from_bits(456) + ) ); assert!( - Entity::from_raw_and_generation(123, NonZero::::new(456).unwrap()) - <= Entity::from_raw_and_generation(123, NonZero::::new(456).unwrap()) + Entity::from_raw_and_generation( + EntityRow::new(NonMaxU32::new(123).unwrap()), + EntityGeneration::from_bits(456) + ) <= Entity::from_raw_and_generation( + EntityRow::new(NonMaxU32::new(123).unwrap()), + EntityGeneration::from_bits(456) + ) ); assert!( - !(Entity::from_raw_and_generation(123, NonZero::::new(456).unwrap()) - < Entity::from_raw_and_generation(123, NonZero::::new(456).unwrap())) + !(Entity::from_raw_and_generation( + EntityRow::new(NonMaxU32::new(123).unwrap()), + EntityGeneration::from_bits(456) + ) < Entity::from_raw_and_generation( + EntityRow::new(NonMaxU32::new(123).unwrap()), + EntityGeneration::from_bits(456) + )) ); assert!( - !(Entity::from_raw_and_generation(123, NonZero::::new(456).unwrap()) - > Entity::from_raw_and_generation(123, NonZero::::new(456).unwrap())) + !(Entity::from_raw_and_generation( + EntityRow::new(NonMaxU32::new(123).unwrap()), + EntityGeneration::from_bits(456) + ) > Entity::from_raw_and_generation( + EntityRow::new(NonMaxU32::new(123).unwrap()), + EntityGeneration::from_bits(456) + )) ); assert!( - Entity::from_raw_and_generation(9, NonZero::::new(1).unwrap()) - < Entity::from_raw_and_generation(1, NonZero::::new(9).unwrap()) + Entity::from_raw_and_generation( + EntityRow::new(NonMaxU32::new(9).unwrap()), + EntityGeneration::from_bits(1) + ) < Entity::from_raw_and_generation( + EntityRow::new(NonMaxU32::new(1).unwrap()), + EntityGeneration::from_bits(9) + ) ); assert!( - Entity::from_raw_and_generation(1, NonZero::::new(9).unwrap()) - > Entity::from_raw_and_generation(9, NonZero::::new(1).unwrap()) + Entity::from_raw_and_generation( + EntityRow::new(NonMaxU32::new(1).unwrap()), + EntityGeneration::from_bits(9) + ) > Entity::from_raw_and_generation( + EntityRow::new(NonMaxU32::new(9).unwrap()), + EntityGeneration::from_bits(1) + ) ); assert!( - Entity::from_raw_and_generation(1, NonZero::::new(1).unwrap()) - < Entity::from_raw_and_generation(2, NonZero::::new(1).unwrap()) + Entity::from_raw_and_generation( + EntityRow::new(NonMaxU32::new(1).unwrap()), + EntityGeneration::from_bits(1) + ) > Entity::from_raw_and_generation( + EntityRow::new(NonMaxU32::new(2).unwrap()), + EntityGeneration::from_bits(1) + ) ); assert!( - Entity::from_raw_and_generation(1, NonZero::::new(1).unwrap()) - <= Entity::from_raw_and_generation(2, NonZero::::new(1).unwrap()) + Entity::from_raw_and_generation( + EntityRow::new(NonMaxU32::new(1).unwrap()), + EntityGeneration::from_bits(1) + ) >= Entity::from_raw_and_generation( + EntityRow::new(NonMaxU32::new(2).unwrap()), + EntityGeneration::from_bits(1) + ) ); assert!( - Entity::from_raw_and_generation(2, NonZero::::new(2).unwrap()) - > Entity::from_raw_and_generation(1, NonZero::::new(2).unwrap()) + Entity::from_raw_and_generation( + EntityRow::new(NonMaxU32::new(2).unwrap()), + EntityGeneration::from_bits(2) + ) < Entity::from_raw_and_generation( + EntityRow::new(NonMaxU32::new(1).unwrap()), + EntityGeneration::from_bits(2) + ) ); assert!( - Entity::from_raw_and_generation(2, NonZero::::new(2).unwrap()) - >= Entity::from_raw_and_generation(1, NonZero::::new(2).unwrap()) + Entity::from_raw_and_generation( + EntityRow::new(NonMaxU32::new(2).unwrap()), + EntityGeneration::from_bits(2) + ) <= Entity::from_raw_and_generation( + EntityRow::new(NonMaxU32::new(1).unwrap()), + EntityGeneration::from_bits(2) + ) ); } @@ -1307,12 +1511,16 @@ mod tests { let hash = EntityHash; let first_id = 0xC0FFEE << 8; - let first_hash = hash.hash_one(Entity::from_raw(first_id)); + let first_hash = hash.hash_one(Entity::from_raw(EntityRow::new( + NonMaxU32::new(first_id).unwrap(), + ))); for i in 1..=255 { let id = first_id + i; - let hash = hash.hash_one(Entity::from_raw(id)); - assert_eq!(hash.wrapping_sub(first_hash) as u32, i); + let hash = hash.hash_one(Entity::from_raw(EntityRow::new( + NonMaxU32::new(id).unwrap(), + ))); + assert_eq!(first_hash.wrapping_sub(hash) as u32, i); } } @@ -1323,20 +1531,24 @@ mod tests { let hash = EntityHash; let first_id = 0xC0FFEE; - let first_hash = hash.hash_one(Entity::from_raw(first_id)) >> 57; + let first_hash = hash.hash_one(Entity::from_raw(EntityRow::new( + NonMaxU32::new(first_id).unwrap(), + ))) >> 57; for bit in 0..u32::BITS { let id = first_id ^ (1 << bit); - let hash = hash.hash_one(Entity::from_raw(id)) >> 57; + let hash = hash.hash_one(Entity::from_raw(EntityRow::new( + NonMaxU32::new(id).unwrap(), + ))) >> 57; assert_ne!(hash, first_hash); } } #[test] fn entity_debug() { - let entity = Entity::from_raw(42); + let entity = Entity::from_raw(EntityRow::new(NonMaxU32::new(42).unwrap())); let string = format!("{:?}", entity); - assert_eq!(string, "42v1#4294967338"); + assert_eq!(string, "42v0#4294967253"); let entity = Entity::PLACEHOLDER; let string = format!("{:?}", entity); @@ -1345,9 +1557,9 @@ mod tests { #[test] fn entity_display() { - let entity = Entity::from_raw(42); + let entity = Entity::from_raw(EntityRow::new(NonMaxU32::new(42).unwrap())); let string = format!("{}", entity); - assert_eq!(string, "42v1"); + assert_eq!(string, "42v0"); let entity = Entity::PLACEHOLDER; let string = format!("{}", entity); diff --git a/crates/bevy_ecs/src/error/command_handling.rs b/crates/bevy_ecs/src/error/command_handling.rs index d85ad4a87e..bf2741d376 100644 --- a/crates/bevy_ecs/src/error/command_handling.rs +++ b/crates/bevy_ecs/src/error/command_handling.rs @@ -7,22 +7,17 @@ use crate::{ world::{error::EntityMutableFetchError, World}, }; -use super::{default_error_handler, BevyError, ErrorContext}; +use super::{BevyError, ErrorContext, ErrorHandler}; -/// Takes a [`Command`] that returns a Result and uses a given error handler function to convert it into +/// Takes a [`Command`] that potentially returns a Result and uses a given error handler function to convert it into /// a [`Command`] that internally handles an error if it occurs and returns `()`. -pub trait HandleError { +pub trait HandleError: Send + 'static { /// Takes a [`Command`] that returns a Result and uses a given error handler function to convert it into /// a [`Command`] that internally handles an error if it occurs and returns `()`. - fn handle_error_with(self, error_handler: fn(BevyError, ErrorContext)) -> impl Command; + fn handle_error_with(self, error_handler: ErrorHandler) -> impl Command; /// Takes a [`Command`] that returns a Result and uses the default error handler function to convert it into /// a [`Command`] that internally handles an error if it occurs and returns `()`. - fn handle_error(self) -> impl Command - where - Self: Sized, - { - self.handle_error_with(default_error_handler()) - } + fn handle_error(self) -> impl Command; } impl HandleError> for C @@ -30,7 +25,7 @@ where C: Command>, E: Into, { - fn handle_error_with(self, error_handler: fn(BevyError, ErrorContext)) -> impl Command { + fn handle_error_with(self, error_handler: ErrorHandler) -> impl Command { move |world: &mut World| match self.apply(world) { Ok(_) => {} Err(err) => (error_handler)( @@ -41,6 +36,18 @@ where ), } } + + fn handle_error(self) -> impl Command { + move |world: &mut World| match self.apply(world) { + Ok(_) => {} + Err(err) => world.default_error_handler()( + err.into(), + ErrorContext::Command { + name: type_name::().into(), + }, + ), + } + } } impl HandleError for C @@ -52,6 +59,13 @@ where self.apply(world); } } + + #[inline] + fn handle_error(self) -> impl Command { + move |world: &mut World| { + self.apply(world); + } + } } impl HandleError for C @@ -63,10 +77,7 @@ where self } #[inline] - fn handle_error(self) -> impl Command - where - Self: Sized, - { + fn handle_error(self) -> impl Command { self } } diff --git a/crates/bevy_ecs/src/error/handler.rs b/crates/bevy_ecs/src/error/handler.rs index 688b599473..d53e1b3e05 100644 --- a/crates/bevy_ecs/src/error/handler.rs +++ b/crates/bevy_ecs/src/error/handler.rs @@ -1,9 +1,8 @@ -#[cfg(feature = "configurable_error_handler")] -use bevy_platform::sync::OnceLock; use core::fmt::Display; -use crate::{component::Tick, error::BevyError}; +use crate::{component::Tick, error::BevyError, prelude::Resource}; use alloc::borrow::Cow; +use derive_more::derive::{Deref, DerefMut}; /// Context for a [`BevyError`] to aid in debugging. #[derive(Debug, PartialEq, Eq, Clone)] @@ -77,53 +76,6 @@ impl ErrorContext { } } -/// A global error handler. This can be set at startup, as long as it is set before -/// any uses. This should generally be configured _before_ initializing the app. -/// -/// This should be set inside of your `main` function, before initializing the Bevy app. -/// The value of this error handler can be accessed using the [`default_error_handler`] function, -/// which calls [`OnceLock::get_or_init`] to get the value. -/// -/// **Note:** this is only available when the `configurable_error_handler` feature of `bevy_ecs` (or `bevy`) is enabled! -/// -/// # Example -/// -/// ``` -/// # use bevy_ecs::error::{GLOBAL_ERROR_HANDLER, warn}; -/// GLOBAL_ERROR_HANDLER.set(warn).expect("The error handler can only be set once, globally."); -/// // initialize Bevy App here -/// ``` -/// -/// To use this error handler in your app for custom error handling logic: -/// -/// ```rust -/// use bevy_ecs::error::{default_error_handler, GLOBAL_ERROR_HANDLER, BevyError, ErrorContext, panic}; -/// -/// fn handle_errors(error: BevyError, ctx: ErrorContext) { -/// let error_handler = default_error_handler(); -/// error_handler(error, ctx); -/// } -/// ``` -/// -/// # Warning -/// -/// As this can *never* be overwritten, library code should never set this value. -#[cfg(feature = "configurable_error_handler")] -pub static GLOBAL_ERROR_HANDLER: OnceLock = OnceLock::new(); - -/// The default error handler. This defaults to [`panic()`], -/// but if set, the [`GLOBAL_ERROR_HANDLER`] will be used instead, enabling error handler customization. -/// The `configurable_error_handler` feature must be enabled to change this from the panicking default behavior, -/// as there may be runtime overhead. -#[inline] -pub fn default_error_handler() -> fn(BevyError, ErrorContext) { - #[cfg(not(feature = "configurable_error_handler"))] - return panic; - - #[cfg(feature = "configurable_error_handler")] - return *GLOBAL_ERROR_HANDLER.get_or_init(|| panic); -} - macro_rules! inner { ($call:path, $e:ident, $c:ident) => { $call!( @@ -135,6 +87,25 @@ macro_rules! inner { }; } +/// Defines how Bevy reacts to errors. +pub type ErrorHandler = fn(BevyError, ErrorContext); + +/// Error handler to call when an error is not handled otherwise. +/// Defaults to [`panic()`]. +/// +/// When updated while a [`Schedule`] is running, it doesn't take effect for +/// that schedule until it's completed. +/// +/// [`Schedule`]: crate::schedule::Schedule +#[derive(Resource, Deref, DerefMut, Copy, Clone)] +pub struct DefaultErrorHandler(pub ErrorHandler); + +impl Default for DefaultErrorHandler { + fn default() -> Self { + Self(panic) + } +} + /// Error handler that panics with the system error. #[track_caller] #[inline] diff --git a/crates/bevy_ecs/src/error/mod.rs b/crates/bevy_ecs/src/error/mod.rs index 950deee3ec..231bdda940 100644 --- a/crates/bevy_ecs/src/error/mod.rs +++ b/crates/bevy_ecs/src/error/mod.rs @@ -7,8 +7,9 @@ //! All [`BevyError`]s returned by a system, observer or command are handled by an "error handler". By default, the //! [`panic`] error handler function is used, resulting in a panic with the error message attached. //! -//! You can change the default behavior by registering a custom error handler. -//! Modify the [`GLOBAL_ERROR_HANDLER`] value to set a custom error handler function for your entire app. +//! You can change the default behavior by registering a custom error handler: +//! Use [`DefaultErrorHandler`] to set a custom error handler function for a world, +//! or `App::set_error_handler` for a whole app. //! In practice, this is generally feature-flagged: panicking or loudly logging errors in development, //! and quietly logging or ignoring them in production to avoid crashing the app. //! @@ -33,10 +34,8 @@ //! The [`ErrorContext`] allows you to access additional details relevant to providing //! context surrounding the error – such as the system's [`name`] – in your error messages. //! -//! Remember to turn on the `configurable_error_handler` feature to set a global error handler! -//! //! ```rust, ignore -//! use bevy_ecs::error::{GLOBAL_ERROR_HANDLER, BevyError, ErrorContext}; +//! use bevy_ecs::error::{BevyError, ErrorContext, DefaultErrorHandler}; //! use log::trace; //! //! fn my_error_handler(error: BevyError, ctx: ErrorContext) { @@ -48,10 +47,9 @@ //! } //! //! fn main() { -//! // This requires the "configurable_error_handler" feature to be enabled to be in scope. -//! GLOBAL_ERROR_HANDLER.set(my_error_handler).expect("The error handler can only be set once."); -//! -//! // Initialize your Bevy App here +//! let mut world = World::new(); +//! world.insert_resource(DefaultErrorHandler(my_error_handler)); +//! // Use your world here //! } //! ``` //! diff --git a/crates/bevy_ecs/src/event/mod.rs b/crates/bevy_ecs/src/event/mod.rs index 9c19dc1689..3bb422b7bb 100644 --- a/crates/bevy_ecs/src/event/mod.rs +++ b/crates/bevy_ecs/src/event/mod.rs @@ -24,8 +24,13 @@ pub use mut_iterators::{EventMutIterator, EventMutIteratorWithId}; pub use mutator::EventMutator; pub use reader::EventReader; pub use registry::{EventRegistry, ShouldUpdateEvents}; +#[expect( + deprecated, + reason = "`EventUpdates` was renamed to `EventUpdateSystems`." +)] pub use update::{ - event_update_condition, event_update_system, signal_event_update_system, EventUpdates, + event_update_condition, event_update_system, signal_event_update_system, EventUpdateSystems, + EventUpdates, }; pub use writer::EventWriter; diff --git a/crates/bevy_ecs/src/event/update.rs b/crates/bevy_ecs/src/event/update.rs index c7b43aef00..bdde1af0db 100644 --- a/crates/bevy_ecs/src/event/update.rs +++ b/crates/bevy_ecs/src/event/update.rs @@ -13,7 +13,11 @@ use super::registry::ShouldUpdateEvents; #[doc(hidden)] #[derive(SystemSet, Clone, Debug, PartialEq, Eq, Hash)] -pub struct EventUpdates; +pub struct EventUpdateSystems; + +/// Deprecated alias for [`EventUpdateSystems`]. +#[deprecated(since = "0.17.0", note = "Renamed to `EventUpdateSystems`.")] +pub type EventUpdates = EventUpdateSystems; /// Signals the [`event_update_system`] to run after `FixedUpdate` systems. /// diff --git a/crates/bevy_ecs/src/event/writer.rs b/crates/bevy_ecs/src/event/writer.rs index a1c42f8b60..5854ab34fb 100644 --- a/crates/bevy_ecs/src/event/writer.rs +++ b/crates/bevy_ecs/src/event/writer.rs @@ -98,38 +98,4 @@ impl<'w, E: Event> EventWriter<'w, E> { { self.events.send_default() } - - /// Sends an `event`, which can later be read by [`EventReader`](super::EventReader)s. - /// This method returns the [ID](`EventId`) of the sent `event`. - /// - /// See [`Events`] for details. - #[deprecated(since = "0.16.0", note = "Use `EventWriter::write` instead.")] - #[track_caller] - pub fn send(&mut self, event: E) -> EventId { - self.write(event) - } - - /// Sends a list of `events` all at once, which can later be read by [`EventReader`](super::EventReader)s. - /// This is more efficient than sending each event individually. - /// This method returns the [IDs](`EventId`) of the sent `events`. - /// - /// See [`Events`] for details. - #[deprecated(since = "0.16.0", note = "Use `EventWriter::write_batch` instead.")] - #[track_caller] - pub fn send_batch(&mut self, events: impl IntoIterator) -> SendBatchIds { - self.write_batch(events) - } - - /// Sends the default value of the event. Useful when the event is an empty struct. - /// This method returns the [ID](`EventId`) of the sent `event`. - /// - /// See [`Events`] for details. - #[deprecated(since = "0.16.0", note = "Use `EventWriter::write_default` instead.")] - #[track_caller] - pub fn send_default(&mut self) -> EventId - where - E: Default, - { - self.write_default() - } } diff --git a/crates/bevy_ecs/src/hierarchy.rs b/crates/bevy_ecs/src/hierarchy.rs index 9f4b0d0f8f..158219d547 100644 --- a/crates/bevy_ecs/src/hierarchy.rs +++ b/crates/bevy_ecs/src/hierarchy.rs @@ -106,13 +106,6 @@ impl ChildOf { pub fn parent(&self) -> Entity { self.0 } - - /// The parent entity of this child entity. - #[deprecated(since = "0.16.0", note = "Use child_of.parent() instead")] - #[inline] - pub fn get(&self) -> Entity { - self.0 - } } // TODO: We need to impl either FromWorld or Default so ChildOf can be registered as Reflect. @@ -281,6 +274,12 @@ impl<'w> EntityWorldMut<'w> { self.add_related::(children) } + /// Removes all the children from this entity. + /// See also [`clear_related`](Self::clear_related) + pub fn clear_children(&mut self) -> &mut Self { + self.clear_related::() + } + /// Insert children at specific index. /// See also [`insert_related`](Self::insert_related). pub fn insert_children(&mut self, index: usize, children: &[Entity]) -> &mut Self { @@ -338,20 +337,6 @@ impl<'w> EntityWorldMut<'w> { }); self } - - /// Removes the [`ChildOf`] component, if it exists. - #[deprecated(since = "0.16.0", note = "Use entity_mut.remove::()")] - pub fn remove_parent(&mut self) -> &mut Self { - self.remove::(); - self - } - - /// Inserts the [`ChildOf`] component with the given `parent` entity, if it exists. - #[deprecated(since = "0.16.0", note = "Use entity_mut.insert(ChildOf(entity))")] - pub fn set_parent(&mut self, parent: Entity) -> &mut Self { - self.insert(ChildOf(parent)); - self - } } impl<'a> EntityCommands<'a> { @@ -369,6 +354,12 @@ impl<'a> EntityCommands<'a> { self.add_related::(children) } + /// Removes all the children from this entity. + /// See also [`clear_related`](Self::clear_related) + pub fn clear_children(&mut self) -> &mut Self { + self.clear_related::() + } + /// Insert children at specific index. /// See also [`insert_related`](Self::insert_related). pub fn insert_children(&mut self, index: usize, children: &[Entity]) -> &mut Self { @@ -422,20 +413,6 @@ impl<'a> EntityCommands<'a> { self.with_related::(bundle); self } - - /// Removes the [`ChildOf`] component, if it exists. - #[deprecated(since = "0.16.0", note = "Use entity_commands.remove::()")] - pub fn remove_parent(&mut self) -> &mut Self { - self.remove::(); - self - } - - /// Inserts the [`ChildOf`] component with the given `parent` entity, if it exists. - #[deprecated(since = "0.16.0", note = "Use entity_commands.insert(ChildOf(entity))")] - pub fn set_parent(&mut self, parent: Entity) -> &mut Self { - self.insert(ChildOf(parent)); - self - } } /// An `on_insert` component hook that when run, will validate that the parent of a given entity @@ -499,7 +476,7 @@ pub fn validate_parent_has_component( #[macro_export] macro_rules! children { [$($child:expr),*$(,)?] => { - $crate::hierarchy::Children::spawn(($($crate::spawn::Spawn($child)),*)) + $crate::hierarchy::Children::spawn($crate::recursive_spawn!($($child),*)) }; } @@ -656,6 +633,43 @@ mod tests { ); } + // regression test for https://github.com/bevyengine/bevy/pull/19134 + #[test] + fn insert_children_index_bound() { + let mut world = World::new(); + let child1 = world.spawn_empty().id(); + let child2 = world.spawn_empty().id(); + let child3 = world.spawn_empty().id(); + let child4 = world.spawn_empty().id(); + + let mut entity_world_mut = world.spawn_empty(); + + let first_children = entity_world_mut.add_children(&[child1, child2]).id(); + let hierarchy = get_hierarchy(&world, first_children); + assert_eq!( + hierarchy, + Node::new_with(first_children, vec![Node::new(child1), Node::new(child2)]) + ); + + let root = world + .entity_mut(first_children) + .insert_children(usize::MAX, &[child3, child4]) + .id(); + let hierarchy = get_hierarchy(&world, root); + assert_eq!( + hierarchy, + Node::new_with( + root, + vec![ + Node::new(child1), + Node::new(child2), + Node::new(child3), + Node::new(child4), + ] + ) + ); + } + #[test] fn remove_children() { let mut world = World::new(); @@ -719,6 +733,39 @@ mod tests { assert_eq!(world.entity(id).get::().unwrap().len(), 2,); } + #[test] + fn spawn_many_children() { + let mut world = World::new(); + + // 12 children should result in a flat tuple + let id = world + .spawn(children![(), (), (), (), (), (), (), (), (), (), (), ()]) + .id(); + + assert_eq!(world.entity(id).get::().unwrap().len(), 12,); + + // 13 will start nesting, but should nonetheless produce a flat hierarchy + let id = world + .spawn(children![ + (), + (), + (), + (), + (), + (), + (), + (), + (), + (), + (), + (), + (), + ]) + .id(); + + assert_eq!(world.entity(id).get::().unwrap().len(), 13,); + } + #[test] fn replace_children() { let mut world = World::new(); diff --git a/crates/bevy_ecs/src/identifier/error.rs b/crates/bevy_ecs/src/identifier/error.rs deleted file mode 100644 index a4679a12c6..0000000000 --- a/crates/bevy_ecs/src/identifier/error.rs +++ /dev/null @@ -1,29 +0,0 @@ -//! Error types for [`super::Identifier`] conversions. An ID can be converted -//! to various kinds, but these can fail if they are not valid forms of those -//! kinds. The error type in this module encapsulates the various failure modes. -use core::fmt; - -/// An Error type for [`super::Identifier`], mostly for providing error -/// handling for conversions of an ID to a type abstracting over the ID bits. -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -#[non_exhaustive] -pub enum IdentifierError { - /// A given ID has an invalid value for initializing to a [`crate::identifier::Identifier`]. - InvalidIdentifier, - /// A given ID has an invalid configuration of bits for converting to an [`crate::entity::Entity`]. - InvalidEntityId(u64), -} - -impl fmt::Display for IdentifierError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::InvalidIdentifier => write!( - f, - "The given id contains a zero value high component, which is invalid" - ), - Self::InvalidEntityId(_) => write!(f, "The given id is not a valid entity."), - } - } -} - -impl core::error::Error for IdentifierError {} diff --git a/crates/bevy_ecs/src/identifier/kinds.rs b/crates/bevy_ecs/src/identifier/kinds.rs deleted file mode 100644 index a5f57365fc..0000000000 --- a/crates/bevy_ecs/src/identifier/kinds.rs +++ /dev/null @@ -1,11 +0,0 @@ -/// The kinds of ID that [`super::Identifier`] can represent. Each -/// variant imposes different usages of the low/high segments -/// of the ID. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -#[repr(u8)] -pub enum IdKind { - /// An ID variant that is compatible with [`crate::entity::Entity`]. - Entity = 0, - /// A future ID variant. - Placeholder = 0b1000_0000, -} diff --git a/crates/bevy_ecs/src/identifier/masks.rs b/crates/bevy_ecs/src/identifier/masks.rs deleted file mode 100644 index 30ece9d8e3..0000000000 --- a/crates/bevy_ecs/src/identifier/masks.rs +++ /dev/null @@ -1,233 +0,0 @@ -use core::num::NonZero; - -use super::kinds::IdKind; - -/// Mask for extracting the value portion of a 32-bit high segment. This -/// yields 31-bits of total value, as the final bit (the most significant) -/// is reserved as a flag bit. Can be negated to extract the flag bit. -pub(crate) const HIGH_MASK: u32 = 0x7FFF_FFFF; - -/// Abstraction over masks needed to extract values/components of an [`super::Identifier`]. -pub(crate) struct IdentifierMask; - -impl IdentifierMask { - /// Returns the low component from a `u64` value - #[inline(always)] - pub(crate) const fn get_low(value: u64) -> u32 { - // This will truncate to the lowest 32 bits - value as u32 - } - - /// Returns the high component from a `u64` value - #[inline(always)] - pub(crate) const fn get_high(value: u64) -> u32 { - // This will discard the lowest 32 bits - (value >> u32::BITS) as u32 - } - - /// Pack a low and high `u32` values into a single `u64` value. - #[inline(always)] - pub(crate) const fn pack_into_u64(low: u32, high: u32) -> u64 { - ((high as u64) << u32::BITS) | (low as u64) - } - - /// Pack the [`IdKind`] bits into a high segment. - #[inline(always)] - pub(crate) const fn pack_kind_into_high(value: u32, kind: IdKind) -> u32 { - value | ((kind as u32) << 24) - } - - /// Extract the value component from a high segment of an [`super::Identifier`]. - #[inline(always)] - pub(crate) const fn extract_value_from_high(value: u32) -> u32 { - value & HIGH_MASK - } - - /// Extract the ID kind component from a high segment of an [`super::Identifier`]. - #[inline(always)] - pub(crate) const fn extract_kind_from_high(value: u32) -> IdKind { - // The negated HIGH_MASK will extract just the bit we need for kind. - let kind_mask = !HIGH_MASK; - let bit = value & kind_mask; - - if bit == kind_mask { - IdKind::Placeholder - } else { - IdKind::Entity - } - } - - /// Offsets a masked generation value by the specified amount, wrapping to 1 instead of 0. - /// Will never be greater than [`HIGH_MASK`] or less than `1`, and increments are masked to - /// never be greater than [`HIGH_MASK`]. - #[inline(always)] - pub(crate) const fn inc_masked_high_by(lhs: NonZero, rhs: u32) -> NonZero { - let lo = (lhs.get() & HIGH_MASK).wrapping_add(rhs & HIGH_MASK); - // Checks high 32 bit for whether we have overflowed 31 bits. - let overflowed = lo >> 31; - - // SAFETY: - // - Adding the overflow flag will offset overflows to start at 1 instead of 0 - // - The sum of `0x7FFF_FFFF` + `u32::MAX` + 1 (overflow) == `0x7FFF_FFFF` - // - If the operation doesn't overflow at 31 bits, no offsetting takes place - unsafe { NonZero::::new_unchecked(lo.wrapping_add(overflowed) & HIGH_MASK) } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn get_u64_parts() { - // Two distinct bit patterns per low/high component - let value: u64 = 0x7FFF_FFFF_0000_000C; - - assert_eq!(IdentifierMask::get_low(value), 0x0000_000C); - assert_eq!(IdentifierMask::get_high(value), 0x7FFF_FFFF); - } - - #[test] - fn extract_kind() { - // All bits are ones. - let high: u32 = 0xFFFF_FFFF; - - assert_eq!( - IdentifierMask::extract_kind_from_high(high), - IdKind::Placeholder - ); - - // Second and second to last bits are ones. - let high: u32 = 0x4000_0002; - - assert_eq!(IdentifierMask::extract_kind_from_high(high), IdKind::Entity); - } - - #[test] - fn extract_high_value() { - // All bits are ones. - let high: u32 = 0xFFFF_FFFF; - - // Excludes the most significant bit as that is a flag bit. - assert_eq!(IdentifierMask::extract_value_from_high(high), 0x7FFF_FFFF); - - // Start bit and end bit are ones. - let high: u32 = 0x8000_0001; - - assert_eq!(IdentifierMask::extract_value_from_high(high), 0x0000_0001); - - // Classic bit pattern. - let high: u32 = 0xDEAD_BEEF; - - assert_eq!(IdentifierMask::extract_value_from_high(high), 0x5EAD_BEEF); - } - - #[test] - fn pack_kind_bits() { - // All bits are ones expect the most significant bit, which is zero - let high: u32 = 0x7FFF_FFFF; - - assert_eq!( - IdentifierMask::pack_kind_into_high(high, IdKind::Placeholder), - 0xFFFF_FFFF - ); - - // Arbitrary bit pattern - let high: u32 = 0x00FF_FF00; - - assert_eq!( - IdentifierMask::pack_kind_into_high(high, IdKind::Entity), - // Remains unchanged as before - 0x00FF_FF00 - ); - - // Bit pattern that almost spells a word - let high: u32 = 0x40FF_EEEE; - - assert_eq!( - IdentifierMask::pack_kind_into_high(high, IdKind::Placeholder), - 0xC0FF_EEEE // Milk and no sugar, please. - ); - } - - #[test] - fn pack_into_u64() { - let high: u32 = 0x7FFF_FFFF; - let low: u32 = 0x0000_00CC; - - assert_eq!( - IdentifierMask::pack_into_u64(low, high), - 0x7FFF_FFFF_0000_00CC - ); - } - - #[test] - fn incrementing_masked_nonzero_high_is_safe() { - // Adding from lowest value with lowest to highest increment - // No result should ever be greater than 0x7FFF_FFFF or HIGH_MASK - assert_eq!( - NonZero::::MIN, - IdentifierMask::inc_masked_high_by(NonZero::::MIN, 0) - ); - assert_eq!( - NonZero::::new(2).unwrap(), - IdentifierMask::inc_masked_high_by(NonZero::::MIN, 1) - ); - assert_eq!( - NonZero::::new(3).unwrap(), - IdentifierMask::inc_masked_high_by(NonZero::::MIN, 2) - ); - assert_eq!( - NonZero::::MIN, - IdentifierMask::inc_masked_high_by(NonZero::::MIN, HIGH_MASK) - ); - assert_eq!( - NonZero::::MIN, - IdentifierMask::inc_masked_high_by(NonZero::::MIN, u32::MAX) - ); - // Adding from absolute highest value with lowest to highest increment - // No result should ever be greater than 0x7FFF_FFFF or HIGH_MASK - assert_eq!( - NonZero::::new(HIGH_MASK).unwrap(), - IdentifierMask::inc_masked_high_by(NonZero::::MAX, 0) - ); - assert_eq!( - NonZero::::MIN, - IdentifierMask::inc_masked_high_by(NonZero::::MAX, 1) - ); - assert_eq!( - NonZero::::new(2).unwrap(), - IdentifierMask::inc_masked_high_by(NonZero::::MAX, 2) - ); - assert_eq!( - NonZero::::new(HIGH_MASK).unwrap(), - IdentifierMask::inc_masked_high_by(NonZero::::MAX, HIGH_MASK) - ); - assert_eq!( - NonZero::::new(HIGH_MASK).unwrap(), - IdentifierMask::inc_masked_high_by(NonZero::::MAX, u32::MAX) - ); - // Adding from actual highest value with lowest to highest increment - // No result should ever be greater than 0x7FFF_FFFF or HIGH_MASK - assert_eq!( - NonZero::::new(HIGH_MASK).unwrap(), - IdentifierMask::inc_masked_high_by(NonZero::::new(HIGH_MASK).unwrap(), 0) - ); - assert_eq!( - NonZero::::MIN, - IdentifierMask::inc_masked_high_by(NonZero::::new(HIGH_MASK).unwrap(), 1) - ); - assert_eq!( - NonZero::::new(2).unwrap(), - IdentifierMask::inc_masked_high_by(NonZero::::new(HIGH_MASK).unwrap(), 2) - ); - assert_eq!( - NonZero::::new(HIGH_MASK).unwrap(), - IdentifierMask::inc_masked_high_by(NonZero::::new(HIGH_MASK).unwrap(), HIGH_MASK) - ); - assert_eq!( - NonZero::::new(HIGH_MASK).unwrap(), - IdentifierMask::inc_masked_high_by(NonZero::::new(HIGH_MASK).unwrap(), u32::MAX) - ); - } -} diff --git a/crates/bevy_ecs/src/identifier/mod.rs b/crates/bevy_ecs/src/identifier/mod.rs deleted file mode 100644 index c08ea7b4aa..0000000000 --- a/crates/bevy_ecs/src/identifier/mod.rs +++ /dev/null @@ -1,249 +0,0 @@ -//! A module for the unified [`Identifier`] ID struct, for use as a representation -//! of multiple types of IDs in a single, packed type. Allows for describing an [`crate::entity::Entity`], -//! or other IDs that can be packed and expressed within a `u64` sized type. -//! [`Identifier`]s cannot be created directly, only able to be converted from other -//! compatible IDs. -#[cfg(feature = "bevy_reflect")] -use bevy_reflect::Reflect; - -use self::{error::IdentifierError, kinds::IdKind, masks::IdentifierMask}; -use core::{hash::Hash, num::NonZero}; - -pub mod error; -pub(crate) mod kinds; -pub(crate) mod masks; - -/// A unified identifier for all entity and similar IDs. -/// -/// Has the same size as a `u64` integer, but the layout is split between a 32-bit low -/// segment, a 31-bit high segment, and the significant bit reserved as type flags to denote -/// entity kinds. -#[derive(Debug, Clone, Copy)] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] -#[cfg_attr(feature = "bevy_reflect", reflect(opaque))] -#[cfg_attr(feature = "bevy_reflect", reflect(Debug, Hash, PartialEq, Clone))] -// Alignment repr necessary to allow LLVM to better output -// optimized codegen for `to_bits`, `PartialEq` and `Ord`. -#[repr(C, align(8))] -pub struct Identifier { - // Do not reorder the fields here. The ordering is explicitly used by repr(C) - // to make this struct equivalent to a u64. - #[cfg(target_endian = "little")] - low: u32, - high: NonZero, - #[cfg(target_endian = "big")] - low: u32, -} - -impl Identifier { - /// Construct a new [`Identifier`]. The `high` parameter is masked with the - /// `kind` so to pack the high value and bit flags into the same field. - #[inline(always)] - pub const fn new(low: u32, high: u32, kind: IdKind) -> Result { - // the high bits are masked to cut off the most significant bit - // as these are used for the type flags. This means that the high - // portion is only 31 bits, but this still provides 2^31 - // values/kinds/ids that can be stored in this segment. - let masked_value = IdentifierMask::extract_value_from_high(high); - - let packed_high = IdentifierMask::pack_kind_into_high(masked_value, kind); - - // If the packed high component ends up being zero, that means that we tried - // to initialize an Identifier into an invalid state. - if packed_high == 0 { - Err(IdentifierError::InvalidIdentifier) - } else { - // SAFETY: The high value has been checked to ensure it is never - // zero. - unsafe { - Ok(Self { - low, - high: NonZero::::new_unchecked(packed_high), - }) - } - } - } - - /// Returns the value of the low segment of the [`Identifier`]. - #[inline(always)] - pub const fn low(self) -> u32 { - self.low - } - - /// Returns the value of the high segment of the [`Identifier`]. This - /// does not apply any masking. - #[inline(always)] - pub const fn high(self) -> NonZero { - self.high - } - - /// Returns the masked value of the high segment of the [`Identifier`]. - /// Does not include the flag bits. - #[inline(always)] - pub const fn masked_high(self) -> u32 { - IdentifierMask::extract_value_from_high(self.high.get()) - } - - /// Returns the kind of [`Identifier`] from the high segment. - #[inline(always)] - pub const fn kind(self) -> IdKind { - IdentifierMask::extract_kind_from_high(self.high.get()) - } - - /// Convert the [`Identifier`] into a `u64`. - #[inline(always)] - pub const fn to_bits(self) -> u64 { - IdentifierMask::pack_into_u64(self.low, self.high.get()) - } - - /// Convert a `u64` into an [`Identifier`]. - /// - /// # Panics - /// - /// This method will likely panic if given `u64` values that did not come from [`Identifier::to_bits`]. - #[inline(always)] - pub const fn from_bits(value: u64) -> Self { - let id = Self::try_from_bits(value); - - match id { - Ok(id) => id, - Err(_) => panic!("Attempted to initialize invalid bits as an id"), - } - } - - /// Convert a `u64` into an [`Identifier`]. - /// - /// This method is the fallible counterpart to [`Identifier::from_bits`]. - #[inline(always)] - pub const fn try_from_bits(value: u64) -> Result { - let high = NonZero::::new(IdentifierMask::get_high(value)); - - match high { - Some(high) => Ok(Self { - low: IdentifierMask::get_low(value), - high, - }), - None => Err(IdentifierError::InvalidIdentifier), - } - } -} - -// By not short-circuiting in comparisons, we get better codegen. -// See -impl PartialEq for Identifier { - #[inline] - fn eq(&self, other: &Self) -> bool { - // By using `to_bits`, the codegen can be optimized out even - // further potentially. Relies on the correct alignment/field - // order of `Entity`. - self.to_bits() == other.to_bits() - } -} - -impl Eq for Identifier {} - -// The derive macro codegen output is not optimal and can't be optimized as well -// by the compiler. This impl resolves the issue of non-optimal codegen by relying -// on comparing against the bit representation of `Entity` instead of comparing -// the fields. The result is then LLVM is able to optimize the codegen for Entity -// far beyond what the derive macro can. -// See -impl PartialOrd for Identifier { - #[inline] - fn partial_cmp(&self, other: &Self) -> Option { - // Make use of our `Ord` impl to ensure optimal codegen output - Some(self.cmp(other)) - } -} - -// The derive macro codegen output is not optimal and can't be optimized as well -// by the compiler. This impl resolves the issue of non-optimal codegen by relying -// on comparing against the bit representation of `Entity` instead of comparing -// the fields. The result is then LLVM is able to optimize the codegen for Entity -// far beyond what the derive macro can. -// See -impl Ord for Identifier { - #[inline] - fn cmp(&self, other: &Self) -> core::cmp::Ordering { - // This will result in better codegen for ordering comparisons, plus - // avoids pitfalls with regards to macro codegen relying on property - // position when we want to compare against the bit representation. - self.to_bits().cmp(&other.to_bits()) - } -} - -impl Hash for Identifier { - #[inline] - fn hash(&self, state: &mut H) { - self.to_bits().hash(state); - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn id_construction() { - let id = Identifier::new(12, 55, IdKind::Entity).unwrap(); - - assert_eq!(id.low(), 12); - assert_eq!(id.high().get(), 55); - assert_eq!( - IdentifierMask::extract_kind_from_high(id.high().get()), - IdKind::Entity - ); - } - - #[test] - fn from_bits() { - // This high value should correspond to the max high() value - // and also Entity flag. - let high = 0x7FFFFFFF; - let low = 0xC; - let bits: u64 = (high << u32::BITS) | low; - - let id = Identifier::try_from_bits(bits).unwrap(); - - assert_eq!(id.to_bits(), 0x7FFFFFFF0000000C); - assert_eq!(id.low(), low as u32); - assert_eq!(id.high().get(), 0x7FFFFFFF); - assert_eq!( - IdentifierMask::extract_kind_from_high(id.high().get()), - IdKind::Entity - ); - } - - #[rustfmt::skip] - #[test] - #[expect( - clippy::nonminimal_bool, - reason = "This intentionally tests all possible comparison operators as separate functions; thus, we don't want to rewrite these comparisons to use different operators." - )] - fn id_comparison() { - assert!(Identifier::new(123, 456, IdKind::Entity).unwrap() == Identifier::new(123, 456, IdKind::Entity).unwrap()); - assert!(Identifier::new(123, 456, IdKind::Placeholder).unwrap() == Identifier::new(123, 456, IdKind::Placeholder).unwrap()); - assert!(Identifier::new(123, 789, IdKind::Entity).unwrap() != Identifier::new(123, 456, IdKind::Entity).unwrap()); - assert!(Identifier::new(123, 456, IdKind::Entity).unwrap() != Identifier::new(123, 789, IdKind::Entity).unwrap()); - assert!(Identifier::new(123, 456, IdKind::Entity).unwrap() != Identifier::new(456, 123, IdKind::Entity).unwrap()); - assert!(Identifier::new(123, 456, IdKind::Entity).unwrap() != Identifier::new(123, 456, IdKind::Placeholder).unwrap()); - - // ordering is by flag then high then by low - - assert!(Identifier::new(123, 456, IdKind::Entity).unwrap() >= Identifier::new(123, 456, IdKind::Entity).unwrap()); - assert!(Identifier::new(123, 456, IdKind::Entity).unwrap() <= Identifier::new(123, 456, IdKind::Entity).unwrap()); - assert!(!(Identifier::new(123, 456, IdKind::Entity).unwrap() < Identifier::new(123, 456, IdKind::Entity).unwrap())); - assert!(!(Identifier::new(123, 456, IdKind::Entity).unwrap() > Identifier::new(123, 456, IdKind::Entity).unwrap())); - - assert!(Identifier::new(9, 1, IdKind::Entity).unwrap() < Identifier::new(1, 9, IdKind::Entity).unwrap()); - assert!(Identifier::new(1, 9, IdKind::Entity).unwrap() > Identifier::new(9, 1, IdKind::Entity).unwrap()); - - assert!(Identifier::new(9, 1, IdKind::Entity).unwrap() < Identifier::new(9, 1, IdKind::Placeholder).unwrap()); - assert!(Identifier::new(1, 9, IdKind::Placeholder).unwrap() > Identifier::new(1, 9, IdKind::Entity).unwrap()); - - assert!(Identifier::new(1, 1, IdKind::Entity).unwrap() < Identifier::new(2, 1, IdKind::Entity).unwrap()); - assert!(Identifier::new(1, 1, IdKind::Entity).unwrap() <= Identifier::new(2, 1, IdKind::Entity).unwrap()); - assert!(Identifier::new(2, 2, IdKind::Entity).unwrap() > Identifier::new(1, 2, IdKind::Entity).unwrap()); - assert!(Identifier::new(2, 2, IdKind::Entity).unwrap() >= Identifier::new(1, 2, IdKind::Entity).unwrap()); - } -} diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 99f95763d5..0a2e1862dd 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -39,7 +39,6 @@ pub mod entity_disabling; pub mod error; pub mod event; pub mod hierarchy; -pub mod identifier; pub mod intern; pub mod label; pub mod name; @@ -64,10 +63,6 @@ pub use bevy_ptr as ptr; /// /// This includes the most common types in this crate, re-exported for your convenience. pub mod prelude { - #[expect( - deprecated, - reason = "`crate::schedule::apply_deferred` is considered deprecated; however, it may still be used by crates which consume `bevy_ecs`, so its removal here may cause confusion. It is intended to be removed in the Bevy 0.17 cycle." - )] #[doc(hidden)] pub use crate::{ bundle::Bundle, @@ -80,21 +75,21 @@ pub mod prelude { hierarchy::{ChildOf, ChildSpawner, ChildSpawnerCommands, Children}, name::{Name, NameOrEntity}, observer::{Observer, Trigger}, - query::{Added, AnyOf, Changed, Has, Or, QueryBuilder, QueryState, With, Without}, + query::{Added, Allows, AnyOf, Changed, Has, Or, QueryBuilder, QueryState, With, Without}, related, relationship::RelationshipTarget, removal_detection::RemovedComponents, resource::Resource, schedule::{ - apply_deferred, common_conditions::*, ApplyDeferred, Condition, IntoScheduleConfigs, - IntoSystemSet, Schedule, Schedules, SystemSet, + common_conditions::*, ApplyDeferred, Condition, IntoScheduleConfigs, IntoSystemSet, + Schedule, Schedules, SystemSet, }, spawn::{Spawn, SpawnRelated}, system::{ Command, Commands, Deferred, EntityCommand, EntityCommands, In, InMut, InRef, IntoSystem, Local, NonSend, NonSendMut, ParamSet, Populated, Query, ReadOnlySystem, Res, ResMut, Single, System, SystemIn, SystemInput, SystemParamBuilder, - SystemParamFunction, + SystemParamFunction, When, }, world::{ EntityMut, EntityRef, EntityWorldMut, FilteredResources, FilteredResourcesMut, @@ -152,7 +147,6 @@ mod tests { use core::{ any::TypeId, marker::PhantomData, - num::NonZero, sync::atomic::{AtomicUsize, Ordering}, }; use std::sync::Mutex; @@ -489,10 +483,9 @@ mod tests { results.lock().unwrap().push((e, i)); }); results.lock().unwrap().sort(); - assert_eq!( - &*results.lock().unwrap(), - &[(e1, 1), (e2, 2), (e3, 3), (e4, 4), (e5, 5)] - ); + let mut expected = [(e1, 1), (e2, 2), (e3, 3), (e4, 4), (e5, 5)]; + expected.sort(); + assert_eq!(&*results.lock().unwrap(), &expected); } #[test] @@ -510,10 +503,9 @@ mod tests { .par_iter(&world) .for_each(|(e, &SparseStored(i))| results.lock().unwrap().push((e, i))); results.lock().unwrap().sort(); - assert_eq!( - &*results.lock().unwrap(), - &[(e1, 1), (e2, 2), (e3, 3), (e4, 4), (e5, 5)] - ); + let mut expected = [(e1, 1), (e2, 2), (e3, 3), (e4, 4), (e5, 5)]; + expected.sort(); + assert_eq!(&*results.lock().unwrap(), &expected); } #[test] @@ -1543,8 +1535,8 @@ mod tests { let mut world_a = World::new(); let world_b = World::new(); let mut query = world_a.query::<&A>(); - let _ = query.get(&world_a, Entity::from_raw(0)); - let _ = query.get(&world_b, Entity::from_raw(0)); + let _ = query.get(&world_a, Entity::from_raw_u32(0).unwrap()); + let _ = query.get(&world_b, Entity::from_raw_u32(0).unwrap()); } #[test] @@ -1697,97 +1689,6 @@ mod tests { assert_eq!(0, query_min_size![(&A, &B), Or<(Changed, Changed)>]); } - #[test] - fn insert_or_spawn_batch() { - let mut world = World::default(); - let e0 = world.spawn(A(0)).id(); - let e1 = Entity::from_raw(1); - - let values = vec![(e0, (B(0), C)), (e1, (B(1), C))]; - - #[expect( - deprecated, - reason = "This needs to be supported for now, and therefore still needs the test." - )] - world.insert_or_spawn_batch(values).unwrap(); - - assert_eq!( - world.get::(e0), - Some(&A(0)), - "existing component was preserved" - ); - assert_eq!( - world.get::(e0), - Some(&B(0)), - "pre-existing entity received correct B component" - ); - assert_eq!( - world.get::(e1), - Some(&B(1)), - "new entity was spawned and received correct B component" - ); - assert_eq!( - world.get::(e0), - Some(&C), - "pre-existing entity received C component" - ); - assert_eq!( - world.get::(e1), - Some(&C), - "new entity was spawned and received C component" - ); - } - - #[test] - fn insert_or_spawn_batch_invalid() { - let mut world = World::default(); - let e0 = world.spawn(A(0)).id(); - let e1 = Entity::from_raw(1); - let e2 = world.spawn_empty().id(); - let invalid_e2 = - Entity::from_raw_and_generation(e2.index(), NonZero::::new(2).unwrap()); - - let values = vec![(e0, (B(0), C)), (e1, (B(1), C)), (invalid_e2, (B(2), C))]; - - #[expect( - deprecated, - reason = "This needs to be supported for now, and therefore still needs the test." - )] - let result = world.insert_or_spawn_batch(values); - - assert_eq!( - result, - Err(vec![invalid_e2]), - "e2 failed to be spawned or inserted into" - ); - - assert_eq!( - world.get::(e0), - Some(&A(0)), - "existing component was preserved" - ); - assert_eq!( - world.get::(e0), - Some(&B(0)), - "pre-existing entity received correct B component" - ); - assert_eq!( - world.get::(e1), - Some(&B(1)), - "new entity was spawned and received correct B component" - ); - assert_eq!( - world.get::(e0), - Some(&C), - "pre-existing entity received C component" - ); - assert_eq!( - world.get::(e1), - Some(&C), - "new entity was spawned and received C component" - ); - } - #[test] fn insert_batch() { let mut world = World::default(); @@ -1876,7 +1777,7 @@ mod tests { fn try_insert_batch() { let mut world = World::default(); let e0 = world.spawn(A(0)).id(); - let e1 = Entity::from_raw(1); + let e1 = Entity::from_raw_u32(1).unwrap(); let values = vec![(e0, (A(1), B(0))), (e1, (A(0), B(1)))]; @@ -1900,7 +1801,7 @@ mod tests { fn try_insert_batch_if_new() { let mut world = World::default(); let e0 = world.spawn(A(0)).id(); - let e1 = Entity::from_raw(1); + let e1 = Entity::from_raw_u32(1).unwrap(); let values = vec![(e0, (A(1), B(0))), (e1, (A(0), B(1)))]; @@ -2843,4 +2744,27 @@ mod tests { )] #[derive(Component)] struct MyEntitiesTuple(#[entities] Vec, #[entities] Entity, usize); + + #[test] + fn clone_entities() { + use crate::entity::{ComponentCloneCtx, SourceComponent}; + + #[derive(Component)] + #[component(clone_behavior = Ignore)] + struct IgnoreClone; + + #[derive(Component)] + #[component(clone_behavior = Default)] + struct DefaultClone; + + #[derive(Component)] + #[component(clone_behavior = Custom(custom_clone))] + struct CustomClone; + + #[derive(Component, Clone)] + #[component(clone_behavior = clone::())] + struct CloneFunction; + + fn custom_clone(_source: &SourceComponent, _ctx: &mut ComponentCloneCtx) {} + } } diff --git a/crates/bevy_ecs/src/name.rs b/crates/bevy_ecs/src/name.rs index dd34f5578a..cd2e946678 100644 --- a/crates/bevy_ecs/src/name.rs +++ b/crates/bevy_ecs/src/name.rs @@ -276,7 +276,7 @@ mod tests { let d1 = query.get(&world, e1).unwrap(); let d2 = query.get(&world, e2).unwrap(); // NameOrEntity Display for entities without a Name should be {index}v{generation} - assert_eq!(d1.to_string(), "0v1"); + assert_eq!(d1.to_string(), "0v0"); // NameOrEntity Display for entities with a Name should be the Name assert_eq!(d2.to_string(), "MyName"); } diff --git a/crates/bevy_ecs/src/observer/entity_observer.rs b/crates/bevy_ecs/src/observer/entity_observer.rs index d69f7764fe..2c2d42b1c9 100644 --- a/crates/bevy_ecs/src/observer/entity_observer.rs +++ b/crates/bevy_ecs/src/observer/entity_observer.rs @@ -3,11 +3,12 @@ use crate::{ Component, ComponentCloneBehavior, ComponentHook, HookContext, Mutable, StorageType, }, entity::{ComponentCloneCtx, Entity, EntityClonerBuilder, EntityMapper, SourceComponent}, - observer::ObserverState, world::World, }; use alloc::vec::Vec; +use super::Observer; + /// Tracks a list of entity observers for the [`Entity`] [`ObservedBy`] is added to. #[derive(Default)] pub struct ObservedBy(pub(crate) Vec); @@ -27,7 +28,7 @@ impl Component for ObservedBy { let Ok(mut entity_mut) = world.get_entity_mut(e) else { continue; }; - let Some(mut state) = entity_mut.get_mut::() else { + let Some(mut state) = entity_mut.get_mut::() else { continue; }; state.despawned_watched_entities += 1; @@ -77,10 +78,10 @@ fn component_clone_observed_by(_source: &SourceComponent, ctx: &mut ComponentClo .entity_mut(target) .insert(ObservedBy(observed_by.clone())); - for observer in &observed_by { + for observer_entity in observed_by.iter().copied() { let mut observer_state = world - .get_mut::(*observer) - .expect("Source observer entity must have ObserverState"); + .get_mut::(observer_entity) + .expect("Source observer entity must have Observer"); observer_state.descriptor.entities.push(target); let event_types = observer_state.descriptor.events.clone(); let components = observer_state.descriptor.components.clone(); diff --git a/crates/bevy_ecs/src/observer/mod.rs b/crates/bevy_ecs/src/observer/mod.rs index 78569bc4ec..2343e66aa1 100644 --- a/crates/bevy_ecs/src/observer/mod.rs +++ b/crates/bevy_ecs/src/observer/mod.rs @@ -315,13 +315,6 @@ impl ObserverDescriptor { self } - pub(crate) fn merge(&mut self, descriptor: &ObserverDescriptor) { - self.events.extend(descriptor.events.iter().copied()); - self.components - .extend(descriptor.components.iter().copied()); - self.entities.extend(descriptor.entities.iter().copied()); - } - /// Returns the `events` that the observer is watching. pub fn events(&self) -> &[ComponentId] { &self.events @@ -349,7 +342,7 @@ pub struct ObserverTrigger { components: SmallVec<[ComponentId; 2]>, /// The entity the trigger targeted. pub target: Entity, - /// The location of the source code that triggered the obserer. + /// The location of the source code that triggered the observer. pub caller: MaybeLocation, } @@ -562,6 +555,10 @@ impl World { /// // ... /// }); /// ``` + /// + /// # Panics + /// + /// Panics if the given system is an exclusive system. pub fn add_observer( &mut self, system: impl IntoObserverSystem, @@ -724,11 +721,10 @@ impl World { pub(crate) fn register_observer(&mut self, observer_entity: Entity) { // SAFETY: References do not alias. let (observer_state, archetypes, observers) = unsafe { - let observer_state: *const ObserverState = - self.get::(observer_entity).unwrap(); + let observer_state: *const Observer = self.get::(observer_entity).unwrap(); // Populate ObservedBy for each observed entity. - for watched_entity in &(*observer_state).descriptor.entities { - let mut entity_mut = self.entity_mut(*watched_entity); + for watched_entity in (*observer_state).descriptor.entities.iter().copied() { + let mut entity_mut = self.entity_mut(watched_entity); let mut observed_by = entity_mut.entry::().or_default().into_mut(); observed_by.0.push(observer_entity); } @@ -849,7 +845,7 @@ mod tests { use crate::component::ComponentId; use crate::{ change_detection::MaybeLocation, - observer::{Observer, ObserverDescriptor, ObserverState, OnReplace}, + observer::{Observer, OnReplace}, prelude::*, traversal::Traversal, }; @@ -1079,7 +1075,7 @@ mod tests { world.add_observer(|_: Trigger, mut res: ResMut| res.observed("add_2")); world.spawn(A).flush(); - assert_eq!(vec!["add_1", "add_2"], world.resource::().0); + assert_eq!(vec!["add_2", "add_1"], world.resource::().0); // Our A entity plus our two observers assert_eq!(world.entities().len(), 3); } @@ -1364,14 +1360,14 @@ mod tests { world.init_resource::(); let event_a = OnRemove::register_component_id(&mut world); - world.spawn(ObserverState { - // SAFETY: we registered `event_a` above and it matches the type of EventA - descriptor: unsafe { ObserverDescriptor::default().with_events(vec![event_a]) }, - runner: |mut world, _trigger, _ptr, _propagate| { + // SAFETY: we registered `event_a` above and it matches the type of EventA + let observe = unsafe { + Observer::with_dynamic_runner(|mut world, _trigger, _ptr, _propagate| { world.resource_mut::().observed("event_a"); - }, - ..Default::default() - }); + }) + .with_event(event_a) + }; + world.spawn(observe); world.commands().queue(move |world: &mut World| { // SAFETY: we registered `event_a` above and it matches the type of EventA diff --git a/crates/bevy_ecs/src/observer/runner.rs b/crates/bevy_ecs/src/observer/runner.rs index d68c495dab..be7bc4ede2 100644 --- a/crates/bevy_ecs/src/observer/runner.rs +++ b/crates/bevy_ecs/src/observer/runner.rs @@ -1,9 +1,9 @@ -use alloc::{boxed::Box, vec, vec::Vec}; +use alloc::{boxed::Box, vec}; use core::any::Any; use crate::{ component::{ComponentHook, ComponentId, HookContext, Mutable, StorageType}, - error::{default_error_handler, ErrorContext}, + error::{ErrorContext, ErrorHandler}, observer::{ObserverDescriptor, ObserverTrigger}, prelude::*, query::DebugCheckedUnwrap, @@ -12,85 +12,6 @@ use crate::{ }; use bevy_ptr::PtrMut; -/// Contains [`Observer`] information. This defines how a given observer behaves. It is the -/// "source of truth" for a given observer entity's behavior. -pub struct ObserverState { - pub(crate) descriptor: ObserverDescriptor, - pub(crate) runner: ObserverRunner, - pub(crate) last_trigger_id: u32, - pub(crate) despawned_watched_entities: u32, -} - -impl Default for ObserverState { - fn default() -> Self { - Self { - runner: |_, _, _, _| {}, - last_trigger_id: 0, - despawned_watched_entities: 0, - descriptor: Default::default(), - } - } -} - -impl ObserverState { - /// Observe the given `event`. This will cause the [`Observer`] to run whenever an event with the given [`ComponentId`] - /// is triggered. - pub fn with_event(mut self, event: ComponentId) -> Self { - self.descriptor.events.push(event); - self - } - - /// Observe the given event list. This will cause the [`Observer`] to run whenever an event with any of the given [`ComponentId`]s - /// is triggered. - pub fn with_events(mut self, events: impl IntoIterator) -> Self { - self.descriptor.events.extend(events); - self - } - - /// Observe the given [`Entity`] list. This will cause the [`Observer`] to run whenever the [`Event`] is triggered - /// for any [`Entity`] target in the list. - pub fn with_entities(mut self, entities: impl IntoIterator) -> Self { - self.descriptor.entities.extend(entities); - self - } - - /// Observe the given [`ComponentId`] list. This will cause the [`Observer`] to run whenever the [`Event`] is triggered - /// for any [`ComponentId`] target in the list. - pub fn with_components(mut self, components: impl IntoIterator) -> Self { - self.descriptor.components.extend(components); - self - } -} - -impl Component for ObserverState { - const STORAGE_TYPE: StorageType = StorageType::SparseSet; - type Mutability = Mutable; - - fn on_add() -> Option { - Some(|mut world, HookContext { entity, .. }| { - world.commands().queue(move |world: &mut World| { - world.register_observer(entity); - }); - }) - } - - fn on_remove() -> Option { - Some(|mut world, HookContext { entity, .. }| { - let descriptor = core::mem::take( - &mut world - .entity_mut(entity) - .get_mut::() - .unwrap() - .as_mut() - .descriptor, - ); - world.commands().queue(move |world: &mut World| { - world.unregister_observer(entity, descriptor); - }); - }) - } -} - /// Type for function that is run when an observer is triggered. /// /// Typically refers to the default runner that runs the system stored in the associated [`Observer`] component, @@ -264,27 +185,71 @@ pub type ObserverRunner = fn(DeferredWorld, ObserverTrigger, PtrMut, propagate: /// Note that the [`Observer`] component is not added to the entity it is observing. Observers should always be their own entities! /// /// You can call [`Observer::watch_entity`] more than once, which allows you to watch multiple entities with the same [`Observer`]. -/// -/// When first added, [`Observer`] will also create an [`ObserverState`] component, which registers the observer with the [`World`] and /// serves as the "source of truth" of the observer. /// /// [`SystemParam`]: crate::system::SystemParam pub struct Observer { - system: Box, - descriptor: ObserverDescriptor, hook_on_add: ComponentHook, - error_handler: Option, + error_handler: Option, + system: Box, + pub(crate) descriptor: ObserverDescriptor, + pub(crate) last_trigger_id: u32, + pub(crate) despawned_watched_entities: u32, + pub(crate) runner: ObserverRunner, } impl Observer { /// Creates a new [`Observer`], which defaults to a "global" observer. This means it will run whenever the event `E` is triggered /// for _any_ entity (or no entity). + /// + /// # Panics + /// + /// Panics if the given system is an exclusive system. pub fn new>(system: I) -> Self { + let system = Box::new(IntoObserverSystem::into_system(system)); + assert!( + !system.is_exclusive(), + concat!( + "Exclusive system `{}` may not be used as observer.\n", + "Instead of `&mut World`, use either `DeferredWorld` if you do not need structural changes, or `Commands` if you do." + ), + system.name() + ); Self { - system: Box::new(IntoObserverSystem::into_system(system)), + system, descriptor: Default::default(), hook_on_add: hook_on_add::, error_handler: None, + runner: observer_system_runner::, + despawned_watched_entities: 0, + last_trigger_id: 0, + } + } + + /// Creates a new [`Observer`] with custom runner, this is mostly used for dynamic event observer + pub fn with_dynamic_runner(runner: ObserverRunner) -> Self { + Self { + system: Box::new(|| {}), + descriptor: Default::default(), + hook_on_add: |mut world, hook_context| { + let default_error_handler = world.default_error_handler(); + world.commands().queue(move |world: &mut World| { + let entity = hook_context.entity; + if let Some(mut observe) = world.get_mut::(entity) { + if observe.descriptor.events.is_empty() { + return; + } + if observe.error_handler.is_none() { + observe.error_handler = Some(default_error_handler); + } + world.register_observer(entity); + } + }); + }, + error_handler: None, + runner, + despawned_watched_entities: 0, + last_trigger_id: 0, } } @@ -345,6 +310,21 @@ impl Component for Observer { hook(world, context); }) } + fn on_remove() -> Option { + Some(|mut world, HookContext { entity, .. }| { + let descriptor = core::mem::take( + &mut world + .entity_mut(entity) + .get_mut::() + .unwrap() + .as_mut() + .descriptor, + ); + world.commands().queue(move |world: &mut World| { + world.unregister_observer(entity, descriptor); + }); + }) + } } fn observer_system_runner>( @@ -360,12 +340,8 @@ fn observer_system_runner>( .get_entity(observer_trigger.observer) .debug_checked_unwrap() }; - // SAFETY: Observer was triggered so must have an `ObserverState` - let mut state = unsafe { - observer_cell - .get_mut::() - .debug_checked_unwrap() - }; + // SAFETY: Observer was triggered so must have an `Observer` + let mut state = unsafe { observer_cell.get_mut::().debug_checked_unwrap() }; // TODO: Move this check into the observer cache to avoid dynamic dispatch let last_trigger = world.last_trigger_id(); @@ -374,27 +350,18 @@ fn observer_system_runner>( } state.last_trigger_id = last_trigger; - // SAFETY: Observer was triggered so must have an `Observer` component. - let error_handler = unsafe { - observer_cell - .get::() - .debug_checked_unwrap() - .error_handler - .debug_checked_unwrap() - }; - let trigger: Trigger = Trigger::new( // SAFETY: Caller ensures `ptr` is castable to `&mut T` unsafe { ptr.deref_mut() }, propagate, observer_trigger, ); + // SAFETY: // - observer was triggered so must have an `Observer` component. // - observer cannot be dropped or mutated until after the system pointer is already dropped. let system: *mut dyn ObserverSystem = unsafe { - let mut observe = observer_cell.get_mut::().debug_checked_unwrap(); - let system = observe.system.downcast_mut::().unwrap(); + let system = state.system.downcast_mut::().debug_checked_unwrap(); &mut *system }; @@ -409,7 +376,10 @@ fn observer_system_runner>( match (*system).validate_param_unsafe(world) { Ok(()) => { if let Err(err) = (*system).run_unsafe(trigger, world) { - error_handler( + let handler = state + .error_handler + .unwrap_or_else(|| world.default_error_handler()); + handler( err, ErrorContext::Observer { name: (*system).name(), @@ -421,7 +391,10 @@ fn observer_system_runner>( } Err(e) => { if !e.skipped { - error_handler( + let handler = state + .error_handler + .unwrap_or_else(|| world.default_error_handler()); + handler( e.into(), ErrorContext::Observer { name: (*system).name(), @@ -448,52 +421,31 @@ fn hook_on_add>( ) { world.commands().queue(move |world: &mut World| { let event_id = E::register_component_id(world); - let mut components = Vec::new(); + let mut components = vec![]; B::component_ids(&mut world.components_registrator(), &mut |id| { components.push(id); }); - let mut descriptor = ObserverDescriptor { - events: vec![event_id], - components, - ..Default::default() - }; + if let Some(mut observe) = world.get_mut::(entity) { + observe.descriptor.events.push(event_id); + observe.descriptor.components.extend(components); - let error_handler = default_error_handler(); - - // Initialize System - let system: *mut dyn ObserverSystem = - if let Some(mut observe) = world.get_mut::(entity) { - descriptor.merge(&observe.descriptor); - if observe.error_handler.is_none() { - observe.error_handler = Some(error_handler); - } - let system = observe.system.downcast_mut::().unwrap(); - &mut *system - } else { - return; - }; - // SAFETY: World reference is exclusive and initialize does not touch system, so references do not alias - unsafe { - (*system).initialize(world); - } - - { - let mut entity = world.entity_mut(entity); - if let crate::world::Entry::Vacant(entry) = entity.entry::() { - entry.insert(ObserverState { - descriptor, - runner: observer_system_runner::, - ..Default::default() - }); + let system: *mut dyn ObserverSystem = observe.system.downcast_mut::().unwrap(); + // SAFETY: World reference is exclusive and initialize does not touch system, so references do not alias + unsafe { + (*system).initialize(world); } + world.register_observer(entity); } }); } - #[cfg(test)] mod tests { use super::*; - use crate::{event::Event, observer::Trigger}; + use crate::{ + error::{ignore, DefaultErrorHandler}, + event::Event, + observer::Trigger, + }; #[derive(Event)] struct TriggerEvent; @@ -521,12 +473,31 @@ mod tests { Err("I failed!".into()) } + // Using observer error handler let mut world = World::default(); world.init_resource::(); - let observer = Observer::new(system).with_error_handler(crate::error::ignore); - world.spawn(observer); - Schedule::default().run(&mut world); + world.spawn(Observer::new(system).with_error_handler(ignore)); + world.trigger(TriggerEvent); + assert!(world.resource::().0); + + // Using world error handler + let mut world = World::default(); + world.init_resource::(); + world.spawn(Observer::new(system)); + // Test that the correct handler is used when the observer was added + // before the default handler + world.insert_resource(DefaultErrorHandler(ignore)); world.trigger(TriggerEvent); assert!(world.resource::().0); } + + #[test] + #[should_panic( + expected = "Exclusive system `bevy_ecs::observer::runner::tests::exclusive_system_cannot_be_observer::system` may not be used as observer.\nInstead of `&mut World`, use either `DeferredWorld` if you do not need structural changes, or `Commands` if you do." + )] + fn exclusive_system_cannot_be_observer() { + fn system(_: Trigger, _world: &mut World) {} + let mut world = World::default(); + world.add_observer(system); + } } diff --git a/crates/bevy_ecs/src/query/access.rs b/crates/bevy_ecs/src/query/access.rs index a9cefc6996..9c63cb5a74 100644 --- a/crates/bevy_ecs/src/query/access.rs +++ b/crates/bevy_ecs/src/query/access.rs @@ -257,9 +257,10 @@ impl Access { /// This is for components whose values are not accessed (and thus will never cause conflicts), /// but whose presence in an archetype may affect query results. /// - /// Currently, this is only used for [`Has`]. + /// Currently, this is only used for [`Has`] and [`Allows`]. /// /// [`Has`]: crate::query::Has + /// [`Allows`]: crate::query::filter::Allows pub fn add_archetypal(&mut self, index: T) { self.archetypal.grow_and_insert(index.sparse_set_index()); } @@ -430,75 +431,56 @@ impl Access { /// Adds all access from `other`. pub fn extend(&mut self, other: &Access) { - let component_read_and_writes_inverted = - self.component_read_and_writes_inverted || other.component_read_and_writes_inverted; - let component_writes_inverted = - self.component_writes_inverted || other.component_writes_inverted; - - match ( - self.component_read_and_writes_inverted, + invertible_union_with( + &mut self.component_read_and_writes, + &mut self.component_read_and_writes_inverted, + &other.component_read_and_writes, other.component_read_and_writes_inverted, - ) { - (true, true) => { - self.component_read_and_writes - .intersect_with(&other.component_read_and_writes); - } - (true, false) => { - self.component_read_and_writes - .difference_with(&other.component_read_and_writes); - } - (false, true) => { - // We have to grow here because the new bits are going to get flipped to 1. - self.component_read_and_writes.grow( - self.component_read_and_writes - .len() - .max(other.component_read_and_writes.len()), - ); - self.component_read_and_writes.toggle_range(..); - self.component_read_and_writes - .intersect_with(&other.component_read_and_writes); - } - (false, false) => { - self.component_read_and_writes - .union_with(&other.component_read_and_writes); - } - } - - match ( - self.component_writes_inverted, + ); + invertible_union_with( + &mut self.component_writes, + &mut self.component_writes_inverted, + &other.component_writes, other.component_writes_inverted, - ) { - (true, true) => { - self.component_writes - .intersect_with(&other.component_writes); - } - (true, false) => { - self.component_writes - .difference_with(&other.component_writes); - } - (false, true) => { - // We have to grow here because the new bits are going to get flipped to 1. - self.component_writes.grow( - self.component_writes - .len() - .max(other.component_writes.len()), - ); - self.component_writes.toggle_range(..); - self.component_writes - .intersect_with(&other.component_writes); - } - (false, false) => { - self.component_writes.union_with(&other.component_writes); - } - } + ); self.reads_all_resources = self.reads_all_resources || other.reads_all_resources; self.writes_all_resources = self.writes_all_resources || other.writes_all_resources; - self.component_read_and_writes_inverted = component_read_and_writes_inverted; - self.component_writes_inverted = component_writes_inverted; self.resource_read_and_writes .union_with(&other.resource_read_and_writes); self.resource_writes.union_with(&other.resource_writes); + self.archetypal.union_with(&other.archetypal); + } + + /// Removes any access from `self` that would conflict with `other`. + /// This removes any reads and writes for any component written by `other`, + /// and removes any writes for any component read by `other`. + pub fn remove_conflicting_access(&mut self, other: &Access) { + invertible_difference_with( + &mut self.component_read_and_writes, + &mut self.component_read_and_writes_inverted, + &other.component_writes, + other.component_writes_inverted, + ); + invertible_difference_with( + &mut self.component_writes, + &mut self.component_writes_inverted, + &other.component_read_and_writes, + other.component_read_and_writes_inverted, + ); + + if other.reads_all_resources { + self.writes_all_resources = false; + self.resource_writes.clear(); + } + if other.writes_all_resources { + self.reads_all_resources = false; + self.resource_read_and_writes.clear(); + } + self.resource_read_and_writes + .difference_with(&other.resource_writes); + self.resource_writes + .difference_with(&other.resource_read_and_writes); } /// Returns `true` if the access and `other` can be active at the same time, @@ -838,6 +820,55 @@ impl Access { } } +/// Performs an in-place union of `other` into `self`, where either set may be inverted. +/// +/// Each set corresponds to a `FixedBitSet` if `inverted` is `false`, +/// or to the infinite (co-finite) complement of the `FixedBitSet` if `inverted` is `true`. +/// +/// This updates the `self` set to include any elements in the `other` set. +/// Note that this may change `self_inverted` to `true` if we add an infinite +/// set to a finite one, resulting in a new infinite set. +fn invertible_union_with( + self_set: &mut FixedBitSet, + self_inverted: &mut bool, + other_set: &FixedBitSet, + other_inverted: bool, +) { + match (*self_inverted, other_inverted) { + (true, true) => self_set.intersect_with(other_set), + (true, false) => self_set.difference_with(other_set), + (false, true) => { + *self_inverted = true; + // We have to grow here because the new bits are going to get flipped to 1. + self_set.grow(other_set.len()); + self_set.toggle_range(..); + self_set.intersect_with(other_set); + } + (false, false) => self_set.union_with(other_set), + } +} + +/// Performs an in-place set difference of `other` from `self`, where either set may be inverted. +/// +/// Each set corresponds to a `FixedBitSet` if `inverted` is `false`, +/// or to the infinite (co-finite) complement of the `FixedBitSet` if `inverted` is `true`. +/// +/// This updates the `self` set to remove any elements in the `other` set. +/// Note that this may change `self_inverted` to `false` if we remove an +/// infinite set from another infinite one, resulting in a finite difference. +fn invertible_difference_with( + self_set: &mut FixedBitSet, + self_inverted: &mut bool, + other_set: &FixedBitSet, + other_inverted: bool, +) { + // We can share the implementation of `invertible_union_with` with some algebra: + // A - B = A & !B = !(!A | B) + *self_inverted = !*self_inverted; + invertible_union_with(self_set, self_inverted, other_set, other_inverted); + *self_inverted = !*self_inverted; +} + /// Error returned when attempting to iterate over items included in an [`Access`] /// if the access excludes items rather than including them. #[derive(Clone, Copy, PartialEq, Eq, Debug, Error)] @@ -1289,6 +1320,14 @@ impl Clone for FilteredAccessSet { } impl FilteredAccessSet { + /// Creates an empty [`FilteredAccessSet`]. + pub const fn new() -> Self { + Self { + combined_access: Access::new(), + filtered_accesses: Vec::new(), + } + } + /// Returns a reference to the unfiltered access of the entire set. #[inline] pub fn combined_access(&self) -> &Access { @@ -1412,15 +1451,13 @@ impl FilteredAccessSet { impl Default for FilteredAccessSet { fn default() -> Self { - Self { - combined_access: Default::default(), - filtered_accesses: Vec::new(), - } + Self::new() } } #[cfg(test)] mod tests { + use super::{invertible_difference_with, invertible_union_with}; use crate::query::{ access::AccessFilters, Access, AccessConflicts, ComponentAccessKind, FilteredAccess, FilteredAccessSet, UnboundedAccessError, @@ -1763,4 +1800,99 @@ mod tests { }), ); } + + /// Create a `FixedBitSet` with a given number of total bits and a given list of bits to set. + /// Setting the number of bits is important in tests since the `PartialEq` impl checks that the length matches. + fn bit_set(bits: usize, iter: impl IntoIterator) -> FixedBitSet { + let mut result = FixedBitSet::with_capacity(bits); + result.extend(iter); + result + } + + #[test] + fn invertible_union_with_tests() { + let invertible_union = |mut self_inverted: bool, other_inverted: bool| { + // Check all four possible bit states: In both sets, the first, the second, or neither + let mut self_set = bit_set(4, [0, 1]); + let other_set = bit_set(4, [0, 2]); + invertible_union_with( + &mut self_set, + &mut self_inverted, + &other_set, + other_inverted, + ); + (self_set, self_inverted) + }; + + // Check each combination of `inverted` flags + let (s, i) = invertible_union(false, false); + // [0, 1] | [0, 2] = [0, 1, 2] + assert_eq!((s, i), (bit_set(4, [0, 1, 2]), false)); + + let (s, i) = invertible_union(false, true); + // [0, 1] | [1, 3, ...] = [0, 1, 3, ...] + assert_eq!((s, i), (bit_set(4, [2]), true)); + + let (s, i) = invertible_union(true, false); + // [2, 3, ...] | [0, 2] = [0, 2, 3, ...] + assert_eq!((s, i), (bit_set(4, [1]), true)); + + let (s, i) = invertible_union(true, true); + // [2, 3, ...] | [1, 3, ...] = [1, 2, 3, ...] + assert_eq!((s, i), (bit_set(4, [0]), true)); + } + + #[test] + fn invertible_union_with_different_lengths() { + // When adding a large inverted set to a small normal set, + // make sure we invert the bits beyond the original length. + // Failing to call `grow` before `toggle_range` would cause bit 1 to be zero, + // which would incorrectly treat it as included in the output set. + let mut self_set = bit_set(1, [0]); + let mut self_inverted = false; + let other_set = bit_set(3, [0, 1]); + let other_inverted = true; + invertible_union_with( + &mut self_set, + &mut self_inverted, + &other_set, + other_inverted, + ); + + // [0] | [2, ...] = [0, 2, ...] + assert_eq!((self_set, self_inverted), (bit_set(3, [1]), true)); + } + + #[test] + fn invertible_difference_with_tests() { + let invertible_difference = |mut self_inverted: bool, other_inverted: bool| { + // Check all four possible bit states: In both sets, the first, the second, or neither + let mut self_set = bit_set(4, [0, 1]); + let other_set = bit_set(4, [0, 2]); + invertible_difference_with( + &mut self_set, + &mut self_inverted, + &other_set, + other_inverted, + ); + (self_set, self_inverted) + }; + + // Check each combination of `inverted` flags + let (s, i) = invertible_difference(false, false); + // [0, 1] - [0, 2] = [1] + assert_eq!((s, i), (bit_set(4, [1]), false)); + + let (s, i) = invertible_difference(false, true); + // [0, 1] - [1, 3, ...] = [0] + assert_eq!((s, i), (bit_set(4, [0]), false)); + + let (s, i) = invertible_difference(true, false); + // [2, 3, ...] - [0, 2] = [3, ...] + assert_eq!((s, i), (bit_set(4, [0, 1, 2]), true)); + + let (s, i) = invertible_difference(true, true); + // [2, 3, ...] - [1, 3, ...] = [2] + assert_eq!((s, i), (bit_set(4, [2]), false)); + } } diff --git a/crates/bevy_ecs/src/query/builder.rs b/crates/bevy_ecs/src/query/builder.rs index 81819cb9ac..b545caad8f 100644 --- a/crates/bevy_ecs/src/query/builder.rs +++ b/crates/bevy_ecs/src/query/builder.rs @@ -248,11 +248,9 @@ impl<'w, D: QueryData, F: QueryFilter> QueryBuilder<'w, D, F> { pub fn transmute_filtered( &mut self, ) -> &mut QueryBuilder<'w, NewD, NewF> { - let mut fetch_state = NewD::init_state(self.world); + let fetch_state = NewD::init_state(self.world); let filter_state = NewF::init_state(self.world); - NewD::set_access(&mut fetch_state, &self.access); - let mut access = FilteredAccess::default(); NewD::update_component_access(&fetch_state, &mut access); NewF::update_component_access(&filter_state, &mut access); @@ -275,7 +273,10 @@ impl<'w, D: QueryData, F: QueryFilter> QueryBuilder<'w, D, F> { #[cfg(test)] mod tests { - use crate::{prelude::*, world::FilteredEntityRef}; + use crate::{ + prelude::*, + world::{EntityMutExcept, EntityRefExcept, FilteredEntityMut, FilteredEntityRef}, + }; use std::dbg; #[derive(Component, PartialEq, Debug)] @@ -422,6 +423,89 @@ mod tests { } } + #[test] + fn builder_provide_access() { + let mut world = World::new(); + world.spawn((A(0), B(1))); + + let mut query = + QueryBuilder::<(Entity, FilteredEntityRef, FilteredEntityMut)>::new(&mut world) + .data::<&mut A>() + .data::<&B>() + .build(); + + // The `FilteredEntityRef` only has read access, so the `FilteredEntityMut` can have read access without conflicts + let (_entity, entity_ref_1, mut entity_ref_2) = query.single_mut(&mut world).unwrap(); + assert!(entity_ref_1.get::().is_some()); + assert!(entity_ref_1.get::().is_some()); + assert!(entity_ref_2.get::().is_some()); + assert!(entity_ref_2.get_mut::().is_none()); + assert!(entity_ref_2.get::().is_some()); + assert!(entity_ref_2.get_mut::().is_none()); + + let mut query = + QueryBuilder::<(Entity, FilteredEntityMut, FilteredEntityMut)>::new(&mut world) + .data::<&mut A>() + .data::<&B>() + .build(); + + // The first `FilteredEntityMut` has write access to A, so the second one cannot have write access + let (_entity, mut entity_ref_1, mut entity_ref_2) = query.single_mut(&mut world).unwrap(); + assert!(entity_ref_1.get::().is_some()); + assert!(entity_ref_1.get_mut::().is_some()); + assert!(entity_ref_1.get::().is_some()); + assert!(entity_ref_1.get_mut::().is_none()); + assert!(entity_ref_2.get::().is_none()); + assert!(entity_ref_2.get_mut::().is_none()); + assert!(entity_ref_2.get::().is_some()); + assert!(entity_ref_2.get_mut::().is_none()); + + let mut query = QueryBuilder::<(FilteredEntityMut, &mut A, &B)>::new(&mut world) + .data::<&mut A>() + .data::<&mut B>() + .build(); + + // Any `A` access would conflict with `&mut A`, and write access to `B` would conflict with `&B`. + let (mut entity_ref, _a, _b) = query.single_mut(&mut world).unwrap(); + assert!(entity_ref.get::().is_none()); + assert!(entity_ref.get_mut::().is_none()); + assert!(entity_ref.get::().is_some()); + assert!(entity_ref.get_mut::().is_none()); + + let mut query = QueryBuilder::<(FilteredEntityMut, &mut A, &B)>::new(&mut world) + .data::() + .build(); + + // Same as above, but starting from "all" access + let (mut entity_ref, _a, _b) = query.single_mut(&mut world).unwrap(); + assert!(entity_ref.get::().is_none()); + assert!(entity_ref.get_mut::().is_none()); + assert!(entity_ref.get::().is_some()); + assert!(entity_ref.get_mut::().is_none()); + + let mut query = QueryBuilder::<(FilteredEntityMut, EntityMutExcept)>::new(&mut world) + .data::() + .build(); + + // Removing `EntityMutExcept` just leaves A + let (mut entity_ref_1, _entity_ref_2) = query.single_mut(&mut world).unwrap(); + assert!(entity_ref_1.get::().is_some()); + assert!(entity_ref_1.get_mut::().is_some()); + assert!(entity_ref_1.get::().is_none()); + assert!(entity_ref_1.get_mut::().is_none()); + + let mut query = QueryBuilder::<(FilteredEntityMut, EntityRefExcept)>::new(&mut world) + .data::() + .build(); + + // Removing `EntityRefExcept` just leaves A, plus read access + let (mut entity_ref_1, _entity_ref_2) = query.single_mut(&mut world).unwrap(); + assert!(entity_ref_1.get::().is_some()); + assert!(entity_ref_1.get_mut::().is_some()); + assert!(entity_ref_1.get::().is_some()); + assert!(entity_ref_1.get_mut::().is_none()); + } + /// Regression test for issue #14348 #[test] fn builder_static_dense_dynamic_sparse() { diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index cd632f7b14..3c1ff5262c 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -31,6 +31,8 @@ use variadics_please::all_tuples; /// Gets the identifier of the queried entity. /// - **[`EntityLocation`].** /// Gets the location metadata of the queried entity. +/// - **[`SpawnDetails`].** +/// Gets the tick the entity was spawned at. /// - **[`EntityRef`].** /// Read-only access to arbitrary components on the queried entity. /// - **[`EntityMut`].** @@ -291,6 +293,22 @@ pub unsafe trait QueryData: WorldQuery { /// This function manually implements subtyping for the query items. fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort>; + /// Offers additional access above what we requested in `update_component_access`. + /// Implementations may add additional access that is a subset of `available_access` + /// and does not conflict with anything in `access`, + /// and must update `access` to include that access. + /// + /// This is used by [`WorldQuery`] types like [`FilteredEntityRef`] + /// and [`FilteredEntityMut`] to support dynamic access. + /// + /// Called when constructing a [`QueryLens`](crate::system::QueryLens) or calling [`QueryState::from_builder`](super::QueryState::from_builder) + fn provide_extra_access( + _state: &mut Self::State, + _access: &mut Access, + _available_access: &Access, + ) { + } + /// Fetch [`Self::Item`](`QueryData::Item`) for either the given `entity` in the current [`Table`], /// or for the given `entity` in the current [`Archetype`]. This must always be called after /// [`WorldQuery::set_table`] with a `table_row` in the range of the current [`Table`] or after @@ -470,12 +488,80 @@ unsafe impl QueryData for EntityLocation { /// SAFETY: access is read only unsafe impl ReadOnlyQueryData for EntityLocation {} -/// SAFETY: -/// `fetch` accesses all components in a readonly way. -/// This is sound because `update_component_access` and `update_archetype_component_access` set read access for all components and panic when appropriate. -/// Filters are unchanged. -unsafe impl<'a> WorldQuery for EntityRef<'a> { - type Fetch<'w> = UnsafeWorldCell<'w>; +/// The `SpawnDetails` query parameter fetches the [`Tick`] the entity was spawned at. +/// +/// To evaluate whether the spawn happened since the last time the system ran, the system +/// param [`SystemChangeTick`](bevy_ecs::system::SystemChangeTick) needs to be used. +/// +/// If the query should filter for spawned entities instead, use the +/// [`Spawned`](bevy_ecs::query::Spawned) query filter instead. +/// +/// # Examples +/// +/// ``` +/// # use bevy_ecs::component::Component; +/// # use bevy_ecs::entity::Entity; +/// # use bevy_ecs::system::Query; +/// # use bevy_ecs::query::Spawned; +/// # use bevy_ecs::query::SpawnDetails; +/// +/// fn print_spawn_details(query: Query<(Entity, SpawnDetails)>) { +/// for (entity, spawn_details) in &query { +/// if spawn_details.is_spawned() { +/// print!("new "); +/// } +/// print!( +/// "entity {:?} spawned at {:?}", +/// entity, +/// spawn_details.spawned_at() +/// ); +/// match spawn_details.spawned_by().into_option() { +/// Some(location) => println!(" by {:?}", location), +/// None => println!() +/// } +/// } +/// } +/// +/// # bevy_ecs::system::assert_is_system(print_spawn_details); +/// ``` +#[derive(Clone, Copy, Debug)] +pub struct SpawnDetails { + spawned_by: MaybeLocation, + spawned_at: Tick, + last_run: Tick, + this_run: Tick, +} + +impl SpawnDetails { + /// Returns `true` if the entity spawned since the last time this system ran. + /// Otherwise, returns `false`. + pub fn is_spawned(self) -> bool { + self.spawned_at.is_newer_than(self.last_run, self.this_run) + } + + /// Returns the `Tick` this entity spawned at. + pub fn spawned_at(self) -> Tick { + self.spawned_at + } + + /// Returns the source code location from which this entity has been spawned. + pub fn spawned_by(self) -> MaybeLocation { + self.spawned_by + } +} + +#[doc(hidden)] +#[derive(Clone)] +pub struct SpawnDetailsFetch<'w> { + entities: &'w Entities, + last_run: Tick, + this_run: Tick, +} + +// SAFETY: +// No components are accessed. +unsafe impl WorldQuery for SpawnDetails { + type Fetch<'w> = SpawnDetailsFetch<'w>; type State = (); fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { @@ -485,10 +571,116 @@ unsafe impl<'a> WorldQuery for EntityRef<'a> { unsafe fn init_fetch<'w>( world: UnsafeWorldCell<'w>, _state: &Self::State, - _last_run: Tick, - _this_run: Tick, + last_run: Tick, + this_run: Tick, ) -> Self::Fetch<'w> { - world + SpawnDetailsFetch { + entities: world.entities(), + last_run, + this_run, + } + } + + const IS_DENSE: bool = true; + + #[inline] + unsafe fn set_archetype<'w>( + _fetch: &mut Self::Fetch<'w>, + _state: &Self::State, + _archetype: &'w Archetype, + _table: &'w Table, + ) { + } + + #[inline] + unsafe fn set_table<'w>(_fetch: &mut Self::Fetch<'w>, _state: &Self::State, _table: &'w Table) { + } + + fn update_component_access(_state: &Self::State, _access: &mut FilteredAccess) {} + + fn init_state(_world: &mut World) {} + + fn get_state(_components: &Components) -> Option<()> { + Some(()) + } + + fn matches_component_set( + _state: &Self::State, + _set_contains_id: &impl Fn(ComponentId) -> bool, + ) -> bool { + true + } +} + +// SAFETY: +// No components are accessed. +// Is its own ReadOnlyQueryData. +unsafe impl QueryData for SpawnDetails { + const IS_READ_ONLY: bool = true; + type ReadOnly = Self; + type Item<'w> = Self; + + fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> { + item + } + + #[inline(always)] + unsafe fn fetch<'w>( + fetch: &mut Self::Fetch<'w>, + entity: Entity, + _table_row: TableRow, + ) -> Self::Item<'w> { + // SAFETY: only living entities are queried + let (spawned_by, spawned_at) = unsafe { + fetch + .entities + .entity_get_spawned_or_despawned_unchecked(entity) + }; + Self { + spawned_by, + spawned_at, + last_run: fetch.last_run, + this_run: fetch.this_run, + } + } +} + +/// SAFETY: access is read only +unsafe impl ReadOnlyQueryData for SpawnDetails {} + +/// The [`WorldQuery::Fetch`] type for WorldQueries that can fetch multiple components from an entity +/// ([`EntityRef`], [`EntityMut`], etc.) +#[derive(Copy, Clone)] +#[doc(hidden)] +pub struct EntityFetch<'w> { + world: UnsafeWorldCell<'w>, + last_run: Tick, + this_run: Tick, +} + +/// SAFETY: +/// `fetch` accesses all components in a readonly way. +/// This is sound because `update_component_access` and `update_archetype_component_access` set read access for all components and panic when appropriate. +/// Filters are unchanged. +unsafe impl<'a> WorldQuery for EntityRef<'a> { + type Fetch<'w> = EntityFetch<'w>; + type State = (); + + fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { + fetch + } + + unsafe fn init_fetch<'w>( + world: UnsafeWorldCell<'w>, + _state: &Self::State, + last_run: Tick, + this_run: Tick, + ) -> Self::Fetch<'w> { + EntityFetch { + world, + last_run, + this_run, + } } const IS_DENSE: bool = true; @@ -540,12 +732,17 @@ unsafe impl<'a> QueryData for EntityRef<'a> { #[inline(always)] unsafe fn fetch<'w>( - world: &mut Self::Fetch<'w>, + fetch: &mut Self::Fetch<'w>, entity: Entity, _table_row: TableRow, ) -> Self::Item<'w> { // SAFETY: `fetch` must be called with an entity that exists in the world - let cell = unsafe { world.get_entity(entity).debug_checked_unwrap() }; + let cell = unsafe { + fetch + .world + .get_entity_with_ticks(entity, fetch.last_run, fetch.this_run) + .debug_checked_unwrap() + }; // SAFETY: Read-only access to every component has been registered. unsafe { EntityRef::new(cell) } } @@ -556,7 +753,7 @@ unsafe impl ReadOnlyQueryData for EntityRef<'_> {} /// SAFETY: The accesses of `Self::ReadOnly` are a subset of the accesses of `Self` unsafe impl<'a> WorldQuery for EntityMut<'a> { - type Fetch<'w> = UnsafeWorldCell<'w>; + type Fetch<'w> = EntityFetch<'w>; type State = (); fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { @@ -566,10 +763,14 @@ unsafe impl<'a> WorldQuery for EntityMut<'a> { unsafe fn init_fetch<'w>( world: UnsafeWorldCell<'w>, _state: &Self::State, - _last_run: Tick, - _this_run: Tick, + last_run: Tick, + this_run: Tick, ) -> Self::Fetch<'w> { - world + EntityFetch { + world, + last_run, + this_run, + } } const IS_DENSE: bool = true; @@ -621,12 +822,17 @@ unsafe impl<'a> QueryData for EntityMut<'a> { #[inline(always)] unsafe fn fetch<'w>( - world: &mut Self::Fetch<'w>, + fetch: &mut Self::Fetch<'w>, entity: Entity, _table_row: TableRow, ) -> Self::Item<'w> { // SAFETY: `fetch` must be called with an entity that exists in the world - let cell = unsafe { world.get_entity(entity).debug_checked_unwrap() }; + let cell = unsafe { + fetch + .world + .get_entity_with_ticks(entity, fetch.last_run, fetch.this_run) + .debug_checked_unwrap() + }; // SAFETY: mutable access to every component has been registered. unsafe { EntityMut::new(cell) } } @@ -634,8 +840,8 @@ unsafe impl<'a> QueryData for EntityMut<'a> { /// SAFETY: The accesses of `Self::ReadOnly` are a subset of the accesses of `Self` unsafe impl<'a> WorldQuery for FilteredEntityRef<'a> { - type Fetch<'w> = (UnsafeWorldCell<'w>, Access); - type State = FilteredAccess; + type Fetch<'w> = (EntityFetch<'w>, Access); + type State = Access; fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { fetch @@ -646,12 +852,19 @@ unsafe impl<'a> WorldQuery for FilteredEntityRef<'a> { unsafe fn init_fetch<'w>( world: UnsafeWorldCell<'w>, _state: &Self::State, - _last_run: Tick, - _this_run: Tick, + last_run: Tick, + this_run: Tick, ) -> Self::Fetch<'w> { let mut access = Access::default(); access.read_all_components(); - (world, access) + ( + EntityFetch { + world, + last_run, + this_run, + }, + access, + ) } #[inline] @@ -661,18 +874,12 @@ unsafe impl<'a> WorldQuery for FilteredEntityRef<'a> { _: &'w Archetype, _table: &Table, ) { - fetch.1.clone_from(&state.access); + fetch.1.clone_from(state); } #[inline] unsafe fn set_table<'w>(fetch: &mut Self::Fetch<'w>, state: &Self::State, _: &'w Table) { - fetch.1.clone_from(&state.access); - } - - #[inline] - fn set_access<'w>(state: &mut Self::State, access: &FilteredAccess) { - state.clone_from(access); - state.access_mut().clear_writes(); + fetch.1.clone_from(state); } fn update_component_access( @@ -680,18 +887,18 @@ unsafe impl<'a> WorldQuery for FilteredEntityRef<'a> { filtered_access: &mut FilteredAccess, ) { assert!( - filtered_access.access().is_compatible(&state.access), + filtered_access.access().is_compatible(state), "FilteredEntityRef conflicts with a previous access in this query. Exclusive access cannot coincide with any other accesses.", ); - filtered_access.access.extend(&state.access); + filtered_access.access.extend(state); } fn init_state(_world: &mut World) -> Self::State { - FilteredAccess::default() + Access::default() } fn get_state(_components: &Components) -> Option { - Some(FilteredAccess::default()) + Some(Access::default()) } fn matches_component_set( @@ -712,14 +919,38 @@ unsafe impl<'a> QueryData for FilteredEntityRef<'a> { item } + #[inline] + fn provide_extra_access( + state: &mut Self::State, + access: &mut Access, + available_access: &Access, + ) { + // Claim any extra access that doesn't conflict with other subqueries + // This is used when constructing a `QueryLens` or creating a query from a `QueryBuilder` + // Start with the entire available access, since that is the most we can possibly access + state.clone_from(available_access); + // Prevent all writes, since `FilteredEntityRef` only performs read access + state.clear_writes(); + // Prevent any access that would conflict with other accesses in the current query + state.remove_conflicting_access(access); + // Finally, add the resulting access to the query access + // to make sure a later `FilteredEntityMut` won't conflict with this. + access.extend(state); + } + #[inline(always)] unsafe fn fetch<'w>( - (world, access): &mut Self::Fetch<'w>, + (fetch, access): &mut Self::Fetch<'w>, entity: Entity, _table_row: TableRow, ) -> Self::Item<'w> { // SAFETY: `fetch` must be called with an entity that exists in the world - let cell = unsafe { world.get_entity(entity).debug_checked_unwrap() }; + let cell = unsafe { + fetch + .world + .get_entity_with_ticks(entity, fetch.last_run, fetch.this_run) + .debug_checked_unwrap() + }; // SAFETY: mutable access to every component has been registered. unsafe { FilteredEntityRef::new(cell, access.clone()) } } @@ -730,8 +961,8 @@ unsafe impl ReadOnlyQueryData for FilteredEntityRef<'_> {} /// SAFETY: The accesses of `Self::ReadOnly` are a subset of the accesses of `Self` unsafe impl<'a> WorldQuery for FilteredEntityMut<'a> { - type Fetch<'w> = (UnsafeWorldCell<'w>, Access); - type State = FilteredAccess; + type Fetch<'w> = (EntityFetch<'w>, Access); + type State = Access; fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { fetch @@ -742,12 +973,19 @@ unsafe impl<'a> WorldQuery for FilteredEntityMut<'a> { unsafe fn init_fetch<'w>( world: UnsafeWorldCell<'w>, _state: &Self::State, - _last_run: Tick, - _this_run: Tick, + last_run: Tick, + this_run: Tick, ) -> Self::Fetch<'w> { let mut access = Access::default(); access.write_all_components(); - (world, access) + ( + EntityFetch { + world, + last_run, + this_run, + }, + access, + ) } #[inline] @@ -757,17 +995,12 @@ unsafe impl<'a> WorldQuery for FilteredEntityMut<'a> { _: &'w Archetype, _table: &Table, ) { - fetch.1.clone_from(&state.access); + fetch.1.clone_from(state); } #[inline] unsafe fn set_table<'w>(fetch: &mut Self::Fetch<'w>, state: &Self::State, _: &'w Table) { - fetch.1.clone_from(&state.access); - } - - #[inline] - fn set_access<'w>(state: &mut Self::State, access: &FilteredAccess) { - state.clone_from(access); + fetch.1.clone_from(state); } fn update_component_access( @@ -775,18 +1008,18 @@ unsafe impl<'a> WorldQuery for FilteredEntityMut<'a> { filtered_access: &mut FilteredAccess, ) { assert!( - filtered_access.access().is_compatible(&state.access), + filtered_access.access().is_compatible(state), "FilteredEntityMut conflicts with a previous access in this query. Exclusive access cannot coincide with any other accesses.", ); - filtered_access.access.extend(&state.access); + filtered_access.access.extend(state); } fn init_state(_world: &mut World) -> Self::State { - FilteredAccess::default() + Access::default() } fn get_state(_components: &Components) -> Option { - Some(FilteredAccess::default()) + Some(Access::default()) } fn matches_component_set( @@ -807,14 +1040,36 @@ unsafe impl<'a> QueryData for FilteredEntityMut<'a> { item } + #[inline] + fn provide_extra_access( + state: &mut Self::State, + access: &mut Access, + available_access: &Access, + ) { + // Claim any extra access that doesn't conflict with other subqueries + // This is used when constructing a `QueryLens` or creating a query from a `QueryBuilder` + // Start with the entire available access, since that is the most we can possibly access + state.clone_from(available_access); + // Prevent any access that would conflict with other accesses in the current query + state.remove_conflicting_access(access); + // Finally, add the resulting access to the query access + // to make sure a later `FilteredEntityRef` or `FilteredEntityMut` won't conflict with this. + access.extend(state); + } + #[inline(always)] unsafe fn fetch<'w>( - (world, access): &mut Self::Fetch<'w>, + (fetch, access): &mut Self::Fetch<'w>, entity: Entity, _table_row: TableRow, ) -> Self::Item<'w> { // SAFETY: `fetch` must be called with an entity that exists in the world - let cell = unsafe { world.get_entity(entity).debug_checked_unwrap() }; + let cell = unsafe { + fetch + .world + .get_entity_with_ticks(entity, fetch.last_run, fetch.this_run) + .debug_checked_unwrap() + }; // SAFETY: mutable access to every component has been registered. unsafe { FilteredEntityMut::new(cell, access.clone()) } } @@ -827,7 +1082,7 @@ unsafe impl<'a, B> WorldQuery for EntityRefExcept<'a, B> where B: Bundle, { - type Fetch<'w> = UnsafeWorldCell<'w>; + type Fetch<'w> = EntityFetch<'w>; type State = SmallVec<[ComponentId; 4]>; fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { @@ -837,10 +1092,14 @@ where unsafe fn init_fetch<'w>( world: UnsafeWorldCell<'w>, _: &Self::State, - _: Tick, - _: Tick, + last_run: Tick, + this_run: Tick, ) -> Self::Fetch<'w> { - world + EntityFetch { + world, + last_run, + this_run, + } } const IS_DENSE: bool = true; @@ -907,11 +1166,14 @@ where } unsafe fn fetch<'w>( - world: &mut Self::Fetch<'w>, + fetch: &mut Self::Fetch<'w>, entity: Entity, _: TableRow, ) -> Self::Item<'w> { - let cell = world.get_entity(entity).unwrap(); + let cell = fetch + .world + .get_entity_with_ticks(entity, fetch.last_run, fetch.this_run) + .unwrap(); EntityRefExcept::new(cell) } } @@ -927,7 +1189,7 @@ unsafe impl<'a, B> WorldQuery for EntityMutExcept<'a, B> where B: Bundle, { - type Fetch<'w> = UnsafeWorldCell<'w>; + type Fetch<'w> = EntityFetch<'w>; type State = SmallVec<[ComponentId; 4]>; fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { @@ -937,10 +1199,14 @@ where unsafe fn init_fetch<'w>( world: UnsafeWorldCell<'w>, _: &Self::State, - _: Tick, - _: Tick, + last_run: Tick, + this_run: Tick, ) -> Self::Fetch<'w> { - world + EntityFetch { + world, + last_run, + this_run, + } } const IS_DENSE: bool = true; @@ -1008,11 +1274,14 @@ where } unsafe fn fetch<'w>( - world: &mut Self::Fetch<'w>, + fetch: &mut Self::Fetch<'w>, entity: Entity, _: TableRow, ) -> Self::Item<'w> { - let cell = world.get_entity(entity).unwrap(); + let cell = fetch + .world + .get_entity_with_ticks(entity, fetch.last_run, fetch.this_run) + .unwrap(); EntityMutExcept::new(cell) } } @@ -2079,6 +2348,16 @@ macro_rules! impl_tuple_query_data { )*) } + #[inline] + fn provide_extra_access( + state: &mut Self::State, + access: &mut Access, + available_access: &Access, + ) { + let ($($name,)*) = state; + $($name::provide_extra_access($name, access, available_access);)* + } + #[inline(always)] unsafe fn fetch<'w>( fetch: &mut Self::Fetch<'w>, @@ -2493,10 +2772,11 @@ impl Copy for StorageSwitch {} #[cfg(test)] mod tests { - use bevy_ecs_macros::QueryData; - use super::*; + use crate::change_detection::DetectChanges; use crate::system::{assert_is_system, Query}; + use bevy_ecs::prelude::Schedule; + use bevy_ecs_macros::QueryData; #[derive(Component)] pub struct A; @@ -2590,4 +2870,34 @@ mod tests { assert_is_system(client_system); } + + // Test that EntityRef::get_ref::() returns a Ref value with the correct + // ticks when the EntityRef was retrieved from a Query. + // See: https://github.com/bevyengine/bevy/issues/13735 + #[test] + fn test_entity_ref_query_with_ticks() { + #[derive(Component)] + pub struct C; + + 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"); + } + } + } + } + + let mut world = World::new(); + let mut schedule = Schedule::default(); + schedule.add_systems(system); + world.spawn(C); + + // reset the change ticks + world.clear_trackers(); + + // we want EntityRef to use the change ticks of the system + schedule.run(&mut world); + } } diff --git a/crates/bevy_ecs/src/query/filter.rs b/crates/bevy_ecs/src/query/filter.rs index e4e1f0fd66..38c7cfcb32 100644 --- a/crates/bevy_ecs/src/query/filter.rs +++ b/crates/bevy_ecs/src/query/filter.rs @@ -1,7 +1,7 @@ use crate::{ archetype::Archetype, component::{Component, ComponentId, Components, StorageType, Tick}, - entity::Entity, + entity::{Entities, Entity}, query::{DebugCheckedUnwrap, FilteredAccess, StorageSwitch, WorldQuery}, storage::{ComponentSparseSet, Table, TableRow}, world::{unsafe_world_cell::UnsafeWorldCell, World}, @@ -17,6 +17,8 @@ use variadics_please::all_tuples; /// [`With`] and [`Without`] filters can be applied to check if the queried entity does or does not contain a particular component. /// - **Change detection filters.** /// [`Added`] and [`Changed`] filters can be applied to detect component changes to an entity. +/// - **Spawned filter.** +/// [`Spawned`] filter can be applied to check if the queried entity was spawned recently. /// - **`QueryFilter` tuples.** /// If every element of a tuple implements `QueryFilter`, then the tuple itself also implements the same trait. /// This enables a single `Query` to filter over multiple conditions. @@ -555,6 +557,63 @@ all_tuples!( S ); +/// Allows a query to contain entities with the component `T`, bypassing [`DefaultQueryFilters`]. +/// +/// [`DefaultQueryFilters`]: crate::entity_disabling::DefaultQueryFilters +pub struct Allows(PhantomData); + +/// SAFETY: +/// `update_component_access` does not add any accesses. +/// This is sound because [`QueryFilter::filter_fetch`] does not access any components. +/// `update_component_access` adds an archetypal filter for `T`. +/// This is sound because it doesn't affect the query +unsafe impl WorldQuery for Allows { + type Fetch<'w> = (); + type State = ComponentId; + + fn shrink_fetch<'wlong: 'wshort, 'wshort>(_: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> {} + + #[inline] + unsafe fn init_fetch(_: UnsafeWorldCell, _: &ComponentId, _: Tick, _: Tick) {} + + // Even if the component is sparse, this implementation doesn't do anything with it + const IS_DENSE: bool = true; + + #[inline] + unsafe fn set_archetype(_: &mut (), _: &ComponentId, _: &Archetype, _: &Table) {} + + #[inline] + unsafe fn set_table(_: &mut (), _: &ComponentId, _: &Table) {} + + #[inline] + fn update_component_access(&id: &ComponentId, access: &mut FilteredAccess) { + access.access_mut().add_archetypal(id); + } + + fn init_state(world: &mut World) -> ComponentId { + world.register_component::() + } + + fn get_state(components: &Components) -> Option { + components.component_id::() + } + + fn matches_component_set(_: &ComponentId, _: &impl Fn(ComponentId) -> bool) -> bool { + // Allows always matches + true + } +} + +// SAFETY: WorldQuery impl performs no access at all +unsafe impl QueryFilter for Allows { + const IS_ARCHETYPAL: bool = true; + + #[inline(always)] + unsafe fn filter_fetch(_: &mut Self::Fetch<'_>, _: Entity, _: TableRow) -> bool { + true + } +} + /// A filter on a component that only retains results the first time after they have been added. /// /// A common use for this filter is one-time initialization. @@ -567,8 +626,8 @@ all_tuples!( /// # Deferred /// /// Note, that entity modifications issued with [`Commands`](crate::system::Commands) -/// are visible only after deferred operations are applied, -/// typically at the end of the schedule iteration. +/// are visible only after deferred operations are applied, typically after the system +/// that queued them. /// /// # Time complexity /// @@ -792,9 +851,8 @@ unsafe impl QueryFilter for Added { /// # Deferred /// /// Note, that entity modifications issued with [`Commands`](crate::system::Commands) -/// (like entity creation or entity component addition or removal) -/// are visible only after deferred operations are applied, -/// typically at the end of the schedule iteration. +/// (like entity creation or entity component addition or removal) are visible only +/// after deferred operations are applied, typically after the system that queued them. /// /// # Time complexity /// @@ -1005,6 +1063,146 @@ unsafe impl QueryFilter for Changed { } } +/// A filter that only retains results the first time after the entity has been spawned. +/// +/// A common use for this filter is one-time initialization. +/// +/// To retain all results without filtering but still check whether they were spawned after the +/// system last ran, use [`SpawnDetails`](crate::query::SpawnDetails) instead. +/// +/// **Note** that this includes entities that spawned before the first time this Query was run. +/// +/// # Deferred +/// +/// Note, that entity spawns issued with [`Commands`](crate::system::Commands) +/// are visible only after deferred operations are applied, typically after the +/// system that queued them. +/// +/// # Time complexity +/// +/// `Spawned` is not [`ArchetypeFilter`], which practically means that if query matches million +/// entities, `Spawned` filter will iterate over all of them even if none of them were spawned. +/// +/// For example, these two systems are roughly equivalent in terms of performance: +/// +/// ``` +/// # use bevy_ecs::entity::Entity; +/// # use bevy_ecs::system::Query; +/// # use bevy_ecs::query::Spawned; +/// # use bevy_ecs::query::SpawnDetails; +/// +/// fn system1(query: Query) { +/// for entity in &query { /* entity spawned */ } +/// } +/// +/// fn system2(query: Query<(Entity, SpawnDetails)>) { +/// for (entity, spawned) in &query { +/// if spawned.is_spawned() { /* entity spawned */ } +/// } +/// } +/// ``` +/// +/// # Examples +/// +/// ``` +/// # use bevy_ecs::component::Component; +/// # use bevy_ecs::query::Spawned; +/// # use bevy_ecs::system::IntoSystem; +/// # use bevy_ecs::system::Query; +/// # +/// # #[derive(Component, Debug)] +/// # struct Name {}; +/// +/// fn print_spawning_entities(query: Query<&Name, Spawned>) { +/// for name in &query { +/// println!("Entity spawned: {:?}", name); +/// } +/// } +/// +/// # bevy_ecs::system::assert_is_system(print_spawning_entities); +/// ``` +pub struct Spawned; + +#[doc(hidden)] +#[derive(Clone)] +pub struct SpawnedFetch<'w> { + entities: &'w Entities, + last_run: Tick, + this_run: Tick, +} + +// SAFETY: WorldQuery impl accesses no components or component ticks +unsafe impl WorldQuery for Spawned { + type Fetch<'w> = SpawnedFetch<'w>; + type State = (); + + fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { + fetch + } + + #[inline] + unsafe fn init_fetch<'w>( + world: UnsafeWorldCell<'w>, + _state: &(), + last_run: Tick, + this_run: Tick, + ) -> Self::Fetch<'w> { + SpawnedFetch { + entities: world.entities(), + last_run, + this_run, + } + } + + const IS_DENSE: bool = true; + + #[inline] + unsafe fn set_archetype<'w>( + _fetch: &mut Self::Fetch<'w>, + _state: &(), + _archetype: &'w Archetype, + _table: &'w Table, + ) { + } + + #[inline] + unsafe fn set_table<'w>(_fetch: &mut Self::Fetch<'w>, _state: &(), _table: &'w Table) {} + + #[inline] + fn update_component_access(_state: &(), _access: &mut FilteredAccess) {} + + fn init_state(_world: &mut World) {} + + fn get_state(_components: &Components) -> Option<()> { + Some(()) + } + + fn matches_component_set(_state: &(), _set_contains_id: &impl Fn(ComponentId) -> bool) -> bool { + true + } +} + +// SAFETY: WorldQuery impl accesses no components or component ticks +unsafe impl QueryFilter for Spawned { + const IS_ARCHETYPAL: bool = false; + + #[inline(always)] + unsafe fn filter_fetch( + fetch: &mut Self::Fetch<'_>, + entity: Entity, + _table_row: TableRow, + ) -> bool { + // SAFETY: only living entities are queried + let spawned = unsafe { + fetch + .entities + .entity_get_spawned_or_despawned_unchecked(entity) + .1 + }; + spawned.is_newer_than(fetch.last_run, fetch.this_run) + } +} + /// A marker trait to indicate that the filter works at an archetype level. /// /// This is needed to implement [`ExactSizeIterator`] for @@ -1015,7 +1213,7 @@ unsafe impl QueryFilter for Changed { /// [Tuples](prim@tuple) and [`Or`] filters are automatically implemented with the trait only if its containing types /// also implement the same trait. /// -/// [`Added`] and [`Changed`] works with entities, and therefore are not archetypal. As such +/// [`Added`], [`Changed`] and [`Spawned`] work with entities, and therefore are not archetypal. As such /// they do not implement [`ArchetypeFilter`]. #[diagnostic::on_unimplemented( message = "`{Self}` is not a valid `Query` filter based on archetype information", diff --git a/crates/bevy_ecs/src/query/state.rs b/crates/bevy_ecs/src/query/state.rs index e9a00f4646..cae8e592e7 100644 --- a/crates/bevy_ecs/src/query/state.rs +++ b/crates/bevy_ecs/src/query/state.rs @@ -287,7 +287,14 @@ impl QueryState { pub fn from_builder(builder: &mut QueryBuilder) -> Self { let mut fetch_state = D::init_state(builder.world_mut()); let filter_state = F::init_state(builder.world_mut()); - D::set_access(&mut fetch_state, builder.access()); + + let mut component_access = FilteredAccess::default(); + D::update_component_access(&fetch_state, &mut component_access); + D::provide_extra_access( + &mut fetch_state, + component_access.access_mut(), + builder.access().access(), + ); let mut component_access = builder.access().clone(); @@ -452,8 +459,8 @@ impl QueryState { /// /// This is equivalent to `self.iter().next().is_none()`, and thus the worst case runtime will be `O(n)` /// where `n` is the number of *potential* matches. This can be notably expensive for queries that rely - /// on non-archetypal filters such as [`Added`] or [`Changed`] which must individually check each query - /// result for a match. + /// on non-archetypal filters such as [`Added`], [`Changed`] or [`Spawned`] which must individually check + /// each query result for a match. /// /// # Panics /// @@ -461,6 +468,7 @@ impl QueryState { /// /// [`Added`]: crate::query::Added /// [`Changed`]: crate::query::Changed + /// [`Spawned`]: crate::query::Spawned #[inline] pub fn is_empty(&self, world: &World, last_run: Tick, this_run: Tick) -> bool { self.validate_world(world.id()); @@ -753,29 +761,27 @@ impl QueryState { let mut fetch_state = NewD::get_state(world.components()).expect("Could not create fetch_state, Please initialize all referenced components before transmuting."); let filter_state = NewF::get_state(world.components()).expect("Could not create filter_state, Please initialize all referenced components before transmuting."); - fn to_readonly(mut access: FilteredAccess) -> FilteredAccess { - access.access_mut().clear_writes(); - access - } - - let self_access = if D::IS_READ_ONLY && self.component_access.access().has_any_write() { + let mut self_access = self.component_access.clone(); + if D::IS_READ_ONLY { // The current state was transmuted from a mutable // `QueryData` to a read-only one. // Ignore any write access in the current state. - &to_readonly(self.component_access.clone()) - } else { - &self.component_access - }; + self_access.access_mut().clear_writes(); + } - NewD::set_access(&mut fetch_state, self_access); NewD::update_component_access(&fetch_state, &mut component_access); + NewD::provide_extra_access( + &mut fetch_state, + component_access.access_mut(), + self_access.access(), + ); let mut filter_component_access = FilteredAccess::default(); NewF::update_component_access(&filter_state, &mut filter_component_access); component_access.extend(&filter_component_access); assert!( - component_access.is_subset(self_access), + component_access.is_subset(&self_access), "Transmuted state for {} attempts to access terms that are not allowed by original state {}.", core::any::type_name::<(NewD, NewF)>(), core::any::type_name::<(D, F)>() ); @@ -787,7 +793,7 @@ impl QueryState { is_dense: self.is_dense, fetch_state, filter_state, - component_access: self.component_access.clone(), + component_access: self_access, matched_tables: self.matched_tables.clone(), matched_archetypes: self.matched_archetypes.clone(), #[cfg(feature = "trace")] @@ -881,8 +887,12 @@ impl QueryState { } } - NewD::set_access(&mut new_fetch_state, &joined_component_access); NewD::update_component_access(&new_fetch_state, &mut component_access); + NewD::provide_extra_access( + &mut new_fetch_state, + component_access.access_mut(), + joined_component_access.access(), + ); let mut new_filter_component_access = FilteredAccess::default(); NewF::update_component_access(&new_filter_state, &mut new_filter_component_access); @@ -984,7 +994,7 @@ impl QueryState { /// /// assert_eq!(component_values, [&A(0), &A(1), &A(2)]); /// - /// let wrong_entity = Entity::from_raw(365); + /// let wrong_entity = Entity::from_raw_u32(365).unwrap(); /// /// assert_eq!(match query_state.get_many(&mut world, [wrong_entity]).unwrap_err() {QueryEntityError::EntityDoesNotExist(error) => error.entity, _ => panic!()}, wrong_entity); /// ``` @@ -1022,7 +1032,7 @@ impl QueryState { /// /// assert_eq!(component_values, [&A(0), &A(1), &A(2)]); /// - /// let wrong_entity = Entity::from_raw(365); + /// let wrong_entity = Entity::from_raw_u32(365).unwrap(); /// /// assert_eq!(match query_state.get_many_unique(&mut world, UniqueEntityArray::from([wrong_entity])).unwrap_err() {QueryEntityError::EntityDoesNotExist(error) => error.entity, _ => panic!()}, wrong_entity); /// ``` @@ -1078,7 +1088,7 @@ impl QueryState { /// /// assert_eq!(component_values, [&A(5), &A(6), &A(7)]); /// - /// let wrong_entity = Entity::from_raw(57); + /// let wrong_entity = Entity::from_raw_u32(57).unwrap(); /// let invalid_entity = world.spawn_empty().id(); /// /// assert_eq!(match query_state.get_many(&mut world, [wrong_entity]).unwrap_err() {QueryEntityError::EntityDoesNotExist(error) => error.entity, _ => panic!()}, wrong_entity); @@ -1124,7 +1134,7 @@ impl QueryState { /// /// assert_eq!(component_values, [&A(5), &A(6), &A(7)]); /// - /// let wrong_entity = Entity::from_raw(57); + /// let wrong_entity = Entity::from_raw_u32(57).unwrap(); /// let invalid_entity = world.spawn_empty().id(); /// /// assert_eq!(match query_state.get_many_unique(&mut world, UniqueEntityArray::from([wrong_entity])).unwrap_err() {QueryEntityError::EntityDoesNotExist(error) => error.entity, _ => panic!()}, wrong_entity); @@ -1452,7 +1462,7 @@ impl QueryState { /// /// # assert_eq!(component_values, [&A(5), &A(6), &A(7)]); /// - /// # let wrong_entity = Entity::from_raw(57); + /// # let wrong_entity = Entity::from_raw_u32(57).unwrap(); /// # let invalid_entity = world.spawn_empty().id(); /// /// # assert_eq!(match query_state.get_many(&mut world, [wrong_entity]).unwrap_err() {QueryEntityError::EntityDoesNotExist(error) => error.entity, _ => panic!()}, wrong_entity); @@ -1773,7 +1783,7 @@ impl QueryState { /// /// fn my_system(query: Query<&A>) -> Result { /// let a = query.single()?; - /// + /// /// // Do something with `a` /// Ok(()) /// } @@ -1791,16 +1801,6 @@ impl QueryState { self.query(world).single_inner() } - /// A deprecated alias for [`QueryState::single`]. - #[deprecated(since = "0.16.0", note = "Please use `single` instead.")] - #[inline] - pub fn get_single<'w>( - &mut self, - world: &'w World, - ) -> Result, QuerySingleError> { - self.single(world) - } - /// Returns a single mutable query result when there is exactly one entity matching /// the query. /// @@ -1818,15 +1818,6 @@ impl QueryState { self.query_mut(world).single_inner() } - /// A deprecated alias for [`QueryState::single_mut`]. - #[deprecated(since = "0.16.0", note = "Please use `single` instead.")] - pub fn get_single_mut<'w>( - &mut self, - world: &'w mut World, - ) -> Result, QuerySingleError> { - self.single_mut(world) - } - /// Returns a query result when there is exactly one entity matching the query. /// /// If the number of query results is not exactly one, a [`QuerySingleError`] is returned @@ -1894,7 +1885,7 @@ mod tests { let world_2 = World::new(); let mut query_state = world_1.query::(); - let _panics = query_state.get(&world_2, Entity::from_raw(0)); + let _panics = query_state.get(&world_2, Entity::from_raw_u32(0).unwrap()); } #[test] @@ -2062,12 +2053,12 @@ mod tests { fn can_transmute_filtered_entity() { let mut world = World::new(); let entity = world.spawn((A(0), B(1))).id(); - let query = - QueryState::<(Entity, &A, &B)>::new(&mut world).transmute::(&world); + let query = QueryState::<(Entity, &A, &B)>::new(&mut world) + .transmute::<(Entity, FilteredEntityRef)>(&world); let mut query = query; // Our result is completely untyped - let entity_ref = query.single(&world).unwrap(); + let (_entity, entity_ref) = query.single(&world).unwrap(); assert_eq!(entity, entity_ref.id()); assert_eq!(0, entity_ref.get::().unwrap().0); @@ -2285,11 +2276,11 @@ mod tests { let query_1 = QueryState::<&mut A>::new(&mut world); let query_2 = QueryState::<&mut B>::new(&mut world); - let mut new_query: QueryState = query_1.join(&world, &query_2); + let mut new_query: QueryState<(Entity, FilteredEntityMut)> = query_1.join(&world, &query_2); - let mut entity = new_query.single_mut(&mut world).unwrap(); - assert!(entity.get_mut::().is_some()); - assert!(entity.get_mut::().is_some()); + let (_entity, mut entity_mut) = new_query.single_mut(&mut world).unwrap(); + assert!(entity_mut.get_mut::().is_some()); + assert!(entity_mut.get_mut::().is_some()); } #[test] @@ -2315,6 +2306,10 @@ mod tests { let mut query = QueryState::>::new(&mut world); assert_eq!(3, query.iter(&world).count()); + // Allows should bypass the filter entirely + let mut query = QueryState::<(), Allows>::new(&mut world); + assert_eq!(3, query.iter(&world).count()); + // Other filters should still be respected let mut query = QueryState::, Without>::new(&mut world); assert_eq!(1, query.iter(&world).count()); diff --git a/crates/bevy_ecs/src/query/world_query.rs b/crates/bevy_ecs/src/query/world_query.rs index da147770e0..a6bcbf58bd 100644 --- a/crates/bevy_ecs/src/query/world_query.rs +++ b/crates/bevy_ecs/src/query/world_query.rs @@ -13,11 +13,11 @@ use variadics_please::all_tuples; /// # Safety /// /// Implementor must ensure that -/// [`update_component_access`], [`matches_component_set`], [`QueryData::fetch`], [`QueryFilter::filter_fetch`] and [`init_fetch`] +/// [`update_component_access`], [`QueryData::provide_extra_access`], [`matches_component_set`], [`QueryData::fetch`], [`QueryFilter::filter_fetch`] and [`init_fetch`] /// obey the following: /// -/// - For each component mutably accessed by [`QueryData::fetch`], [`update_component_access`] should add write access unless read or write access has already been added, in which case it should panic. -/// - For each component readonly accessed by [`QueryData::fetch`] or [`QueryFilter::filter_fetch`], [`update_component_access`] should add read access unless write access has already been added, in which case it should panic. +/// - For each component mutably accessed by [`QueryData::fetch`], [`update_component_access`] or [`QueryData::provide_extra_access`] should add write access unless read or write access has already been added, in which case it should panic. +/// - For each component readonly accessed by [`QueryData::fetch`] or [`QueryFilter::filter_fetch`], [`update_component_access`] or [`QueryData::provide_extra_access`] should add read access unless write access has already been added, in which case it should panic. /// - If `fetch` mutably accesses the same component twice, [`update_component_access`] should panic. /// - [`update_component_access`] may not add a `Without` filter for a component unless [`matches_component_set`] always returns `false` when the component set contains that component. /// - [`update_component_access`] may not add a `With` filter for a component unless [`matches_component_set`] always returns `false` when the component set doesn't contain that component. @@ -27,9 +27,11 @@ use variadics_please::all_tuples; /// - Each filter in that disjunction must be a conjunction of the corresponding element's filter with the previous `access` /// - For each resource readonly accessed by [`init_fetch`], [`update_component_access`] should add read access. /// - Mutable resource access is not allowed. +/// - Any access added during [`QueryData::provide_extra_access`] must be a subset of `available_access`, and must not conflict with any access in `access`. /// /// When implementing [`update_component_access`], note that `add_read` and `add_write` both also add a `With` filter, whereas `extend_access` does not change the filters. /// +/// [`QueryData::provide_extra_access`]: crate::query::QueryData::provide_extra_access /// [`QueryData::fetch`]: crate::query::QueryData::fetch /// [`QueryFilter::filter_fetch`]: crate::query::QueryFilter::filter_fetch /// [`init_fetch`]: Self::init_fetch @@ -101,12 +103,6 @@ pub unsafe trait WorldQuery { /// - `state` must be the [`State`](Self::State) that `fetch` was initialized with. unsafe fn set_table<'w>(fetch: &mut Self::Fetch<'w>, state: &Self::State, table: &'w Table); - /// Sets available accesses for implementors with dynamic access such as [`FilteredEntityRef`](crate::world::FilteredEntityRef) - /// or [`FilteredEntityMut`](crate::world::FilteredEntityMut). - /// - /// Called when constructing a [`QueryLens`](crate::system::QueryLens) or calling [`QueryState::from_builder`](super::QueryState::from_builder) - fn set_access(_state: &mut Self::State, _access: &FilteredAccess) {} - /// Adds any component accesses used by this [`WorldQuery`] to `access`. /// /// Used to check which queries are disjoint and can run in parallel diff --git a/crates/bevy_ecs/src/relationship/related_methods.rs b/crates/bevy_ecs/src/relationship/related_methods.rs index 98ef8d0832..5a23214463 100644 --- a/crates/bevy_ecs/src/relationship/related_methods.rs +++ b/crates/bevy_ecs/src/relationship/related_methods.rs @@ -47,6 +47,11 @@ impl<'w> EntityWorldMut<'w> { self } + /// Removes the relation `R` between this entity and all its related entities. + pub fn clear_related(&mut self) -> &mut Self { + self.remove::() + } + /// Relates the given entities to this entity with the relation `R`, starting at this particular index. /// /// If the `related` has duplicates, a related entity will take the index of its last occurrence in `related`. @@ -81,7 +86,7 @@ impl<'w> EntityWorldMut<'w> { let id = self.id(); self.world_scope(|world| { for (offset, related) in related.iter().enumerate() { - let index = index + offset; + let index = index.saturating_add(offset); if world .get::(*related) .is_some_and(|relationship| relationship.get() == id) @@ -376,6 +381,13 @@ impl<'a> EntityCommands<'a> { }) } + /// Removes the relation `R` between this entity and all its related entities. + pub fn clear_related(&mut self) -> &mut Self { + self.queue(|mut entity: EntityWorldMut| { + entity.clear_related::(); + }) + } + /// Relates the given entities to this entity with the relation `R`, starting at this particular index. /// /// If the `related` has duplicates, a related entity will take the index of its last occurrence in `related`. @@ -519,6 +531,16 @@ impl<'w, R: Relationship> RelatedSpawner<'w, R> { pub fn target_entity(&self) -> Entity { self.target } + + /// Returns a reference to the underlying [`World`]. + pub fn world(&self) -> &World { + self.world + } + + /// Returns a mutable reference to the underlying [`World`]. + pub fn world_mut(&mut self) -> &mut World { + self.world + } } /// Uses commands to spawn related "source" entities with the given [`Relationship`], targeting @@ -613,4 +635,19 @@ mod tests { assert!(!world.entity(entity).contains::()); } } + + #[test] + fn remove_all_related() { + let mut world = World::new(); + + let a = world.spawn_empty().id(); + let b = world.spawn(ChildOf(a)).id(); + let c = world.spawn(ChildOf(a)).id(); + + world.entity_mut(a).clear_related::(); + + assert_eq!(world.entity(a).get::(), None); + assert_eq!(world.entity(b).get::(), None); + assert_eq!(world.entity(c).get::(), None); + } } diff --git a/crates/bevy_ecs/src/relationship/relationship_source_collection.rs b/crates/bevy_ecs/src/relationship/relationship_source_collection.rs index c2c9bd94d8..d4ea45f64f 100644 --- a/crates/bevy_ecs/src/relationship/relationship_source_collection.rs +++ b/crates/bevy_ecs/src/relationship/relationship_source_collection.rs @@ -1,5 +1,12 @@ -use crate::entity::{hash_set::EntityHashSet, Entity}; +use alloc::collections::{btree_set, BTreeSet}; +use core::{ + hash::BuildHasher, + ops::{Deref, DerefMut}, +}; + +use crate::entity::{Entity, EntityHashSet, EntityIndexSet}; use alloc::vec::Vec; +use indexmap::IndexSet; use smallvec::SmallVec; /// The internal [`Entity`] collection used by a [`RelationshipTarget`](crate::relationship::RelationshipTarget) component. @@ -213,15 +220,14 @@ impl OrderedRelationshipSourceCollection for Vec { fn place_most_recent(&mut self, index: usize) { if let Some(entity) = self.pop() { - let index = index.min(self.len().saturating_sub(1)); + let index = index.min(self.len()); self.insert(index, entity); } } fn place(&mut self, entity: Entity, index: usize) { if let Some(current) = <[Entity]>::iter(self).position(|e| *e == entity) { - // The len is at least 1, so the subtraction is safe. - let index = index.min(self.len().saturating_sub(1)); + let index = index.min(self.len()); Vec::remove(self, current); self.insert(index, entity); }; @@ -445,6 +451,138 @@ impl OrderedRelationshipSourceCollection for SmallVec<[Entity; N } } +impl RelationshipSourceCollection for IndexSet { + type SourceIter<'a> + = core::iter::Copied> + where + S: 'a; + + fn new() -> Self { + IndexSet::default() + } + + fn reserve(&mut self, additional: usize) { + self.reserve(additional); + } + + fn with_capacity(capacity: usize) -> Self { + IndexSet::with_capacity_and_hasher(capacity, S::default()) + } + + fn add(&mut self, entity: Entity) -> bool { + self.insert(entity) + } + + fn remove(&mut self, entity: Entity) -> bool { + self.shift_remove(&entity) + } + + fn iter(&self) -> Self::SourceIter<'_> { + self.iter().copied() + } + + fn len(&self) -> usize { + self.len() + } + + fn clear(&mut self) { + self.clear(); + } + + fn shrink_to_fit(&mut self) { + self.shrink_to_fit(); + } + + fn extend_from_iter(&mut self, entities: impl IntoIterator) { + self.extend(entities); + } +} + +impl RelationshipSourceCollection for EntityIndexSet { + type SourceIter<'a> = core::iter::Copied>; + + fn new() -> Self { + EntityIndexSet::new() + } + + fn reserve(&mut self, additional: usize) { + self.deref_mut().reserve(additional); + } + + fn with_capacity(capacity: usize) -> Self { + EntityIndexSet::with_capacity(capacity) + } + + fn add(&mut self, entity: Entity) -> bool { + self.insert(entity) + } + + fn remove(&mut self, entity: Entity) -> bool { + self.deref_mut().shift_remove(&entity) + } + + fn iter(&self) -> Self::SourceIter<'_> { + self.iter().copied() + } + + fn len(&self) -> usize { + self.deref().len() + } + + fn clear(&mut self) { + self.deref_mut().clear(); + } + + fn shrink_to_fit(&mut self) { + self.deref_mut().shrink_to_fit(); + } + + fn extend_from_iter(&mut self, entities: impl IntoIterator) { + self.extend(entities); + } +} + +impl RelationshipSourceCollection for BTreeSet { + type SourceIter<'a> = core::iter::Copied>; + + fn new() -> Self { + BTreeSet::new() + } + + fn with_capacity(_: usize) -> Self { + // BTreeSet doesn't have a capacity + Self::new() + } + + fn reserve(&mut self, _: usize) { + // BTreeSet doesn't have a capacity + } + + fn add(&mut self, entity: Entity) -> bool { + self.insert(entity) + } + + fn remove(&mut self, entity: Entity) -> bool { + self.remove(&entity) + } + + fn iter(&self) -> Self::SourceIter<'_> { + self.iter().copied() + } + + fn len(&self) -> usize { + self.len() + } + + fn clear(&mut self) { + self.clear(); + } + + fn shrink_to_fit(&mut self) { + // BTreeSet doesn't have a capacity + } +} + #[cfg(test)] mod tests { use super::*; @@ -547,6 +685,40 @@ mod tests { assert_eq!(a, world.get::(c).unwrap().0); } + #[test] + fn entity_index_map() { + #[derive(Component)] + #[relationship(relationship_target = RelTarget)] + struct Rel(Entity); + + #[derive(Component)] + #[relationship_target(relationship = Rel, linked_spawn)] + struct RelTarget(EntityHashSet); + + let mut world = World::new(); + let a = world.spawn_empty().id(); + let b = world.spawn_empty().id(); + let c = world.spawn_empty().id(); + + let d = world.spawn_empty().id(); + + world.entity_mut(a).add_related::(&[b, c, d]); + + let rel_target = world.get::(a).unwrap(); + let collection = rel_target.collection(); + + // Insertions should maintain ordering + assert!(collection.iter().eq(&[d, c, b])); + + world.entity_mut(c).despawn(); + + let rel_target = world.get::(a).unwrap(); + let collection = rel_target.collection(); + + // Removals should maintain ordering + assert!(collection.iter().eq(&[d, b])); + } + #[test] #[should_panic] fn one_to_one_relationship_shared_target() { @@ -557,7 +729,6 @@ mod tests { #[derive(Component)] #[relationship_target(relationship = Above)] struct Below(Entity); - let mut world = World::new(); let a = world.spawn_empty().id(); let b = world.spawn_empty().id(); diff --git a/crates/bevy_ecs/src/schedule/executor/mod.rs b/crates/bevy_ecs/src/schedule/executor/mod.rs index 0a78b5805d..a601284fb0 100644 --- a/crates/bevy_ecs/src/schedule/executor/mod.rs +++ b/crates/bevy_ecs/src/schedule/executor/mod.rs @@ -6,6 +6,7 @@ mod single_threaded; use alloc::{borrow::Cow, vec, vec::Vec}; use core::any::TypeId; +#[expect(deprecated, reason = "We still need to support this.")] pub use self::{simple::SimpleExecutor, single_threaded::SingleThreadedExecutor}; #[cfg(feature = "std")] @@ -18,7 +19,7 @@ use crate::{ component::{ComponentId, Tick}, error::{BevyError, ErrorContext, Result}, prelude::{IntoSystemSet, SystemSet}, - query::Access, + query::{Access, FilteredAccessSet}, schedule::{BoxedCondition, InternedSystemSet, NodeId, SystemTypeSet}, system::{ScheduleSystem, System, SystemIn, SystemParamValidationError}, world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, World}, @@ -53,6 +54,10 @@ pub enum ExecutorKind { SingleThreaded, /// Like [`SingleThreaded`](ExecutorKind::SingleThreaded) but calls [`apply_deferred`](crate::system::System::apply_deferred) /// immediately after running each system. + #[deprecated( + since = "0.17.0", + note = "Use SingleThreaded instead. See https://github.com/bevyengine/bevy/issues/18453 for motivation." + )] Simple, /// Runs the schedule using a thread pool. Non-conflicting systems can run in parallel. #[cfg(feature = "std")] @@ -118,17 +123,6 @@ impl SystemSchedule { } } -/// See [`ApplyDeferred`]. -#[deprecated( - since = "0.16.0", - note = "Use `ApplyDeferred` instead. This was previously a function but is now a marker struct System." -)] -#[expect( - non_upper_case_globals, - reason = "This item is deprecated; as such, its previous name needs to stay." -)] -pub const apply_deferred: ApplyDeferred = ApplyDeferred; - /// A special [`System`] that instructs the executor to call /// [`System::apply_deferred`] on the systems that have run but not applied /// their [`Deferred`] system parameters (like [`Commands`]) or other system buffers. @@ -174,6 +168,10 @@ impl System for ApplyDeferred { const { &Access::new() } } + fn component_access_set(&self) -> &FilteredAccessSet { + const { &FilteredAccessSet::new() } + } + fn archetype_component_access(&self) -> &Access { // This system accesses no archetype components. const { &Access::new() } @@ -267,38 +265,54 @@ impl IntoSystemSet<()> for ApplyDeferred { mod __rust_begin_short_backtrace { use core::hint::black_box; + #[cfg(feature = "std")] + use crate::world::unsafe_world_cell::UnsafeWorldCell; use crate::{ error::Result, system::{ReadOnlySystem, ScheduleSystem}, - world::{unsafe_world_cell::UnsafeWorldCell, World}, + world::World, }; /// # Safety /// See `System::run_unsafe`. + // This is only used by `MultiThreadedExecutor`, and would be dead code without `std`. + #[cfg(feature = "std")] #[inline(never)] pub(super) unsafe fn run_unsafe(system: &mut ScheduleSystem, world: UnsafeWorldCell) -> Result { let result = system.run_unsafe((), world); + // Call `black_box` to prevent this frame from being tail-call optimized away black_box(()); result } /// # Safety /// See `ReadOnlySystem::run_unsafe`. - #[cfg_attr( - not(feature = "std"), - expect(dead_code, reason = "currently only used with the std feature") - )] + // This is only used by `MultiThreadedExecutor`, and would be dead code without `std`. + #[cfg(feature = "std")] #[inline(never)] pub(super) unsafe fn readonly_run_unsafe( system: &mut dyn ReadOnlySystem, world: UnsafeWorldCell, ) -> O { + // Call `black_box` to prevent this frame from being tail-call optimized away black_box(system.run_unsafe((), world)) } #[inline(never)] pub(super) fn run(system: &mut ScheduleSystem, world: &mut World) -> Result { let result = system.run((), world); + // Call `black_box` to prevent this frame from being tail-call optimized away + black_box(()); + result + } + + #[inline(never)] + pub(super) fn run_without_applying_deferred( + system: &mut ScheduleSystem, + world: &mut World, + ) -> Result { + let result = system.run_without_applying_deferred((), world); + // Call `black_box` to prevent this frame from being tail-call optimized away black_box(()); result } @@ -308,6 +322,7 @@ mod __rust_begin_short_backtrace { system: &mut dyn ReadOnlySystem, world: &mut World, ) -> O { + // Call `black_box` to prevent this frame from being tail-call optimized away black_box(system.run((), world)) } } @@ -325,6 +340,7 @@ mod tests { struct TestComponent; const EXECUTORS: [ExecutorKind; 3] = [ + #[expect(deprecated, reason = "We still need to test this.")] ExecutorKind::Simple, ExecutorKind::SingleThreaded, ExecutorKind::MultiThreaded, @@ -385,6 +401,7 @@ mod tests { let mut world = World::new(); let mut schedule = Schedule::default(); + #[expect(deprecated, reason = "We still need to test this.")] schedule.set_executor_kind(ExecutorKind::Simple); schedule.add_systems(look_for_missing_resource); schedule.run(&mut world); diff --git a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs index dd029c91c8..763504eaec 100644 --- a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs +++ b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs @@ -1,7 +1,7 @@ use alloc::{boxed::Box, vec::Vec}; use bevy_platform::sync::Arc; use bevy_tasks::{ComputeTaskPool, Scope, TaskPool, ThreadExecutor}; -use bevy_utils::{default, syncunsafecell::SyncUnsafeCell}; +use bevy_utils::syncunsafecell::SyncUnsafeCell; use concurrent_queue::ConcurrentQueue; use core::{any::Any, panic::AssertUnwindSafe}; use fixedbitset::FixedBitSet; @@ -13,10 +13,8 @@ use std::sync::{Mutex, MutexGuard}; use tracing::{info_span, Span}; use crate::{ - archetype::ArchetypeComponentId, - error::{default_error_handler, BevyError, ErrorContext, Result}, + error::{ErrorContext, ErrorHandler, Result}, prelude::Resource, - query::Access, schedule::{is_apply_deferred, BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule}, system::ScheduleSystem, world::{unsafe_world_cell::UnsafeWorldCell, World}, @@ -62,8 +60,13 @@ impl<'env, 'sys> Environment<'env, 'sys> { /// Per-system data used by the [`MultiThreadedExecutor`]. // Copied here because it can't be read from the system when it's running. struct SystemTaskMetadata { - /// The [`ArchetypeComponentId`] access of the system. - archetype_component_access: Access, + /// The set of systems whose `component_access_set()` conflicts with this one. + conflicting_systems: FixedBitSet, + /// The set of systems whose `component_access_set()` conflicts with this system's conditions. + /// Note that this is separate from `conflicting_systems` to handle the case where + /// a system is skipped by an earlier system set condition or system stepping, + /// and needs access to run its conditions but not for itself. + condition_conflicting_systems: FixedBitSet, /// Indices of the systems that directly depend on the system. dependents: Vec, /// Is `true` if the system does not access `!Send` data. @@ -97,8 +100,8 @@ pub struct MultiThreadedExecutor { pub struct ExecutorState { /// Metadata for scheduling and running system tasks. system_task_metadata: Vec, - /// Union of the accesses of all currently running systems. - active_access: Access, + /// The set of systems whose `component_access_set()` conflicts with this system set's conditions. + set_condition_conflicting_systems: Vec, /// Returns `true` if a system with non-`Send` access is running. local_thread_running: bool, /// Returns `true` if an exclusive system is running. @@ -131,7 +134,7 @@ pub struct ExecutorState { struct Context<'scope, 'env, 'sys> { environment: &'env Environment<'env, 'sys>, scope: &'scope Scope<'scope, 'env, ()>, - error_handler: fn(BevyError, ErrorContext), + error_handler: ErrorHandler, } impl Default for MultiThreadedExecutor { @@ -164,7 +167,8 @@ impl SystemExecutor for MultiThreadedExecutor { state.system_task_metadata = Vec::with_capacity(sys_count); for index in 0..sys_count { state.system_task_metadata.push(SystemTaskMetadata { - archetype_component_access: default(), + conflicting_systems: FixedBitSet::with_capacity(sys_count), + condition_conflicting_systems: FixedBitSet::with_capacity(sys_count), dependents: schedule.system_dependents[index].clone(), is_send: schedule.systems[index].is_send(), is_exclusive: schedule.systems[index].is_exclusive(), @@ -174,6 +178,60 @@ impl SystemExecutor for MultiThreadedExecutor { } } + { + #[cfg(feature = "trace")] + let _span = info_span!("calculate conflicting systems").entered(); + for index1 in 0..sys_count { + let system1 = &schedule.systems[index1]; + for index2 in 0..index1 { + let system2 = &schedule.systems[index2]; + if !system2 + .component_access_set() + .is_compatible(system1.component_access_set()) + { + state.system_task_metadata[index1] + .conflicting_systems + .insert(index2); + state.system_task_metadata[index2] + .conflicting_systems + .insert(index1); + } + } + + for index2 in 0..sys_count { + let system2 = &schedule.systems[index2]; + if schedule.system_conditions[index1].iter().any(|condition| { + !system2 + .component_access_set() + .is_compatible(condition.component_access_set()) + }) { + state.system_task_metadata[index1] + .condition_conflicting_systems + .insert(index2); + } + } + } + + state.set_condition_conflicting_systems.clear(); + state.set_condition_conflicting_systems.reserve(set_count); + for set_idx in 0..set_count { + let mut conflicting_systems = FixedBitSet::with_capacity(sys_count); + for sys_index in 0..sys_count { + let system = &schedule.systems[sys_index]; + if schedule.set_conditions[set_idx].iter().any(|condition| { + !system + .component_access_set() + .is_compatible(condition.component_access_set()) + }) { + conflicting_systems.insert(sys_index); + } + } + state + .set_condition_conflicting_systems + .push(conflicting_systems); + } + } + state.num_dependencies_remaining = Vec::with_capacity(sys_count); } @@ -182,7 +240,7 @@ impl SystemExecutor for MultiThreadedExecutor { schedule: &mut SystemSchedule, world: &mut World, _skip_systems: Option<&FixedBitSet>, - error_handler: fn(BevyError, ErrorContext), + error_handler: ErrorHandler, ) { let state = self.state.get_mut().unwrap(); // reset counts @@ -257,7 +315,6 @@ impl SystemExecutor for MultiThreadedExecutor { debug_assert!(state.ready_systems.is_clear()); debug_assert!(state.running_systems.is_clear()); - state.active_access.clear(); state.evaluated_sets.clear(); state.skipped_systems.clear(); state.completed_systems.clear(); @@ -345,9 +402,9 @@ impl ExecutorState { fn new() -> Self { Self { system_task_metadata: Vec::new(), + set_condition_conflicting_systems: Vec::new(), num_running_systems: 0, num_dependencies_remaining: Vec::new(), - active_access: default(), local_thread_running: false, exclusive_running: false, evaluated_sets: FixedBitSet::new(), @@ -368,8 +425,6 @@ impl ExecutorState { self.finish_system_and_handle_dependents(result); } - self.rebuild_active_access(); - // SAFETY: // - `finish_system_and_handle_dependents` has updated the currently running systems. // - `rebuild_active_access` locks access for all currently running systems. @@ -429,6 +484,7 @@ impl ExecutorState { system, conditions, context.environment.world_cell, + context.error_handler, ) } { self.skip_system_and_signal_dependents(system_index); @@ -488,37 +544,30 @@ impl ExecutorState { { for condition in &mut conditions.set_conditions[set_idx] { condition.update_archetype_component_access(world); - if !condition - .archetype_component_access() - .is_compatible(&self.active_access) - { - return false; - } + } + if !self.set_condition_conflicting_systems[set_idx].is_disjoint(&self.running_systems) { + return false; } } for condition in &mut conditions.system_conditions[system_index] { condition.update_archetype_component_access(world); - if !condition - .archetype_component_access() - .is_compatible(&self.active_access) - { - return false; - } + } + if !system_meta + .condition_conflicting_systems + .is_disjoint(&self.running_systems) + { + return false; } if !self.skipped_systems.contains(system_index) { system.update_archetype_component_access(world); - if !system - .archetype_component_access() - .is_compatible(&self.active_access) + if !system_meta + .conflicting_systems + .is_disjoint(&self.running_systems) { return false; } - - self.system_task_metadata[system_index] - .archetype_component_access - .clone_from(system.archetype_component_access()); } true @@ -536,9 +585,9 @@ impl ExecutorState { system: &mut ScheduleSystem, conditions: &mut Conditions, world: UnsafeWorldCell, + error_handler: ErrorHandler, ) -> bool { let mut should_run = !self.skipped_systems.contains(system_index); - let error_handler = default_error_handler(); for set_idx in conditions.sets_with_conditions_of_systems[system_index].ones() { if self.evaluated_sets.contains(set_idx) { @@ -551,7 +600,11 @@ impl ExecutorState { // required by the conditions. // - `update_archetype_component_access` has been called for each run condition. let set_conditions_met = unsafe { - evaluate_and_fold_conditions(&mut conditions.set_conditions[set_idx], world) + evaluate_and_fold_conditions( + &mut conditions.set_conditions[set_idx], + world, + error_handler, + ) }; if !set_conditions_met { @@ -569,7 +622,11 @@ impl ExecutorState { // required by the conditions. // - `update_archetype_component_access` has been called for each run condition. let system_conditions_met = unsafe { - evaluate_and_fold_conditions(&mut conditions.system_conditions[system_index], world) + evaluate_and_fold_conditions( + &mut conditions.system_conditions[system_index], + world, + error_handler, + ) }; if !system_conditions_met { @@ -648,9 +705,6 @@ impl ExecutorState { context.system_completed(system_index, res, system); }; - self.active_access - .extend(&system_meta.archetype_component_access); - if system_meta.is_send { context.scope.spawn(task); } else { @@ -741,15 +795,6 @@ impl ExecutorState { } } } - - fn rebuild_active_access(&mut self) { - self.active_access.clear(); - for index in self.running_systems.ones() { - let system_meta = &self.system_task_metadata[index]; - self.active_access - .extend(&system_meta.archetype_component_access); - } - } } fn apply_deferred( @@ -786,9 +831,8 @@ fn apply_deferred( unsafe fn evaluate_and_fold_conditions( conditions: &mut [BoxedCondition], world: UnsafeWorldCell, + error_handler: ErrorHandler, ) -> bool { - let error_handler = default_error_handler(); - #[expect( clippy::unnecessary_fold, reason = "Short-circuiting here would prevent conditions from mutating their own state as needed." diff --git a/crates/bevy_ecs/src/schedule/executor/simple.rs b/crates/bevy_ecs/src/schedule/executor/simple.rs index a237a356de..584c5a1073 100644 --- a/crates/bevy_ecs/src/schedule/executor/simple.rs +++ b/crates/bevy_ecs/src/schedule/executor/simple.rs @@ -1,3 +1,5 @@ +#![expect(deprecated, reason = "Everything here is deprecated")] + use core::panic::AssertUnwindSafe; use fixedbitset::FixedBitSet; @@ -8,7 +10,7 @@ use tracing::info_span; use std::eprintln; use crate::{ - error::{default_error_handler, BevyError, ErrorContext}, + error::{ErrorContext, ErrorHandler}, schedule::{ executor::is_apply_deferred, BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule, }, @@ -20,6 +22,10 @@ use super::__rust_begin_short_backtrace; /// A variant of [`SingleThreadedExecutor`](crate::schedule::SingleThreadedExecutor) that calls /// [`apply_deferred`](crate::system::System::apply_deferred) immediately after running each system. #[derive(Default)] +#[deprecated( + since = "0.17.0", + note = "Use SingleThreadedExecutor instead. See https://github.com/bevyengine/bevy/issues/18453 for motivation." +)] pub struct SimpleExecutor { /// Systems sets whose conditions have been evaluated. evaluated_sets: FixedBitSet, @@ -44,7 +50,7 @@ impl SystemExecutor for SimpleExecutor { schedule: &mut SystemSchedule, world: &mut World, _skip_systems: Option<&FixedBitSet>, - error_handler: fn(BevyError, ErrorContext), + error_handler: ErrorHandler, ) { // If stepping is enabled, make sure we skip those systems that should // not be run. @@ -67,8 +73,11 @@ impl SystemExecutor for SimpleExecutor { } // evaluate system set's conditions - let set_conditions_met = - evaluate_and_fold_conditions(&mut schedule.set_conditions[set_idx], world); + let set_conditions_met = evaluate_and_fold_conditions( + &mut schedule.set_conditions[set_idx], + world, + error_handler, + ); if !set_conditions_met { self.completed_systems @@ -80,8 +89,11 @@ impl SystemExecutor for SimpleExecutor { } // evaluate system's conditions - let system_conditions_met = - evaluate_and_fold_conditions(&mut schedule.system_conditions[system_index], world); + let system_conditions_met = evaluate_and_fold_conditions( + &mut schedule.system_conditions[system_index], + world, + error_handler, + ); should_run &= system_conditions_met; @@ -165,10 +177,15 @@ impl SimpleExecutor { } } } - -fn evaluate_and_fold_conditions(conditions: &mut [BoxedCondition], world: &mut World) -> bool { - let error_handler = default_error_handler(); - +#[deprecated( + since = "0.17.0", + note = "Use SingleThreadedExecutor instead. See https://github.com/bevyengine/bevy/issues/18453 for motivation." +)] +fn evaluate_and_fold_conditions( + conditions: &mut [BoxedCondition], + world: &mut World, + error_handler: ErrorHandler, +) -> bool { #[expect( clippy::unnecessary_fold, reason = "Short-circuiting here would prevent conditions from mutating their own state as needed." diff --git a/crates/bevy_ecs/src/schedule/executor/single_threaded.rs b/crates/bevy_ecs/src/schedule/executor/single_threaded.rs index b42f47726d..0076103637 100644 --- a/crates/bevy_ecs/src/schedule/executor/single_threaded.rs +++ b/crates/bevy_ecs/src/schedule/executor/single_threaded.rs @@ -8,7 +8,7 @@ use tracing::info_span; use std::eprintln; use crate::{ - error::{default_error_handler, BevyError, ErrorContext}, + error::{ErrorContext, ErrorHandler}, schedule::{is_apply_deferred, BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule}, world::World, }; @@ -50,7 +50,7 @@ impl SystemExecutor for SingleThreadedExecutor { schedule: &mut SystemSchedule, world: &mut World, _skip_systems: Option<&FixedBitSet>, - error_handler: fn(BevyError, ErrorContext), + error_handler: ErrorHandler, ) { // If stepping is enabled, make sure we skip those systems that should // not be run. @@ -73,8 +73,11 @@ impl SystemExecutor for SingleThreadedExecutor { } // evaluate system set's conditions - let set_conditions_met = - evaluate_and_fold_conditions(&mut schedule.set_conditions[set_idx], world); + let set_conditions_met = evaluate_and_fold_conditions( + &mut schedule.set_conditions[set_idx], + world, + error_handler, + ); if !set_conditions_met { self.completed_systems @@ -86,8 +89,11 @@ impl SystemExecutor for SingleThreadedExecutor { } // evaluate system's conditions - let system_conditions_met = - evaluate_and_fold_conditions(&mut schedule.system_conditions[system_index], world); + let system_conditions_met = evaluate_and_fold_conditions( + &mut schedule.system_conditions[system_index], + world, + error_handler, + ); should_run &= system_conditions_met; @@ -128,33 +134,16 @@ impl SystemExecutor for SingleThreadedExecutor { } let f = AssertUnwindSafe(|| { - if system.is_exclusive() { - if let Err(err) = __rust_begin_short_backtrace::run(system, world) { - error_handler( - err, - ErrorContext::System { - name: system.name(), - last_run: system.get_last_run(), - }, - ); - } - } else { - // Use run_unsafe to avoid immediately applying deferred buffers - let world = world.as_unsafe_world_cell(); - system.update_archetype_component_access(world); - // SAFETY: We have exclusive, single-threaded access to the world and - // update_archetype_component_access is being called immediately before this. - unsafe { - if let Err(err) = __rust_begin_short_backtrace::run_unsafe(system, world) { - error_handler( - err, - ErrorContext::System { - name: system.name(), - last_run: system.get_last_run(), - }, - ); - } - }; + if let Err(err) = + __rust_begin_short_backtrace::run_without_applying_deferred(system, world) + { + error_handler( + err, + ErrorContext::System { + name: system.name(), + last_run: system.get_last_run(), + }, + ); } }); @@ -210,9 +199,11 @@ impl SingleThreadedExecutor { } } -fn evaluate_and_fold_conditions(conditions: &mut [BoxedCondition], world: &mut World) -> bool { - let error_handler: fn(BevyError, ErrorContext) = default_error_handler(); - +fn evaluate_and_fold_conditions( + conditions: &mut [BoxedCondition], + world: &mut World, + error_handler: ErrorHandler, +) -> bool { #[expect( clippy::unnecessary_fold, reason = "Short-circuiting here would prevent conditions from mutating their own state as needed." diff --git a/crates/bevy_ecs/src/schedule/mod.rs b/crates/bevy_ecs/src/schedule/mod.rs index aeaf8e3929..81912d2f72 100644 --- a/crates/bevy_ecs/src/schedule/mod.rs +++ b/crates/bevy_ecs/src/schedule/mod.rs @@ -37,7 +37,7 @@ mod tests { }; #[derive(SystemSet, Clone, Debug, PartialEq, Eq, Hash)] - enum TestSet { + enum TestSystems { A, B, C, @@ -142,7 +142,7 @@ mod tests { make_function_system(1).before(named_system), make_function_system(0) .after(named_system) - .in_set(TestSet::A), + .in_set(TestSystems::A), )); schedule.run(&mut world); @@ -153,12 +153,12 @@ mod tests { assert_eq!(world.resource::().0, vec![]); // modify the schedule after it's been initialized and test ordering with sets - schedule.configure_sets(TestSet::A.after(named_system)); + schedule.configure_sets(TestSystems::A.after(named_system)); schedule.add_systems(( make_function_system(3) - .before(TestSet::A) + .before(TestSystems::A) .after(named_system), - make_function_system(4).after(TestSet::A), + make_function_system(4).after(TestSystems::A), )); schedule.run(&mut world); @@ -347,14 +347,14 @@ mod tests { world.init_resource::(); - schedule.configure_sets(TestSet::A.run_if(|| false).run_if(|| false)); - schedule.add_systems(counting_system.in_set(TestSet::A)); - schedule.configure_sets(TestSet::B.run_if(|| true).run_if(|| false)); - schedule.add_systems(counting_system.in_set(TestSet::B)); - schedule.configure_sets(TestSet::C.run_if(|| false).run_if(|| true)); - schedule.add_systems(counting_system.in_set(TestSet::C)); - schedule.configure_sets(TestSet::D.run_if(|| true).run_if(|| true)); - schedule.add_systems(counting_system.in_set(TestSet::D)); + schedule.configure_sets(TestSystems::A.run_if(|| false).run_if(|| false)); + schedule.add_systems(counting_system.in_set(TestSystems::A)); + schedule.configure_sets(TestSystems::B.run_if(|| true).run_if(|| false)); + schedule.add_systems(counting_system.in_set(TestSystems::B)); + schedule.configure_sets(TestSystems::C.run_if(|| false).run_if(|| true)); + schedule.add_systems(counting_system.in_set(TestSystems::C)); + schedule.configure_sets(TestSystems::D.run_if(|| true).run_if(|| true)); + schedule.add_systems(counting_system.in_set(TestSystems::D)); schedule.run(&mut world); assert_eq!(world.resource::().0.load(Ordering::Relaxed), 1); @@ -367,14 +367,14 @@ mod tests { world.init_resource::(); - schedule.configure_sets(TestSet::A.run_if(|| false)); - schedule.add_systems(counting_system.in_set(TestSet::A).run_if(|| false)); - schedule.configure_sets(TestSet::B.run_if(|| true)); - schedule.add_systems(counting_system.in_set(TestSet::B).run_if(|| false)); - schedule.configure_sets(TestSet::C.run_if(|| false)); - schedule.add_systems(counting_system.in_set(TestSet::C).run_if(|| true)); - schedule.configure_sets(TestSet::D.run_if(|| true)); - schedule.add_systems(counting_system.in_set(TestSet::D).run_if(|| true)); + schedule.configure_sets(TestSystems::A.run_if(|| false)); + schedule.add_systems(counting_system.in_set(TestSystems::A).run_if(|| false)); + schedule.configure_sets(TestSystems::B.run_if(|| true)); + schedule.add_systems(counting_system.in_set(TestSystems::B).run_if(|| false)); + schedule.configure_sets(TestSystems::C.run_if(|| false)); + schedule.add_systems(counting_system.in_set(TestSystems::C).run_if(|| true)); + schedule.configure_sets(TestSystems::D.run_if(|| true)); + schedule.add_systems(counting_system.in_set(TestSystems::D).run_if(|| true)); schedule.run(&mut world); assert_eq!(world.resource::().0.load(Ordering::Relaxed), 1); @@ -440,12 +440,12 @@ mod tests { let mut schedule = Schedule::default(); schedule.configure_sets( - TestSet::A + TestSystems::A .run_if(|res1: Res| res1.is_changed()) .run_if(|res2: Res| res2.is_changed()), ); - schedule.add_systems(counting_system.in_set(TestSet::A)); + schedule.add_systems(counting_system.in_set(TestSystems::A)); // both resource were just added. schedule.run(&mut world); @@ -489,13 +489,14 @@ mod tests { world.init_resource::(); let mut schedule = Schedule::default(); - schedule - .configure_sets(TestSet::A.run_if(|res1: Res| res1.is_changed())); + schedule.configure_sets( + TestSystems::A.run_if(|res1: Res| res1.is_changed()), + ); schedule.add_systems( counting_system .run_if(|res2: Res| res2.is_changed()) - .in_set(TestSet::A), + .in_set(TestSystems::A), ); // both resource were just added. @@ -537,7 +538,7 @@ mod tests { #[should_panic] fn dependency_loop() { let mut schedule = Schedule::default(); - schedule.configure_sets(TestSet::X.after(TestSet::X)); + schedule.configure_sets(TestSystems::X.after(TestSystems::X)); } #[test] @@ -545,8 +546,8 @@ mod tests { let mut world = World::new(); let mut schedule = Schedule::default(); - schedule.configure_sets(TestSet::A.after(TestSet::B)); - schedule.configure_sets(TestSet::B.after(TestSet::A)); + schedule.configure_sets(TestSystems::A.after(TestSystems::B)); + schedule.configure_sets(TestSystems::B.after(TestSystems::A)); let result = schedule.initialize(&mut world); assert!(matches!( @@ -572,7 +573,7 @@ mod tests { #[should_panic] fn hierarchy_loop() { let mut schedule = Schedule::default(); - schedule.configure_sets(TestSet::X.in_set(TestSet::X)); + schedule.configure_sets(TestSystems::X.in_set(TestSystems::X)); } #[test] @@ -580,8 +581,8 @@ mod tests { let mut world = World::new(); let mut schedule = Schedule::default(); - schedule.configure_sets(TestSet::A.in_set(TestSet::B)); - schedule.configure_sets(TestSet::B.in_set(TestSet::A)); + schedule.configure_sets(TestSystems::A.in_set(TestSystems::B)); + schedule.configure_sets(TestSystems::B.in_set(TestSystems::A)); let result = schedule.initialize(&mut world); assert!(matches!(result, Err(ScheduleBuildError::HierarchyCycle(_)))); @@ -647,13 +648,13 @@ mod tests { }); // Add `A`. - schedule.configure_sets(TestSet::A); + schedule.configure_sets(TestSystems::A); // Add `B` as child of `A`. - schedule.configure_sets(TestSet::B.in_set(TestSet::A)); + schedule.configure_sets(TestSystems::B.in_set(TestSystems::A)); // Add `X` as child of both `A` and `B`. - schedule.configure_sets(TestSet::X.in_set(TestSet::A).in_set(TestSet::B)); + schedule.configure_sets(TestSystems::X.in_set(TestSystems::A).in_set(TestSystems::B)); // `X` cannot be the `A`'s child and grandchild at the same time. let result = schedule.initialize(&mut world); @@ -669,8 +670,8 @@ mod tests { let mut schedule = Schedule::default(); // Add `B` and give it both kinds of relationships with `A`. - schedule.configure_sets(TestSet::B.in_set(TestSet::A)); - schedule.configure_sets(TestSet::B.after(TestSet::A)); + schedule.configure_sets(TestSystems::B.in_set(TestSystems::A)); + schedule.configure_sets(TestSystems::B.after(TestSystems::A)); let result = schedule.initialize(&mut world); assert!(matches!( result, @@ -686,13 +687,13 @@ mod tests { fn foo() {} // Add `foo` to both `A` and `C`. - schedule.add_systems(foo.in_set(TestSet::A).in_set(TestSet::C)); + schedule.add_systems(foo.in_set(TestSystems::A).in_set(TestSystems::C)); // Order `A -> B -> C`. schedule.configure_sets(( - TestSet::A, - TestSet::B.after(TestSet::A), - TestSet::C.after(TestSet::B), + TestSystems::A, + TestSystems::B.after(TestSystems::A), + TestSystems::C.after(TestSystems::B), )); let result = schedule.initialize(&mut world); @@ -1237,6 +1238,7 @@ mod tests { /// verify the [`SimpleExecutor`] supports stepping #[test] + #[expect(deprecated, reason = "We still need to test this.")] fn simple_executor() { assert_executor_supports_stepping!(ExecutorKind::Simple); } diff --git a/crates/bevy_ecs/src/schedule/schedule.rs b/crates/bevy_ecs/src/schedule/schedule.rs index 584e621bf8..b15860406b 100644 --- a/crates/bevy_ecs/src/schedule/schedule.rs +++ b/crates/bevy_ecs/src/schedule/schedule.rs @@ -27,7 +27,6 @@ use tracing::info_span; use crate::{ component::{ComponentId, Components, Tick}, - error::default_error_handler, prelude::Component, resource::Resource, schedule::*, @@ -218,6 +217,7 @@ impl Schedules { fn make_executor(kind: ExecutorKind) -> Box { match kind { + #[expect(deprecated, reason = "We still need to support this.")] ExecutorKind::Simple => Box::new(SimpleExecutor::new()), ExecutorKind::SingleThreaded => Box::new(SingleThreadedExecutor::new()), #[cfg(feature = "std")] @@ -441,7 +441,7 @@ impl Schedule { self.initialize(world) .unwrap_or_else(|e| panic!("Error when initializing schedule {:?}: {e}", self.label)); - let error_handler = default_error_handler(); + let error_handler = world.default_error_handler(); #[cfg(not(feature = "bevy_debug_stepping"))] self.executor @@ -2060,6 +2060,7 @@ mod tests { use bevy_ecs_macros::ScheduleLabel; use crate::{ + error::{ignore, panic, DefaultErrorHandler, Result}, prelude::{ApplyDeferred, Res, Resource}, schedule::{ tests::ResMut, IntoScheduleConfigs, Schedule, ScheduleBuildSettings, SystemSet, @@ -2809,4 +2810,32 @@ mod tests { .expect("CheckSystemRan Resource Should Exist"); assert_eq!(value.0, 2); } + + #[test] + fn test_default_error_handler() { + #[derive(Resource, Default)] + struct Ran(bool); + + fn system(mut ran: ResMut) -> Result { + ran.0 = true; + Err("I failed!".into()) + } + + // Test that the default error handler is used + let mut world = World::default(); + world.init_resource::(); + world.insert_resource(DefaultErrorHandler(ignore)); + let mut schedule = Schedule::default(); + schedule.add_systems(system).run(&mut world); + assert!(world.resource::().0); + + // Test that the handler doesn't change within the schedule + schedule.add_systems( + (|world: &mut World| { + world.insert_resource(DefaultErrorHandler(panic)); + }) + .before(system), + ); + schedule.run(&mut world); + } } diff --git a/crates/bevy_ecs/src/spawn.rs b/crates/bevy_ecs/src/spawn.rs index 5235889ffb..d5014f2240 100644 --- a/crates/bevy_ecs/src/spawn.rs +++ b/crates/bevy_ecs/src/spawn.rs @@ -356,6 +356,151 @@ impl SpawnRelated for T { #[macro_export] macro_rules! related { ($relationship_target:ty [$($child:expr),*$(,)?]) => { - <$relationship_target>::spawn(($($crate::spawn::Spawn($child)),*)) + <$relationship_target>::spawn($crate::recursive_spawn!($($child),*)) + }; +} + +// A tail-recursive spawn utility. +// +// Since `SpawnableList` is only implemented for tuples +// up to twelve elements long, this macro will nest +// longer sequences recursively. By default, this recursion +// will top out at around 1400 elements, but it would be +// ill-advised to spawn that many entities with this method. +// +// For spawning large batches of entities at a time, +// consider `SpawnIter` or eagerly spawning with `Commands`. +#[macro_export] +#[doc(hidden)] +macro_rules! recursive_spawn { + // direct expansion + ($a:expr) => { + $crate::spawn::Spawn($a) + }; + ($a:expr, $b:expr) => { + ( + $crate::spawn::Spawn($a), + $crate::spawn::Spawn($b), + ) + }; + ($a:expr, $b:expr, $c:expr) => { + ( + $crate::spawn::Spawn($a), + $crate::spawn::Spawn($b), + $crate::spawn::Spawn($c), + ) + }; + ($a:expr, $b:expr, $c:expr, $d:expr) => { + ( + $crate::spawn::Spawn($a), + $crate::spawn::Spawn($b), + $crate::spawn::Spawn($c), + $crate::spawn::Spawn($d), + ) + }; + ($a:expr, $b:expr, $c:expr, $d:expr, $e:expr) => { + ( + $crate::spawn::Spawn($a), + $crate::spawn::Spawn($b), + $crate::spawn::Spawn($c), + $crate::spawn::Spawn($d), + $crate::spawn::Spawn($e), + ) + }; + ($a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr) => { + ( + $crate::spawn::Spawn($a), + $crate::spawn::Spawn($b), + $crate::spawn::Spawn($c), + $crate::spawn::Spawn($d), + $crate::spawn::Spawn($e), + $crate::spawn::Spawn($f), + ) + }; + ($a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr, $g:expr) => { + ( + $crate::spawn::Spawn($a), + $crate::spawn::Spawn($b), + $crate::spawn::Spawn($c), + $crate::spawn::Spawn($d), + $crate::spawn::Spawn($e), + $crate::spawn::Spawn($f), + $crate::spawn::Spawn($g), + ) + }; + ($a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr, $g:expr, $h:expr) => { + ( + $crate::spawn::Spawn($a), + $crate::spawn::Spawn($b), + $crate::spawn::Spawn($c), + $crate::spawn::Spawn($d), + $crate::spawn::Spawn($e), + $crate::spawn::Spawn($f), + $crate::spawn::Spawn($g), + $crate::spawn::Spawn($h), + ) + }; + ($a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr, $g:expr, $h:expr, $i:expr) => { + ( + $crate::spawn::Spawn($a), + $crate::spawn::Spawn($b), + $crate::spawn::Spawn($c), + $crate::spawn::Spawn($d), + $crate::spawn::Spawn($e), + $crate::spawn::Spawn($f), + $crate::spawn::Spawn($g), + $crate::spawn::Spawn($h), + $crate::spawn::Spawn($i), + ) + }; + ($a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr, $g:expr, $h:expr, $i:expr, $j:expr) => { + ( + $crate::spawn::Spawn($a), + $crate::spawn::Spawn($b), + $crate::spawn::Spawn($c), + $crate::spawn::Spawn($d), + $crate::spawn::Spawn($e), + $crate::spawn::Spawn($f), + $crate::spawn::Spawn($g), + $crate::spawn::Spawn($h), + $crate::spawn::Spawn($i), + $crate::spawn::Spawn($j), + ) + }; + ($a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr, $g:expr, $h:expr, $i:expr, $j:expr, $k:expr) => { + ( + $crate::spawn::Spawn($a), + $crate::spawn::Spawn($b), + $crate::spawn::Spawn($c), + $crate::spawn::Spawn($d), + $crate::spawn::Spawn($e), + $crate::spawn::Spawn($f), + $crate::spawn::Spawn($g), + $crate::spawn::Spawn($h), + $crate::spawn::Spawn($i), + $crate::spawn::Spawn($j), + $crate::spawn::Spawn($k), + ) + }; + + // recursive expansion + ( + $a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr, + $g:expr, $h:expr, $i:expr, $j:expr, $k:expr, $($rest:expr),* + ) => { + ( + $crate::spawn::Spawn($a), + $crate::spawn::Spawn($b), + $crate::spawn::Spawn($c), + $crate::spawn::Spawn($d), + $crate::spawn::Spawn($e), + $crate::spawn::Spawn($f), + $crate::spawn::Spawn($g), + $crate::spawn::Spawn($h), + $crate::spawn::Spawn($i), + $crate::spawn::Spawn($j), + $crate::spawn::Spawn($k), + $crate::recursive_spawn!($($rest),*) + ) }; } diff --git a/crates/bevy_ecs/src/storage/blob_vec.rs b/crates/bevy_ecs/src/storage/blob_vec.rs index 2451fccb14..85852a2bea 100644 --- a/crates/bevy_ecs/src/storage/blob_vec.rs +++ b/crates/bevy_ecs/src/storage/blob_vec.rs @@ -366,6 +366,13 @@ impl BlobVec { unsafe { core::slice::from_raw_parts(self.data.as_ptr() as *const UnsafeCell, self.len) } } + /// Returns the drop function for values stored in the vector, + /// or `None` if they don't need to be dropped. + #[inline] + pub fn get_drop(&self) -> Option)> { + self.drop + } + /// Clears the vector, removing (and dropping) all values. /// /// Note that this method has no effect on the allocated capacity of the vector. diff --git a/crates/bevy_ecs/src/storage/sparse_set.rs b/crates/bevy_ecs/src/storage/sparse_set.rs index bb79382e06..69305e8a14 100644 --- a/crates/bevy_ecs/src/storage/sparse_set.rs +++ b/crates/bevy_ecs/src/storage/sparse_set.rs @@ -300,6 +300,13 @@ impl ComponentSparseSet { }) } + /// Returns the drop function for the component type stored in the sparse set, + /// or `None` if it doesn't need to be dropped. + #[inline] + pub fn get_drop(&self) -> Option)> { + self.dense.get_drop() + } + /// Removes the `entity` from this sparse set and returns a pointer to the associated value (if /// it exists). #[must_use = "The returned pointer must be used to drop the removed component."] @@ -652,10 +659,11 @@ mod tests { use super::SparseSets; use crate::{ component::{Component, ComponentDescriptor, ComponentId, ComponentInfo}, - entity::Entity, + entity::{Entity, EntityRow}, storage::SparseSet, }; use alloc::{vec, vec::Vec}; + use nonmax::NonMaxU32; #[derive(Debug, Eq, PartialEq)] struct Foo(usize); @@ -663,11 +671,11 @@ mod tests { #[test] fn sparse_set() { let mut set = SparseSet::::default(); - let e0 = Entity::from_raw(0); - let e1 = Entity::from_raw(1); - let e2 = Entity::from_raw(2); - let e3 = Entity::from_raw(3); - let e4 = Entity::from_raw(4); + let e0 = Entity::from_raw(EntityRow::new(NonMaxU32::new(0).unwrap())); + let e1 = Entity::from_raw(EntityRow::new(NonMaxU32::new(1).unwrap())); + let e2 = Entity::from_raw(EntityRow::new(NonMaxU32::new(2).unwrap())); + let e3 = Entity::from_raw(EntityRow::new(NonMaxU32::new(3).unwrap())); + let e4 = Entity::from_raw(EntityRow::new(NonMaxU32::new(4).unwrap())); set.insert(e1, Foo(1)); set.insert(e2, Foo(2)); diff --git a/crates/bevy_ecs/src/storage/table/column.rs b/crates/bevy_ecs/src/storage/table/column.rs index d4690d264c..522df222c6 100644 --- a/crates/bevy_ecs/src/storage/table/column.rs +++ b/crates/bevy_ecs/src/storage/table/column.rs @@ -697,4 +697,11 @@ impl Column { changed_by.get_unchecked(row.as_usize()) }) } + + /// Returns the drop function for elements of the column, + /// or `None` if they don't need to be dropped. + #[inline] + pub fn get_drop(&self) -> Option)> { + self.data.get_drop() + } } diff --git a/crates/bevy_ecs/src/storage/table/mod.rs b/crates/bevy_ecs/src/storage/table/mod.rs index 0f80b77f51..fd8eb410e4 100644 --- a/crates/bevy_ecs/src/storage/table/mod.rs +++ b/crates/bevy_ecs/src/storage/table/mod.rs @@ -823,11 +823,12 @@ mod tests { use crate::{ change_detection::MaybeLocation, component::{Component, ComponentIds, Components, ComponentsRegistrator, Tick}, - entity::Entity, + entity::{Entity, EntityRow}, ptr::OwningPtr, storage::{TableBuilder, TableId, TableRow, Tables}, }; use alloc::vec::Vec; + use nonmax::NonMaxU32; #[derive(Component)] struct W(T); @@ -856,7 +857,9 @@ mod tests { let mut table = TableBuilder::with_capacity(0, columns.len()) .add_column(components.get_info(component_id).unwrap()) .build(); - let entities = (0..200).map(Entity::from_raw).collect::>(); + let entities = (0..200) + .map(|index| Entity::from_raw(EntityRow::new(NonMaxU32::new(index).unwrap()))) + .collect::>(); for entity in &entities { // SAFETY: we allocate and immediately set data afterwards unsafe { diff --git a/crates/bevy_ecs/src/system/adapter_system.rs b/crates/bevy_ecs/src/system/adapter_system.rs index 825389a307..5953a43d70 100644 --- a/crates/bevy_ecs/src/system/adapter_system.rs +++ b/crates/bevy_ecs/src/system/adapter_system.rs @@ -131,6 +131,12 @@ where self.system.component_access() } + fn component_access_set( + &self, + ) -> &crate::query::FilteredAccessSet { + self.system.component_access_set() + } + #[inline] fn archetype_component_access( &self, diff --git a/crates/bevy_ecs/src/system/builder.rs b/crates/bevy_ecs/src/system/builder.rs index 6261b9e355..441d4d42fc 100644 --- a/crates/bevy_ecs/src/system/builder.rs +++ b/crates/bevy_ecs/src/system/builder.rs @@ -8,6 +8,7 @@ use crate::{ resource::Resource, system::{ DynSystemParam, DynSystemParamState, Local, ParamSet, Query, SystemMeta, SystemParam, + SystemParamValidationError, When, }, world::{ FilteredResources, FilteredResourcesBuilder, FilteredResourcesMut, @@ -710,6 +711,49 @@ unsafe impl<'w, 's, T: FnOnce(&mut FilteredResourcesMutBuilder)> } } +/// A [`SystemParamBuilder`] for an [`Option`]. +#[derive(Clone)] +pub struct OptionBuilder(T); + +// SAFETY: `OptionBuilder` builds a state that is valid for `P`, and any state valid for `P` is valid for `Option

` +unsafe impl> SystemParamBuilder> + for OptionBuilder +{ + fn build(self, world: &mut World, meta: &mut SystemMeta) -> as SystemParam>::State { + self.0.build(world, meta) + } +} + +/// A [`SystemParamBuilder`] for a [`Result`] of [`SystemParamValidationError`]. +#[derive(Clone)] +pub struct ResultBuilder(T); + +// SAFETY: `ResultBuilder` builds a state that is valid for `P`, and any state valid for `P` is valid for `Result` +unsafe impl> + SystemParamBuilder> for ResultBuilder +{ + fn build( + self, + world: &mut World, + meta: &mut SystemMeta, + ) -> as SystemParam>::State { + self.0.build(world, meta) + } +} + +/// A [`SystemParamBuilder`] for a [`When`]. +#[derive(Clone)] +pub struct WhenBuilder(T); + +// SAFETY: `WhenBuilder` builds a state that is valid for `P`, and any state valid for `P` is valid for `When

` +unsafe impl> SystemParamBuilder> + for WhenBuilder +{ + fn build(self, world: &mut World, meta: &mut SystemMeta) -> as SystemParam>::State { + self.0.build(world, meta) + } +} + #[cfg(test)] mod tests { use crate::{ diff --git a/crates/bevy_ecs/src/system/combinator.rs b/crates/bevy_ecs/src/system/combinator.rs index 2b22931ba6..9d11de9525 100644 --- a/crates/bevy_ecs/src/system/combinator.rs +++ b/crates/bevy_ecs/src/system/combinator.rs @@ -5,7 +5,7 @@ use crate::{ archetype::ArchetypeComponentId, component::{ComponentId, Tick}, prelude::World, - query::Access, + query::{Access, FilteredAccessSet}, schedule::InternedSystemSet, system::{input::SystemInput, SystemIn, SystemParamValidationError}, world::unsafe_world_cell::UnsafeWorldCell, @@ -114,7 +114,7 @@ pub struct CombinatorSystem { a: A, b: B, name: Cow<'static, str>, - component_access: Access, + component_access_set: FilteredAccessSet, archetype_component_access: Access, } @@ -122,13 +122,13 @@ impl CombinatorSystem { /// Creates a new system that combines two inner systems. /// /// The returned system will only be usable if `Func` implements [`Combine`]. - pub const fn new(a: A, b: B, name: Cow<'static, str>) -> Self { + pub fn new(a: A, b: B, name: Cow<'static, str>) -> Self { Self { _marker: PhantomData, a, b, name, - component_access: Access::new(), + component_access_set: FilteredAccessSet::default(), archetype_component_access: Access::new(), } } @@ -148,7 +148,11 @@ where } fn component_access(&self) -> &Access { - &self.component_access + self.component_access_set.combined_access() + } + + fn component_access_set(&self) -> &FilteredAccessSet { + &self.component_access_set } fn archetype_component_access(&self) -> &Access { @@ -211,8 +215,10 @@ where fn initialize(&mut self, world: &mut World) { self.a.initialize(world); self.b.initialize(world); - self.component_access.extend(self.a.component_access()); - self.component_access.extend(self.b.component_access()); + self.component_access_set + .extend(self.a.component_access_set().clone()); + self.component_access_set + .extend(self.b.component_access_set().clone()); } fn update_archetype_component_access(&mut self, world: UnsafeWorldCell) { @@ -343,7 +349,7 @@ pub struct PipeSystem { a: A, b: B, name: Cow<'static, str>, - component_access: Access, + component_access_set: FilteredAccessSet, archetype_component_access: Access, } @@ -354,12 +360,12 @@ where for<'a> B::In: SystemInput = A::Out>, { /// Creates a new system that pipes two inner systems. - pub const fn new(a: A, b: B, name: Cow<'static, str>) -> Self { + pub fn new(a: A, b: B, name: Cow<'static, str>) -> Self { Self { a, b, name, - component_access: Access::new(), + component_access_set: FilteredAccessSet::default(), archetype_component_access: Access::new(), } } @@ -379,7 +385,11 @@ where } fn component_access(&self) -> &Access { - &self.component_access + self.component_access_set.combined_access() + } + + fn component_access_set(&self) -> &FilteredAccessSet { + &self.component_access_set } fn archetype_component_access(&self) -> &Access { @@ -443,8 +453,10 @@ where fn initialize(&mut self, world: &mut World) { self.a.initialize(world); self.b.initialize(world); - self.component_access.extend(self.a.component_access()); - self.component_access.extend(self.b.component_access()); + self.component_access_set + .extend(self.a.component_access_set().clone()); + self.component_access_set + .extend(self.b.component_access_set().clone()); } fn update_archetype_component_access(&mut self, world: UnsafeWorldCell) { diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index 7f0d7d84de..8b10b64b28 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -12,12 +12,11 @@ pub use parallel_scope::*; use alloc::boxed::Box; use core::marker::PhantomData; -use log::error; use crate::{ self as bevy_ecs, bundle::{Bundle, InsertMode, NoBundleEffect}, - change_detection::{MaybeLocation, Mut}, + change_detection::Mut, component::{Component, ComponentId, Mutable}, entity::{Entities, Entity, EntityClonerBuilder, EntityDoesNotExistError}, error::{ignore, warn, BevyError, CommandWithEntity, ErrorContext, HandleError}, @@ -89,8 +88,8 @@ use crate::{ /// A [`Command`] can return a [`Result`](crate::error::Result), /// which will be passed to an [error handler](crate::error) if the `Result` is an error. /// -/// The [default error handler](crate::error::default_error_handler) panics. -/// It can be configured by setting the `GLOBAL_ERROR_HANDLER`. +/// The default error handler panics. It can be configured via +/// the [`DefaultErrorHandler`](crate::error::DefaultErrorHandler) resource. /// /// Alternatively, you can customize the error handler for a specific command /// by calling [`Commands::queue_handled`]. @@ -509,7 +508,7 @@ impl<'w, 's> Commands<'w, 's> { /// Pushes a generic [`Command`] to the command queue. /// /// If the [`Command`] returns a [`Result`], - /// it will be handled using the [default error handler](crate::error::default_error_handler). + /// it will be handled using the [default error handler](crate::error::DefaultErrorHandler). /// /// To use a custom error handler, see [`Commands::queue_handled`]. /// @@ -624,57 +623,6 @@ impl<'w, 's> Commands<'w, 's> { } } - /// Pushes a [`Command`] to the queue for creating entities, if needed, - /// and for adding a bundle to each entity. - /// - /// `bundles_iter` is a type that can be converted into an ([`Entity`], [`Bundle`]) iterator - /// (it can also be a collection). - /// - /// When the command is applied, - /// for each (`Entity`, `Bundle`) pair in the given `bundles_iter`, - /// the `Entity` is spawned, if it does not exist already. - /// Then, the `Bundle` is added to the entity. - /// - /// This method is equivalent to iterating `bundles_iter`, - /// calling [`spawn`](Self::spawn) for each bundle, - /// and passing it to [`insert`](EntityCommands::insert), - /// but it is faster due to memory pre-allocation. - /// - /// # Note - /// - /// Spawning a specific `entity` value is rarely the right choice. Most apps should use [`Commands::spawn_batch`]. - /// This method should generally only be used for sharing entities across apps, and only when they have a scheme - /// worked out to share an ID space (which doesn't happen by default). - #[track_caller] - #[deprecated( - since = "0.16.0", - note = "This can cause extreme performance problems when used with lots of arbitrary free entities. See #18054 on GitHub." - )] - pub fn insert_or_spawn_batch(&mut self, bundles_iter: I) - where - I: IntoIterator + Send + Sync + 'static, - B: Bundle, - { - let caller = MaybeLocation::caller(); - self.queue(move |world: &mut World| { - - #[expect( - deprecated, - reason = "This needs to be supported for now, and the outer item is deprecated too." - )] - if let Err(invalid_entities) = world.insert_or_spawn_batch_with_caller( - bundles_iter, - caller, - ) { - error!( - "{caller}: Failed to 'insert or spawn' bundle of type {} into the following invalid entities: {:?}", - core::any::type_name::(), - invalid_entities - ); - } - }); - } - /// Adds a series of [`Bundles`](Bundle) to each [`Entity`] they are paired with, /// based on a batch of `(Entity, Bundle)` pairs. /// @@ -695,7 +643,7 @@ impl<'w, 's> Commands<'w, 's> { /// This command will fail if any of the given entities do not exist. /// /// It will internally return a [`TryInsertBatchError`](crate::world::error::TryInsertBatchError), - /// which will be handled by the [default error handler](crate::error::default_error_handler). + /// which will be handled by the [default error handler](crate::error::DefaultErrorHandler). #[track_caller] pub fn insert_batch(&mut self, batch: I) where @@ -726,7 +674,7 @@ impl<'w, 's> Commands<'w, 's> { /// This command will fail if any of the given entities do not exist. /// /// It will internally return a [`TryInsertBatchError`](crate::world::error::TryInsertBatchError), - /// which will be handled by the [default error handler](crate::error::default_error_handler). + /// which will be handled by the [default error handler](crate::error::DefaultErrorHandler). #[track_caller] pub fn insert_batch_if_new(&mut self, batch: I) where @@ -1123,6 +1071,10 @@ impl<'w, 's> Commands<'w, 's> { /// **Calling [`observe`](EntityCommands::observe) on the returned /// [`EntityCommands`] will observe the observer itself, which you very /// likely do not want.** + /// + /// # Panics + /// + /// Panics if the given system is an exclusive system. pub fn add_observer( &mut self, observer: impl IntoObserverSystem, @@ -1223,8 +1175,8 @@ impl<'w, 's> Commands<'w, 's> { /// An [`EntityCommand`] can return a [`Result`](crate::error::Result), /// which will be passed to an [error handler](crate::error) if the `Result` is an error. /// -/// The [default error handler](crate::error::default_error_handler) panics. -/// It can be configured by setting the `GLOBAL_ERROR_HANDLER`. +/// The default error handler panics. It can be configured via +/// the [`DefaultErrorHandler`](crate::error::DefaultErrorHandler) resource. /// /// Alternatively, you can customize the error handler for a specific command /// by calling [`EntityCommands::queue_handled`]. @@ -1797,14 +1749,6 @@ impl<'a> EntityCommands<'a> { pub fn despawn(&mut self) { self.queue_handled(entity_command::despawn(), warn); } - /// Despawns the provided entity and its descendants. - #[deprecated( - since = "0.16.0", - note = "Use entity.despawn(), which now automatically despawns recursively." - )] - pub fn despawn_recursive(&mut self) { - self.despawn(); - } /// Despawns the entity. /// @@ -1824,7 +1768,7 @@ impl<'a> EntityCommands<'a> { /// Pushes an [`EntityCommand`] to the queue, /// which will get executed for the current [`Entity`]. /// - /// The [default error handler](crate::error::default_error_handler) + /// The [default error handler](crate::error::DefaultErrorHandler) /// will be used to handle error cases. /// Every [`EntityCommand`] checks whether the entity exists at the time of execution /// and returns an error if it does not. diff --git a/crates/bevy_ecs/src/system/exclusive_function_system.rs b/crates/bevy_ecs/src/system/exclusive_function_system.rs index 15027d2aef..9107993f95 100644 --- a/crates/bevy_ecs/src/system/exclusive_function_system.rs +++ b/crates/bevy_ecs/src/system/exclusive_function_system.rs @@ -1,7 +1,7 @@ use crate::{ archetype::ArchetypeComponentId, component::{ComponentId, Tick}, - query::Access, + query::{Access, FilteredAccessSet}, schedule::{InternedSystemSet, SystemSet}, system::{ check_system_change_tick, ExclusiveSystemParam, ExclusiveSystemParamItem, IntoSystem, @@ -86,6 +86,11 @@ where self.system_meta.component_access_set.combined_access() } + #[inline] + fn component_access_set(&self) -> &FilteredAccessSet { + &self.system_meta.component_access_set + } + #[inline] fn archetype_component_access(&self) -> &Access { &self.system_meta.archetype_component_access diff --git a/crates/bevy_ecs/src/system/function_system.rs b/crates/bevy_ecs/src/system/function_system.rs index b0bbe187ed..5cf3fe2a44 100644 --- a/crates/bevy_ecs/src/system/function_system.rs +++ b/crates/bevy_ecs/src/system/function_system.rs @@ -183,7 +183,7 @@ impl SystemMeta { /// [`SystemState`] values created can be cached to improve performance, /// and *must* be cached and reused in order for system parameters that rely on local state to work correctly. /// These include: -/// - [`Added`](crate::query::Added) and [`Changed`](crate::query::Changed) query filters +/// - [`Added`](crate::query::Added), [`Changed`](crate::query::Changed) and [`Spawned`](crate::query::Spawned) query filters /// - [`Local`](crate::system::Local) variables that hold state /// - [`EventReader`](crate::event::EventReader) system parameters, which rely on a [`Local`](crate::system::Local) to track which events have been seen /// @@ -693,6 +693,11 @@ where self.system_meta.component_access_set.combined_access() } + #[inline] + fn component_access_set(&self) -> &FilteredAccessSet { + &self.system_meta.component_access_set + } + #[inline] fn archetype_component_access(&self) -> &Access { &self.system_meta.archetype_component_access diff --git a/crates/bevy_ecs/src/system/mod.rs b/crates/bevy_ecs/src/system/mod.rs index 1bdd26add2..011c220c85 100644 --- a/crates/bevy_ecs/src/system/mod.rs +++ b/crates/bevy_ecs/src/system/mod.rs @@ -153,7 +153,7 @@ pub use system_name::*; pub use system_param::*; pub use system_registry::*; -use crate::world::World; +use crate::world::{FromWorld, World}; /// Conversion trait to turn something into a [`System`]. /// @@ -228,6 +228,77 @@ pub trait IntoSystem: Sized { IntoAdapterSystem::new(f, self) } + /// Passes a mutable reference to `value` as input to the system each run, + /// turning it into a system that takes no input. + /// + /// `Self` can have any [`SystemInput`] type that takes a mutable reference + /// to `T`, such as [`InMut`]. + /// + /// # Example + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # + /// fn my_system(InMut(value): InMut) { + /// *value += 1; + /// if *value > 10 { + /// println!("Value is greater than 10!"); + /// } + /// } + /// + /// # let mut schedule = Schedule::default(); + /// schedule.add_systems(my_system.with_input(0)); + /// # bevy_ecs::system::assert_is_system(my_system.with_input(0)); + /// ``` + fn with_input(self, value: T) -> WithInputWrapper + where + for<'i> In: SystemInput = &'i mut T>, + T: Send + Sync + 'static, + { + WithInputWrapper::new(self, value) + } + + /// Passes a mutable reference to a value of type `T` created via + /// [`FromWorld`] as input to the system each run, turning it into a system + /// that takes no input. + /// + /// `Self` can have any [`SystemInput`] type that takes a mutable reference + /// to `T`, such as [`InMut`]. + /// + /// # Example + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # + /// struct MyData { + /// value: usize, + /// } + /// + /// impl FromWorld for MyData { + /// fn from_world(world: &mut World) -> Self { + /// // Fetch from the world the data needed to create `MyData` + /// # MyData { value: 0 } + /// } + /// } + /// + /// fn my_system(InMut(data): InMut) { + /// data.value += 1; + /// if data.value > 10 { + /// println!("Value is greater than 10!"); + /// } + /// } + /// # let mut schedule = Schedule::default(); + /// schedule.add_systems(my_system.with_input_from::()); + /// # bevy_ecs::system::assert_is_system(my_system.with_input_from::()); + /// ``` + fn with_input_from(self) -> WithInputFromWrapper + where + for<'i> In: SystemInput = &'i mut T>, + T: FromWorld + Send + Sync + 'static, + { + WithInputFromWrapper::new(self) + } + /// Get the [`TypeId`] of the [`System`] produced after calling [`into_system`](`IntoSystem::into_system`). #[inline] fn system_type_id(&self) -> TypeId { @@ -339,7 +410,7 @@ mod tests { error::Result, name::Name, prelude::{AnyOf, EntityRef, Trigger}, - query::{Added, Changed, Or, With, Without}, + query::{Added, Changed, Or, SpawnDetails, Spawned, With, Without}, removal_detection::RemovedComponents, resource::Resource, schedule::{ @@ -347,8 +418,8 @@ mod tests { Schedule, }, system::{ - Commands, In, IntoSystem, Local, NonSend, NonSendMut, ParamSet, Query, Res, ResMut, - Single, StaticSystemParam, System, SystemState, + Commands, In, InMut, IntoSystem, Local, NonSend, NonSendMut, ParamSet, Query, Res, + ResMut, Single, StaticSystemParam, System, SystemState, }, world::{DeferredWorld, EntityMut, FromWorld, OnAdd, World}, }; @@ -1255,6 +1326,25 @@ mod tests { } } + #[test] + fn system_state_spawned() { + let mut world = World::default(); + world.spawn_empty(); + let spawn_tick = world.change_tick(); + + let mut system_state: SystemState>> = + SystemState::new(&mut world); + { + let query = system_state.get(&world); + assert_eq!(query.unwrap().spawned_at(), spawn_tick); + } + + { + let query = system_state.get(&world); + assert!(query.is_none()); + } + } + #[test] #[should_panic] fn system_state_invalid_world() { @@ -1480,6 +1570,21 @@ mod tests { let mut sys = IntoSystem::into_system(mutable_query); sys.initialize(&mut world); } + + { + let mut world = World::new(); + + fn mutable_query(mut query: Query<(&mut A, &mut B, SpawnDetails), Spawned>) { + for _ in &mut query {} + + immutable_query(query.as_readonly()); + } + + fn immutable_query(_: Query<(&A, &B, SpawnDetails), Spawned>) {} + + let mut sys = IntoSystem::into_system(mutable_query); + sys.initialize(&mut world); + } } #[test] @@ -1879,4 +1984,40 @@ mod tests { .commands() .queue(|_world: &mut World| -> () { todo!() }); } + + #[test] + fn with_input() { + fn sys(InMut(v): InMut) { + *v += 1; + } + + let mut world = World::new(); + let mut system = IntoSystem::into_system(sys.with_input(42)); + system.initialize(&mut world); + system.run((), &mut world); + assert_eq!(*system.value(), 43); + } + + #[test] + fn with_input_from() { + struct TestData(usize); + + impl FromWorld for TestData { + fn from_world(_world: &mut World) -> Self { + Self(5) + } + } + + fn sys(InMut(v): InMut) { + v.0 += 1; + } + + let mut world = World::new(); + let mut system = IntoSystem::into_system(sys.with_input_from::()); + assert!(system.value().is_none()); + system.initialize(&mut world); + assert!(system.value().is_some()); + system.run((), &mut world); + assert_eq!(system.value().unwrap().0, 6); + } } diff --git a/crates/bevy_ecs/src/system/observer_system.rs b/crates/bevy_ecs/src/system/observer_system.rs index d042154631..9bd35c5361 100644 --- a/crates/bevy_ecs/src/system/observer_system.rs +++ b/crates/bevy_ecs/src/system/observer_system.rs @@ -7,7 +7,7 @@ use crate::{ error::Result, never::Never, prelude::{Bundle, Trigger}, - query::Access, + query::{Access, FilteredAccessSet}, schedule::{Fallible, Infallible}, system::{input::SystemIn, System}, world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, World}, @@ -122,6 +122,11 @@ where self.observer.component_access() } + #[inline] + fn component_access_set(&self) -> &FilteredAccessSet { + self.observer.component_access_set() + } + #[inline] fn archetype_component_access(&self) -> &Access { self.observer.archetype_component_access() diff --git a/crates/bevy_ecs/src/system/query.rs b/crates/bevy_ecs/src/system/query.rs index 183bdecfb4..c1d9e671a0 100644 --- a/crates/bevy_ecs/src/system/query.rs +++ b/crates/bevy_ecs/src/system/query.rs @@ -440,7 +440,7 @@ use core::{ /// |[`get_many`]|O(k)| /// |[`get_many_mut`]|O(k2)| /// |Archetype-based filtering ([`With`], [`Without`], [`Or`])|O(a)| -/// |Change detection filtering ([`Added`], [`Changed`])|O(a + n)| +/// |Change detection filtering ([`Added`], [`Changed`], [`Spawned`])|O(a + n)| /// /// [component storage types]: crate::component::StorageType /// [`Table`]: crate::storage::Table @@ -449,6 +449,7 @@ use core::{ /// [`Or`]: crate::query::Or /// [`Added`]: crate::query::Added /// [`Changed`]: crate::query::Changed +/// [`Spawned`]: crate::query::Spawned /// /// # `Iterator::for_each` /// @@ -933,7 +934,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// /// type Item = &'a Entity; /// type IntoIter = UniqueEntityIter>; - /// + /// /// fn into_iter(self) -> Self::IntoIter { /// // SAFETY: `Friends` ensures that it unique_list contains only unique entities. /// unsafe { UniqueEntityIter::from_iterator_unchecked(self.unique_list.iter()) } @@ -1414,7 +1415,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// /// assert_eq!(component_values, [&A(0), &A(1), &A(2)]); /// - /// let wrong_entity = Entity::from_raw(365); + /// let wrong_entity = Entity::from_raw_u32(365).unwrap(); /// /// assert_eq!( /// match query.get_many([wrong_entity]).unwrap_err() { @@ -1429,7 +1430,6 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// /// - [`get_many_mut`](Self::get_many_mut) to get mutable query items. /// - [`get_many_unique`](Self::get_many_unique) to only handle unique inputs. - /// - [`many`](Self::many) for the panicking version. #[inline] pub fn get_many( &self, @@ -1466,7 +1466,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// /// assert_eq!(component_values, [&A(0), &A(1), &A(2)]); /// - /// let wrong_entity = Entity::from_raw(365); + /// let wrong_entity = Entity::from_raw_u32(365).unwrap(); /// /// assert_eq!( /// match query.get_many_unique(UniqueEntityArray::from([wrong_entity])).unwrap_err() { @@ -1489,60 +1489,6 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { self.as_readonly().get_many_unique_inner(entities) } - /// Returns the read-only query items for the given array of [`Entity`]. - /// - /// # Panics - /// - /// This method panics if there is a query mismatch or a non-existing entity. - /// - /// # Examples - /// ``` no_run - /// use bevy_ecs::prelude::*; - /// - /// #[derive(Component)] - /// struct Targets([Entity; 3]); - /// - /// #[derive(Component)] - /// struct Position{ - /// x: i8, - /// y: i8 - /// }; - /// - /// impl Position { - /// fn distance(&self, other: &Position) -> i8 { - /// // Manhattan distance is way easier to compute! - /// (self.x - other.x).abs() + (self.y - other.y).abs() - /// } - /// } - /// - /// fn check_all_targets_in_range(targeting_query: Query<(Entity, &Targets, &Position)>, targets_query: Query<&Position>){ - /// for (targeting_entity, targets, origin) in &targeting_query { - /// // We can use "destructuring" to unpack the results nicely - /// let [target_1, target_2, target_3] = targets_query.many(targets.0); - /// - /// assert!(target_1.distance(origin) <= 5); - /// assert!(target_2.distance(origin) <= 5); - /// assert!(target_3.distance(origin) <= 5); - /// } - /// } - /// ``` - /// - /// # See also - /// - /// - [`get_many`](Self::get_many) for the non-panicking version. - #[inline] - #[track_caller] - #[deprecated( - since = "0.16.0", - note = "Use `get_many` instead and handle the Result." - )] - pub fn many(&self, entities: [Entity; N]) -> [ROQueryItem<'_, D>; N] { - match self.get_many(entities) { - Ok(items) => items, - Err(error) => panic!("Cannot get query results: {error}"), - } - } - /// Returns the query item for the given [`Entity`]. /// /// In case of a nonexisting entity or mismatched component, a [`QueryEntityError`] is returned instead. @@ -1665,7 +1611,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// let entities: [Entity; 3] = entities.try_into().unwrap(); /// /// world.spawn(A(73)); - /// let wrong_entity = Entity::from_raw(57); + /// let wrong_entity = Entity::from_raw_u32(57).unwrap(); /// let invalid_entity = world.spawn_empty().id(); /// /// @@ -1712,7 +1658,6 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// # See also /// /// - [`get_many`](Self::get_many) to get read-only query items without checking for duplicate entities. - /// - [`many_mut`](Self::many_mut) for the panicking version. #[inline] pub fn get_many_mut( &mut self, @@ -1740,7 +1685,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// let entity_set: UniqueEntityArray<3> = entity_set.try_into().unwrap(); /// /// world.spawn(A(73)); - /// let wrong_entity = Entity::from_raw(57); + /// let wrong_entity = Entity::from_raw_u32(57).unwrap(); /// let invalid_entity = world.spawn_empty().id(); /// /// @@ -1882,67 +1827,6 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { Ok(values.map(|x| unsafe { x.assume_init() })) } - /// Returns the query items for the given array of [`Entity`]. - /// - /// # Panics - /// - /// This method panics if there is a query mismatch, a non-existing entity, or the same `Entity` is included more than once in the array. - /// - /// # Examples - /// - /// ``` no_run - /// use bevy_ecs::prelude::*; - /// - /// #[derive(Component)] - /// struct Spring{ - /// connected_entities: [Entity; 2], - /// strength: f32, - /// } - /// - /// #[derive(Component)] - /// struct Position { - /// x: f32, - /// y: f32, - /// } - /// - /// #[derive(Component)] - /// struct Force { - /// x: f32, - /// y: f32, - /// } - /// - /// fn spring_forces(spring_query: Query<&Spring>, mut mass_query: Query<(&Position, &mut Force)>){ - /// for spring in &spring_query { - /// // We can use "destructuring" to unpack our query items nicely - /// let [(position_1, mut force_1), (position_2, mut force_2)] = mass_query.many_mut(spring.connected_entities); - /// - /// force_1.x += spring.strength * (position_1.x - position_2.x); - /// force_1.y += spring.strength * (position_1.y - position_2.y); - /// - /// // Silence borrow-checker: I have split your mutable borrow! - /// force_2.x += spring.strength * (position_2.x - position_1.x); - /// force_2.y += spring.strength * (position_2.y - position_1.y); - /// } - /// } - /// ``` - /// - /// # See also - /// - /// - [`get_many_mut`](Self::get_many_mut) for the non panicking version. - /// - [`many`](Self::many) to get read-only query items. - #[inline] - #[track_caller] - #[deprecated( - since = "0.16.0", - note = "Use `get_many_mut` instead and handle the Result." - )] - pub fn many_mut(&mut self, entities: [Entity; N]) -> [D::Item<'_>; N] { - match self.get_many_mut(entities) { - Ok(items) => items, - Err(error) => panic!("Cannot get query result: {error}"), - } - } - /// Returns the query item for the given [`Entity`]. /// /// In case of a nonexisting entity or mismatched component, a [`QueryEntityError`] is returned instead. @@ -1998,12 +1882,6 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { self.as_readonly().single_inner() } - /// A deprecated alias for [`single`](Self::single). - #[deprecated(since = "0.16.0", note = "Please use `single` instead")] - pub fn get_single(&self) -> Result, QuerySingleError> { - self.single() - } - /// Returns a single query item when there is exactly one entity matching the query. /// /// If the number of query items is not exactly one, a [`QuerySingleError`] is returned instead. @@ -2033,12 +1911,6 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { self.reborrow().single_inner() } - /// A deprecated alias for [`single_mut`](Self::single_mut). - #[deprecated(since = "0.16.0", note = "Please use `single_mut` instead")] - pub fn get_single_mut(&mut self) -> Result, QuerySingleError> { - self.single_mut() - } - /// Returns a single query item when there is exactly one entity matching the query. /// This consumes the [`Query`] to return results with the actual "inner" world lifetime. /// @@ -2085,8 +1957,8 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// /// This is equivalent to `self.iter().next().is_none()`, and thus the worst case runtime will be `O(n)` /// where `n` is the number of *potential* matches. This can be notably expensive for queries that rely - /// on non-archetypal filters such as [`Added`] or [`Changed`] which must individually check each query - /// result for a match. + /// on non-archetypal filters such as [`Added`], [`Changed`] or [`Spawned`] which must individually check + /// each query result for a match. /// /// # Example /// @@ -2109,6 +1981,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// /// [`Added`]: crate::query::Added /// [`Changed`]: crate::query::Changed + /// [`Spawned`]: crate::query::Spawned #[inline] pub fn is_empty(&self) -> bool { self.as_nop().iter().next().is_none() @@ -2147,9 +2020,9 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// /// For example, this can transform a `Query<(&A, &mut B)>` to a `Query<&B>`. /// This can be useful for passing the query to another function. Note that since - /// filter terms are dropped, non-archetypal filters like [`Added`](crate::query::Added) and - /// [`Changed`](crate::query::Changed) will not be respected. To maintain or change filter - /// terms see [`Self::transmute_lens_filtered`] + /// filter terms are dropped, non-archetypal filters like [`Added`](crate::query::Added), + /// [`Changed`](crate::query::Changed) and [`Spawned`](crate::query::Spawned) will not be + /// respected. To maintain or change filter terms see [`Self::transmute_lens_filtered`] /// /// ## Panics /// @@ -2204,12 +2077,12 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// * Tuples of query data and `#[derive(QueryData)]` structs have the union of the access of their subqueries /// * [`EntityMut`](crate::world::EntityMut) has read and write access to all components, but no required access /// * [`EntityRef`](crate::world::EntityRef) has read access to all components, but no required access - /// * [`Entity`], [`EntityLocation`], [`&Archetype`], [`Has`], and [`PhantomData`] have no access at all, + /// * [`Entity`], [`EntityLocation`], [`SpawnDetails`], [`&Archetype`], [`Has`], and [`PhantomData`] have no access at all, /// so can be added to any query /// * [`FilteredEntityRef`](crate::world::FilteredEntityRef) and [`FilteredEntityMut`](crate::world::FilteredEntityMut) /// have access determined by the [`QueryBuilder`](crate::query::QueryBuilder) used to construct them. - /// Any query can be transmuted to them, and they will receive the access of the source query, - /// but only if they are the top-level query and not nested + /// Any query can be transmuted to them, and they will receive the access of the source query. + /// When combined with other `QueryData`, they will receive any access of the source query that does not conflict with the other data. /// * [`Added`](crate::query::Added) and [`Changed`](crate::query::Changed) filters have read and required access to `T` /// * [`With`](crate::query::With) and [`Without`](crate::query::Without) filters have no access at all, /// so can be added to any query @@ -2281,9 +2154,9 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// // Anything can be transmuted to `FilteredEntityRef` or `FilteredEntityMut` /// // This will create a `FilteredEntityMut` that only has read access to `T` /// assert_valid_transmute::<&T, FilteredEntityMut>(); - /// // This transmute will succeed, but the `FilteredEntityMut` will have no access! - /// // It must be the top-level query to be given access, but here it is nested in a tuple. - /// assert_valid_transmute::<&T, (Entity, FilteredEntityMut)>(); + /// // This will create a `FilteredEntityMut` that has no access to `T`, + /// // read access to `U`, and write access to `V`. + /// assert_valid_transmute::<(&mut T, &mut U, &mut V), (&mut T, &U, FilteredEntityMut)>(); /// /// // `Added` and `Changed` filters have the same access as `&T` data /// // Remember that they are only evaluated on the transmuted query, not the original query! @@ -2294,6 +2167,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// ``` /// /// [`EntityLocation`]: crate::entity::EntityLocation + /// [`SpawnDetails`]: crate::query::SpawnDetails /// [`&Archetype`]: crate::archetype::Archetype /// [`Has`]: crate::query::Has #[track_caller] @@ -2306,9 +2180,9 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// /// For example, this can transform a `Query<(&A, &mut B)>` to a `Query<&B>`. /// This can be useful for passing the query to another function. Note that since - /// filter terms are dropped, non-archetypal filters like [`Added`](crate::query::Added) and - /// [`Changed`](crate::query::Changed) will not be respected. To maintain or change filter - /// terms see [`Self::transmute_lens_filtered`] + /// filter terms are dropped, non-archetypal filters like [`Added`](crate::query::Added), + /// [`Changed`](crate::query::Changed) and [`Spawned`](crate::query::Spawned) will not be + /// respected. To maintain or change filter terms see [`Self::transmute_lens_filtered`] /// /// ## Panics /// @@ -2363,7 +2237,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// * `&mut T` -> `&T` /// * `&mut T` -> `Ref` /// * [`EntityMut`](crate::world::EntityMut) -> [`EntityRef`](crate::world::EntityRef) - /// + /// /// [`EntityLocation`]: crate::entity::EntityLocation /// [`&Archetype`]: crate::archetype::Archetype /// @@ -2379,8 +2253,9 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// /// Note that the lens will iterate the same tables and archetypes as the original query. This means that /// additional archetypal query terms like [`With`](crate::query::With) and [`Without`](crate::query::Without) - /// will not necessarily be respected and non-archetypal terms like [`Added`](crate::query::Added) and - /// [`Changed`](crate::query::Changed) will only be respected if they are in the type signature. + /// will not necessarily be respected and non-archetypal terms like [`Added`](crate::query::Added), + /// [`Changed`](crate::query::Changed) and [`Spawned`](crate::query::Spawned) will only be respected if they + /// are in the type signature. #[track_caller] pub fn transmute_lens_filtered( &mut self, @@ -2393,8 +2268,8 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// /// Note that the lens will iterate the same tables and archetypes as the original query. This means that /// additional archetypal query terms like [`With`](crate::query::With) and [`Without`](crate::query::Without) - /// will not necessarily be respected and non-archetypal terms like [`Added`](crate::query::Added) and - /// [`Changed`](crate::query::Changed) will only be respected if they are in the type signature. + /// will not necessarily be respected and non-archetypal terms like [`Added`](crate::query::Added), + /// [`Changed`](crate::query::Changed) and [`Spawned`](crate::query::Spawned) will only be respected if they /// /// # See also /// @@ -2430,7 +2305,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// /// For example, this can take a `Query<&A>` and a `Query<&B>` and return a `Query<(&A, &B)>`. /// The returned query will only return items with both `A` and `B`. Note that since filters - /// are dropped, non-archetypal filters like `Added` and `Changed` will not be respected. + /// are dropped, non-archetypal filters like `Added`, `Changed` and `Spawned` will not be respected. /// To maintain or change filter terms see `Self::join_filtered`. /// /// ## Example @@ -2492,7 +2367,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// /// For example, this can take a `Query<&A>` and a `Query<&B>` and return a `Query<(&A, &B)>`. /// The returned query will only return items with both `A` and `B`. Note that since filters - /// are dropped, non-archetypal filters like `Added` and `Changed` will not be respected. + /// are dropped, non-archetypal filters like `Added`, `Changed` and `Spawned` will not be respected. /// To maintain or change filter terms see `Self::join_filtered`. /// /// ## Panics @@ -2519,8 +2394,8 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// Note that the lens with iterate a subset of the original queries' tables /// and archetypes. This means that additional archetypal query terms like /// `With` and `Without` will not necessarily be respected and non-archetypal - /// terms like `Added` and `Changed` will only be respected if they are in - /// the type signature. + /// terms like `Added`, `Changed` and `Spawned` will only be respected if they + /// are in the type signature. pub fn join_filtered< 'a, OtherD: QueryData, @@ -2540,8 +2415,8 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// Note that the lens with iterate a subset of the original queries' tables /// and archetypes. This means that additional archetypal query terms like /// `With` and `Without` will not necessarily be respected and non-archetypal - /// terms like `Added` and `Changed` will only be respected if they are in - /// the type signature. + /// terms like `Added`, `Changed` and `Spawned` will only be respected if they + /// are in the type signature. /// /// # See also /// @@ -2722,8 +2597,9 @@ impl<'w, D: QueryData, F: QueryFilter> Single<'w, D, F> { /// This will cause the system to be skipped, according to the rules laid out in [`SystemParamValidationError`](crate::system::SystemParamValidationError). /// /// Much like [`Query::is_empty`] the worst case runtime will be `O(n)` where `n` is the number of *potential* matches. -/// This can be notably expensive for queries that rely on non-archetypal filters such as [`Added`](crate::query::Added) or [`Changed`](crate::query::Changed) -/// which must individually check each query result for a match. +/// This can be notably expensive for queries that rely on non-archetypal filters such as [`Added`](crate::query::Added), +/// [`Changed`](crate::query::Changed) of [`Spawned`](crate::query::Spawned) which must individually check each query +/// result for a match. /// /// See [`Query`] for more details. /// diff --git a/crates/bevy_ecs/src/system/schedule_system.rs b/crates/bevy_ecs/src/system/schedule_system.rs index 75fad2b7e9..8ef7d9ed57 100644 --- a/crates/bevy_ecs/src/system/schedule_system.rs +++ b/crates/bevy_ecs/src/system/schedule_system.rs @@ -4,9 +4,9 @@ use crate::{ archetype::ArchetypeComponentId, component::{ComponentId, Tick}, error::Result, - query::Access, - system::{input::SystemIn, BoxedSystem, System}, - world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, World}, + query::{Access, FilteredAccessSet}, + system::{input::SystemIn, BoxedSystem, System, SystemInput}, + world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, FromWorld, World}, }; use super::{IntoSystem, SystemParamValidationError}; @@ -36,6 +36,11 @@ impl> System for InfallibleSystemWrapper { } #[inline] + fn component_access_set(&self) -> &FilteredAccessSet { + self.0.component_access_set() + } + + #[inline(always)] fn archetype_component_access(&self) -> &Access { self.0.archetype_component_access() } @@ -113,5 +118,238 @@ impl> System for InfallibleSystemWrapper { } } +/// See [`IntoSystem::with_input`] for details. +pub struct WithInputWrapper +where + for<'i> S: System = &'i mut T>>, + T: Send + Sync + 'static, +{ + system: S, + value: T, +} + +impl WithInputWrapper +where + for<'i> S: System = &'i mut T>>, + T: Send + Sync + 'static, +{ + /// Wraps the given system with the given input value. + pub fn new(system: impl IntoSystem, value: T) -> Self { + Self { + system: IntoSystem::into_system(system), + value, + } + } + + /// Returns a reference to the input value. + pub fn value(&self) -> &T { + &self.value + } + + /// Returns a mutable reference to the input value. + pub fn value_mut(&mut self) -> &mut T { + &mut self.value + } +} + +impl System for WithInputWrapper +where + for<'i> S: System = &'i mut T>>, + T: Send + Sync + 'static, +{ + type In = (); + + type Out = S::Out; + + fn name(&self) -> Cow<'static, str> { + self.system.name() + } + + fn component_access(&self) -> &Access { + self.system.component_access() + } + + fn component_access_set(&self) -> &FilteredAccessSet { + self.system.component_access_set() + } + + fn archetype_component_access(&self) -> &Access { + self.system.archetype_component_access() + } + + fn is_send(&self) -> bool { + self.system.is_send() + } + + fn is_exclusive(&self) -> bool { + self.system.is_exclusive() + } + + fn has_deferred(&self) -> bool { + self.system.has_deferred() + } + + unsafe fn run_unsafe( + &mut self, + _input: SystemIn<'_, Self>, + world: UnsafeWorldCell, + ) -> Self::Out { + self.system.run_unsafe(&mut self.value, world) + } + + fn apply_deferred(&mut self, world: &mut World) { + self.system.apply_deferred(world); + } + + fn queue_deferred(&mut self, world: DeferredWorld) { + self.system.queue_deferred(world); + } + + unsafe fn validate_param_unsafe( + &mut self, + world: UnsafeWorldCell, + ) -> Result<(), SystemParamValidationError> { + self.system.validate_param_unsafe(world) + } + + fn initialize(&mut self, world: &mut World) { + self.system.initialize(world); + } + + fn update_archetype_component_access(&mut self, world: UnsafeWorldCell) { + self.system.update_archetype_component_access(world); + } + + fn check_change_tick(&mut self, change_tick: Tick) { + self.system.check_change_tick(change_tick); + } + + fn get_last_run(&self) -> Tick { + self.system.get_last_run() + } + + fn set_last_run(&mut self, last_run: Tick) { + self.system.set_last_run(last_run); + } +} + +/// Constructed in [`IntoSystem::with_input_from`]. +pub struct WithInputFromWrapper { + system: S, + value: Option, +} + +impl WithInputFromWrapper +where + for<'i> S: System = &'i mut T>>, + T: Send + Sync + 'static, +{ + /// Wraps the given system. + pub fn new(system: impl IntoSystem) -> Self { + Self { + system: IntoSystem::into_system(system), + value: None, + } + } + + /// Returns a reference to the input value, if it has been initialized. + pub fn value(&self) -> Option<&T> { + self.value.as_ref() + } + + /// Returns a mutable reference to the input value, if it has been initialized. + pub fn value_mut(&mut self) -> Option<&mut T> { + self.value.as_mut() + } +} + +impl System for WithInputFromWrapper +where + for<'i> S: System = &'i mut T>>, + T: FromWorld + Send + Sync + 'static, +{ + type In = (); + + type Out = S::Out; + + fn name(&self) -> Cow<'static, str> { + self.system.name() + } + + fn component_access(&self) -> &Access { + self.system.component_access() + } + + fn component_access_set(&self) -> &FilteredAccessSet { + self.system.component_access_set() + } + + fn archetype_component_access(&self) -> &Access { + self.system.archetype_component_access() + } + + fn is_send(&self) -> bool { + self.system.is_send() + } + + fn is_exclusive(&self) -> bool { + self.system.is_exclusive() + } + + fn has_deferred(&self) -> bool { + self.system.has_deferred() + } + + unsafe fn run_unsafe( + &mut self, + _input: SystemIn<'_, Self>, + world: UnsafeWorldCell, + ) -> Self::Out { + let value = self + .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) + } + + fn apply_deferred(&mut self, world: &mut World) { + self.system.apply_deferred(world); + } + + fn queue_deferred(&mut self, world: DeferredWorld) { + self.system.queue_deferred(world); + } + + unsafe fn validate_param_unsafe( + &mut self, + world: UnsafeWorldCell, + ) -> Result<(), SystemParamValidationError> { + self.system.validate_param_unsafe(world) + } + + fn initialize(&mut self, world: &mut World) { + self.system.initialize(world); + if self.value.is_none() { + self.value = Some(T::from_world(world)); + } + } + + fn update_archetype_component_access(&mut self, world: UnsafeWorldCell) { + self.system.update_archetype_component_access(world); + } + + fn check_change_tick(&mut self, change_tick: Tick) { + self.system.check_change_tick(change_tick); + } + + fn get_last_run(&self) -> Tick { + self.system.get_last_run() + } + + fn set_last_run(&mut self, last_run: Tick) { + self.system.set_last_run(last_run); + } +} + /// Type alias for a `BoxedSystem` that a `Schedule` can store. pub type ScheduleSystem = BoxedSystem<(), Result>; diff --git a/crates/bevy_ecs/src/system/system.rs b/crates/bevy_ecs/src/system/system.rs index 69f5ae980a..18ec7f44cd 100644 --- a/crates/bevy_ecs/src/system/system.rs +++ b/crates/bevy_ecs/src/system/system.rs @@ -9,7 +9,7 @@ use thiserror::Error; use crate::{ archetype::ArchetypeComponentId, component::{ComponentId, Tick}, - query::Access, + query::{Access, FilteredAccessSet}, schedule::InternedSystemSet, system::{input::SystemInput, SystemIn}, world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, World}, @@ -44,8 +44,13 @@ pub trait System: Send + Sync + 'static { fn type_id(&self) -> TypeId { TypeId::of::() } + /// Returns the system's component [`Access`]. fn component_access(&self) -> &Access; + + /// Returns the system's component [`FilteredAccessSet`]. + fn component_access_set(&self) -> &FilteredAccessSet; + /// Returns the system's archetype component [`Access`]. fn archetype_component_access(&self) -> &Access; /// Returns true if the system is [`Send`]. diff --git a/crates/bevy_ecs/src/system/system_param.rs b/crates/bevy_ecs/src/system/system_param.rs index 99d4c72df6..6ee7e08fb5 100644 --- a/crates/bevy_ecs/src/system/system_param.rs +++ b/crates/bevy_ecs/src/system/system_param.rs @@ -477,87 +477,12 @@ unsafe impl<'a, D: QueryData + 'static, F: QueryFilter + 'static> SystemParam fo } } -// SAFETY: Relevant query ComponentId and ArchetypeComponentId access is applied to SystemMeta. If -// this Query conflicts with any prior access, a panic will occur. -unsafe impl<'a, D: QueryData + 'static, F: QueryFilter + 'static> SystemParam - for Option> -{ - type State = QueryState; - type Item<'w, 's> = Option>; - - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { - Single::init_state(world, system_meta) - } - - unsafe fn new_archetype( - state: &mut Self::State, - archetype: &Archetype, - system_meta: &mut SystemMeta, - ) { - // SAFETY: Delegate to existing `SystemParam` implementations. - unsafe { Single::new_archetype(state, archetype, system_meta) }; - } - - #[inline] - unsafe fn get_param<'w, 's>( - state: &'s mut Self::State, - system_meta: &SystemMeta, - world: UnsafeWorldCell<'w>, - change_tick: Tick, - ) -> Self::Item<'w, 's> { - state.validate_world(world.id()); - // SAFETY: State ensures that the components it accesses are not accessible elsewhere. - // The caller ensures the world matches the one used in init_state. - let query = unsafe { - state.query_unchecked_manual_with_ticks(world, system_meta.last_run, change_tick) - }; - match query.single_inner() { - Ok(single) => Some(Single { - item: single, - _filter: PhantomData, - }), - Err(QuerySingleError::NoEntities(_)) => None, - Err(QuerySingleError::MultipleEntities(e)) => panic!("{}", e), - } - } - - #[inline] - unsafe fn validate_param( - state: &Self::State, - system_meta: &SystemMeta, - world: UnsafeWorldCell, - ) -> Result<(), SystemParamValidationError> { - // SAFETY: State ensures that the components it accesses are not mutably accessible elsewhere - // and the query is read only. - // The caller ensures the world matches the one used in init_state. - let query = unsafe { - state.query_unchecked_manual_with_ticks( - world, - system_meta.last_run, - world.change_tick(), - ) - }; - match query.single_inner() { - Ok(_) | Err(QuerySingleError::NoEntities(_)) => Ok(()), - Err(QuerySingleError::MultipleEntities(_)) => Err( - SystemParamValidationError::skipped::("Multiple matching entities"), - ), - } - } -} - // SAFETY: QueryState is constrained to read-only fetches, so it only reads World. unsafe impl<'a, D: ReadOnlyQueryData + 'static, F: QueryFilter + 'static> ReadOnlySystemParam for Single<'a, D, F> { } -// SAFETY: QueryState is constrained to read-only fetches, so it only reads World. -unsafe impl<'a, D: ReadOnlyQueryData + 'static, F: QueryFilter + 'static> ReadOnlySystemParam - for Option> -{ -} - // SAFETY: Relevant query ComponentId and ArchetypeComponentId access is applied to SystemMeta. If // this Query conflicts with any prior access, a panic will occur. unsafe impl SystemParam @@ -931,40 +856,6 @@ unsafe impl<'a, T: Resource> SystemParam for Res<'a, T> { } } -// SAFETY: Only reads a single World resource -unsafe impl<'a, T: Resource> ReadOnlySystemParam for Option> {} - -// SAFETY: this impl defers to `Res`, which initializes and validates the correct world access. -unsafe impl<'a, T: Resource> SystemParam for Option> { - type State = ComponentId; - type Item<'w, 's> = Option>; - - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { - Res::::init_state(world, system_meta) - } - - #[inline] - unsafe fn get_param<'w, 's>( - &mut component_id: &'s mut Self::State, - system_meta: &SystemMeta, - world: UnsafeWorldCell<'w>, - change_tick: Tick, - ) -> Self::Item<'w, 's> { - world - .get_resource_with_ticks(component_id) - .map(|(ptr, ticks, caller)| Res { - value: ptr.deref(), - ticks: Ticks { - added: ticks.added.deref(), - changed: ticks.changed.deref(), - last_run: system_meta.last_run, - this_run: change_tick, - }, - changed_by: caller.map(|caller| caller.deref()), - }) - } -} - // SAFETY: Res ComponentId and ArchetypeComponentId access is applied to SystemMeta. If this Res // conflicts with any prior access, a panic will occur. unsafe impl<'a, T: Resource> SystemParam for ResMut<'a, T> { @@ -1045,37 +936,6 @@ unsafe impl<'a, T: Resource> SystemParam for ResMut<'a, T> { } } -// SAFETY: this impl defers to `ResMut`, which initializes and validates the correct world access. -unsafe impl<'a, T: Resource> SystemParam for Option> { - type State = ComponentId; - type Item<'w, 's> = Option>; - - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { - ResMut::::init_state(world, system_meta) - } - - #[inline] - unsafe fn get_param<'w, 's>( - &mut component_id: &'s mut Self::State, - system_meta: &SystemMeta, - world: UnsafeWorldCell<'w>, - change_tick: Tick, - ) -> Self::Item<'w, 's> { - world - .get_resource_mut_by_id(component_id) - .map(|value| ResMut { - value: value.value.deref_mut::(), - ticks: TicksMut { - added: value.ticks.added, - changed: value.ticks.changed, - last_run: system_meta.last_run, - this_run: change_tick, - }, - changed_by: value.changed_by, - }) - } -} - /// SAFETY: only reads world unsafe impl<'w> ReadOnlySystemParam for &'w World {} @@ -1470,7 +1330,7 @@ unsafe impl SystemParam for Deferred<'_, T> { } /// A dummy type that is [`!Send`](Send), to force systems to run on the main thread. -pub struct NonSendMarker; +pub struct NonSendMarker(PhantomData<*mut ()>); // SAFETY: No world access. unsafe impl SystemParam for NonSendMarker { @@ -1489,7 +1349,7 @@ unsafe impl SystemParam for NonSendMarker { _world: UnsafeWorldCell<'world>, _change_tick: Tick, ) -> Self::Item<'world, 'state> { - Self + Self(PhantomData) } } @@ -1644,37 +1504,6 @@ unsafe impl<'a, T: 'static> SystemParam for NonSend<'a, T> { } } -// SAFETY: Only reads a single World non-send resource -unsafe impl ReadOnlySystemParam for Option> {} - -// SAFETY: this impl defers to `NonSend`, which initializes and validates the correct world access. -unsafe impl SystemParam for Option> { - type State = ComponentId; - type Item<'w, 's> = Option>; - - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { - NonSend::::init_state(world, system_meta) - } - - #[inline] - unsafe fn get_param<'w, 's>( - &mut component_id: &'s mut Self::State, - system_meta: &SystemMeta, - world: UnsafeWorldCell<'w>, - change_tick: Tick, - ) -> Self::Item<'w, 's> { - world - .get_non_send_with_ticks(component_id) - .map(|(ptr, ticks, caller)| NonSend { - value: ptr.deref(), - ticks: ticks.read(), - last_run: system_meta.last_run, - this_run: change_tick, - changed_by: caller.map(|caller| caller.deref()), - }) - } -} - // SAFETY: NonSendMut ComponentId and ArchetypeComponentId access is applied to SystemMeta. If this // NonSendMut conflicts with any prior access, a panic will occur. unsafe impl<'a, T: 'static> SystemParam for NonSendMut<'a, T> { @@ -1753,32 +1582,6 @@ unsafe impl<'a, T: 'static> SystemParam for NonSendMut<'a, T> { } } -// SAFETY: this impl defers to `NonSendMut`, which initializes and validates the correct world access. -unsafe impl<'a, T: 'static> SystemParam for Option> { - type State = ComponentId; - type Item<'w, 's> = Option>; - - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { - NonSendMut::::init_state(world, system_meta) - } - - #[inline] - unsafe fn get_param<'w, 's>( - &mut component_id: &'s mut Self::State, - system_meta: &SystemMeta, - world: UnsafeWorldCell<'w>, - change_tick: Tick, - ) -> Self::Item<'w, 's> { - world - .get_non_send_with_ticks(component_id) - .map(|(ptr, ticks, caller)| NonSendMut { - value: ptr.assert_unique().deref_mut(), - ticks: TicksMut::from_tick_cells(ticks, system_meta.last_run, change_tick), - changed_by: caller.map(|caller| caller.deref_mut()), - }) - } -} - // SAFETY: Only reads World archetypes unsafe impl<'a> ReadOnlySystemParam for &'a Archetypes {} @@ -1916,6 +1719,197 @@ unsafe impl SystemParam for SystemChangeTick { } } +// SAFETY: Delegates to `T`, which ensures the safety requirements are met +unsafe impl SystemParam for Option { + type State = T::State; + + type Item<'world, 'state> = Option>; + + fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { + T::init_state(world, system_meta) + } + + #[inline] + unsafe fn get_param<'world, 'state>( + state: &'state mut Self::State, + system_meta: &SystemMeta, + 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)) + } + + unsafe fn new_archetype( + state: &mut Self::State, + archetype: &Archetype, + system_meta: &mut SystemMeta, + ) { + // SAFETY: The caller ensures that `archetype` is from the World the state was initialized from in `init_state`. + unsafe { T::new_archetype(state, archetype, system_meta) }; + } + + fn apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) { + T::apply(state, system_meta, world); + } + + fn queue(state: &mut Self::State, system_meta: &SystemMeta, world: DeferredWorld) { + T::queue(state, system_meta, world); + } +} + +// SAFETY: Delegates to `T`, which ensures the safety requirements are met +unsafe impl ReadOnlySystemParam for Option {} + +// SAFETY: Delegates to `T`, which ensures the safety requirements are met +unsafe impl SystemParam for Result { + type State = T::State; + + type Item<'world, 'state> = Result, SystemParamValidationError>; + + fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { + T::init_state(world, system_meta) + } + + #[inline] + unsafe fn get_param<'world, 'state>( + state: &'state mut Self::State, + system_meta: &SystemMeta, + world: UnsafeWorldCell<'world>, + change_tick: Tick, + ) -> Self::Item<'world, 'state> { + T::validate_param(state, system_meta, world) + .map(|()| T::get_param(state, system_meta, world, change_tick)) + } + + unsafe fn new_archetype( + state: &mut Self::State, + archetype: &Archetype, + system_meta: &mut SystemMeta, + ) { + // SAFETY: The caller ensures that `archetype` is from the World the state was initialized from in `init_state`. + unsafe { T::new_archetype(state, archetype, system_meta) }; + } + + fn apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) { + T::apply(state, system_meta, world); + } + + fn queue(state: &mut Self::State, system_meta: &SystemMeta, world: DeferredWorld) { + T::queue(state, system_meta, world); + } +} + +// SAFETY: Delegates to `T`, which ensures the safety requirements are met +unsafe impl ReadOnlySystemParam for Result {} + +/// A [`SystemParam`] that wraps another parameter and causes its system to skip instead of failing when the parameter is invalid. +/// +/// # Example +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # #[derive(Resource)] +/// # struct SomeResource; +/// // This system will fail if `SomeResource` is not present. +/// fn fails_on_missing_resource(res: Res) {} +/// +/// // This system will skip without error if `SomeResource` is not present. +/// fn skips_on_missing_resource(res: When>) { +/// // The inner parameter is available using `Deref` +/// let some_resource: &SomeResource = &res; +/// } +/// # bevy_ecs::system::assert_is_system(skips_on_missing_resource); +/// ``` +#[derive(Debug)] +pub struct When(pub T); + +impl When { + /// Returns the inner `T`. + /// + /// The inner value is `pub`, so you can also obtain it by destructuring the parameter: + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # #[derive(Resource)] + /// # struct SomeResource; + /// fn skips_on_missing_resource(When(res): When>) { + /// let some_resource: Res = res; + /// } + /// # bevy_ecs::system::assert_is_system(skips_on_missing_resource); + /// ``` + pub fn into_inner(self) -> T { + self.0 + } +} + +impl Deref for When { + type Target = T; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for When { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +// SAFETY: Delegates to `T`, which ensures the safety requirements are met +unsafe impl SystemParam for When { + type State = T::State; + + type Item<'world, 'state> = When>; + + fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { + T::init_state(world, system_meta) + } + + #[inline] + unsafe fn validate_param( + state: &Self::State, + system_meta: &SystemMeta, + world: UnsafeWorldCell, + ) -> Result<(), SystemParamValidationError> { + T::validate_param(state, system_meta, world).map_err(|mut e| { + e.skipped = true; + e + }) + } + + #[inline] + unsafe fn get_param<'world, 'state>( + state: &'state mut Self::State, + system_meta: &SystemMeta, + world: UnsafeWorldCell<'world>, + change_tick: Tick, + ) -> Self::Item<'world, 'state> { + When(T::get_param(state, system_meta, world, change_tick)) + } + + unsafe fn new_archetype( + state: &mut Self::State, + archetype: &Archetype, + system_meta: &mut SystemMeta, + ) { + // SAFETY: The caller ensures that `archetype` is from the World the state was initialized from in `init_state`. + unsafe { T::new_archetype(state, archetype, system_meta) }; + } + + fn apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) { + T::apply(state, system_meta, world); + } + + fn queue(state: &mut Self::State, system_meta: &SystemMeta, world: DeferredWorld) { + T::queue(state, system_meta, world); + } +} + +// SAFETY: Delegates to `T`, which ensures the safety requirements are met +unsafe impl ReadOnlySystemParam for When {} + // SAFETY: When initialized with `init_state`, `get_param` returns an empty `Vec` and does no access. // Therefore, `init_state` trivially registers all access, and no accesses can conflict. // Note that the safety requirements for non-empty `Vec`s are handled by the `SystemParamBuilder` impl that builds them. @@ -2699,11 +2693,12 @@ pub struct SystemParamValidationError { /// By default, this will result in a panic. See [`crate::error`] for more information. /// /// This is the default behavior, and is suitable for system params that should *always* be valid, - /// either because sensible fallback behavior exists (like [`Query`] or because + /// either because sensible fallback behavior exists (like [`Query`]) or because /// failures in validation should be considered a bug in the user's logic that must be immediately addressed (like [`Res`]). /// /// If `true`, the system should be skipped. - /// This is suitable for system params that are intended to only operate in certain application states, such as [`Single`]. + /// This is set by wrapping the system param in [`When`], + /// and indicates that the system is intended to only operate in certain application states. pub skipped: bool, /// A message describing the validation error. diff --git a/crates/bevy_ecs/src/traversal.rs b/crates/bevy_ecs/src/traversal.rs index 342ad47849..306ae7c92d 100644 --- a/crates/bevy_ecs/src/traversal.rs +++ b/crates/bevy_ecs/src/traversal.rs @@ -10,7 +10,7 @@ use crate::{entity::Entity, query::ReadOnlyQueryData, relationship::Relationship /// Infinite loops are possible, and are not checked for. While looping can be desirable in some contexts /// (for example, an observer that triggers itself multiple times before stopping), following an infinite /// traversal loop without an eventual exit will cause your application to hang. Each implementer of `Traversal` -/// for documenting possible looping behavior, and consumers of those implementations are responsible for +/// is responsible for documenting possible looping behavior, and consumers of those implementations are responsible for /// avoiding infinite loops in their code. /// /// Traversals may be parameterized with additional data. For example, in observer event propagation, the diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index a9887c5248..64610f8e4e 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -1,29 +1,26 @@ use crate::{ - archetype::{Archetype, ArchetypeId, Archetypes}, + archetype::{Archetype, ArchetypeId}, bundle::{ - Bundle, BundleEffect, BundleFromComponents, BundleId, BundleInfo, BundleInserter, - DynamicBundle, InsertMode, + Bundle, BundleEffect, BundleFromComponents, BundleInserter, BundleRemover, DynamicBundle, + InsertMode, }, change_detection::{MaybeLocation, MutUntyped}, component::{ Component, ComponentId, ComponentTicks, Components, ComponentsRegistrator, Mutable, - StorageType, + StorageType, Tick, }, entity::{ - ContainsEntity, Entities, Entity, EntityCloner, EntityClonerBuilder, EntityEquivalent, - EntityLocation, + ContainsEntity, Entity, EntityCloner, EntityClonerBuilder, EntityEquivalent, EntityLocation, }, event::Event, observer::Observer, - query::{Access, ReadOnlyQueryData}, + query::{Access, DebugCheckedUnwrap, ReadOnlyQueryData}, relationship::RelationshipHookMode, - removal_detection::RemovedComponentEvents, resource::Resource, - storage::Storages, system::IntoObserverSystem, world::{ - error::EntityComponentError, unsafe_world_cell::UnsafeEntityCell, DeferredWorld, Mut, Ref, - World, ON_DESPAWN, ON_REMOVE, ON_REPLACE, + error::EntityComponentError, unsafe_world_cell::UnsafeEntityCell, Mut, Ref, World, + ON_DESPAWN, ON_REMOVE, ON_REPLACE, }, }; use alloc::vec::Vec; @@ -299,6 +296,11 @@ impl<'w> EntityRef<'w> { pub fn spawned_by(&self) -> MaybeLocation { self.cell.spawned_by() } + + /// Returns the [`Tick`] at which this entity has been spawned. + pub fn spawned_at(&self) -> Tick { + self.cell.spawned_at() + } } impl<'w> From> for EntityRef<'w> { @@ -986,6 +988,11 @@ impl<'w> EntityMut<'w> { pub fn spawned_by(&self) -> MaybeLocation { self.cell.spawned_by() } + + /// Returns the [`Tick`] at which this entity has been spawned. + pub fn spawned_at(&self) -> Tick { + self.cell.spawned_at() + } } impl<'w> From<&'w mut EntityMut<'_>> for EntityMut<'w> { @@ -1116,26 +1123,38 @@ impl<'w> EntityWorldMut<'w> { fn as_unsafe_entity_cell_readonly(&self) -> UnsafeEntityCell<'_> { self.assert_not_despawned(); + let last_change_tick = self.world.last_change_tick; + let change_tick = self.world.read_change_tick(); UnsafeEntityCell::new( self.world.as_unsafe_world_cell_readonly(), self.entity, self.location, + last_change_tick, + change_tick, ) } fn as_unsafe_entity_cell(&mut self) -> UnsafeEntityCell<'_> { self.assert_not_despawned(); + let last_change_tick = self.world.last_change_tick; + let change_tick = self.world.change_tick(); UnsafeEntityCell::new( self.world.as_unsafe_world_cell(), self.entity, self.location, + last_change_tick, + change_tick, ) } fn into_unsafe_entity_cell(self) -> UnsafeEntityCell<'w> { self.assert_not_despawned(); + let last_change_tick = self.world.last_change_tick; + let change_tick = self.world.change_tick(); UnsafeEntityCell::new( self.world.as_unsafe_world_cell(), self.entity, self.location, + last_change_tick, + change_tick, ) } @@ -1978,281 +1997,57 @@ impl<'w> EntityWorldMut<'w> { /// # Panics /// /// If the entity has been despawned while this `EntityWorldMut` is still alive. - // TODO: BundleRemover? #[must_use] #[track_caller] pub fn take(&mut self) -> Option { self.assert_not_despawned(); - let world = &mut self.world; - let storages = &mut world.storages; - // SAFETY: These come from the same world. - let mut registrator = - unsafe { ComponentsRegistrator::new(&mut world.components, &mut world.component_ids) }; - let bundle_id = world.bundles.register_info::(&mut registrator, storages); - // SAFETY: We just ensured this bundle exists - let bundle_info = unsafe { world.bundles.get_unchecked(bundle_id) }; - let old_location = self.location; - // SAFETY: `archetype_id` exists because it is referenced in the old `EntityLocation` which is valid, - // components exist in `bundle_info` because `Bundles::init_info` initializes a `BundleInfo` containing all components of the bundle type `T` - let new_archetype_id = unsafe { - bundle_info.remove_bundle_from_archetype( - &mut world.archetypes, - storages, - ®istrator, - &world.observers, - old_location.archetype_id, - false, - )? - }; - - if new_archetype_id == old_location.archetype_id { - return None; - } - let entity = self.entity; - // SAFETY: Archetypes and Bundles cannot be mutably aliased through DeferredWorld - let (old_archetype, bundle_info, mut deferred_world) = unsafe { - let bundle_info: *const BundleInfo = bundle_info; - let world = world.as_unsafe_world_cell(); - ( - &world.archetypes()[old_location.archetype_id], - &*bundle_info, - world.into_deferred(), + let location = self.location; + + let mut remover = + // SAFETY: The archetype id must be valid since this entity is in it. + unsafe { BundleRemover::new::(self.world, self.location.archetype_id, true) }?; + // SAFETY: The passed location has the sane archetype as the remover, since they came from the same location. + let (new_location, result) = unsafe { + remover.remove( + entity, + location, + MaybeLocation::caller(), + |sets, table, components, bundle_components| { + let mut bundle_components = bundle_components.iter().copied(); + ( + false, + T::from_components(&mut (sets, table), &mut |(sets, table)| { + let component_id = bundle_components.next().unwrap(); + // SAFETY: the component existed to be removed, so its id must be valid. + let component_info = components.get_info_unchecked(component_id); + match component_info.storage_type() { + StorageType::Table => { + table + .as_mut() + // SAFETY: The table must be valid if the component is in it. + .debug_checked_unwrap() + // SAFETY: The remover is cleaning this up. + .take_component(component_id, location.table_row) + } + StorageType::SparseSet => sets + .get_mut(component_id) + .unwrap() + .remove_and_forget(entity) + .unwrap(), + } + }), + ) + }, ) }; + self.location = new_location; - // SAFETY: all bundle components exist in World - unsafe { - trigger_on_replace_and_on_remove_hooks_and_observers( - &mut deferred_world, - old_archetype, - entity, - bundle_info, - MaybeLocation::caller(), - ); - } - - let archetypes = &mut world.archetypes; - let storages = &mut world.storages; - let components = &mut world.components; - let entities = &mut world.entities; - let removed_components = &mut world.removed_components; - - let entity = self.entity; - let mut bundle_components = bundle_info.iter_explicit_components(); - // SAFETY: bundle components are iterated in order, which guarantees that the component type - // matches - let result = unsafe { - T::from_components(storages, &mut |storages| { - let component_id = bundle_components.next().unwrap(); - // SAFETY: - // - entity location is valid - // - table row is removed below, without dropping the contents - // - `components` comes from the same world as `storages` - // - the component exists on the entity - take_component( - storages, - components, - removed_components, - component_id, - entity, - old_location, - ) - }) - }; - - #[expect( - clippy::undocumented_unsafe_blocks, - reason = "Needs to be documented; see #17345." - )] - unsafe { - Self::move_entity_from_remove::( - entity, - &mut self.location, - old_location.archetype_id, - old_location, - entities, - archetypes, - storages, - new_archetype_id, - ); - } self.world.flush(); self.update_location(); Some(result) } - /// # Safety - /// - /// `new_archetype_id` must have the same or a subset of the components - /// in `old_archetype_id`. Probably more safety stuff too, audit a call to - /// this fn as if the code here was written inline - /// - /// when DROP is true removed components will be dropped otherwise they will be forgotten - // We use a const generic here so that we are less reliant on - // inlining for rustc to optimize out the `match DROP` - unsafe fn move_entity_from_remove( - entity: Entity, - self_location: &mut EntityLocation, - old_archetype_id: ArchetypeId, - old_location: EntityLocation, - entities: &mut Entities, - archetypes: &mut Archetypes, - storages: &mut Storages, - new_archetype_id: ArchetypeId, - ) { - let old_archetype = &mut archetypes[old_archetype_id]; - let remove_result = old_archetype.swap_remove(old_location.archetype_row); - // if an entity was moved into this entity's archetype row, update its archetype row - if let Some(swapped_entity) = remove_result.swapped_entity { - let swapped_location = entities.get(swapped_entity).unwrap(); - - entities.set( - swapped_entity.index(), - EntityLocation { - archetype_id: swapped_location.archetype_id, - archetype_row: old_location.archetype_row, - table_id: swapped_location.table_id, - table_row: swapped_location.table_row, - }, - ); - } - let old_table_row = remove_result.table_row; - let old_table_id = old_archetype.table_id(); - let new_archetype = &mut archetypes[new_archetype_id]; - - let new_location = if old_table_id == new_archetype.table_id() { - new_archetype.allocate(entity, old_table_row) - } else { - let (old_table, new_table) = storages - .tables - .get_2_mut(old_table_id, new_archetype.table_id()); - - let move_result = if DROP { - // SAFETY: old_table_row exists - unsafe { old_table.move_to_and_drop_missing_unchecked(old_table_row, new_table) } - } else { - // SAFETY: old_table_row exists - unsafe { old_table.move_to_and_forget_missing_unchecked(old_table_row, new_table) } - }; - - // SAFETY: move_result.new_row is a valid position in new_archetype's table - let new_location = unsafe { new_archetype.allocate(entity, move_result.new_row) }; - - // if an entity was moved into this entity's table row, update its table row - if let Some(swapped_entity) = move_result.swapped_entity { - let swapped_location = entities.get(swapped_entity).unwrap(); - - entities.set( - swapped_entity.index(), - EntityLocation { - archetype_id: swapped_location.archetype_id, - archetype_row: swapped_location.archetype_row, - table_id: swapped_location.table_id, - table_row: old_location.table_row, - }, - ); - archetypes[swapped_location.archetype_id] - .set_entity_table_row(swapped_location.archetype_row, old_table_row); - } - - new_location - }; - - *self_location = new_location; - // SAFETY: The entity is valid and has been moved to the new location already. - unsafe { - entities.set(entity.index(), new_location); - } - } - - /// Remove the components of `bundle` from `entity`. - /// - /// # Safety - /// - A `BundleInfo` with the corresponding `BundleId` must have been initialized. - unsafe fn remove_bundle(&mut self, bundle: BundleId, caller: MaybeLocation) -> EntityLocation { - let entity = self.entity; - let world = &mut self.world; - let location = self.location; - // SAFETY: the caller guarantees that the BundleInfo for this id has been initialized. - let bundle_info = world.bundles.get_unchecked(bundle); - - // SAFETY: `archetype_id` exists because it is referenced in `location` which is valid - // and components in `bundle_info` must exist due to this function's safety invariants. - let new_archetype_id = bundle_info - .remove_bundle_from_archetype( - &mut world.archetypes, - &mut world.storages, - &world.components, - &world.observers, - location.archetype_id, - // components from the bundle that are not present on the entity are ignored - true, - ) - .expect("intersections should always return a result"); - - if new_archetype_id == location.archetype_id { - return location; - } - - // SAFETY: Archetypes and Bundles cannot be mutably aliased through DeferredWorld - let (old_archetype, bundle_info, mut deferred_world) = unsafe { - let bundle_info: *const BundleInfo = bundle_info; - let world = world.as_unsafe_world_cell(); - ( - &world.archetypes()[location.archetype_id], - &*bundle_info, - world.into_deferred(), - ) - }; - - // SAFETY: all bundle components exist in World - unsafe { - trigger_on_replace_and_on_remove_hooks_and_observers( - &mut deferred_world, - old_archetype, - entity, - bundle_info, - caller, - ); - } - - let old_archetype = &world.archetypes[location.archetype_id]; - for component_id in bundle_info.iter_explicit_components() { - if old_archetype.contains(component_id) { - world.removed_components.send(component_id, entity); - - // Make sure to drop components stored in sparse sets. - // Dense components are dropped later in `move_to_and_drop_missing_unchecked`. - if let Some(StorageType::SparseSet) = old_archetype.get_storage_type(component_id) { - world - .storages - .sparse_sets - .get_mut(component_id) - // Set exists because the component existed on the entity - .unwrap() - .remove(entity); - } - } - } - - // SAFETY: `new_archetype_id` is a subset of the components in `old_location.archetype_id` - // because it is created by removing a bundle from these components. - let mut new_location = location; - Self::move_entity_from_remove::( - entity, - &mut new_location, - location.archetype_id, - location, - &mut world.entities, - &mut world.archetypes, - &mut world.storages, - new_archetype_id, - ); - - new_location - } - /// Removes any components in the [`Bundle`] from the entity. /// /// See [`EntityCommands::remove`](crate::system::EntityCommands::remove) for more details. @@ -2260,7 +2055,6 @@ impl<'w> EntityWorldMut<'w> { /// # Panics /// /// If the entity has been despawned while this `EntityWorldMut` is still alive. - // TODO: BundleRemover? #[track_caller] pub fn remove(&mut self) -> &mut Self { self.remove_with_caller::(MaybeLocation::caller()) @@ -2269,18 +2063,25 @@ impl<'w> EntityWorldMut<'w> { #[inline] pub(crate) fn remove_with_caller(&mut self, caller: MaybeLocation) -> &mut Self { self.assert_not_despawned(); - let storages = &mut self.world.storages; - // SAFETY: These come from the same world. - let mut registrator = unsafe { - ComponentsRegistrator::new(&mut self.world.components, &mut self.world.component_ids) - }; - let bundle_info = self - .world - .bundles - .register_info::(&mut registrator, storages); - // SAFETY: the `BundleInfo` is initialized above - self.location = unsafe { self.remove_bundle(bundle_info, caller) }; + let Some(mut remover) = + // SAFETY: The archetype id must be valid since this entity is in it. + (unsafe { BundleRemover::new::(self.world, self.location.archetype_id, false) }) + else { + return self; + }; + // SAFETY: The remover archetype came from the passed location and the removal can not fail. + let new_location = unsafe { + remover.remove( + self.entity, + self.location, + caller, + BundleRemover::empty_pre_remove, + ) + } + .0; + + self.location = new_location; self.world.flush(); self.update_location(); self @@ -2302,16 +2103,31 @@ impl<'w> EntityWorldMut<'w> { ) -> &mut Self { self.assert_not_despawned(); let storages = &mut self.world.storages; + let bundles = &mut self.world.bundles; // SAFETY: These come from the same world. let mut registrator = unsafe { ComponentsRegistrator::new(&mut self.world.components, &mut self.world.component_ids) }; - let bundles = &mut self.world.bundles; - let bundle_id = bundles.register_contributed_bundle_info::(&mut registrator, storages); - // SAFETY: the dynamic `BundleInfo` is initialized above - self.location = unsafe { self.remove_bundle(bundle_id, caller) }; + // SAFETY: We just created the bundle, and the archetype is valid, since we are in it. + let Some(mut remover) = (unsafe { + BundleRemover::new_with_id(self.world, self.location.archetype_id, bundle_id, false) + }) else { + return self; + }; + // SAFETY: The remover archetype came from the passed location and the removal can not fail. + let new_location = unsafe { + remover.remove( + self.entity, + self.location, + caller, + BundleRemover::empty_pre_remove, + ) + } + .0; + + self.location = new_location; self.world.flush(); self.update_location(); self @@ -2358,8 +2174,24 @@ impl<'w> EntityWorldMut<'w> { .bundles .init_dynamic_info(&mut self.world.storages, ®istrator, to_remove); - // SAFETY: the `BundleInfo` for the components to remove is initialized above - self.location = unsafe { self.remove_bundle(remove_bundle, caller) }; + // SAFETY: We just created the bundle, and the archetype is valid, since we are in it. + let Some(mut remover) = (unsafe { + BundleRemover::new_with_id(self.world, self.location.archetype_id, remove_bundle, false) + }) else { + return self; + }; + // SAFETY: The remover archetype came from the passed location and the removal can not fail. + let new_location = unsafe { + remover.remove( + self.entity, + self.location, + caller, + BundleRemover::empty_pre_remove, + ) + } + .0; + + self.location = new_location; self.world.flush(); self.update_location(); self @@ -2393,8 +2225,24 @@ impl<'w> EntityWorldMut<'w> { component_id, ); - // SAFETY: the `BundleInfo` for this `component_id` is initialized above - self.location = unsafe { self.remove_bundle(bundle_id, caller) }; + // SAFETY: We just created the bundle, and the archetype is valid, since we are in it. + let Some(mut remover) = (unsafe { + BundleRemover::new_with_id(self.world, self.location.archetype_id, bundle_id, false) + }) else { + return self; + }; + // SAFETY: The remover archetype came from the passed location and the removal can not fail. + let new_location = unsafe { + remover.remove( + self.entity, + self.location, + caller, + BundleRemover::empty_pre_remove, + ) + } + .0; + + self.location = new_location; self.world.flush(); self.update_location(); self @@ -2419,9 +2267,24 @@ impl<'w> EntityWorldMut<'w> { component_ids, ); - // SAFETY: the `BundleInfo` for this `bundle_id` is initialized above - unsafe { self.remove_bundle(bundle_id, MaybeLocation::caller()) }; + // SAFETY: We just created the bundle, and the archetype is valid, since we are in it. + let Some(mut remover) = (unsafe { + BundleRemover::new_with_id(self.world, self.location.archetype_id, bundle_id, false) + }) else { + return self; + }; + // SAFETY: The remover archetype came from the passed location and the removal can not fail. + let new_location = unsafe { + remover.remove( + self.entity, + self.location, + MaybeLocation::caller(), + BundleRemover::empty_pre_remove, + ) + } + .0; + self.location = new_location; self.world.flush(); self.update_location(); self @@ -2449,8 +2312,24 @@ impl<'w> EntityWorldMut<'w> { component_ids.as_slice(), ); - // SAFETY: the `BundleInfo` for this `component_id` is initialized above - self.location = unsafe { self.remove_bundle(bundle_id, caller) }; + // SAFETY: We just created the bundle, and the archetype is valid, since we are in it. + let Some(mut remover) = (unsafe { + BundleRemover::new_with_id(self.world, self.location.archetype_id, bundle_id, false) + }) else { + return self; + }; + // SAFETY: The remover archetype came from the passed location and the removal can not fail. + let new_location = unsafe { + remover.remove( + self.entity, + self.location, + caller, + BundleRemover::empty_pre_remove, + ) + } + .0; + + self.location = new_location; self.world.flush(); self.update_location(); self @@ -2473,15 +2352,6 @@ impl<'w> EntityWorldMut<'w> { self.despawn_with_caller(MaybeLocation::caller()); } - /// Despawns the provided entity and its descendants. - #[deprecated( - since = "0.16.0", - note = "Use entity.despawn(), which now automatically despawns recursively." - )] - pub fn despawn_recursive(self) { - self.despawn(); - } - pub(crate) fn despawn_with_caller(self, caller: MaybeLocation) { self.assert_not_despawned(); let world = self.world; @@ -2555,6 +2425,7 @@ impl<'w> EntityWorldMut<'w> { .expect("entity should exist at this point."); let table_row; let moved_entity; + let change_tick = world.change_tick(); { let archetype = &mut world.archetypes[self.location.archetype_id]; @@ -2564,7 +2435,7 @@ impl<'w> EntityWorldMut<'w> { // SAFETY: swapped_entity is valid and the swapped entity's components are // moved to the new location immediately after. unsafe { - world.entities.set( + world.entities.set_spawn_despawn( swapped_entity.index(), EntityLocation { archetype_id: swapped_location.archetype_id, @@ -2572,6 +2443,8 @@ impl<'w> EntityWorldMut<'w> { table_id: swapped_location.table_id, table_row: swapped_location.table_row, }, + caller, + change_tick, ); } } @@ -2593,7 +2466,7 @@ impl<'w> EntityWorldMut<'w> { // SAFETY: `moved_entity` is valid and the provided `EntityLocation` accurately reflects // the current location of the entity and its component data. unsafe { - world.entities.set( + world.entities.set_spawn_despawn( moved_entity.index(), EntityLocation { archetype_id: moved_location.archetype_id, @@ -2601,19 +2474,14 @@ impl<'w> EntityWorldMut<'w> { table_id: moved_location.table_id, table_row, }, + caller, + change_tick, ); } world.archetypes[moved_location.archetype_id] .set_entity_table_row(moved_location.archetype_row, table_row); } world.flush(); - - // SAFETY: No structural changes - unsafe { - world - .entities_mut() - .set_spawned_or_despawned_by(self.entity.index(), caller); - } } /// Ensures any commands triggered by the actions of Self are applied, equivalent to [`World::flush`] @@ -2773,6 +2641,8 @@ impl<'w> EntityWorldMut<'w> { /// # Panics /// /// If the entity has been despawned while this `EntityWorldMut` is still alive. + /// + /// Panics if the given system is an exclusive system. #[track_caller] pub fn observe( &mut self, @@ -2952,46 +2822,35 @@ impl<'w> EntityWorldMut<'w> { .entity_get_spawned_or_despawned_by(self.entity) .map(|location| location.unwrap()) } -} -/// # Safety -/// All components in the archetype must exist in world -unsafe fn trigger_on_replace_and_on_remove_hooks_and_observers( - deferred_world: &mut DeferredWorld, - archetype: &Archetype, - entity: Entity, - bundle_info: &BundleInfo, - caller: MaybeLocation, -) { - let bundle_components_in_archetype = || { - bundle_info - .iter_explicit_components() - .filter(|component_id| archetype.contains(*component_id)) - }; - if archetype.has_replace_observer() { - deferred_world.trigger_observers( - ON_REPLACE, - entity, - bundle_components_in_archetype(), - caller, - ); + /// Returns the [`Tick`] at which this entity has last been spawned. + pub fn spawned_at(&self) -> Tick { + self.assert_not_despawned(); + + // SAFETY: entity being alive was asserted + unsafe { + self.world() + .entities() + .entity_get_spawned_or_despawned_unchecked(self.entity) + .1 + } } - deferred_world.trigger_on_replace( - archetype, - entity, - bundle_components_in_archetype(), - caller, - RelationshipHookMode::Run, - ); - if archetype.has_remove_observer() { - deferred_world.trigger_observers( - ON_REMOVE, - entity, - bundle_components_in_archetype(), - caller, - ); + + /// Reborrows this entity in a temporary scope. + /// This is useful for executing a function that requires a `EntityWorldMut` + /// but you do not want to move out the entity ownership. + pub fn reborrow_scope(&mut self, f: impl FnOnce(EntityWorldMut) -> U) -> U { + let Self { + entity, location, .. + } = *self; + self.world_scope(move |world| { + f(EntityWorldMut { + world, + entity, + location, + }) + }) } - deferred_world.trigger_on_remove(archetype, entity, bundle_components_in_archetype(), caller); } /// A view into a single entity and component in a world, which may either be vacant or occupied. @@ -3344,14 +3203,6 @@ impl<'w, 'a, T: Component> VacantEntry<'w, 'a, T> { /// /// let filtered_entity: FilteredEntityRef = query.single(&mut world).unwrap(); /// let component: &A = filtered_entity.get().unwrap(); -/// -/// // Here `FilteredEntityRef` is nested in a tuple, so it does not have access to `&A`. -/// let mut query = QueryBuilder::<(Entity, FilteredEntityRef)>::new(&mut world) -/// .data::<&A>() -/// .build(); -/// -/// let (_, filtered_entity) = query.single(&mut world).unwrap(); -/// assert!(filtered_entity.get::().is_none()); /// ``` #[derive(Clone)] pub struct FilteredEntityRef<'w> { @@ -3506,6 +3357,11 @@ impl<'w> FilteredEntityRef<'w> { pub fn spawned_by(&self) -> MaybeLocation { self.entity.spawned_by() } + + /// Returns the [`Tick`] at which this entity has been spawned. + pub fn spawned_at(&self) -> Tick { + self.entity.spawned_at() + } } impl<'w> From> for FilteredEntityRef<'w> { @@ -3675,14 +3531,6 @@ unsafe impl EntityEquivalent for FilteredEntityRef<'_> {} /// /// let mut filtered_entity: FilteredEntityMut = query.single_mut(&mut world).unwrap(); /// let component: Mut = filtered_entity.get_mut().unwrap(); -/// -/// // Here `FilteredEntityMut` is nested in a tuple, so it does not have access to `&mut A`. -/// let mut query = QueryBuilder::<(Entity, FilteredEntityMut)>::new(&mut world) -/// .data::<&mut A>() -/// .build(); -/// -/// let (_, mut filtered_entity) = query.single_mut(&mut world).unwrap(); -/// assert!(filtered_entity.get_mut::().is_none()); /// ``` pub struct FilteredEntityMut<'w> { entity: UnsafeEntityCell<'w>, @@ -3887,6 +3735,11 @@ impl<'w> FilteredEntityMut<'w> { pub fn spawned_by(&self) -> MaybeLocation { self.entity.spawned_by() } + + /// Returns the [`Tick`] at which this entity has been spawned. + pub fn spawned_at(&self) -> Tick { + self.entity.spawned_at() + } } impl<'a> From> for FilteredEntityMut<'a> { @@ -4085,6 +3938,11 @@ where self.entity.spawned_by() } + /// Returns the [`Tick`] at which this entity has been spawned. + pub fn spawned_at(&self) -> Tick { + self.entity.spawned_at() + } + /// Gets the component of the given [`ComponentId`] from the entity. /// /// **You should prefer to use the typed API [`Self::get`] where possible and only @@ -4329,6 +4187,11 @@ where self.entity.spawned_by() } + /// Returns the [`Tick`] at which this entity has been spawned. + pub fn spawned_at(&self) -> Tick { + self.entity.spawned_at() + } + /// Returns `true` if the current entity has a component of type `T`. /// Otherwise, this returns `false`. /// @@ -4457,7 +4320,7 @@ where /// # Safety /// /// - [`OwningPtr`] and [`StorageType`] iterators must correspond to the -/// [`BundleInfo`] used to construct [`BundleInserter`] +/// [`BundleInfo`](crate::bundle::BundleInfo) used to construct [`BundleInserter`] /// - [`Entity`] must correspond to [`EntityLocation`] unsafe fn insert_dynamic_bundle< 'a, @@ -4505,50 +4368,6 @@ unsafe fn insert_dynamic_bundle< } } -/// Moves component data out of storage. -/// -/// This function leaves the underlying memory unchanged, but the component behind -/// returned pointer is semantically owned by the caller and will not be dropped in its original location. -/// Caller is responsible to drop component data behind returned pointer. -/// -/// # Safety -/// - `location.table_row` must be in bounds of column of component id `component_id` -/// - `component_id` must be valid -/// - `components` must come from the same world as `self` -/// - The relevant table row **must be removed** by the caller once all components are taken, without dropping the value -/// -/// # Panics -/// Panics if the entity did not have the component. -#[inline] -pub(crate) unsafe fn take_component<'a>( - storages: &'a mut Storages, - components: &Components, - removed_components: &mut RemovedComponentEvents, - component_id: ComponentId, - entity: Entity, - location: EntityLocation, -) -> OwningPtr<'a> { - // SAFETY: caller promises component_id to be valid - let component_info = unsafe { components.get_info_unchecked(component_id) }; - removed_components.send(component_id, entity); - match component_info.storage_type() { - StorageType::Table => { - let table = &mut storages.tables[location.table_id]; - // SAFETY: - // - archetypes only store valid table_rows - // - index is in bounds as promised by caller - // - promote is safe because the caller promises to remove the table row without dropping it immediately afterwards - unsafe { table.take_component(component_id, location.table_row) } - } - StorageType::SparseSet => storages - .sparse_sets - .get_mut(component_id) - .unwrap() - .remove_and_forget(entity) - .unwrap(), - } -} - /// Types that can be used to fetch components from an entity dynamically by /// [`ComponentId`]s. /// @@ -4912,7 +4731,7 @@ mod tests { use core::panic::AssertUnwindSafe; use std::sync::OnceLock; - use crate::component::HookContext; + use crate::component::{HookContext, Tick}; use crate::{ change_detection::{MaybeLocation, MutUntyped}, component::ComponentId, @@ -6200,22 +6019,27 @@ mod tests { #[component(on_remove = get_tracked)] struct C; - static TRACKED: OnceLock = OnceLock::new(); + static TRACKED: OnceLock<(MaybeLocation, Tick)> = OnceLock::new(); fn get_tracked(world: DeferredWorld, HookContext { entity, .. }: HookContext) { TRACKED.get_or_init(|| { - world + let by = world .entities .entity_get_spawned_or_despawned_by(entity) - .map(|l| l.unwrap()) + .map(|l| l.unwrap()); + let at = world + .entities + .entity_get_spawned_or_despawned_at(entity) + .unwrap(); + (by, at) }); } #[track_caller] - fn caller_spawn(world: &mut World) -> (Entity, MaybeLocation) { + fn caller_spawn(world: &mut World) -> (Entity, MaybeLocation, Tick) { let caller = MaybeLocation::caller(); - (world.spawn(C).id(), caller) + (world.spawn(C).id(), caller, world.change_tick()) } - let (entity, spawner) = caller_spawn(&mut world); + let (entity, spawner, spawn_tick) = caller_spawn(&mut world); assert_eq!( spawner, @@ -6226,13 +6050,13 @@ mod tests { ); #[track_caller] - fn caller_despawn(world: &mut World, entity: Entity) -> MaybeLocation { + fn caller_despawn(world: &mut World, entity: Entity) -> (MaybeLocation, Tick) { world.despawn(entity); - MaybeLocation::caller() + (MaybeLocation::caller(), world.change_tick()) } - let despawner = caller_despawn(&mut world, entity); + let (despawner, despawn_tick) = caller_despawn(&mut world, entity); - assert_eq!(spawner, *TRACKED.get().unwrap()); + assert_eq!((spawner, spawn_tick), *TRACKED.get().unwrap()); assert_eq!( despawner, world @@ -6240,6 +6064,13 @@ mod tests { .entity_get_spawned_or_despawned_by(entity) .map(|l| l.unwrap()) ); + assert_eq!( + despawn_tick, + world + .entities() + .entity_get_spawned_or_despawned_at(entity) + .unwrap() + ); } #[test] diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 9bd8d699c6..3a1195aea3 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -14,6 +14,7 @@ pub mod unsafe_world_cell; #[cfg(feature = "bevy_reflect")] pub mod reflect; +use crate::error::{DefaultErrorHandler, ErrorHandler}; pub use crate::{ change_detection::{Mut, Ref, CHECK_TICK_THRESHOLD}, world::command_queue::CommandQueue, @@ -30,10 +31,6 @@ pub use filtered_resource::*; pub use identifier::WorldId; pub use spawn_batch::*; -#[expect( - deprecated, - reason = "We need to support `AllocAtWithoutReplacement` for now." -)] use crate::{ archetype::{ArchetypeId, ArchetypeRow, Archetypes}, bundle::{ @@ -46,9 +43,7 @@ use crate::{ ComponentTicks, Components, ComponentsQueuedRegistrator, ComponentsRegistrator, Mutable, RequiredComponents, RequiredComponentsError, Tick, }, - entity::{ - AllocAtWithoutReplacement, Entities, Entity, EntityDoesNotExistError, EntityLocation, - }, + entity::{Entities, Entity, EntityDoesNotExistError, EntityLocation}, entity_disabling::DefaultQueryFilters, event::{Event, EventId, Events, SendBatchIds}, observer::Observers, @@ -982,6 +977,8 @@ impl World { self.as_unsafe_world_cell_readonly(), entity, location, + self.last_change_tick, + self.read_change_tick(), ); // SAFETY: `&self` gives read access to the entire world. unsafe { EntityRef::new(cell) } @@ -991,6 +988,8 @@ impl World { /// Returns a mutable iterator over all entities in the `World`. pub fn iter_entities_mut(&mut self) -> impl Iterator> + '_ { + let last_change_tick = self.last_change_tick; + let change_tick = self.change_tick(); let world_cell = self.as_unsafe_world_cell(); world_cell.archetypes().iter().flat_map(move |archetype| { archetype @@ -1007,7 +1006,13 @@ impl World { }; // SAFETY: entity exists and location accurately specifies the archetype where the entity is stored. - let cell = UnsafeEntityCell::new(world_cell, entity, location); + let cell = UnsafeEntityCell::new( + world_cell, + entity, + location, + last_change_tick, + change_tick, + ); // SAFETY: We have exclusive access to the entire world. We only create one borrow for each entity, // so none will conflict with one another. unsafe { EntityMut::new(cell) } @@ -1181,9 +1186,6 @@ impl World { .unwrap_or(EntityLocation::INVALID); } - self.entities - .set_spawned_or_despawned_by(entity.index(), caller); - // SAFETY: entity and location are valid, as they were just created above let mut entity = unsafe { EntityWorldMut::new(self, entity, entity_location) }; after_effect.apply(&mut entity); @@ -1203,10 +1205,9 @@ impl World { // SAFETY: no components are allocated by archetype.allocate() because the archetype is // empty let location = unsafe { archetype.allocate(entity, table_row) }; - self.entities.set(entity.index(), location); - + let change_tick = self.change_tick(); self.entities - .set_spawned_or_despawned_by(entity.index(), caller); + .set_spawn_despawn(entity.index(), location, caller, change_tick); EntityWorldMut::new(self, entity, location) } @@ -2222,176 +2223,6 @@ impl World { unsafe { self.as_unsafe_world_cell().get_non_send_resource_mut() } } - /// For a given batch of ([`Entity`], [`Bundle`]) pairs, either spawns each [`Entity`] with the given - /// bundle (if the entity does not exist), or inserts the [`Bundle`] (if the entity already exists). - /// This is faster than doing equivalent operations one-by-one. - /// Returns `Ok` if all entities were successfully inserted into or spawned. Otherwise it returns an `Err` - /// with a list of entities that could not be spawned or inserted into. A "spawn or insert" operation can - /// only fail if an [`Entity`] is passed in with an "invalid generation" that conflicts with an existing [`Entity`]. - /// - /// # Note - /// Spawning a specific `entity` value is rarely the right choice. Most apps should use [`World::spawn_batch`]. - /// This method should generally only be used for sharing entities across apps, and only when they have a scheme - /// worked out to share an ID space (which doesn't happen by default). - /// - /// ``` - /// use bevy_ecs::{entity::Entity, world::World, component::Component}; - /// #[derive(Component)] - /// struct A(&'static str); - /// #[derive(Component, PartialEq, Debug)] - /// struct B(f32); - /// - /// let mut world = World::new(); - /// let e0 = world.spawn_empty().id(); - /// let e1 = world.spawn_empty().id(); - /// world.insert_or_spawn_batch(vec![ - /// (e0, (A("a"), B(0.0))), // the first entity - /// (e1, (A("b"), B(1.0))), // the second entity - /// ]); - /// - /// assert_eq!(world.get::(e0), Some(&B(0.0))); - /// ``` - #[track_caller] - #[deprecated( - since = "0.16.0", - note = "This can cause extreme performance problems when used with lots of arbitrary free entities. See #18054 on GitHub." - )] - pub fn insert_or_spawn_batch(&mut self, iter: I) -> Result<(), Vec> - where - I: IntoIterator, - I::IntoIter: Iterator, - B: Bundle, - { - #[expect( - deprecated, - reason = "This needs to be supported for now, and the outer function is deprecated too." - )] - self.insert_or_spawn_batch_with_caller(iter, MaybeLocation::caller()) - } - - /// Split into a new function so we can pass the calling location into the function when using - /// as a command. - #[inline] - #[deprecated( - since = "0.16.0", - note = "This can cause extreme performance problems when used with lots of arbitrary free entities. See #18054 on GitHub." - )] - pub(crate) fn insert_or_spawn_batch_with_caller( - &mut self, - iter: I, - caller: MaybeLocation, - ) -> Result<(), Vec> - where - I: IntoIterator, - I::IntoIter: Iterator, - B: Bundle, - { - self.flush(); - let change_tick = self.change_tick(); - - // SAFETY: These come from the same world. `Self.components_registrator` can't be used since we borrow other fields too. - let mut registrator = - unsafe { ComponentsRegistrator::new(&mut self.components, &mut self.component_ids) }; - let bundle_id = self - .bundles - .register_info::(&mut registrator, &mut self.storages); - enum SpawnOrInsert<'w> { - Spawn(BundleSpawner<'w>), - Insert(BundleInserter<'w>, ArchetypeId), - } - - impl<'w> SpawnOrInsert<'w> { - fn entities(&mut self) -> &mut Entities { - match self { - SpawnOrInsert::Spawn(spawner) => spawner.entities(), - SpawnOrInsert::Insert(inserter, _) => inserter.entities(), - } - } - } - // SAFETY: we initialized this bundle_id in `init_info` - let mut spawn_or_insert = SpawnOrInsert::Spawn(unsafe { - BundleSpawner::new_with_id(self, bundle_id, change_tick) - }); - - let mut invalid_entities = Vec::new(); - for (entity, bundle) in iter { - #[expect( - deprecated, - reason = "This needs to be supported for now, and the outer function is deprecated too." - )] - match spawn_or_insert - .entities() - .alloc_at_without_replacement(entity) - { - AllocAtWithoutReplacement::Exists(location) => { - match spawn_or_insert { - SpawnOrInsert::Insert(ref mut inserter, archetype) - if location.archetype_id == archetype => - { - // SAFETY: `entity` is valid, `location` matches entity, bundle matches inserter - unsafe { - inserter.insert( - entity, - location, - bundle, - InsertMode::Replace, - caller, - RelationshipHookMode::Run, - ) - }; - } - _ => { - // SAFETY: we initialized this bundle_id in `init_info` - let mut inserter = unsafe { - BundleInserter::new_with_id( - self, - location.archetype_id, - bundle_id, - change_tick, - ) - }; - // SAFETY: `entity` is valid, `location` matches entity, bundle matches inserter - unsafe { - inserter.insert( - entity, - location, - bundle, - InsertMode::Replace, - caller, - RelationshipHookMode::Run, - ) - }; - spawn_or_insert = - SpawnOrInsert::Insert(inserter, location.archetype_id); - } - }; - } - AllocAtWithoutReplacement::DidNotExist => { - if let SpawnOrInsert::Spawn(ref mut spawner) = spawn_or_insert { - // SAFETY: `entity` is allocated (but non existent), bundle matches inserter - unsafe { spawner.spawn_non_existent(entity, bundle, caller) }; - } else { - // SAFETY: we initialized this bundle_id in `init_info` - let mut spawner = - unsafe { BundleSpawner::new_with_id(self, bundle_id, change_tick) }; - // SAFETY: `entity` is valid, `location` matches entity, bundle matches inserter - unsafe { spawner.spawn_non_existent(entity, bundle, caller) }; - spawn_or_insert = SpawnOrInsert::Spawn(spawner); - } - } - AllocAtWithoutReplacement::ExistsWithWrongGeneration => { - invalid_entities.push(entity); - } - } - } - - if invalid_entities.is_empty() { - Ok(()) - } else { - Err(invalid_entities) - } - } - /// For a given batch of ([`Entity`], [`Bundle`]) pairs, /// adds the `Bundle` of components to each `Entity`. /// This is faster than doing equivalent operations one-by-one. @@ -3145,6 +2976,7 @@ impl World { sparse_sets.check_change_ticks(change_tick); resources.check_change_ticks(change_tick); non_send_resources.check_change_ticks(change_tick); + self.entities.check_change_ticks(change_tick); if let Some(mut schedules) = self.get_resource_mut::() { schedules.check_change_ticks(change_tick); @@ -3215,6 +3047,16 @@ impl World { // SAFETY: We just initialized the bundle so its id should definitely be valid. unsafe { self.bundles.get(id).debug_checked_unwrap() } } + + /// Convenience method for accessing the world's default error handler, + /// which can be overwritten with [`DefaultErrorHandler`]. + #[inline] + pub fn default_error_handler(&self) -> ErrorHandler { + self.get_resource::() + .copied() + .unwrap_or_default() + .0 + } } impl World { @@ -4438,22 +4280,38 @@ mod tests { world.entities.entity_get_spawned_or_despawned_by(entity), MaybeLocation::new(Some(Location::caller())) ); + assert_eq!( + world.entities.entity_get_spawned_or_despawned_at(entity), + Some(world.change_tick()) + ); world.despawn(entity); assert_eq!( world.entities.entity_get_spawned_or_despawned_by(entity), MaybeLocation::new(Some(Location::caller())) ); + assert_eq!( + world.entities.entity_get_spawned_or_despawned_at(entity), + Some(world.change_tick()) + ); let new = world.spawn_empty().id(); assert_eq!(entity.index(), new.index()); assert_eq!( world.entities.entity_get_spawned_or_despawned_by(entity), MaybeLocation::new(None) ); + assert_eq!( + world.entities.entity_get_spawned_or_despawned_at(entity), + None + ); world.despawn(new); assert_eq!( world.entities.entity_get_spawned_or_despawned_by(entity), MaybeLocation::new(None) ); + assert_eq!( + world.entities.entity_get_spawned_or_despawned_at(entity), + None + ); } #[test] diff --git a/crates/bevy_ecs/src/world/unsafe_world_cell.rs b/crates/bevy_ecs/src/world/unsafe_world_cell.rs index b46b4a154b..3f74f855a6 100644 --- a/crates/bevy_ecs/src/world/unsafe_world_cell.rs +++ b/crates/bevy_ecs/src/world/unsafe_world_cell.rs @@ -7,6 +7,7 @@ use crate::{ change_detection::{MaybeLocation, MutUntyped, Ticks, TicksMut}, component::{ComponentId, ComponentTicks, Components, Mutable, StorageType, Tick, TickCells}, entity::{ContainsEntity, Entities, Entity, EntityDoesNotExistError, EntityLocation}, + error::{DefaultErrorHandler, ErrorHandler}, observer::Observers, prelude::Component, query::{DebugCheckedUnwrap, ReadOnlyQueryData}, @@ -365,7 +366,31 @@ impl<'w> UnsafeWorldCell<'w> { .entities() .get(entity) .ok_or(EntityDoesNotExistError::new(entity, self.entities()))?; - Ok(UnsafeEntityCell::new(self, entity, location)) + Ok(UnsafeEntityCell::new( + self, + entity, + location, + self.last_change_tick(), + self.change_tick(), + )) + } + + /// Retrieves an [`UnsafeEntityCell`] that exposes read and write operations for the given `entity`. + /// Similar to the [`UnsafeWorldCell`], you are in charge of making sure that no aliasing rules are violated. + #[inline] + pub fn get_entity_with_ticks( + self, + entity: Entity, + last_run: Tick, + this_run: Tick, + ) -> Result, EntityDoesNotExistError> { + let location = self + .entities() + .get(entity) + .ok_or(EntityDoesNotExistError::new(entity, self.entities()))?; + Ok(UnsafeEntityCell::new( + self, entity, location, last_run, this_run, + )) } /// Gets a reference to the resource of the given type if it exists @@ -681,6 +706,18 @@ impl<'w> UnsafeWorldCell<'w> { (*self.ptr).last_trigger_id = (*self.ptr).last_trigger_id.wrapping_add(1); } } + + /// Convenience method for accessing the world's default error handler, + /// + /// # Safety + /// Must have read access to [`DefaultErrorHandler`]. + #[inline] + pub unsafe fn default_error_handler(&self) -> ErrorHandler { + self.get_resource::() + .copied() + .unwrap_or_default() + .0 + } } impl Debug for UnsafeWorldCell<'_> { @@ -696,6 +733,8 @@ pub struct UnsafeEntityCell<'w> { world: UnsafeWorldCell<'w>, entity: Entity, location: EntityLocation, + last_run: Tick, + this_run: Tick, } impl<'w> UnsafeEntityCell<'w> { @@ -704,11 +743,15 @@ impl<'w> UnsafeEntityCell<'w> { world: UnsafeWorldCell<'w>, entity: Entity, location: EntityLocation, + last_run: Tick, + this_run: Tick, ) -> Self { UnsafeEntityCell { world, entity, location, + last_run, + this_run, } } @@ -807,8 +850,8 @@ impl<'w> UnsafeEntityCell<'w> { /// - no other mutable references to the component exist at the same time #[inline] pub unsafe fn get_ref(self) -> Option> { - let last_change_tick = self.world.last_change_tick(); - let change_tick = self.world.change_tick(); + let last_change_tick = self.last_run; + let change_tick = self.this_run; let component_id = self.world.components().get_id(TypeId::of::())?; // SAFETY: @@ -909,12 +952,7 @@ impl<'w> UnsafeEntityCell<'w> { #[inline] pub unsafe fn get_mut_assume_mutable(self) -> Option> { // SAFETY: same safety requirements - unsafe { - self.get_mut_using_ticks_assume_mutable( - self.world.last_change_tick(), - self.world.change_tick(), - ) - } + unsafe { self.get_mut_using_ticks_assume_mutable(self.last_run, self.this_run) } } /// # Safety @@ -976,14 +1014,8 @@ impl<'w> UnsafeEntityCell<'w> { }; if Q::matches_component_set(&state, &|id| archetype.contains(id)) { // SAFETY: state was initialized above using the world passed into this function - let mut fetch = unsafe { - Q::init_fetch( - self.world, - &state, - self.world.last_change_tick(), - self.world.change_tick(), - ) - }; + let mut fetch = + unsafe { Q::init_fetch(self.world, &state, self.last_run, self.this_run) }; // SAFETY: Table is guaranteed to exist let table = unsafe { self.world @@ -1070,11 +1102,7 @@ impl<'w> UnsafeEntityCell<'w> { .map(|(value, cells, caller)| MutUntyped { // SAFETY: world access validated by caller and ties world lifetime to `MutUntyped` lifetime value: value.assert_unique(), - ticks: TicksMut::from_tick_cells( - cells, - self.world.last_change_tick(), - self.world.change_tick(), - ), + ticks: TicksMut::from_tick_cells(cells, self.last_run, self.this_run), changed_by: caller.map(|caller| caller.deref_mut()), }) .ok_or(GetEntityMutByIdError::ComponentNotFound) @@ -1118,11 +1146,7 @@ impl<'w> UnsafeEntityCell<'w> { .map(|(value, cells, caller)| MutUntyped { // SAFETY: world access validated by caller and ties world lifetime to `MutUntyped` lifetime value: value.assert_unique(), - ticks: TicksMut::from_tick_cells( - cells, - self.world.last_change_tick(), - self.world.change_tick(), - ), + ticks: TicksMut::from_tick_cells(cells, self.last_run, self.this_run), changed_by: caller.map(|caller| caller.deref_mut()), }) .ok_or(GetEntityMutByIdError::ComponentNotFound) @@ -1136,6 +1160,17 @@ impl<'w> UnsafeEntityCell<'w> { .entity_get_spawned_or_despawned_by(self.entity) .map(|o| o.unwrap()) } + + /// Returns the [`Tick`] at which this entity has been spawned. + pub fn spawned_at(self) -> Tick { + // SAFETY: UnsafeEntityCell is only constructed for living entities and offers no despawn method + unsafe { + self.world() + .entities() + .entity_get_spawned_or_despawned_unchecked(self.entity) + .1 + } + } } /// Error that may be returned when calling [`UnsafeEntityCell::get_mut_by_id`]. diff --git a/crates/bevy_gilrs/src/lib.rs b/crates/bevy_gilrs/src/lib.rs index ce0d5f27f0..b9f1d9d286 100644 --- a/crates/bevy_gilrs/src/lib.rs +++ b/crates/bevy_gilrs/src/lib.rs @@ -23,7 +23,7 @@ use core::cell::RefCell; use bevy_app::{App, Plugin, PostUpdate, PreStartup, PreUpdate}; use bevy_ecs::entity::EntityHashMap; use bevy_ecs::prelude::*; -use bevy_input::InputSystem; +use bevy_input::InputSystems; use bevy_platform::collections::HashMap; use gilrs::GilrsBuilder; use gilrs_system::{gilrs_event_startup_system, gilrs_event_system}; @@ -84,7 +84,11 @@ pub struct GilrsPlugin; /// Updates the running gamepad rumble effects. #[derive(Debug, PartialEq, Eq, Clone, Hash, SystemSet)] -pub struct RumbleSystem; +pub struct RumbleSystems; + +/// Deprecated alias for [`RumbleSystems`]. +#[deprecated(since = "0.17.0", note = "Renamed to `RumbleSystems`.")] +pub type RumbleSystem = RumbleSystems; impl Plugin for GilrsPlugin { fn build(&self, app: &mut App) { @@ -106,10 +110,29 @@ impl Plugin for GilrsPlugin { app.init_resource::(); app.init_resource::() .add_systems(PreStartup, gilrs_event_startup_system) - .add_systems(PreUpdate, gilrs_event_system.before(InputSystem)) - .add_systems(PostUpdate, play_gilrs_rumble.in_set(RumbleSystem)); + .add_systems(PreUpdate, gilrs_event_system.before(InputSystems)) + .add_systems(PostUpdate, play_gilrs_rumble.in_set(RumbleSystems)); } Err(err) => error!("Failed to start Gilrs. {}", err), } } } + +#[cfg(test)] +mod tests { + use super::*; + + // Regression test for https://github.com/bevyengine/bevy/issues/17697 + #[test] + fn world_is_truly_send() { + let mut app = App::new(); + app.add_plugins(GilrsPlugin); + let world = core::mem::take(app.world_mut()); + + let handler = std::thread::spawn(move || { + drop(world); + }); + + handler.join().unwrap(); + } +} diff --git a/crates/bevy_gizmos/src/aabb.rs b/crates/bevy_gizmos/src/aabb.rs index 16dc7ed773..4ac9e5f2ac 100644 --- a/crates/bevy_gizmos/src/aabb.rs +++ b/crates/bevy_gizmos/src/aabb.rs @@ -14,7 +14,7 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::primitives::Aabb; use bevy_transform::{ components::{GlobalTransform, Transform}, - TransformSystem, + TransformSystems, }; use crate::{ @@ -39,7 +39,7 @@ impl Plugin for AabbGizmoPlugin { }), ) .after(bevy_render::view::VisibilitySystems::CalculateBounds) - .after(TransformSystem::TransformPropagate), + .after(TransformSystems::Propagate), ); } } diff --git a/crates/bevy_gizmos/src/gizmos.rs b/crates/bevy_gizmos/src/gizmos.rs index b51dd672fe..06a6a71f1f 100644 --- a/crates/bevy_gizmos/src/gizmos.rs +++ b/crates/bevy_gizmos/src/gizmos.rs @@ -134,7 +134,7 @@ pub struct Swap(PhantomData); /// .add_systems(EndOfMyContext, end_gizmo_context::) /// .add_systems( /// Last, -/// propagate_gizmos::.before(UpdateGizmoMeshes), +/// propagate_gizmos::.before(GizmoMeshSystems), /// ); /// } /// } diff --git a/crates/bevy_gizmos/src/lib.rs b/crates/bevy_gizmos/src/lib.rs index 3cd2c7c404..581a30091d 100755 --- a/crates/bevy_gizmos/src/lib.rs +++ b/crates/bevy_gizmos/src/lib.rs @@ -24,7 +24,7 @@ extern crate self as bevy_gizmos; /// System set label for the systems handling the rendering of gizmos. #[derive(SystemSet, Clone, Debug, Hash, PartialEq, Eq)] -pub enum GizmoRenderSystem { +pub enum GizmoRenderSystems { /// Adds gizmos to the [`Transparent2d`](bevy_core_pipeline::core_2d::Transparent2d) render phase #[cfg(feature = "bevy_sprite")] QueueLineGizmos2d, @@ -33,6 +33,10 @@ pub enum GizmoRenderSystem { QueueLineGizmos3d, } +/// Deprecated alias for [`GizmoRenderSystems`]. +#[deprecated(since = "0.17.0", note = "Renamed to `GizmoRenderSystems`.")] +pub type GizmoRenderSystem = GizmoRenderSystems; + #[cfg(feature = "bevy_render")] pub mod aabb; pub mod arcs; @@ -120,7 +124,7 @@ use { }, renderer::RenderDevice, sync_world::{MainEntity, TemporaryRenderEntity}, - Extract, ExtractSchedule, Render, RenderApp, RenderSet, + Extract, ExtractSchedule, Render, RenderApp, RenderSystems, }, bytemuck::cast_slice, }; @@ -185,7 +189,7 @@ impl Plugin for GizmoPlugin { if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app.add_systems( Render, - prepare_line_gizmo_bind_group.in_set(RenderSet::PrepareBindGroups), + prepare_line_gizmo_bind_group.in_set(RenderSystems::PrepareBindGroups), ); render_app.add_systems(ExtractSchedule, (extract_gizmo_data, extract_linegizmos)); @@ -268,20 +272,20 @@ impl AppGizmoBuilder for App { .add_systems( RunFixedMainLoop, start_gizmo_context:: - .in_set(bevy_app::RunFixedMainLoopSystem::BeforeFixedMainLoop), + .in_set(bevy_app::RunFixedMainLoopSystems::BeforeFixedMainLoop), ) .add_systems(FixedFirst, clear_gizmo_context::) .add_systems(FixedLast, collect_requested_gizmos::) .add_systems( RunFixedMainLoop, end_gizmo_context:: - .in_set(bevy_app::RunFixedMainLoopSystem::AfterFixedMainLoop), + .in_set(bevy_app::RunFixedMainLoopSystems::AfterFixedMainLoop), ) .add_systems( Last, ( - propagate_gizmos::.before(UpdateGizmoMeshes), - update_gizmo_meshes::.in_set(UpdateGizmoMeshes), + propagate_gizmos::.before(GizmoMeshSystems), + update_gizmo_meshes::.in_set(GizmoMeshSystems), ), ); @@ -332,7 +336,7 @@ pub fn start_gizmo_context( /// /// Pop the default gizmos context out of the [`Swap`] gizmo storage. /// -/// This must be called before [`UpdateGizmoMeshes`] in the [`Last`] schedule. +/// This must be called before [`GizmoMeshSystems`] in the [`Last`] schedule. pub fn end_gizmo_context( mut swap: ResMut>>, mut default: ResMut>, @@ -367,7 +371,7 @@ where /// Propagate the contextual gizmo into the `Update` storage for rendering. /// -/// This should be before [`UpdateGizmoMeshes`]. +/// This should be before [`GizmoMeshSystems`]. pub fn propagate_gizmos( mut update_storage: ResMut>, contextual_storage: Res>, @@ -380,7 +384,11 @@ pub fn propagate_gizmos( /// System set for updating the rendering meshes for drawing gizmos. #[derive(SystemSet, Clone, Debug, PartialEq, Eq, Hash)] -pub struct UpdateGizmoMeshes; +pub struct GizmoMeshSystems; + +/// Deprecated alias for [`GizmoMeshSystems`]. +#[deprecated(since = "0.17.0", note = "Renamed to `GizmoMeshSystems`.")] +pub type UpdateGizmoMeshes = GizmoMeshSystems; /// Prepare gizmos for rendering. /// diff --git a/crates/bevy_gizmos/src/light.rs b/crates/bevy_gizmos/src/light.rs index 7f7dadacc2..1bd6ee3cac 100644 --- a/crates/bevy_gizmos/src/light.rs +++ b/crates/bevy_gizmos/src/light.rs @@ -24,7 +24,7 @@ use bevy_math::{ }; use bevy_pbr::{DirectionalLight, PointLight, SpotLight}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; -use bevy_transform::{components::GlobalTransform, TransformSystem}; +use bevy_transform::{components::GlobalTransform, TransformSystems}; use crate::{ config::{GizmoConfigGroup, GizmoConfigStore}, @@ -126,7 +126,7 @@ impl Plugin for LightGizmoPlugin { config.config::().1.draw_all }), ) - .after(TransformSystem::TransformPropagate), + .after(TransformSystems::Propagate), ); } } diff --git a/crates/bevy_gizmos/src/pipeline_2d.rs b/crates/bevy_gizmos/src/pipeline_2d.rs index 3a43055491..15ed1c3ab0 100644 --- a/crates/bevy_gizmos/src/pipeline_2d.rs +++ b/crates/bevy_gizmos/src/pipeline_2d.rs @@ -1,7 +1,7 @@ use crate::{ config::{GizmoLineJoint, GizmoLineStyle, GizmoMeshConfig}, line_gizmo_vertex_buffer_layouts, line_joint_gizmo_vertex_buffer_layouts, DrawLineGizmo, - DrawLineJointGizmo, GizmoRenderSystem, GpuLineGizmo, LineGizmoUniformBindgroupLayout, + DrawLineJointGizmo, GizmoRenderSystems, GpuLineGizmo, LineGizmoUniformBindgroupLayout, SetLineGizmoBindGroup, LINE_JOINT_SHADER_HANDLE, LINE_SHADER_HANDLE, }; use bevy_app::{App, Plugin}; @@ -25,7 +25,7 @@ use bevy_render::{ }, render_resource::*, view::{ExtractedView, Msaa, RenderLayers, ViewTarget}, - Render, RenderApp, RenderSet, + Render, RenderApp, RenderSystems, }; use bevy_sprite::{Mesh2dPipeline, Mesh2dPipelineKey, SetMesh2dViewBindGroup}; use tracing::error; @@ -46,8 +46,8 @@ impl Plugin for LineGizmo2dPlugin { .init_resource::>() .configure_sets( Render, - GizmoRenderSystem::QueueLineGizmos2d - .in_set(RenderSet::Queue) + GizmoRenderSystems::QueueLineGizmos2d + .in_set(RenderSystems::Queue) .ambiguous_with(bevy_sprite::queue_sprites) .ambiguous_with( bevy_sprite::queue_material2d_meshes::, @@ -56,7 +56,7 @@ impl Plugin for LineGizmo2dPlugin { .add_systems( Render, (queue_line_gizmos_2d, queue_line_joint_gizmos_2d) - .in_set(GizmoRenderSystem::QueueLineGizmos2d) + .in_set(GizmoRenderSystems::QueueLineGizmos2d) .after(prepare_assets::), ); } diff --git a/crates/bevy_gizmos/src/pipeline_3d.rs b/crates/bevy_gizmos/src/pipeline_3d.rs index 799793e6cb..87d865f8ca 100644 --- a/crates/bevy_gizmos/src/pipeline_3d.rs +++ b/crates/bevy_gizmos/src/pipeline_3d.rs @@ -1,7 +1,7 @@ use crate::{ config::{GizmoLineJoint, GizmoLineStyle, GizmoMeshConfig}, line_gizmo_vertex_buffer_layouts, line_joint_gizmo_vertex_buffer_layouts, DrawLineGizmo, - DrawLineJointGizmo, GizmoRenderSystem, GpuLineGizmo, LineGizmoUniformBindgroupLayout, + DrawLineJointGizmo, GizmoRenderSystems, GpuLineGizmo, LineGizmoUniformBindgroupLayout, SetLineGizmoBindGroup, LINE_JOINT_SHADER_HANDLE, LINE_SHADER_HANDLE, }; use bevy_app::{App, Plugin}; @@ -30,7 +30,7 @@ use bevy_render::{ }, render_resource::*, view::{ExtractedView, Msaa, RenderLayers, ViewTarget}, - Render, RenderApp, RenderSet, + Render, RenderApp, RenderSystems, }; use tracing::error; @@ -49,14 +49,14 @@ impl Plugin for LineGizmo3dPlugin { .init_resource::>() .configure_sets( Render, - GizmoRenderSystem::QueueLineGizmos3d - .in_set(RenderSet::Queue) + GizmoRenderSystems::QueueLineGizmos3d + .in_set(RenderSystems::Queue) .ambiguous_with(bevy_pbr::queue_material_meshes::), ) .add_systems( Render, (queue_line_gizmos_3d, queue_line_joint_gizmos_3d) - .in_set(GizmoRenderSystem::QueueLineGizmos3d) + .in_set(GizmoRenderSystems::QueueLineGizmos3d) .after(prepare_assets::), ); } diff --git a/crates/bevy_gltf/src/lib.rs b/crates/bevy_gltf/src/lib.rs index ebcf49744a..02c14f4197 100644 --- a/crates/bevy_gltf/src/lib.rs +++ b/crates/bevy_gltf/src/lib.rs @@ -97,11 +97,15 @@ mod vertex_attributes; extern crate alloc; +use alloc::sync::Arc; +use std::sync::Mutex; + use bevy_platform::collections::HashMap; use bevy_app::prelude::*; use bevy_asset::AssetApp; -use bevy_image::CompressedImageFormats; +use bevy_ecs::prelude::Resource; +use bevy_image::{CompressedImageFormats, ImageSamplerDescriptor}; use bevy_mesh::MeshVertexAttribute; use bevy_render::renderer::RenderDevice; @@ -115,10 +119,57 @@ pub mod prelude { pub use {assets::*, label::GltfAssetLabel, loader::*}; +// Has to store an Arc> as there is no other way to mutate fields of asset loaders. +/// Stores default [`ImageSamplerDescriptor`] in main world. +#[derive(Resource)] +pub struct DefaultGltfImageSampler(Arc>); + +impl DefaultGltfImageSampler { + /// Creates a new [`DefaultGltfImageSampler`]. + pub fn new(descriptor: &ImageSamplerDescriptor) -> Self { + Self(Arc::new(Mutex::new(descriptor.clone()))) + } + + /// Returns the current default [`ImageSamplerDescriptor`]. + pub fn get(&self) -> ImageSamplerDescriptor { + self.0.lock().unwrap().clone() + } + + /// Makes a clone of internal [`Arc`] pointer. + /// + /// Intended only to be used by code with no access to ECS. + pub fn get_internal(&self) -> Arc> { + self.0.clone() + } + + /// Replaces default [`ImageSamplerDescriptor`]. + /// + /// Doesn't apply to samplers already built on top of it, i.e. `GltfLoader`'s output. + /// Assets need to manually be reloaded. + pub fn set(&self, descriptor: &ImageSamplerDescriptor) { + *self.0.lock().unwrap() = descriptor.clone(); + } +} + /// Adds support for glTF file loading to the app. -#[derive(Default)] pub struct GltfPlugin { - custom_vertex_attributes: HashMap, MeshVertexAttribute>, + /// The default image sampler to lay glTF sampler data on top of. + /// + /// Can be modified with [`DefaultGltfImageSampler`] resource. + pub default_sampler: ImageSamplerDescriptor, + /// Registry for custom vertex attributes. + /// + /// To specify, use [`GltfPlugin::add_custom_vertex_attribute`]. + pub custom_vertex_attributes: HashMap, MeshVertexAttribute>, +} + +impl Default for GltfPlugin { + fn default() -> Self { + GltfPlugin { + default_sampler: ImageSamplerDescriptor::linear(), + custom_vertex_attributes: HashMap::default(), + } + } } impl GltfPlugin { @@ -157,9 +208,13 @@ impl Plugin for GltfPlugin { Some(render_device) => CompressedImageFormats::from_features(render_device.features()), None => CompressedImageFormats::NONE, }; + let default_sampler_resource = DefaultGltfImageSampler::new(&self.default_sampler); + let default_sampler = default_sampler_resource.get_internal(); + app.insert_resource(default_sampler_resource); app.register_asset_loader(GltfLoader { supported_compressed_formats, custom_vertex_attributes: self.custom_vertex_attributes.clone(), + default_sampler, }); } } diff --git a/crates/bevy_gltf/src/loader/gltf_ext/texture.rs b/crates/bevy_gltf/src/loader/gltf_ext/texture.rs index 5fb5bcce0d..f666752479 100644 --- a/crates/bevy_gltf/src/loader/gltf_ext/texture.rs +++ b/crates/bevy_gltf/src/loader/gltf_ext/texture.rs @@ -39,48 +39,48 @@ pub(crate) fn texture_handle( } /// Extracts the texture sampler data from the glTF [`Texture`]. -pub(crate) fn texture_sampler(texture: &Texture<'_>) -> ImageSamplerDescriptor { +pub(crate) fn texture_sampler( + texture: &Texture<'_>, + default_sampler: &ImageSamplerDescriptor, +) -> ImageSamplerDescriptor { let gltf_sampler = texture.sampler(); + let mut sampler = default_sampler.clone(); - ImageSamplerDescriptor { - address_mode_u: address_mode(&gltf_sampler.wrap_s()), - address_mode_v: address_mode(&gltf_sampler.wrap_t()), + sampler.address_mode_u = address_mode(&gltf_sampler.wrap_s()); + sampler.address_mode_v = address_mode(&gltf_sampler.wrap_t()); - mag_filter: gltf_sampler - .mag_filter() - .map(|mf| match mf { - MagFilter::Nearest => ImageFilterMode::Nearest, - MagFilter::Linear => ImageFilterMode::Linear, - }) - .unwrap_or(ImageSamplerDescriptor::default().mag_filter), - - min_filter: gltf_sampler - .min_filter() - .map(|mf| match mf { - MinFilter::Nearest - | MinFilter::NearestMipmapNearest - | MinFilter::NearestMipmapLinear => ImageFilterMode::Nearest, - MinFilter::Linear - | MinFilter::LinearMipmapNearest - | MinFilter::LinearMipmapLinear => ImageFilterMode::Linear, - }) - .unwrap_or(ImageSamplerDescriptor::default().min_filter), - - mipmap_filter: gltf_sampler - .min_filter() - .map(|mf| match mf { - MinFilter::Nearest - | MinFilter::Linear - | MinFilter::NearestMipmapNearest - | MinFilter::LinearMipmapNearest => ImageFilterMode::Nearest, - MinFilter::NearestMipmapLinear | MinFilter::LinearMipmapLinear => { - ImageFilterMode::Linear - } - }) - .unwrap_or(ImageSamplerDescriptor::default().mipmap_filter), - - ..Default::default() + // Shouldn't parse filters when anisotropic filtering is on, because trilinear is then required by wgpu. + // We also trust user to have provided a valid sampler. + if sampler.anisotropy_clamp != 1 { + if let Some(mag_filter) = gltf_sampler.mag_filter().map(|mf| match mf { + MagFilter::Nearest => ImageFilterMode::Nearest, + MagFilter::Linear => ImageFilterMode::Linear, + }) { + sampler.mag_filter = mag_filter; + } + if let Some(min_filter) = gltf_sampler.min_filter().map(|mf| match mf { + MinFilter::Nearest + | MinFilter::NearestMipmapNearest + | MinFilter::NearestMipmapLinear => ImageFilterMode::Nearest, + MinFilter::Linear | MinFilter::LinearMipmapNearest | MinFilter::LinearMipmapLinear => { + ImageFilterMode::Linear + } + }) { + sampler.min_filter = min_filter; + } + if let Some(mipmap_filter) = gltf_sampler.min_filter().map(|mf| match mf { + MinFilter::Nearest + | MinFilter::Linear + | MinFilter::NearestMipmapNearest + | MinFilter::LinearMipmapNearest => ImageFilterMode::Nearest, + MinFilter::NearestMipmapLinear | MinFilter::LinearMipmapLinear => { + ImageFilterMode::Linear + } + }) { + sampler.mipmap_filter = mipmap_filter; + } } + sampler } pub(crate) fn texture_label(texture: &Texture<'_>) -> GltfAssetLabel { diff --git a/crates/bevy_gltf/src/loader/mod.rs b/crates/bevy_gltf/src/loader/mod.rs index a4e25475b7..f85a739b2e 100644 --- a/crates/bevy_gltf/src/loader/mod.rs +++ b/crates/bevy_gltf/src/loader/mod.rs @@ -1,9 +1,11 @@ mod extensions; mod gltf_ext; +use alloc::sync::Arc; use std::{ io::Error, path::{Path, PathBuf}, + sync::Mutex, }; #[cfg(feature = "bevy_animation")] @@ -146,6 +148,8 @@ pub struct GltfLoader { /// See [this section of the glTF specification](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#meshes-overview) /// for additional details on custom attributes. pub custom_vertex_attributes: HashMap, MeshVertexAttribute>, + /// Arc to default [`ImageSamplerDescriptor`]. + pub default_sampler: Arc>, } /// Specifies optional settings for processing gltfs at load time. By default, all recognized contents of @@ -181,6 +185,12 @@ pub struct GltfLoaderSettings { pub load_lights: bool, /// If true, the loader will include the root of the gltf root node. pub include_source: bool, + /// Overrides the default sampler. Data from sampler node is added on top of that. + /// + /// If None, uses global default which is stored in `DefaultGltfImageSampler` resource. + pub default_sampler: Option, + /// If true, the loader will ignore sampler data from gltf and use the default sampler. + pub override_sampler: bool, } impl Default for GltfLoaderSettings { @@ -191,6 +201,8 @@ impl Default for GltfLoaderSettings { load_cameras: true, load_lights: true, include_source: false, + default_sampler: None, + override_sampler: false, } } } @@ -506,6 +518,10 @@ async fn load_gltf<'a, 'b, 'c>( (animations, named_animations, animation_roots) }; + let default_sampler = match settings.default_sampler.as_ref() { + Some(sampler) => sampler, + None => &loader.default_sampler.lock().unwrap().clone(), + }; // We collect handles to ensure loaded images from paths are not unloaded before they are used elsewhere // in the loader. This prevents "reloads", but it also prevents dropping the is_srgb context on reload. // @@ -522,7 +538,8 @@ async fn load_gltf<'a, 'b, 'c>( &linear_textures, parent_path, loader.supported_compressed_formats, - settings.load_materials, + default_sampler, + settings, ) .await?; image.process_loaded_texture(load_context, &mut _texture_handles); @@ -542,7 +559,8 @@ async fn load_gltf<'a, 'b, 'c>( linear_textures, parent_path, loader.supported_compressed_formats, - settings.load_materials, + default_sampler, + settings, ) .await }); @@ -958,10 +976,15 @@ async fn load_image<'a, 'b>( linear_textures: &HashSet, parent_path: &'b Path, supported_compressed_formats: CompressedImageFormats, - render_asset_usages: RenderAssetUsages, + default_sampler: &ImageSamplerDescriptor, + settings: &GltfLoaderSettings, ) -> Result { let is_srgb = !linear_textures.contains(&gltf_texture.index()); - let sampler_descriptor = texture_sampler(&gltf_texture); + let sampler_descriptor = if settings.override_sampler { + default_sampler.clone() + } else { + texture_sampler(&gltf_texture, default_sampler) + }; match gltf_texture.source().source() { Source::View { view, mime_type } => { @@ -974,7 +997,7 @@ async fn load_image<'a, 'b>( supported_compressed_formats, is_srgb, ImageSampler::Descriptor(sampler_descriptor), - render_asset_usages, + settings.load_materials, )?; Ok(ImageOrPath::Image { image, @@ -996,7 +1019,7 @@ async fn load_image<'a, 'b>( supported_compressed_formats, is_srgb, ImageSampler::Descriptor(sampler_descriptor), - render_asset_usages, + settings.load_materials, )?, label: GltfAssetLabel::Texture(gltf_texture.index()), }) diff --git a/crates/bevy_image/Cargo.toml b/crates/bevy_image/Cargo.toml index a90a3abb80..988325c707 100644 --- a/crates/bevy_image/Cargo.toml +++ b/crates/bevy_image/Cargo.toml @@ -66,7 +66,7 @@ futures-lite = "2.0.1" guillotiere = "0.6.0" rectangle-pack = "0.4" ddsfile = { version = "0.5.2", optional = true } -ktx2 = { version = "0.3.0", optional = true } +ktx2 = { version = "0.4.0", optional = true } # For ktx2 supercompression flate2 = { version = "1.0.22", optional = true } ruzstd = { version = "0.8.0", optional = true } diff --git a/crates/bevy_image/src/image.rs b/crates/bevy_image/src/image.rs index 41b698b78d..5260c70bfc 100644 --- a/crates/bevy_image/src/image.rs +++ b/crates/bevy_image/src/image.rs @@ -344,10 +344,10 @@ impl ImageFormat { pub struct Image { /// Raw pixel data. /// If the image is being used as a storage texture which doesn't need to be initialized by the - /// CPU, then this should be `None` - /// Otherwise, it should always be `Some` + /// CPU, then this should be `None`. + /// Otherwise, it should always be `Some`. pub data: Option>, - // TODO: this nesting makes accessing Image metadata verbose. Either flatten out descriptor or add accessors + // TODO: this nesting makes accessing Image metadata verbose. Either flatten out descriptor or add accessors. pub texture_descriptor: TextureDescriptor, &'static [TextureFormat]>, /// The [`ImageSampler`] to use during rendering. pub sampler: ImageSampler, diff --git a/crates/bevy_image/src/ktx2.rs b/crates/bevy_image/src/ktx2.rs index bffea83d10..0cccbacb07 100644 --- a/crates/bevy_image/src/ktx2.rs +++ b/crates/bevy_image/src/ktx2.rs @@ -10,8 +10,8 @@ use bevy_utils::default; #[cfg(any(feature = "flate2", feature = "ruzstd"))] use ktx2::SupercompressionScheme; use ktx2::{ - BasicDataFormatDescriptor, ChannelTypeQualifiers, ColorModel, DataFormatDescriptorHeader, - Header, SampleInformation, + ChannelTypeQualifiers, ColorModel, DfdBlockBasic, DfdBlockHeaderBasic, DfdHeader, Header, + SampleInformation, }; use wgpu_types::{ AstcBlock, AstcChannel, Extent3d, TextureDimension, TextureFormat, TextureViewDescriptor, @@ -45,28 +45,28 @@ pub fn ktx2_buffer_to_image( // Handle supercompression let mut levels = Vec::new(); if let Some(supercompression_scheme) = supercompression_scheme { - for (_level, _level_data) in ktx2.levels().enumerate() { + for (level_index, level) in ktx2.levels().enumerate() { match supercompression_scheme { #[cfg(feature = "flate2")] SupercompressionScheme::ZLIB => { - let mut decoder = flate2::bufread::ZlibDecoder::new(_level_data); + let mut decoder = flate2::bufread::ZlibDecoder::new(level.data); let mut decompressed = Vec::new(); decoder.read_to_end(&mut decompressed).map_err(|err| { TextureError::SuperDecompressionError(format!( - "Failed to decompress {supercompression_scheme:?} for mip {_level}: {err:?}", + "Failed to decompress {supercompression_scheme:?} for mip {level_index}: {err:?}", )) })?; levels.push(decompressed); } #[cfg(feature = "ruzstd")] SupercompressionScheme::Zstandard => { - let mut cursor = std::io::Cursor::new(_level_data); + let mut cursor = std::io::Cursor::new(level.data); let mut decoder = ruzstd::decoding::StreamingDecoder::new(&mut cursor) .map_err(|err| TextureError::SuperDecompressionError(err.to_string()))?; let mut decompressed = Vec::new(); decoder.read_to_end(&mut decompressed).map_err(|err| { TextureError::SuperDecompressionError(format!( - "Failed to decompress {supercompression_scheme:?} for mip {_level}: {err:?}", + "Failed to decompress {supercompression_scheme:?} for mip {level_index}: {err:?}", )) })?; levels.push(decompressed); @@ -79,7 +79,7 @@ pub fn ktx2_buffer_to_image( } } } else { - levels = ktx2.levels().map(<[u8]>::to_vec).collect(); + levels = ktx2.levels().map(|level| level.data.to_vec()).collect(); } // Identify the format @@ -397,16 +397,15 @@ pub fn ktx2_get_texture_format>( return ktx2_format_to_texture_format(format, is_srgb); } - for data_format_descriptor in ktx2.data_format_descriptors() { - if data_format_descriptor.header == DataFormatDescriptorHeader::BASIC { - let basic_data_format_descriptor = - BasicDataFormatDescriptor::parse(data_format_descriptor.data) - .map_err(|err| TextureError::InvalidData(format!("KTX2: {err:?}")))?; + for data_format_descriptor in ktx2.dfd_blocks() { + if data_format_descriptor.header == DfdHeader::BASIC { + let basic_data_format_descriptor = DfdBlockBasic::parse(data_format_descriptor.data) + .map_err(|err| TextureError::InvalidData(format!("KTX2: {err:?}")))?; let sample_information = basic_data_format_descriptor .sample_information() .collect::>(); - return ktx2_dfd_to_texture_format( - &basic_data_format_descriptor, + return ktx2_dfd_header_to_texture_format( + &basic_data_format_descriptor.header, &sample_information, is_srgb, ); @@ -476,8 +475,8 @@ fn sample_information_to_data_type( } #[cfg(feature = "ktx2")] -pub fn ktx2_dfd_to_texture_format( - data_format_descriptor: &BasicDataFormatDescriptor, +pub fn ktx2_dfd_header_to_texture_format( + data_format_descriptor: &DfdBlockHeaderBasic, sample_information: &[SampleInformation], is_srgb: bool, ) -> Result { @@ -495,7 +494,7 @@ pub fn ktx2_dfd_to_texture_format( let sample = &sample_information[0]; let data_type = sample_information_to_data_type(sample, false)?; - match sample.bit_length { + match sample.bit_length.get() { 8 => match data_type { DataType::Unorm => TextureFormat::R8Unorm, DataType::UnormSrgb => { @@ -577,7 +576,7 @@ pub fn ktx2_dfd_to_texture_format( let sample = &sample_information[0]; let data_type = sample_information_to_data_type(sample, false)?; - match sample.bit_length { + match sample.bit_length.get() { 8 => match data_type { DataType::Unorm => TextureFormat::Rg8Unorm, DataType::UnormSrgb => { @@ -635,27 +634,27 @@ pub fn ktx2_dfd_to_texture_format( } 3 => { if sample_information[0].channel_type == 0 - && sample_information[0].bit_length == 11 + && sample_information[0].bit_length.get() == 11 && sample_information[1].channel_type == 1 - && sample_information[1].bit_length == 11 + && sample_information[1].bit_length.get() == 11 && sample_information[2].channel_type == 2 - && sample_information[2].bit_length == 10 + && sample_information[2].bit_length.get() == 10 { TextureFormat::Rg11b10Ufloat } else if sample_information[0].channel_type == 0 - && sample_information[0].bit_length == 9 + && sample_information[0].bit_length.get() == 9 && sample_information[1].channel_type == 1 - && sample_information[1].bit_length == 9 + && sample_information[1].bit_length.get() == 9 && sample_information[2].channel_type == 2 - && sample_information[2].bit_length == 9 + && sample_information[2].bit_length.get() == 9 { TextureFormat::Rgb9e5Ufloat } else if sample_information[0].channel_type == 0 - && sample_information[0].bit_length == 8 + && sample_information[0].bit_length.get() == 8 && sample_information[1].channel_type == 1 - && sample_information[1].bit_length == 8 + && sample_information[1].bit_length.get() == 8 && sample_information[2].channel_type == 2 - && sample_information[2].bit_length == 8 + && sample_information[2].bit_length.get() == 8 { return Err(TextureError::FormatRequiresTranscodingError( TranscodeFormat::Rgb8, @@ -681,10 +680,10 @@ pub fn ktx2_dfd_to_texture_format( assert_eq!(sample_information[3].channel_type, 15); // Handle one special packed format - if sample_information[0].bit_length == 10 - && sample_information[1].bit_length == 10 - && sample_information[2].bit_length == 10 - && sample_information[3].bit_length == 2 + if sample_information[0].bit_length.get() == 10 + && sample_information[1].bit_length.get() == 10 + && sample_information[2].bit_length.get() == 10 + && sample_information[3].bit_length.get() == 2 { return Ok(TextureFormat::Rgb10a2Unorm); } @@ -708,7 +707,7 @@ pub fn ktx2_dfd_to_texture_format( let sample = &sample_information[0]; let data_type = sample_information_to_data_type(sample, is_srgb)?; - match sample.bit_length { + match sample.bit_length.get() { 8 => match data_type { DataType::Unorm => { if is_rgba { @@ -896,7 +895,7 @@ pub fn ktx2_dfd_to_texture_format( Some(ColorModel::XYZW) => { // Same number of channels in both texel block dimensions and sample info descriptions assert_eq!( - data_format_descriptor.texel_block_dimensions[0] as usize, + data_format_descriptor.texel_block_dimensions[0].get() as usize, sample_information.len() ); match sample_information.len() { @@ -935,7 +934,7 @@ pub fn ktx2_dfd_to_texture_format( let sample = &sample_information[0]; let data_type = sample_information_to_data_type(sample, false)?; - match sample.bit_length { + match sample.bit_length.get() { 8 => match data_type { DataType::Unorm => TextureFormat::Rgba8Unorm, DataType::UnormSrgb => { @@ -1124,8 +1123,8 @@ pub fn ktx2_dfd_to_texture_format( }, Some(ColorModel::ASTC) => TextureFormat::Astc { block: match ( - data_format_descriptor.texel_block_dimensions[0], - data_format_descriptor.texel_block_dimensions[1], + data_format_descriptor.texel_block_dimensions[0].get(), + data_format_descriptor.texel_block_dimensions[1].get(), ) { (4, 4) => AstcBlock::B4x4, (5, 4) => AstcBlock::B5x4, diff --git a/crates/bevy_image/src/texture_atlas.rs b/crates/bevy_image/src/texture_atlas.rs index b5b68b0c41..4caeed8c07 100644 --- a/crates/bevy_image/src/texture_atlas.rs +++ b/crates/bevy_image/src/texture_atlas.rs @@ -222,6 +222,18 @@ impl TextureAtlas { let atlas = texture_atlases.get(&self.layout)?; atlas.textures.get(self.index).copied() } + + /// Returns this [`TextureAtlas`] with the specified index. + pub fn with_index(mut self, index: usize) -> Self { + self.index = index; + self + } + + /// Returns this [`TextureAtlas`] with the specified [`TextureAtlasLayout`] handle. + pub fn with_layout(mut self, layout: Handle) -> Self { + self.layout = layout; + self + } } impl From> for TextureAtlas { diff --git a/crates/bevy_input/src/lib.rs b/crates/bevy_input/src/lib.rs index e1119c3d35..67c8995179 100644 --- a/crates/bevy_input/src/lib.rs +++ b/crates/bevy_input/src/lib.rs @@ -76,7 +76,11 @@ pub struct InputPlugin; /// Label for systems that update the input data. #[derive(Debug, PartialEq, Eq, Clone, Hash, SystemSet)] -pub struct InputSystem; +pub struct InputSystems; + +/// Deprecated alias for [`InputSystems`]. +#[deprecated(since = "0.17.0", note = "Renamed to `InputSystems`.")] +pub type InputSystem = InputSystems; impl Plugin for InputPlugin { fn build(&self, app: &mut App) { @@ -85,7 +89,7 @@ impl Plugin for InputPlugin { .add_event::() .add_event::() .init_resource::>() - .add_systems(PreUpdate, keyboard_input_system.in_set(InputSystem)) + .add_systems(PreUpdate, keyboard_input_system.in_set(InputSystems)) // mouse .add_event::() .add_event::() @@ -98,7 +102,7 @@ impl Plugin for InputPlugin { accumulate_mouse_motion_system, accumulate_mouse_scroll_system, ) - .in_set(InputSystem), + .in_set(InputSystems), ) .add_event::() .add_event::() @@ -122,12 +126,12 @@ impl Plugin for InputPlugin { gamepad_connection_system, gamepad_event_processing_system.after(gamepad_connection_system), ) - .in_set(InputSystem), + .in_set(InputSystems), ) // touch .add_event::() .init_resource::() - .add_systems(PreUpdate, touch_screen_input_system.in_set(InputSystem)); + .add_systems(PreUpdate, touch_screen_input_system.in_set(InputSystems)); #[cfg(feature = "bevy_reflect")] { diff --git a/crates/bevy_input_focus/src/lib.rs b/crates/bevy_input_focus/src/lib.rs index 3f7ecf9e7c..44ff0ef645 100644 --- a/crates/bevy_input_focus/src/lib.rs +++ b/crates/bevy_input_focus/src/lib.rs @@ -136,7 +136,7 @@ pub struct InputFocusVisible(pub bool); /// If no entity has input focus, then the event is dispatched to the main window. /// /// To set up your own bubbling input event, add the [`dispatch_focused_input::`](dispatch_focused_input) system to your app, -/// in the [`InputFocusSet::Dispatch`] system set during [`PreUpdate`]. +/// in the [`InputFocusSystems::Dispatch`] system set during [`PreUpdate`]. #[derive(Clone, Debug, Component)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component, Clone))] pub struct FocusedInput { @@ -195,7 +195,7 @@ impl Plugin for InputDispatchPlugin { dispatch_focused_input::, dispatch_focused_input::, ) - .in_set(InputFocusSet::Dispatch), + .in_set(InputFocusSystems::Dispatch), ); #[cfg(feature = "bevy_reflect")] @@ -209,11 +209,15 @@ impl Plugin for InputDispatchPlugin { /// /// These systems run in the [`PreUpdate`] schedule. #[derive(SystemSet, Debug, PartialEq, Eq, Hash, Clone)] -pub enum InputFocusSet { +pub enum InputFocusSystems { /// System which dispatches bubbled input events to the focused entity, or to the primary window. Dispatch, } +/// Deprecated alias for [`InputFocusSystems`]. +#[deprecated(since = "0.17.0", note = "Renamed to `InputFocusSystems`.")] +pub type InputFocusSet = InputFocusSystems; + /// Sets the initial focus to the primary window, if any. pub fn set_initial_focus( mut input_focus: ResMut, diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 28d234f2b4..38f89d4ef8 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -279,9 +279,6 @@ custom_cursor = ["bevy_winit/custom_cursor"] # Experimental support for nodes that are ignored for UI layouting ghost_nodes = ["bevy_ui/ghost_nodes"] -# Use the configurable global error handler as the default error handler. -configurable_error_handler = ["bevy_ecs/configurable_error_handler"] - # Allows access to the `std` crate. Enabling this feature will prevent compilation # on `no_std` targets, but provides access to certain additional features on # supported platforms. diff --git a/crates/bevy_log/src/lib.rs b/crates/bevy_log/src/lib.rs index 055395bad7..6562fef7fa 100644 --- a/crates/bevy_log/src/lib.rs +++ b/crates/bevy_log/src/lib.rs @@ -56,6 +56,7 @@ use bevy_app::{App, Plugin}; use tracing_log::LogTracer; use tracing_subscriber::{ filter::{FromEnvError, ParseError}, + layer::Layered, prelude::*, registry::Registry, EnvFilter, Layer, @@ -97,6 +98,7 @@ pub(crate) struct FlushGuard(SyncCell); /// level: Level::DEBUG, /// filter: "wgpu=error,bevy_render=info,bevy_ecs=trace".to_string(), /// custom_layer: |_| None, +/// fmt_layer: |_| None, /// })) /// .run(); /// } @@ -240,11 +242,38 @@ pub struct LogPlugin { /// /// Please see the `examples/log_layers.rs` for a complete example. pub custom_layer: fn(app: &mut App) -> Option, + + /// Override the default [`tracing_subscriber::fmt::Layer`] with a custom one. + /// + /// This differs from [`custom_layer`](Self::custom_layer) in that + /// [`fmt_layer`](Self::fmt_layer) allows you to overwrite the default formatter layer, while + /// `custom_layer` only allows you to add additional layers (which are unable to modify the + /// default formatter). + /// + /// For example, you can use [`tracing_subscriber::fmt::Layer::without_time`] to remove the + /// timestamp from the log output. + /// + /// Please see the `examples/log_layers.rs` for a complete example. + pub fmt_layer: fn(app: &mut App) -> Option, } -/// A boxed [`Layer`] that can be used with [`LogPlugin`]. +/// A boxed [`Layer`] that can be used with [`LogPlugin::custom_layer`]. pub type BoxedLayer = Box + Send + Sync + 'static>; +#[cfg(feature = "trace")] +type BaseSubscriber = + Layered + Send + Sync>>, Registry>>; + +#[cfg(feature = "trace")] +type PreFmtSubscriber = Layered, BaseSubscriber>; + +#[cfg(not(feature = "trace"))] +type PreFmtSubscriber = + Layered + Send + Sync>>, Registry>>; + +/// A boxed [`Layer`] that can be used with [`LogPlugin::fmt_layer`]. +pub type BoxedFmtLayer = Box + Send + Sync + 'static>; + /// The default [`LogPlugin`] [`EnvFilter`]. pub const DEFAULT_FILTER: &str = "wgpu=error,naga=warn"; @@ -254,6 +283,7 @@ impl Default for LogPlugin { filter: DEFAULT_FILTER.to_string(), level: Level::INFO, custom_layer: |_| None, + fmt_layer: |_| None, } } } @@ -328,10 +358,12 @@ impl Plugin for LogPlugin { #[cfg(feature = "tracing-tracy")] let tracy_layer = tracing_tracy::TracyLayer::default(); - // note: the implementation of `Default` reads from the env var NO_COLOR - // to decide whether to use ANSI color codes, which is common convention - // https://no-color.org/ - let fmt_layer = tracing_subscriber::fmt::Layer::default().with_writer(std::io::stderr); + let fmt_layer = (self.fmt_layer)(app).unwrap_or_else(|| { + // note: the implementation of `Default` reads from the env var NO_COLOR + // to decide whether to use ANSI color codes, which is common convention + // https://no-color.org/ + Box::new(tracing_subscriber::fmt::Layer::default().with_writer(std::io::stderr)) + }); // bevy_render::renderer logs a `tracy.frame_mark` event every frame // at Level::INFO. Formatted logs should omit it. diff --git a/crates/bevy_macro_utils/src/bevy_manifest.rs b/crates/bevy_macro_utils/src/bevy_manifest.rs index 8d32781069..b6df0e0e0f 100644 --- a/crates/bevy_macro_utils/src/bevy_manifest.rs +++ b/crates/bevy_macro_utils/src/bevy_manifest.rs @@ -95,7 +95,7 @@ impl BevyManifest { return None; }; - let mut path = Self::parse_str::(&format!("::{}", package)); + let mut path = Self::parse_str::(&format!("::{package}")); if let Some(module) = name.strip_prefix("bevy_") { path.segments.push(Self::parse_str(module)); } diff --git a/crates/bevy_math/src/bounding/bounded2d/mod.rs b/crates/bevy_math/src/bounding/bounded2d/mod.rs index bea18f5808..5f11ad5233 100644 --- a/crates/bevy_math/src/bounding/bounded2d/mod.rs +++ b/crates/bevy_math/src/bounding/bounded2d/mod.rs @@ -243,13 +243,9 @@ impl BoundingVolume for Aabb2d { /// and consider storing the original AABB and rotating that every time instead. #[inline(always)] fn rotate_by(&mut self, rotation: impl Into) { - let rotation: Rot2 = rotation.into(); - let abs_rot_mat = Mat2::from_cols( - Vec2::new(rotation.cos, rotation.sin), - Vec2::new(rotation.sin, rotation.cos), - ); - let half_size = abs_rot_mat * self.half_size(); - *self = Self::new(rotation * self.center(), half_size); + let rot_mat = Mat2::from(rotation.into()); + let half_size = rot_mat.abs() * self.half_size(); + *self = Self::new(rot_mat * self.center(), half_size); } } @@ -274,6 +270,8 @@ impl IntersectsVolume for Aabb2d { #[cfg(test)] mod aabb2d_tests { + use approx::assert_relative_eq; + use super::Aabb2d; use crate::{ bounding::{BoundingCircle, BoundingVolume, IntersectsVolume}, @@ -394,6 +392,17 @@ mod aabb2d_tests { assert!(scaled.contains(&a)); } + #[test] + fn rotate() { + let a = Aabb2d { + min: Vec2::new(-2.0, -2.0), + max: Vec2::new(2.0, 2.0), + }; + let rotated = a.rotated_by(core::f32::consts::PI); + assert_relative_eq!(rotated.min, a.min); + assert_relative_eq!(rotated.max, a.max); + } + #[test] fn transform() { let a = Aabb2d { diff --git a/crates/bevy_math/src/bounding/bounded3d/mod.rs b/crates/bevy_math/src/bounding/bounded3d/mod.rs index 5a95b7711f..ca3b359798 100644 --- a/crates/bevy_math/src/bounding/bounded3d/mod.rs +++ b/crates/bevy_math/src/bounding/bounded3d/mod.rs @@ -250,12 +250,7 @@ impl BoundingVolume for Aabb3d { #[inline(always)] fn rotate_by(&mut self, rotation: impl Into) { let rot_mat = Mat3::from_quat(rotation.into()); - let abs_rot_mat = Mat3::from_cols( - rot_mat.x_axis.abs(), - rot_mat.y_axis.abs(), - rot_mat.z_axis.abs(), - ); - let half_size = abs_rot_mat * self.half_size(); + let half_size = rot_mat.abs() * self.half_size(); *self = Self::new(rot_mat * self.center(), half_size); } } @@ -279,6 +274,8 @@ impl IntersectsVolume for Aabb3d { #[cfg(test)] mod aabb3d_tests { + use approx::assert_relative_eq; + use super::Aabb3d; use crate::{ bounding::{BoundingSphere, BoundingVolume, IntersectsVolume}, @@ -398,6 +395,19 @@ mod aabb3d_tests { assert!(scaled.contains(&a)); } + #[test] + fn rotate() { + use core::f32::consts::PI; + let a = Aabb3d { + min: Vec3A::new(-2.0, -2.0, -2.0), + max: Vec3A::new(2.0, 2.0, 2.0), + }; + let rotation = Quat::from_euler(glam::EulerRot::XYZ, PI, PI, 0.0); + let rotated = a.rotated_by(rotation); + assert_relative_eq!(rotated.min, a.min); + assert_relative_eq!(rotated.max, a.max); + } + #[test] fn transform() { let a = Aabb3d { diff --git a/crates/bevy_math/src/primitives/dim2.rs b/crates/bevy_math/src/primitives/dim2.rs index 613345bcd8..d666849840 100644 --- a/crates/bevy_math/src/primitives/dim2.rs +++ b/crates/bevy_math/src/primitives/dim2.rs @@ -1243,13 +1243,6 @@ impl Segment2d { } } - /// Create a new `Segment2d` from its endpoints and compute its geometric center. - #[inline(always)] - #[deprecated(since = "0.16.0", note = "Use the `new` constructor instead")] - pub fn from_points(point1: Vec2, point2: Vec2) -> (Self, Vec2) { - (Self::new(point1, point2), (point1 + point2) / 2.) - } - /// Create a new `Segment2d` centered at the origin with the given direction and length. /// /// The endpoints will be at `-direction * length / 2.0` and `direction * length / 2.0`. diff --git a/crates/bevy_math/src/primitives/dim3.rs b/crates/bevy_math/src/primitives/dim3.rs index a36db0ade5..ea5ccd6e2d 100644 --- a/crates/bevy_math/src/primitives/dim3.rs +++ b/crates/bevy_math/src/primitives/dim3.rs @@ -381,13 +381,6 @@ impl Segment3d { } } - /// Create a new `Segment3d` from its endpoints and compute its geometric center. - #[inline(always)] - #[deprecated(since = "0.16.0", note = "Use the `new` constructor instead")] - pub fn from_points(point1: Vec3, point2: Vec3) -> (Self, Vec3) { - (Self::new(point1, point2), (point1 + point2) / 2.) - } - /// Create a new `Segment3d` centered at the origin with the given direction and length. /// /// The endpoints will be at `-direction * length / 2.0` and `direction * length / 2.0`. diff --git a/crates/bevy_mikktspace/src/generated.rs b/crates/bevy_mikktspace/src/generated.rs index a726eb5bc8..b246b9668d 100644 --- a/crates/bevy_mikktspace/src/generated.rs +++ b/crates/bevy_mikktspace/src/generated.rs @@ -756,7 +756,7 @@ unsafe fn CompareSubGroups(mut pg1: *const SSubGroup, mut pg2: *const SSubGroup) return false; } while i < (*pg1).iNrFaces as usize && bStillSame { - bStillSame = if (*pg1).pTriMembers[i] == (*pg2).pTriMembers[i] { + bStillSame = if (&(*pg1).pTriMembers)[i] == (&(*pg2).pTriMembers)[i] { true } else { false diff --git a/crates/bevy_pbr/src/atmosphere/mod.rs b/crates/bevy_pbr/src/atmosphere/mod.rs index e7f17f0e1e..773852499e 100644 --- a/crates/bevy_pbr/src/atmosphere/mod.rs +++ b/crates/bevy_pbr/src/atmosphere/mod.rs @@ -56,7 +56,7 @@ use bevy_render::{ render_graph::{RenderGraphApp, ViewNodeRunner}, render_resource::{Shader, TextureFormat, TextureUsages}, renderer::RenderAdapter, - Render, RenderApp, RenderSet, + Render, RenderApp, RenderSystems, }; use bevy_core_pipeline::core_3d::{graph::Core3d, Camera3d}; @@ -190,11 +190,11 @@ impl Plugin for AtmospherePlugin { .add_systems( Render, ( - configure_camera_depth_usages.in_set(RenderSet::ManageViews), - queue_render_sky_pipelines.in_set(RenderSet::Queue), - prepare_atmosphere_textures.in_set(RenderSet::PrepareResources), - prepare_atmosphere_transforms.in_set(RenderSet::PrepareResources), - prepare_atmosphere_bind_groups.in_set(RenderSet::PrepareBindGroups), + configure_camera_depth_usages.in_set(RenderSystems::ManageViews), + queue_render_sky_pipelines.in_set(RenderSystems::Queue), + prepare_atmosphere_textures.in_set(RenderSystems::PrepareResources), + prepare_atmosphere_transforms.in_set(RenderSystems::PrepareResources), + prepare_atmosphere_bind_groups.in_set(RenderSystems::PrepareBindGroups), ), ) .add_render_graph_node::>( diff --git a/crates/bevy_pbr/src/decal/clustered.rs b/crates/bevy_pbr/src/decal/clustered.rs index 5272bce80c..cadcd1b871 100644 --- a/crates/bevy_pbr/src/decal/clustered.rs +++ b/crates/bevy_pbr/src/decal/clustered.rs @@ -43,7 +43,7 @@ use bevy_render::{ sync_world::RenderEntity, texture::{FallbackImage, GpuImage}, view::{self, ViewVisibility, Visibility, VisibilityClass}, - Extract, ExtractSchedule, Render, RenderApp, RenderSet, + Extract, ExtractSchedule, Render, RenderApp, RenderSystems, }; use bevy_transform::{components::GlobalTransform, prelude::Transform}; use bytemuck::{Pod, Zeroable}; @@ -173,10 +173,13 @@ impl Plugin for ClusteredDecalPlugin { .add_systems( Render, prepare_decals - .in_set(RenderSet::ManageViews) + .in_set(RenderSystems::ManageViews) .after(prepare_lights), ) - .add_systems(Render, upload_decals.in_set(RenderSet::PrepareResources)); + .add_systems( + Render, + upload_decals.in_set(RenderSystems::PrepareResources), + ); } } diff --git a/crates/bevy_pbr/src/deferred/mod.rs b/crates/bevy_pbr/src/deferred/mod.rs index e40b3a940a..65be474e65 100644 --- a/crates/bevy_pbr/src/deferred/mod.rs +++ b/crates/bevy_pbr/src/deferred/mod.rs @@ -10,7 +10,7 @@ use crate::{ ViewLightsUniformOffset, }; use bevy_app::prelude::*; -use bevy_asset::{load_internal_asset, weak_handle, Handle}; +use bevy_asset::{embedded_asset, load_embedded_asset, Handle}; use bevy_core_pipeline::{ core_3d::graph::{Core3d, Node3d}, deferred::{ @@ -29,14 +29,11 @@ use bevy_render::{ render_resource::{binding_types::uniform_buffer, *}, renderer::{RenderContext, RenderDevice}, view::{ExtractedView, ViewTarget, ViewUniformOffset}, - Render, RenderApp, RenderSet, + Render, RenderApp, RenderSystems, }; pub struct DeferredPbrLightingPlugin; -pub const DEFERRED_LIGHTING_SHADER_HANDLE: Handle = - weak_handle!("f4295279-8890-4748-b654-ca4d2183df1c"); - pub const DEFAULT_PBR_DEFERRED_LIGHTING_PASS_ID: u8 = 1; /// Component with a `depth_id` for specifying which corresponding materials should be rendered by this specific PBR deferred lighting pass. @@ -100,12 +97,7 @@ impl Plugin for DeferredPbrLightingPlugin { )) .add_systems(PostUpdate, insert_deferred_lighting_pass_id_component); - load_internal_asset!( - app, - DEFERRED_LIGHTING_SHADER_HANDLE, - "deferred_lighting.wgsl", - Shader::from_wgsl - ); + embedded_asset!(app, "deferred_lighting.wgsl"); let Some(render_app) = app.get_sub_app_mut(RenderApp) else { return; @@ -115,7 +107,7 @@ impl Plugin for DeferredPbrLightingPlugin { .init_resource::>() .add_systems( Render, - (prepare_deferred_lighting_pipelines.in_set(RenderSet::Prepare),), + (prepare_deferred_lighting_pipelines.in_set(RenderSystems::Prepare),), ) .add_render_graph_node::>( Core3d, @@ -237,6 +229,7 @@ impl ViewNode for DeferredOpaquePass3dPbrLightingNode { pub struct DeferredLightingLayout { mesh_pipeline: MeshPipeline, bind_group_layout_1: BindGroupLayout, + deferred_lighting_shader: Handle, } #[derive(Component)] @@ -360,13 +353,13 @@ impl SpecializedRenderPipeline for DeferredLightingLayout { self.bind_group_layout_1.clone(), ], vertex: VertexState { - shader: DEFERRED_LIGHTING_SHADER_HANDLE, + shader: self.deferred_lighting_shader.clone(), shader_defs: shader_defs.clone(), entry_point: "vertex".into(), buffers: Vec::new(), }, fragment: Some(FragmentState { - shader: DEFERRED_LIGHTING_SHADER_HANDLE, + shader: self.deferred_lighting_shader.clone(), shader_defs, entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { @@ -416,6 +409,7 @@ impl FromWorld for DeferredLightingLayout { Self { mesh_pipeline: world.resource::().clone(), bind_group_layout_1: layout, + deferred_lighting_shader: load_embedded_asset!(world, "deferred_lighting.wgsl"), } } } diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index 1810bc67eb..12785f3e78 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -124,58 +124,31 @@ pub mod graph { use crate::{deferred::DeferredPbrLightingPlugin, graph::NodePbr}; use bevy_app::prelude::*; -use bevy_asset::{load_internal_asset, weak_handle, AssetApp, Assets, Handle}; +use bevy_asset::{load_internal_asset, weak_handle, AssetApp, AssetPath, Assets, Handle}; use bevy_core_pipeline::core_3d::graph::{Core3d, Node3d}; use bevy_ecs::prelude::*; use bevy_image::Image; use bevy_render::{ alpha::AlphaMode, - camera::{sort_cameras, CameraUpdateSystem, Projection}, + camera::{sort_cameras, CameraUpdateSystems, Projection}, extract_component::ExtractComponentPlugin, extract_resource::ExtractResourcePlugin, + load_shader_library, render_graph::RenderGraph, - render_resource::Shader, + render_resource::{Shader, ShaderRef}, sync_component::SyncComponentPlugin, view::VisibilitySystems, - ExtractSchedule, Render, RenderApp, RenderDebugFlags, RenderSet, + ExtractSchedule, Render, RenderApp, RenderDebugFlags, RenderSystems, }; -use bevy_transform::TransformSystem; +use bevy_transform::TransformSystems; + +use std::path::PathBuf; + +fn shader_ref(path: PathBuf) -> ShaderRef { + ShaderRef::Path(AssetPath::from_path_buf(path).with_source("embedded")) +} -pub const PBR_TYPES_SHADER_HANDLE: Handle = - weak_handle!("b0330585-2335-4268-9032-a6c4c2d932f6"); -pub const PBR_BINDINGS_SHADER_HANDLE: Handle = - weak_handle!("13834c18-c7ec-4c4b-bbbd-432c3ba4cace"); -pub const UTILS_HANDLE: Handle = weak_handle!("0a32978f-2744-4608-98b6-4c3000a0638d"); -pub const CLUSTERED_FORWARD_HANDLE: Handle = - weak_handle!("f8e3b4c6-60b7-4b23-8b2e-a6b27bb4ddce"); -pub const PBR_LIGHTING_HANDLE: Handle = - weak_handle!("de0cf697-2876-49a0-aa0f-f015216f70c2"); -pub const PBR_TRANSMISSION_HANDLE: Handle = - weak_handle!("22482185-36bb-4c16-9b93-a20e6d4a2725"); -pub const SHADOWS_HANDLE: Handle = weak_handle!("ff758c5a-3927-4a15-94c3-3fbdfc362590"); -pub const SHADOW_SAMPLING_HANDLE: Handle = - weak_handle!("f6bf5843-54bc-4e39-bd9d-56bfcd77b033"); -pub const PBR_FRAGMENT_HANDLE: Handle = - weak_handle!("1bd3c10d-851b-400c-934a-db489d99cc50"); -pub const PBR_SHADER_HANDLE: Handle = weak_handle!("0eba65ed-3e5b-4752-93ed-e8097e7b0c84"); -pub const PBR_PREPASS_SHADER_HANDLE: Handle = - weak_handle!("9afeaeab-7c45-43ce-b322-4b97799eaeb9"); -pub const PBR_FUNCTIONS_HANDLE: Handle = - weak_handle!("815b8618-f557-4a96-91a5-a2fb7e249fb0"); -pub const PBR_AMBIENT_HANDLE: Handle = weak_handle!("4a90b95b-112a-4a10-9145-7590d6f14260"); -pub const PARALLAX_MAPPING_SHADER_HANDLE: Handle = - weak_handle!("6cf57d9f-222a-429a-bba4-55ba9586e1d4"); -pub const VIEW_TRANSFORMATIONS_SHADER_HANDLE: Handle = - weak_handle!("ec047703-cde3-4876-94df-fed121544abb"); -pub const PBR_PREPASS_FUNCTIONS_SHADER_HANDLE: Handle = - weak_handle!("77b1bd3a-877c-4b2c-981b-b9c68d1b774a"); -pub const PBR_DEFERRED_TYPES_HANDLE: Handle = - weak_handle!("43060da7-a717-4240-80a8-dbddd92bd25d"); -pub const PBR_DEFERRED_FUNCTIONS_HANDLE: Handle = - weak_handle!("9dc46746-c51d-45e3-a321-6a50c3963420"); -pub const RGB9E5_FUNCTIONS_HANDLE: Handle = - weak_handle!("90c19aa3-6a11-4252-8586-d9299352e94f"); const MESHLET_VISIBILITY_BUFFER_RESOLVE_SHADER_HANDLE: Handle = weak_handle!("69187376-3dea-4d0f-b3f5-185bde63d6a2"); @@ -211,110 +184,26 @@ impl Default for PbrPlugin { impl Plugin for PbrPlugin { fn build(&self, app: &mut App) { - load_internal_asset!( - app, - PBR_TYPES_SHADER_HANDLE, - "render/pbr_types.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - PBR_BINDINGS_SHADER_HANDLE, - "render/pbr_bindings.wgsl", - Shader::from_wgsl - ); - load_internal_asset!(app, UTILS_HANDLE, "render/utils.wgsl", Shader::from_wgsl); - load_internal_asset!( - app, - CLUSTERED_FORWARD_HANDLE, - "render/clustered_forward.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - PBR_LIGHTING_HANDLE, - "render/pbr_lighting.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - PBR_TRANSMISSION_HANDLE, - "render/pbr_transmission.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - SHADOWS_HANDLE, - "render/shadows.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - PBR_DEFERRED_TYPES_HANDLE, - "deferred/pbr_deferred_types.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - PBR_DEFERRED_FUNCTIONS_HANDLE, - "deferred/pbr_deferred_functions.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - SHADOW_SAMPLING_HANDLE, - "render/shadow_sampling.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - PBR_FUNCTIONS_HANDLE, - "render/pbr_functions.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - RGB9E5_FUNCTIONS_HANDLE, - "render/rgb9e5.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - PBR_AMBIENT_HANDLE, - "render/pbr_ambient.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - PBR_FRAGMENT_HANDLE, - "render/pbr_fragment.wgsl", - Shader::from_wgsl - ); - load_internal_asset!(app, PBR_SHADER_HANDLE, "render/pbr.wgsl", Shader::from_wgsl); - load_internal_asset!( - app, - PBR_PREPASS_FUNCTIONS_SHADER_HANDLE, - "render/pbr_prepass_functions.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - PBR_PREPASS_SHADER_HANDLE, - "render/pbr_prepass.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - PARALLAX_MAPPING_SHADER_HANDLE, - "render/parallax_mapping.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - VIEW_TRANSFORMATIONS_SHADER_HANDLE, - "render/view_transformations.wgsl", - Shader::from_wgsl - ); + load_shader_library!(app, "render/pbr_types.wgsl"); + load_shader_library!(app, "render/pbr_bindings.wgsl"); + load_shader_library!(app, "render/utils.wgsl"); + load_shader_library!(app, "render/clustered_forward.wgsl"); + load_shader_library!(app, "render/pbr_lighting.wgsl"); + load_shader_library!(app, "render/pbr_transmission.wgsl"); + load_shader_library!(app, "render/shadows.wgsl"); + load_shader_library!(app, "deferred/pbr_deferred_types.wgsl"); + load_shader_library!(app, "deferred/pbr_deferred_functions.wgsl"); + load_shader_library!(app, "render/shadow_sampling.wgsl"); + load_shader_library!(app, "render/pbr_functions.wgsl"); + load_shader_library!(app, "render/rgb9e5.wgsl"); + load_shader_library!(app, "render/pbr_ambient.wgsl"); + load_shader_library!(app, "render/pbr_fragment.wgsl"); + load_shader_library!(app, "render/pbr.wgsl"); + load_shader_library!(app, "render/pbr_prepass_functions.wgsl"); + load_shader_library!(app, "render/pbr_prepass.wgsl"); + load_shader_library!(app, "render/parallax_mapping.wgsl"); + load_shader_library!(app, "render/view_transformations.wgsl"); + // Setup dummy shaders for when MeshletPlugin is not used to prevent shader import errors. load_internal_asset!( app, @@ -401,21 +290,21 @@ impl Plugin for PbrPlugin { ( add_clusters .in_set(SimulationLightSystems::AddClusters) - .after(CameraUpdateSystem), + .after(CameraUpdateSystems), assign_objects_to_clusters .in_set(SimulationLightSystems::AssignLightsToClusters) - .after(TransformSystem::TransformPropagate) + .after(TransformSystems::Propagate) .after(VisibilitySystems::CheckVisibility) - .after(CameraUpdateSystem), + .after(CameraUpdateSystems), clear_directional_light_cascades .in_set(SimulationLightSystems::UpdateDirectionalLightCascades) - .after(TransformSystem::TransformPropagate) - .after(CameraUpdateSystem), + .after(TransformSystems::Propagate) + .after(CameraUpdateSystems), update_directional_light_frusta .in_set(SimulationLightSystems::UpdateLightFrusta) // This must run after CheckVisibility because it relies on `ViewVisibility` .after(VisibilitySystems::CheckVisibility) - .after(TransformSystem::TransformPropagate) + .after(TransformSystems::Propagate) .after(SimulationLightSystems::UpdateDirectionalLightCascades) // We assume that no entity will be both a directional light and a spot light, // so these systems will run independently of one another. @@ -423,11 +312,11 @@ impl Plugin for PbrPlugin { .ambiguous_with(update_spot_light_frusta), update_point_light_frusta .in_set(SimulationLightSystems::UpdateLightFrusta) - .after(TransformSystem::TransformPropagate) + .after(TransformSystems::Propagate) .after(SimulationLightSystems::AssignLightsToClusters), update_spot_light_frusta .in_set(SimulationLightSystems::UpdateLightFrusta) - .after(TransformSystem::TransformPropagate) + .after(TransformSystems::Propagate) .after(SimulationLightSystems::AssignLightsToClusters), ( check_dir_light_mesh_visibility, @@ -435,7 +324,7 @@ impl Plugin for PbrPlugin { ) .in_set(SimulationLightSystems::CheckLightVisibility) .after(VisibilitySystems::CalculateBounds) - .after(TransformSystem::TransformPropagate) + .after(TransformSystems::Propagate) .after(SimulationLightSystems::UpdateLightFrusta) // NOTE: This MUST be scheduled AFTER the core renderer visibility check // because that resets entity `ViewVisibility` for the first view @@ -478,9 +367,9 @@ impl Plugin for PbrPlugin { Render, ( prepare_lights - .in_set(RenderSet::ManageViews) + .in_set(RenderSystems::ManageViews) .after(sort_cameras), - prepare_clusters.in_set(RenderSet::PrepareResources), + prepare_clusters.in_set(RenderSystems::PrepareResources), ), ) .init_resource::() diff --git a/crates/bevy_pbr/src/light_probe/mod.rs b/crates/bevy_pbr/src/light_probe/mod.rs index ebfc7c7e7c..d7323a1e3c 100644 --- a/crates/bevy_pbr/src/light_probe/mod.rs +++ b/crates/bevy_pbr/src/light_probe/mod.rs @@ -27,7 +27,7 @@ use bevy_render::{ sync_world::RenderEntity, texture::{FallbackImage, GpuImage}, view::{ExtractedView, Visibility}, - Extract, ExtractSchedule, Render, RenderApp, RenderSet, + Extract, ExtractSchedule, Render, RenderApp, RenderSystems, }; use bevy_transform::{components::Transform, prelude::GlobalTransform}; use tracing::error; @@ -383,7 +383,7 @@ impl Plugin for LightProbePlugin { .add_systems( Render, (upload_light_probes, prepare_environment_uniform_buffer) - .in_set(RenderSet::PrepareResources), + .in_set(RenderSystems::PrepareResources), ); } } diff --git a/crates/bevy_pbr/src/lightmap/mod.rs b/crates/bevy_pbr/src/lightmap/mod.rs index 4175d6ff61..5ddb1d6c72 100644 --- a/crates/bevy_pbr/src/lightmap/mod.rs +++ b/crates/bevy_pbr/src/lightmap/mod.rs @@ -64,7 +64,7 @@ use fixedbitset::FixedBitSet; use nonmax::{NonMaxU16, NonMaxU32}; use tracing::error; -use crate::{binding_arrays_are_usable, ExtractMeshesSet}; +use crate::{binding_arrays_are_usable, MeshExtractionSystems}; /// The ID of the lightmap shader. pub const LIGHTMAP_SHADER_HANDLE: Handle = @@ -201,9 +201,10 @@ impl Plugin for LightmapPlugin { return; }; - render_app - .init_resource::() - .add_systems(ExtractSchedule, extract_lightmaps.after(ExtractMeshesSet)); + render_app.init_resource::().add_systems( + ExtractSchedule, + extract_lightmaps.after(MeshExtractionSystems), + ); } } diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index 1814996184..af11db1ba6 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -8,7 +8,7 @@ use crate::meshlet::{ }; use crate::*; use bevy_asset::prelude::AssetChanged; -use bevy_asset::{Asset, AssetEvents, AssetId, AssetServer, UntypedAssetId}; +use bevy_asset::{Asset, AssetEventSystems, AssetId, AssetServer, UntypedAssetId}; use bevy_core_pipeline::deferred::{AlphaMask3dDeferred, Opaque3dDeferred}; use bevy_core_pipeline::prepass::{AlphaMask3dPrepass, Opaque3dPrepass}; use bevy_core_pipeline::{ @@ -288,7 +288,7 @@ where PostUpdate, ( mark_meshes_as_changed_if_their_materials_changed::.ambiguous_with_all(), - check_entities_needing_specialization::.after(AssetEvents), + check_entities_needing_specialization::.after(AssetEventSystems), ) .after(mark_3d_meshes_as_changed_if_their_assets_changed), ); @@ -316,9 +316,9 @@ where .add_systems( ExtractSchedule, ( - extract_mesh_materials::.in_set(ExtractMaterialsSet), + extract_mesh_materials::.in_set(MaterialExtractionSystems), early_sweep_material_instances:: - .after(ExtractMaterialsSet) + .after(MaterialExtractionSystems) .before(late_sweep_material_instances), extract_entities_needs_specialization::.after(extract_cameras), ), @@ -327,13 +327,13 @@ where Render, ( specialize_material_meshes:: - .in_set(RenderSet::PrepareMeshes) + .in_set(RenderSystems::PrepareMeshes) .after(prepare_assets::>) .after(prepare_assets::) .after(collect_meshes_for_gpu_building) .after(set_mesh_motion_vector_flags), queue_material_meshes:: - .in_set(RenderSet::QueueMeshes) + .in_set(RenderSystems::QueueMeshes) .after(prepare_assets::>), ), ) @@ -344,7 +344,7 @@ where write_material_bind_group_buffers::, ) .chain() - .in_set(RenderSet::PrepareBindGroups) + .in_set(RenderSystems::PrepareBindGroups) .after(prepare_assets::>), ); @@ -356,14 +356,15 @@ where .add_systems( Render, ( - check_views_lights_need_specialization.in_set(RenderSet::PrepareAssets), + check_views_lights_need_specialization + .in_set(RenderSystems::PrepareAssets), // specialize_shadows:: also needs to run after prepare_assets::>, // which is fine since ManageViews is after PrepareAssets specialize_shadows:: - .in_set(RenderSet::ManageViews) + .in_set(RenderSystems::ManageViews) .after(prepare_lights), queue_shadows:: - .in_set(RenderSet::QueueMeshes) + .in_set(RenderSystems::QueueMeshes) .after(prepare_assets::>), ), ); @@ -373,7 +374,7 @@ where render_app.add_systems( Render, queue_material_meshlet_meshes:: - .in_set(RenderSet::QueueMeshes) + .in_set(RenderSystems::QueueMeshes) .run_if(resource_exists::), ); @@ -381,7 +382,7 @@ where render_app.add_systems( Render, prepare_material_meshlet_meshes_main_opaque_pass:: - .in_set(RenderSet::QueueMeshes) + .in_set(RenderSystems::QueueMeshes) .after(prepare_assets::>) .before(queue_material_meshlet_meshes::) .run_if(resource_exists::), @@ -637,7 +638,11 @@ pub struct RenderMaterialInstance { /// A [`SystemSet`] that contains all `extract_mesh_materials` systems. #[derive(SystemSet, Clone, PartialEq, Eq, Debug, Hash)] -pub struct ExtractMaterialsSet; +pub struct MaterialExtractionSystems; + +/// Deprecated alias for [`MaterialExtractionSystems`]. +#[deprecated(since = "0.17.0", note = "Renamed to `MaterialExtractionSystems`.")] +pub type ExtractMaterialsSet = MaterialExtractionSystems; pub const fn alpha_mode_pipeline_key(alpha_mode: AlphaMode, msaa: &Msaa) -> MeshPipelineKey { match alpha_mode { diff --git a/crates/bevy_pbr/src/meshlet/mod.rs b/crates/bevy_pbr/src/meshlet/mod.rs index 2e483b210c..2375894613 100644 --- a/crates/bevy_pbr/src/meshlet/mod.rs +++ b/crates/bevy_pbr/src/meshlet/mod.rs @@ -79,7 +79,7 @@ use bevy_render::{ renderer::RenderDevice, settings::WgpuFeatures, view::{self, prepare_view_targets, Msaa, Visibility, VisibilityClass}, - ExtractSchedule, Render, RenderApp, RenderSet, + ExtractSchedule, Render, RenderApp, RenderSystems, }; use bevy_transform::components::Transform; use derive_more::From; @@ -277,12 +277,12 @@ impl Plugin for MeshletPlugin { .add_systems( Render, ( - perform_pending_meshlet_mesh_writes.in_set(RenderSet::PrepareAssets), + perform_pending_meshlet_mesh_writes.in_set(RenderSystems::PrepareAssets), configure_meshlet_views .after(prepare_view_targets) - .in_set(RenderSet::ManageViews), - prepare_meshlet_per_frame_resources.in_set(RenderSet::PrepareResources), - prepare_meshlet_view_bind_groups.in_set(RenderSet::PrepareBindGroups), + .in_set(RenderSystems::ManageViews), + prepare_meshlet_per_frame_resources.in_set(RenderSystems::PrepareResources), + prepare_meshlet_view_bind_groups.in_set(RenderSystems::PrepareBindGroups), ), ); } diff --git a/crates/bevy_pbr/src/pbr_material.rs b/crates/bevy_pbr/src/pbr_material.rs index fd1babd8ec..cbd8445483 100644 --- a/crates/bevy_pbr/src/pbr_material.rs +++ b/crates/bevy_pbr/src/pbr_material.rs @@ -1345,7 +1345,7 @@ impl From<&StandardMaterial> for StandardMaterialKey { impl Material for StandardMaterial { fn fragment_shader() -> ShaderRef { - PBR_SHADER_HANDLE.into() + shader_ref(bevy_asset::embedded_path!("render/pbr.wgsl")) } #[inline] @@ -1381,11 +1381,11 @@ impl Material for StandardMaterial { } fn prepass_fragment_shader() -> ShaderRef { - PBR_PREPASS_SHADER_HANDLE.into() + shader_ref(bevy_asset::embedded_path!("render/pbr_prepass.wgsl")) } fn deferred_fragment_shader() -> ShaderRef { - PBR_SHADER_HANDLE.into() + shader_ref(bevy_asset::embedded_path!("render/pbr.wgsl")) } #[cfg(feature = "meshlet")] diff --git a/crates/bevy_pbr/src/prepass/mod.rs b/crates/bevy_pbr/src/prepass/mod.rs index 77f874c168..03a797eba1 100644 --- a/crates/bevy_pbr/src/prepass/mod.rs +++ b/crates/bevy_pbr/src/prepass/mod.rs @@ -19,7 +19,7 @@ use bevy_render::{ renderer::RenderAdapter, sync_world::RenderEntity, view::{RenderVisibilityRanges, RetainedViewEntity, VISIBILITY_RANGES_STORAGE_BUFFER_COUNT}, - ExtractSchedule, Render, RenderApp, RenderDebugFlags, RenderSet, + ExtractSchedule, Render, RenderApp, RenderDebugFlags, RenderSystems, }; pub use prepass_bindings::*; @@ -60,7 +60,7 @@ use bevy_ecs::system::SystemChangeTick; use bevy_platform::collections::HashMap; use bevy_render::sync_world::MainEntityHashMap; use bevy_render::view::RenderVisibleEntities; -use bevy_render::RenderSet::{PrepareAssets, PrepareResources}; +use bevy_render::RenderSystems::{PrepareAssets, PrepareResources}; use core::{hash::Hash, marker::PhantomData}; pub const PREPASS_SHADER_HANDLE: Handle = @@ -126,7 +126,7 @@ where render_app .add_systems( Render, - prepare_prepass_view_bind_group::.in_set(RenderSet::PrepareBindGroups), + prepare_prepass_view_bind_group::.in_set(RenderSystems::PrepareBindGroups), ) .init_resource::() .init_resource::>>() @@ -216,13 +216,13 @@ where ( check_prepass_views_need_specialization.in_set(PrepareAssets), specialize_prepass_material_meshes:: - .in_set(RenderSet::PrepareMeshes) + .in_set(RenderSystems::PrepareMeshes) .after(prepare_assets::>) .after(prepare_assets::) .after(collect_meshes_for_gpu_building) .after(set_mesh_motion_vector_flags), queue_prepass_material_meshes:: - .in_set(RenderSet::QueueMeshes) + .in_set(RenderSystems::QueueMeshes) .after(prepare_assets::>) // queue_material_meshes only writes to `material_bind_group_id`, which `queue_prepass_material_meshes` doesn't read .ambiguous_with(queue_material_meshes::), @@ -233,7 +233,7 @@ where render_app.add_systems( Render, prepare_material_meshlet_meshes_prepass:: - .in_set(RenderSet::QueueMeshes) + .in_set(RenderSystems::QueueMeshes) .after(prepare_assets::>) .before(queue_material_meshlet_meshes::) .run_if(resource_exists::), @@ -983,12 +983,18 @@ pub fn specialize_prepass_material_meshes( AlphaMode::Blend | AlphaMode::Premultiplied | AlphaMode::Add - | AlphaMode::Multiply => continue, + | AlphaMode::Multiply => { + // In case this material was previously in a valid alpha_mode, remove it to + // stop the queue system from assuming its retained cache to be valid. + view_specialized_material_pipeline_cache.remove(visible_entity); + continue; + } } if material.properties.reads_view_transmission_texture { // No-op: Materials reading from `ViewTransmissionTexture` are not rendered in the `Opaque3d` // phase, and are therefore also excluded from the prepass much like alpha-blended materials. + view_specialized_material_pipeline_cache.remove(visible_entity); continue; } diff --git a/crates/bevy_pbr/src/render/fog.rs b/crates/bevy_pbr/src/render/fog.rs index 9394380f71..9d7fd0b18d 100644 --- a/crates/bevy_pbr/src/render/fog.rs +++ b/crates/bevy_pbr/src/render/fog.rs @@ -8,7 +8,7 @@ use bevy_render::{ render_resource::{DynamicUniformBuffer, Shader, ShaderType}, renderer::{RenderDevice, RenderQueue}, view::ExtractedView, - Render, RenderApp, RenderSet, + Render, RenderApp, RenderSystems, }; use crate::{DistanceFog, FogFalloff}; @@ -142,7 +142,7 @@ impl Plugin for FogPlugin { if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app .init_resource::() - .add_systems(Render, prepare_fog.in_set(RenderSet::PrepareResources)); + .add_systems(Render, prepare_fog.in_set(RenderSystems::PrepareResources)); } } } diff --git a/crates/bevy_pbr/src/render/gpu_preprocess.rs b/crates/bevy_pbr/src/render/gpu_preprocess.rs index 912f6192ce..5356c7580e 100644 --- a/crates/bevy_pbr/src/render/gpu_preprocess.rs +++ b/crates/bevy_pbr/src/render/gpu_preprocess.rs @@ -50,7 +50,7 @@ use bevy_render::{ renderer::{RenderContext, RenderDevice, RenderQueue}, settings::WgpuFeatures, view::{ExtractedView, NoIndirectDrawing, ViewUniform, ViewUniformOffset, ViewUniforms}, - Render, RenderApp, RenderSet, + Render, RenderApp, RenderSystems, }; use bevy_utils::TypeIdMap; use bitflags::bitflags; @@ -471,14 +471,14 @@ impl Plugin for GpuMeshPreprocessPlugin { .add_systems( Render, ( - prepare_preprocess_pipelines.in_set(RenderSet::Prepare), + prepare_preprocess_pipelines.in_set(RenderSystems::Prepare), prepare_preprocess_bind_groups .run_if(resource_exists::>) - .in_set(RenderSet::PrepareBindGroups), - write_mesh_culling_data_buffer.in_set(RenderSet::PrepareResourcesFlush), + .in_set(RenderSystems::PrepareBindGroups), + write_mesh_culling_data_buffer.in_set(RenderSystems::PrepareResourcesFlush), ), ) .add_render_graph_node::( diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index d71dccc71a..f57ba9adf3 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -220,7 +220,18 @@ pub fn extract_lights( mut commands: Commands, point_light_shadow_map: Extract>, directional_light_shadow_map: Extract>, - global_point_lights: Extract>, + global_visible_clusterable: Extract>, + previous_point_lights: Query< + Entity, + ( + With, + With, + ), + >, + previous_spot_lights: Query< + Entity, + (With, With), + >, point_lights: Extract< Query<( Entity, @@ -276,6 +287,22 @@ pub fn extract_lights( if directional_light_shadow_map.is_changed() { commands.insert_resource(directional_light_shadow_map.clone()); } + + // Clear previous visible entities for all point/spot lights as they might not be in the + // `global_visible_clusterable` list anymore. + commands.try_insert_batch( + previous_point_lights + .iter() + .map(|render_entity| (render_entity, RenderCubemapVisibleEntities::default())) + .collect::>(), + ); + commands.try_insert_batch( + previous_spot_lights + .iter() + .map(|render_entity| (render_entity, RenderVisibleMeshEntities::default())) + .collect::>(), + ); + // This is the point light shadow map texel size for one face of the cube as a distance of 1.0 // world unit from the light. // point_light_texel_size = 2.0 * 1.0 * tan(PI / 4.0) / cube face width in texels @@ -286,7 +313,7 @@ pub fn extract_lights( let point_light_texel_size = 2.0 / point_light_shadow_map.size as f32; let mut point_lights_values = Vec::with_capacity(*previous_point_lights_len); - for entity in global_point_lights.iter().copied() { + for entity in global_visible_clusterable.iter().copied() { let Ok(( main_entity, render_entity, @@ -350,7 +377,7 @@ pub fn extract_lights( commands.try_insert_batch(point_lights_values); let mut spot_lights_values = Vec::with_capacity(*previous_spot_lights_len); - for entity in global_point_lights.iter().copied() { + for entity in global_visible_clusterable.iter().copied() { if let Ok(( main_entity, render_entity, diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index 4bae79b807..4fc6252cab 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -1,6 +1,6 @@ use crate::material_bind_groups::{MaterialBindGroupIndex, MaterialBindGroupSlot}; use allocator::MeshAllocator; -use bevy_asset::{load_internal_asset, AssetId}; +use bevy_asset::{load_internal_asset, weak_handle, AssetId}; use bevy_core_pipeline::{ core_3d::{AlphaMask3d, Opaque3d, Transmissive3d, Transparent3d, CORE_3D_DEPTH_FORMAT}, deferred::{AlphaMask3dDeferred, Opaque3dDeferred}, @@ -74,7 +74,7 @@ use bevy_render::camera::TemporalJitter; use bevy_render::prelude::Msaa; use bevy_render::sync_world::{MainEntity, MainEntityHashMap}; use bevy_render::view::ExtractedView; -use bevy_render::RenderSet::PrepareAssets; +use bevy_render::RenderSystems::PrepareAssets; use bytemuck::{Pod, Zeroable}; use nonmax::{NonMaxU16, NonMaxU32}; use smallvec::{smallvec, SmallVec}; @@ -196,7 +196,7 @@ impl Plugin for MeshRenderPlugin { .init_resource::() .configure_sets( ExtractSchedule, - ExtractMeshesSet + MeshExtractionSystems .after(view::extract_visibility_ranges) .after(late_sweep_material_instances), ) @@ -206,22 +206,22 @@ impl Plugin for MeshRenderPlugin { extract_skins, extract_morphs, gpu_preprocessing::clear_batched_gpu_instance_buffers:: - .before(ExtractMeshesSet), + .before(MeshExtractionSystems), ), ) .add_systems( Render, ( - set_mesh_motion_vector_flags.in_set(RenderSet::PrepareMeshes), - prepare_skins.in_set(RenderSet::PrepareResources), - prepare_morphs.in_set(RenderSet::PrepareResources), - prepare_mesh_bind_groups.in_set(RenderSet::PrepareBindGroups), + set_mesh_motion_vector_flags.in_set(RenderSystems::PrepareMeshes), + prepare_skins.in_set(RenderSystems::PrepareResources), + prepare_morphs.in_set(RenderSystems::PrepareResources), + prepare_mesh_bind_groups.in_set(RenderSystems::PrepareBindGroups), prepare_mesh_view_bind_groups - .in_set(RenderSet::PrepareBindGroups) + .in_set(RenderSystems::PrepareBindGroups) .after(prepare_oit_buffers), no_gpu_preprocessing::clear_batched_cpu_instance_buffers:: - .in_set(RenderSet::Cleanup) - .after(RenderSet::Render), + .in_set(RenderSystems::Cleanup) + .after(RenderSystems::Render), ), ); } @@ -259,17 +259,17 @@ impl Plugin for MeshRenderPlugin { .init_resource::() .add_systems( ExtractSchedule, - extract_meshes_for_gpu_building.in_set(ExtractMeshesSet), + extract_meshes_for_gpu_building.in_set(MeshExtractionSystems), ) .add_systems( Render, ( gpu_preprocessing::write_batched_instance_buffers:: - .in_set(RenderSet::PrepareResourcesFlush), + .in_set(RenderSystems::PrepareResourcesFlush), gpu_preprocessing::delete_old_work_item_buffers:: - .in_set(RenderSet::PrepareResources), + .in_set(RenderSystems::PrepareResources), collect_meshes_for_gpu_building - .in_set(RenderSet::PrepareMeshes) + .in_set(RenderSystems::PrepareMeshes) // This must be before // `set_mesh_motion_vector_flags` so it doesn't // overwrite those flags. @@ -284,12 +284,12 @@ impl Plugin for MeshRenderPlugin { .insert_resource(cpu_batched_instance_buffer) .add_systems( ExtractSchedule, - extract_meshes_for_cpu_building.in_set(ExtractMeshesSet), + extract_meshes_for_cpu_building.in_set(MeshExtractionSystems), ) .add_systems( Render, no_gpu_preprocessing::write_batched_instance_buffer:: - .in_set(RenderSet::PrepareResourcesFlush), + .in_set(RenderSystems::PrepareResourcesFlush), ); }; @@ -837,12 +837,33 @@ pub struct RenderMeshInstanceGpuQueues(Parallel); pub struct MeshesToReextractNextFrame(MainEntityHashSet); impl RenderMeshInstanceShared { - fn from_components( + /// A gpu builder will provide the mesh instance id + /// during [`RenderMeshInstanceGpuBuilder::update`]. + fn for_gpu_building( previous_transform: Option<&PreviousGlobalTransform>, mesh: &Mesh3d, tag: Option<&MeshTag>, not_shadow_caster: bool, no_automatic_batching: bool, + ) -> Self { + Self::for_cpu_building( + previous_transform, + mesh, + tag, + default(), + not_shadow_caster, + no_automatic_batching, + ) + } + + /// The cpu builder does not have an equivalent [`RenderMeshInstanceGpuBuilder::update`]. + fn for_cpu_building( + previous_transform: Option<&PreviousGlobalTransform>, + mesh: &Mesh3d, + tag: Option<&MeshTag>, + material_bindings_index: MaterialBindingId, + not_shadow_caster: bool, + no_automatic_batching: bool, ) -> Self { let mut mesh_instance_flags = RenderMeshInstanceFlags::empty(); mesh_instance_flags.set(RenderMeshInstanceFlags::SHADOW_CASTER, !not_shadow_caster); @@ -858,8 +879,7 @@ impl RenderMeshInstanceShared { RenderMeshInstanceShared { mesh_asset_id: mesh.id(), flags: mesh_instance_flags, - // This gets filled in later, during `RenderMeshGpuBuilder::update`. - material_bindings_index: default(), + material_bindings_index, lightmap_slab_index: None, tag: tag.map_or(0, |i| **i), } @@ -1296,7 +1316,11 @@ pub struct RenderMeshQueueData<'a> { /// A [`SystemSet`] that encompasses both [`extract_meshes_for_cpu_building`] /// and [`extract_meshes_for_gpu_building`]. #[derive(SystemSet, Clone, PartialEq, Eq, Debug, Hash)] -pub struct ExtractMeshesSet; +pub struct MeshExtractionSystems; + +/// Deprecated alias for [`MeshExtractionSystems`]. +#[deprecated(since = "0.17.0", note = "Renamed to `MeshExtractionSystems`.")] +pub type ExtractMeshesSet = MeshExtractionSystems; /// Extracts meshes from the main world into the render world, populating the /// [`RenderMeshInstances`]. @@ -1305,6 +1329,8 @@ pub struct ExtractMeshesSet; /// [`MeshUniform`] building. pub fn extract_meshes_for_cpu_building( mut render_mesh_instances: ResMut, + mesh_material_ids: Res, + render_material_bindings: Res, render_visibility_ranges: Res, mut render_mesh_instance_queues: Local>>, meshes_query: Extract< @@ -1358,10 +1384,18 @@ pub fn extract_meshes_for_cpu_building( transmitted_receiver, ); - let shared = RenderMeshInstanceShared::from_components( + let mesh_material = mesh_material_ids.mesh_material(MainEntity::from(entity)); + + let material_bindings_index = render_material_bindings + .get(&mesh_material) + .copied() + .unwrap_or_default(); + + let shared = RenderMeshInstanceShared::for_cpu_building( previous_transform, mesh, tag, + material_bindings_index, not_shadow_caster, no_automatic_batching, ); @@ -1566,7 +1600,7 @@ fn extract_mesh_for_gpu_building( transmitted_receiver, ); - let shared = RenderMeshInstanceShared::from_components( + let shared = RenderMeshInstanceShared::for_gpu_building( previous_transform, mesh, tag, diff --git a/crates/bevy_pbr/src/render/pbr_lighting.wgsl b/crates/bevy_pbr/src/render/pbr_lighting.wgsl index 4497b567e9..01e09fe3b4 100644 --- a/crates/bevy_pbr/src/render/pbr_lighting.wgsl +++ b/crates/bevy_pbr/src/render/pbr_lighting.wgsl @@ -278,7 +278,23 @@ fn compute_specular_layer_values_for_point_light( // Representative Point Area Lights. // see http://blog.selfshadow.com/publications/s2013-shading-course/karis/s2013_pbs_epic_notes_v2.pdf p14-16 - let centerToRay = dot(light_to_frag, R) * R - light_to_frag; + var LtFdotR = dot(light_to_frag, R); + + // HACK: the following line is an amendment to fix a discontinuity when a surface + // intersects the light sphere. See https://github.com/bevyengine/bevy/issues/13318 + // + // This sentence in the reference is crux of the problem: "We approximate finding the point with the + // smallest angle to the reflection ray by finding the point with the smallest distance to the ray." + // This approximation turns out to be completely wrong for points inside or near the sphere. + // Clamping this dot product to be positive ensures `centerToRay` lies on ray and not behind it. + // Any non-zero epsilon works here, it just has to be positive to avoid a singularity at zero. + // However, this is still far from physically accurate. Deriving an exact solution would help, + // but really we should adopt a superior solution to area lighting, such as: + // Physically Based Area Lights by Michal Drobot, or + // Polygonal-Light Shading with Linearly Transformed Cosines by Eric Heitz et al. + LtFdotR = max(0.0001, LtFdotR); + + let centerToRay = LtFdotR * R - light_to_frag; let closestPoint = light_to_frag + centerToRay * saturate( light_position_radius * inverseSqrt(dot(centerToRay, centerToRay))); let LspecLengthInverse = inverseSqrt(dot(closestPoint, closestPoint)); diff --git a/crates/bevy_pbr/src/ssao/mod.rs b/crates/bevy_pbr/src/ssao/mod.rs index 9098f82773..9224374c60 100644 --- a/crates/bevy_pbr/src/ssao/mod.rs +++ b/crates/bevy_pbr/src/ssao/mod.rs @@ -33,7 +33,7 @@ use bevy_render::{ sync_world::RenderEntity, texture::{CachedTexture, TextureCache}, view::{Msaa, ViewUniform, ViewUniformOffset, ViewUniforms}, - Extract, ExtractSchedule, Render, RenderApp, RenderSet, + Extract, ExtractSchedule, Render, RenderApp, RenderSystems, }; use bevy_utils::prelude::default; use core::mem; @@ -111,9 +111,9 @@ impl Plugin for ScreenSpaceAmbientOcclusionPlugin { .add_systems( Render, ( - prepare_ssao_pipelines.in_set(RenderSet::Prepare), - prepare_ssao_textures.in_set(RenderSet::PrepareResources), - prepare_ssao_bind_groups.in_set(RenderSet::PrepareBindGroups), + prepare_ssao_pipelines.in_set(RenderSystems::Prepare), + prepare_ssao_textures.in_set(RenderSystems::PrepareResources), + prepare_ssao_bind_groups.in_set(RenderSystems::PrepareBindGroups), ), ) .add_render_graph_node::>( diff --git a/crates/bevy_pbr/src/ssr/mod.rs b/crates/bevy_pbr/src/ssr/mod.rs index 1ee73da8f0..e4cc850d81 100644 --- a/crates/bevy_pbr/src/ssr/mod.rs +++ b/crates/bevy_pbr/src/ssr/mod.rs @@ -37,7 +37,7 @@ use bevy_render::{ }, renderer::{RenderAdapter, RenderContext, RenderDevice, RenderQueue}, view::{ExtractedView, Msaa, ViewTarget, ViewUniformOffset}, - Render, RenderApp, RenderSet, + Render, RenderApp, RenderSystems, }; use bevy_utils::{once, prelude::default}; use tracing::info; @@ -196,10 +196,10 @@ impl Plugin for ScreenSpaceReflectionsPlugin { render_app .init_resource::() - .add_systems(Render, prepare_ssr_pipelines.in_set(RenderSet::Prepare)) + .add_systems(Render, prepare_ssr_pipelines.in_set(RenderSystems::Prepare)) .add_systems( Render, - prepare_ssr_settings.in_set(RenderSet::PrepareResources), + prepare_ssr_settings.in_set(RenderSystems::PrepareResources), ) .add_render_graph_node::>( Core3d, diff --git a/crates/bevy_pbr/src/volumetric_fog/mod.rs b/crates/bevy_pbr/src/volumetric_fog/mod.rs index b9f1d60945..4af1bbb421 100644 --- a/crates/bevy_pbr/src/volumetric_fog/mod.rs +++ b/crates/bevy_pbr/src/volumetric_fog/mod.rs @@ -51,7 +51,7 @@ use bevy_render::{ render_resource::{Shader, SpecializedRenderPipelines}, sync_component::SyncComponentPlugin, view::Visibility, - ExtractSchedule, Render, RenderApp, RenderSet, + ExtractSchedule, Render, RenderApp, RenderSystems, }; use bevy_transform::components::Transform; use render::{ @@ -216,10 +216,10 @@ impl Plugin for VolumetricFogPlugin { .add_systems( Render, ( - render::prepare_volumetric_fog_pipelines.in_set(RenderSet::Prepare), - render::prepare_volumetric_fog_uniforms.in_set(RenderSet::Prepare), + render::prepare_volumetric_fog_pipelines.in_set(RenderSystems::Prepare), + render::prepare_volumetric_fog_uniforms.in_set(RenderSystems::Prepare), render::prepare_view_depth_textures_for_volumetric_fog - .in_set(RenderSet::Prepare) + .in_set(RenderSystems::Prepare) .before(prepare_core_3d_depth_textures), ), ); diff --git a/crates/bevy_pbr/src/wireframe.rs b/crates/bevy_pbr/src/wireframe.rs index 407062064a..7b748c2535 100644 --- a/crates/bevy_pbr/src/wireframe.rs +++ b/crates/bevy_pbr/src/wireframe.rs @@ -5,7 +5,7 @@ use crate::{ use bevy_app::{App, Plugin, PostUpdate, Startup, Update}; use bevy_asset::{ load_internal_asset, prelude::AssetChanged, weak_handle, AsAssetId, Asset, AssetApp, - AssetEvents, AssetId, Assets, Handle, UntypedAssetId, + AssetEventSystems, AssetId, Assets, Handle, UntypedAssetId, }; use bevy_color::{Color, ColorToComponents}; use bevy_core_pipeline::core_3d::{ @@ -51,7 +51,7 @@ use bevy_render::{ ExtractedView, NoIndirectDrawing, RenderVisibilityRanges, RenderVisibleEntities, RetainedViewEntity, ViewDepthTexture, ViewTarget, }, - Extract, Render, RenderApp, RenderDebugFlags, RenderSet, + Extract, Render, RenderApp, RenderDebugFlags, RenderSystems, }; use core::{hash::Hash, ops::Range}; use tracing::error; @@ -115,7 +115,7 @@ impl Plugin for WireframePlugin { .add_systems( PostUpdate, check_wireframe_entities_needing_specialization - .after(AssetEvents) + .after(AssetEventSystems) .run_if(resource_exists::), ); @@ -151,11 +151,11 @@ impl Plugin for WireframePlugin { Render, ( specialize_wireframes - .in_set(RenderSet::PrepareMeshes) + .in_set(RenderSystems::PrepareMeshes) .after(prepare_assets::) .after(prepare_assets::), queue_wireframes - .in_set(RenderSet::QueueMeshes) + .in_set(RenderSystems::QueueMeshes) .after(prepare_assets::), ), ); diff --git a/crates/bevy_picking/src/backend.rs b/crates/bevy_picking/src/backend.rs index fb2f9d7d7a..9e28cc6d7c 100644 --- a/crates/bevy_picking/src/backend.rs +++ b/crates/bevy_picking/src/backend.rs @@ -42,7 +42,7 @@ pub mod prelude { pub use super::{ray::RayMap, HitData, PointerHits}; pub use crate::{ pointer::{PointerId, PointerLocation}, - PickSet, Pickable, + Pickable, PickingSystems, }; } @@ -54,7 +54,7 @@ pub mod prelude { /// /// Note that systems reading these events in [`PreUpdate`](bevy_app::PreUpdate) will not report ordering /// ambiguities with picking backends. Take care to ensure such systems are explicitly ordered -/// against [`PickSet::Backend`](crate::PickSet::Backend), or better, avoid reading `PointerHits` in `PreUpdate`. +/// against [`PickingSystems::Backend`](crate::PickingSystems::Backend), or better, avoid reading `PointerHits` in `PreUpdate`. #[derive(Event, Debug, Clone, Reflect)] #[reflect(Debug, Clone)] pub struct PointerHits { @@ -84,7 +84,7 @@ pub struct PointerHits { } impl PointerHits { - #[expect(missing_docs, reason = "Not all docs are written yet, see #3492.")] + /// Construct [`PointerHits`]. pub fn new(pointer: prelude::PointerId, picks: Vec<(Entity, HitData)>, order: f32) -> Self { Self { pointer, @@ -114,7 +114,7 @@ pub struct HitData { } impl HitData { - #[expect(missing_docs, reason = "Not all docs are written yet, see #3492.")] + /// Construct a [`HitData`]. pub fn new(camera: Entity, depth: f32, position: Option, normal: Option) -> Self { Self { camera, diff --git a/crates/bevy_picking/src/events.rs b/crates/bevy_picking/src/events.rs index 88b3b9bccc..29405099f6 100644 --- a/crates/bevy_picking/src/events.rs +++ b/crates/bevy_picking/src/events.rs @@ -23,7 +23,7 @@ //! //! The order in which interaction events are received is extremely important, and you can read more //! about it on the docs for the dispatcher system: [`pointer_events`]. This system runs in -//! [`PreUpdate`](bevy_app::PreUpdate) in [`PickSet::Hover`](crate::PickSet::Hover). All pointer-event +//! [`PreUpdate`](bevy_app::PreUpdate) in [`PickingSystems::Hover`](crate::PickingSystems::Hover). All pointer-event //! observers resolve during the sync point between [`pointer_events`] and //! [`update_interactions`](crate::hover::update_interactions). //! diff --git a/crates/bevy_picking/src/input.rs b/crates/bevy_picking/src/input.rs index 712e612224..d751e07c94 100644 --- a/crates/bevy_picking/src/input.rs +++ b/crates/bevy_picking/src/input.rs @@ -30,7 +30,7 @@ use crate::pointer::{ Location, PointerAction, PointerButton, PointerId, PointerInput, PointerLocation, }; -use crate::PickSet; +use crate::PickingSystems; /// The picking input prelude. /// @@ -86,7 +86,7 @@ impl Plugin for PointerInputPlugin { touch_pick_events.run_if(PointerInputPlugin::is_touch_enabled), ) .chain() - .in_set(PickSet::Input), + .in_set(PickingSystems::Input), ) .add_systems( Last, diff --git a/crates/bevy_picking/src/lib.rs b/crates/bevy_picking/src/lib.rs index 6afe86b0d6..53387e84c8 100644 --- a/crates/bevy_picking/src/lib.rs +++ b/crates/bevy_picking/src/lib.rs @@ -256,7 +256,7 @@ impl Default for Pickable { /// Groups the stages of the picking process under shared labels. #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] -pub enum PickSet { +pub enum PickingSystems { /// Produces pointer input events. In the [`First`] schedule. Input, /// Runs after input events are generated but before commands are flushed. In the [`First`] @@ -269,13 +269,17 @@ pub enum PickSet { /// Reads [`backend::PointerHits`]s, and updates the hovermap, selection, and highlighting states. In /// the [`PreUpdate`] schedule. Hover, - /// Runs after all the [`PickSet::Hover`] systems are done, before event listeners are triggered. In the + /// Runs after all the [`PickingSystems::Hover`] systems are done, before event listeners are triggered. In the /// [`PreUpdate`] schedule. PostHover, /// Runs after all other picking sets. In the [`PreUpdate`] schedule. Last, } +/// Deprecated alias for [`PickingSystems`]. +#[deprecated(since = "0.17.0", note = "Renamed to `PickingSystems`.")] +pub type PickSet = PickingSystems; + /// One plugin that contains the [`PointerInputPlugin`](input::PointerInputPlugin), [`PickingPlugin`] /// and the [`InteractionPlugin`], this is probably the plugin that will be most used. /// @@ -359,29 +363,29 @@ impl Plugin for PickingPlugin { pointer::PointerInput::receive, backend::ray::RayMap::repopulate.after(pointer::PointerInput::receive), ) - .in_set(PickSet::ProcessInput), + .in_set(PickingSystems::ProcessInput), ) .add_systems( PreUpdate, window::update_window_hits .run_if(Self::window_picking_should_run) - .in_set(PickSet::Backend), + .in_set(PickingSystems::Backend), ) .configure_sets( First, - (PickSet::Input, PickSet::PostInput) - .after(bevy_time::TimeSystem) - .after(bevy_ecs::event::EventUpdates) + (PickingSystems::Input, PickingSystems::PostInput) + .after(bevy_time::TimeSystems) + .after(bevy_ecs::event::EventUpdateSystems) .chain(), ) .configure_sets( PreUpdate, ( - PickSet::ProcessInput.run_if(Self::input_should_run), - PickSet::Backend, - PickSet::Hover.run_if(Self::hover_should_run), - PickSet::PostHover, - PickSet::Last, + PickingSystems::ProcessInput.run_if(Self::input_should_run), + PickingSystems::Backend, + PickingSystems::Hover.run_if(Self::hover_should_run), + PickingSystems::PostHover, + PickingSystems::Last, ) .chain(), ) @@ -427,7 +431,7 @@ impl Plugin for InteractionPlugin { PreUpdate, (generate_hovermap, update_interactions, pointer_events) .chain() - .in_set(PickSet::Hover), + .in_set(PickingSystems::Hover), ); } } diff --git a/crates/bevy_picking/src/mesh_picking/mod.rs b/crates/bevy_picking/src/mesh_picking/mod.rs index 1e7e45bc2d..fe08976eb8 100644 --- a/crates/bevy_picking/src/mesh_picking/mod.rs +++ b/crates/bevy_picking/src/mesh_picking/mod.rs @@ -19,7 +19,7 @@ pub mod ray_cast; use crate::{ backend::{ray::RayMap, HitData, PointerHits}, prelude::*, - PickSet, + PickingSystems, }; use bevy_app::prelude::*; use bevy_ecs::prelude::*; @@ -71,7 +71,7 @@ impl Plugin for MeshPickingPlugin { app.init_resource::() .register_type::() .register_type::() - .add_systems(PreUpdate, update_hits.in_set(PickSet::Backend)); + .add_systems(PreUpdate, update_hits.in_set(PickingSystems::Backend)); } } diff --git a/crates/bevy_platform/Cargo.toml b/crates/bevy_platform/Cargo.toml index 3dc04396b2..44c680394d 100644 --- a/crates/bevy_platform/Cargo.toml +++ b/crates/bevy_platform/Cargo.toml @@ -46,7 +46,6 @@ critical-section = ["dep:critical-section", "portable-atomic/critical-section"] web = ["dep:web-time", "dep:getrandom"] [dependencies] -cfg-if = "1.0.0" critical-section = { version = "1.2.0", default-features = false, optional = true } spin = { version = "0.10.0", default-features = false, features = [ "mutex", diff --git a/crates/bevy_platform/src/cfg.rs b/crates/bevy_platform/src/cfg.rs new file mode 100644 index 0000000000..6f86ce1873 --- /dev/null +++ b/crates/bevy_platform/src/cfg.rs @@ -0,0 +1,264 @@ +//! Provides helpful configuration macros, allowing detection of platform features such as +//! [`alloc`](crate::cfg::alloc) or [`std`](crate::cfg::std) without explicit features. + +/// Provides a `match`-like expression similar to [`cfg_if`] and based on the experimental +/// [`cfg_match`]. +/// The name `switch` is used to avoid conflict with the `match` keyword. +/// Arms are evaluated top to bottom, and an optional wildcard arm can be provided if no match +/// can be made. +/// +/// An arm can either be: +/// - a `cfg(...)` pattern (e.g., `feature = "foo"`) +/// - a wildcard `_` +/// - an alias defined using [`define_alias`] +/// +/// Common aliases are provided by [`cfg`](crate::cfg). +/// Note that aliases are evaluated from the context of the defining crate, not the consumer. +/// +/// # Examples +/// +/// ``` +/// # use bevy_platform::cfg; +/// # fn log(_: &str) {} +/// # fn foo(_: &str) {} +/// # +/// cfg::switch! { +/// #[cfg(feature = "foo")] => { +/// foo("We have the `foo` feature!") +/// } +/// cfg::std => { +/// extern crate std; +/// std::println!("No `foo`, but we have `std`!"); +/// } +/// _ => { +/// log("Don't have `std` or `foo`"); +/// } +/// } +/// ``` +/// +/// [`cfg_if`]: https://crates.io/crates/cfg-if +/// [`cfg_match`]: https://github.com/rust-lang/rust/issues/115585 +#[doc(inline)] +pub use crate::switch; + +/// Defines an alias for a particular configuration. +/// This has two advantages over directly using `#[cfg(...)]`: +/// +/// 1. Complex configurations can be abbreviated to more meaningful shorthand. +/// 2. Features are evaluated in the context of the _defining_ crate, not the consuming. +/// +/// The second advantage is a particularly powerful tool, as it allows consuming crates to use +/// functionality in a defining crate regardless of what crate in the dependency graph enabled the +/// relevant feature. +/// +/// For example, consider a crate `foo` that depends on another crate `bar`. +/// `bar` has a feature "`faster_algorithms`". +/// If `bar` defines a "`faster_algorithms`" alias: +/// +/// ```ignore +/// define_alias! { +/// #[cfg(feature = "faster_algorithms")] => { faster_algorithms } +/// } +/// ``` +/// +/// Now, `foo` can gate its usage of those faster algorithms on the alias, avoiding the need to +/// expose its own "`faster_algorithms`" feature. +/// This also avoids the unfortunate situation where one crate activates "`faster_algorithms`" on +/// `bar` without activating that same feature on `foo`. +/// +/// Once an alias is defined, there are 4 ways you can use it: +/// +/// 1. Evaluate with no contents to return a `bool` indicating if the alias is active. +/// ``` +/// # use bevy_platform::cfg; +/// if cfg::std!() { +/// // Have `std`! +/// } else { +/// // No `std`... +/// } +/// ``` +/// 2. Pass a single code block which will only be compiled if the alias is active. +/// ``` +/// # use bevy_platform::cfg; +/// cfg::std! { +/// // Have `std`! +/// # () +/// } +/// ``` +/// 3. Pass a single `if { ... } else { ... }` expression to conditionally compile either the first +/// or the second code block. +/// ``` +/// # use bevy_platform::cfg; +/// cfg::std! { +/// if { +/// // Have `std`! +/// } else { +/// // No `std`... +/// } +/// } +/// ``` +/// 4. Use in a [`switch`] arm for more complex conditional compilation. +/// ``` +/// # use bevy_platform::cfg; +/// cfg::switch! { +/// cfg::std => { +/// // Have `std`! +/// } +/// cfg::alloc => { +/// // No `std`, but do have `alloc`! +/// } +/// _ => { +/// // No `std` or `alloc`... +/// } +/// } +/// ``` +#[doc(inline)] +pub use crate::define_alias; + +/// Macro which represents an enabled compilation condition. +#[doc(inline)] +pub use crate::enabled; + +/// Macro which represents a disabled compilation condition. +#[doc(inline)] +pub use crate::disabled; + +#[doc(hidden)] +#[macro_export] +macro_rules! switch { + ({ $($tt:tt)* }) => {{ + $crate::switch! { $($tt)* } + }}; + (_ => { $($output:tt)* }) => { + $($output)* + }; + ( + $cond:path => $output:tt + $($( $rest:tt )+)? + ) => { + $cond! { + if { + $crate::switch! { _ => $output } + } else { + $( + $crate::switch! { $($rest)+ } + )? + } + } + }; + ( + #[cfg($cfg:meta)] => $output:tt + $($( $rest:tt )+)? + ) => { + #[cfg($cfg)] + $crate::switch! { _ => $output } + $( + #[cfg(not($cfg))] + $crate::switch! { $($rest)+ } + )? + }; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! disabled { + () => { false }; + (if { $($p:tt)* } else { $($n:tt)* }) => { $($n)* }; + ($($p:tt)*) => {}; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! enabled { + () => { true }; + (if { $($p:tt)* } else { $($n:tt)* }) => { $($p)* }; + ($($p:tt)*) => { $($p)* }; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! define_alias { + ( + #[cfg($meta:meta)] => $p:ident + $(, $( $rest:tt )+)? + ) => { + $crate::define_alias! { + #[cfg($meta)] => { $p } + $( + $($rest)+ + )? + } + }; + ( + #[cfg($meta:meta)] => $p:ident, + $($( $rest:tt )+)? + ) => { + $crate::define_alias! { + #[cfg($meta)] => { $p } + $( + $($rest)+ + )? + } + }; + ( + #[cfg($meta:meta)] => { + $(#[$p_meta:meta])* + $p:ident + } + $($( $rest:tt )+)? + ) => { + $crate::switch! { + #[cfg($meta)] => { + $(#[$p_meta])* + #[doc(inline)] + /// + #[doc = concat!("This macro passes the provided code because `#[cfg(", stringify!($meta), ")]` is currently active.")] + pub use $crate::enabled as $p; + } + _ => { + $(#[$p_meta])* + #[doc(inline)] + /// + #[doc = concat!("This macro suppresses the provided code because `#[cfg(", stringify!($meta), ")]` is _not_ currently active.")] + pub use $crate::disabled as $p; + } + } + + $( + $crate::define_alias! { + $($rest)+ + } + )? + } +} + +define_alias! { + #[cfg(feature = "alloc")] => { + /// Indicates the `alloc` crate is available and can be used. + alloc + } + #[cfg(feature = "std")] => { + /// Indicates the `std` crate is available and can be used. + std + } + #[cfg(panic = "unwind")] => { + /// Indicates that a [`panic`] will be unwound, and can be potentially caught. + panic_unwind + } + #[cfg(panic = "abort")] => { + /// Indicates that a [`panic`] will lead to an abort, and cannot be caught. + panic_abort + } + #[cfg(all(target_arch = "wasm32", feature = "web"))] => { + /// Indicates that this target has access to browser APIs. + web + } + #[cfg(all(feature = "alloc", target_has_atomic = "ptr"))] => { + /// Indicates that this target has access to a native implementation of `Arc`. + arc + } + #[cfg(feature = "critical-section")] => { + /// Indicates `critical-section` is available. + critical_section + } +} diff --git a/crates/bevy_platform/src/lib.rs b/crates/bevy_platform/src/lib.rs index 96f2f9a21c..668442f299 100644 --- a/crates/bevy_platform/src/lib.rs +++ b/crates/bevy_platform/src/lib.rs @@ -9,20 +9,22 @@ //! //! [Bevy]: https://bevyengine.org/ -#[cfg(feature = "std")] -extern crate std; +cfg::std! { + extern crate std; +} -#[cfg(feature = "alloc")] -extern crate alloc; +cfg::alloc! { + extern crate alloc; + pub mod collections; +} + +pub mod cfg; pub mod hash; pub mod sync; pub mod thread; pub mod time; -#[cfg(feature = "alloc")] -pub mod collections; - /// Frequently used items which would typically be included in most contexts. /// /// When adding `no_std` support to a crate for the first time, often there's a substantial refactor @@ -33,10 +35,11 @@ pub mod collections; /// This prelude aims to ease the transition by re-exporting items from `alloc` which would /// otherwise be included in the `std` implicit prelude. pub mod prelude { - #[cfg(feature = "alloc")] - pub use alloc::{ - borrow::ToOwned, boxed::Box, format, string::String, string::ToString, vec, vec::Vec, - }; + crate::cfg::alloc! { + pub use alloc::{ + borrow::ToOwned, boxed::Box, format, string::String, string::ToString, vec, vec::Vec, + }; + } // Items from `std::prelude` that are missing in this module: // * dbg diff --git a/crates/bevy_platform/src/sync/mod.rs b/crates/bevy_platform/src/sync/mod.rs index 8fb7a2fbff..79ceff7ee8 100644 --- a/crates/bevy_platform/src/sync/mod.rs +++ b/crates/bevy_platform/src/sync/mod.rs @@ -14,8 +14,17 @@ pub use once::{Once, OnceLock, OnceState}; pub use poison::{LockResult, PoisonError, TryLockError, TryLockResult}; pub use rwlock::{RwLock, RwLockReadGuard, RwLockWriteGuard}; -#[cfg(feature = "alloc")] -pub use arc::{Arc, Weak}; +crate::cfg::alloc! { + pub use arc::{Arc, Weak}; + + crate::cfg::arc! { + if { + use alloc::sync as arc; + } else { + use portable_atomic_util as arc; + } + } +} pub mod atomic; @@ -25,9 +34,3 @@ mod mutex; mod once; mod poison; mod rwlock; - -#[cfg(all(feature = "alloc", not(target_has_atomic = "ptr")))] -use portable_atomic_util as arc; - -#[cfg(all(feature = "alloc", target_has_atomic = "ptr"))] -use alloc::sync as arc; diff --git a/crates/bevy_platform/src/thread.rs b/crates/bevy_platform/src/thread.rs index e1d593c90b..6e4650382e 100644 --- a/crates/bevy_platform/src/thread.rs +++ b/crates/bevy_platform/src/thread.rs @@ -2,11 +2,13 @@ pub use thread::sleep; -cfg_if::cfg_if! { +crate::cfg::switch! { // TODO: use browser timeouts based on ScheduleRunnerPlugin::build - if #[cfg(feature = "std")] { + // crate::cfg::web => { ... } + crate::cfg::std => { use std::thread; - } else { + } + _ => { mod fallback { use core::{hint::spin_loop, time::Duration}; diff --git a/crates/bevy_platform/src/time/fallback.rs b/crates/bevy_platform/src/time/fallback.rs index c438e6e379..c649f6a49d 100644 --- a/crates/bevy_platform/src/time/fallback.rs +++ b/crates/bevy_platform/src/time/fallback.rs @@ -149,28 +149,31 @@ impl fmt::Debug for Instant { } fn unset_getter() -> Duration { - cfg_if::cfg_if! { - if #[cfg(target_arch = "x86")] { + crate::cfg::switch! { + #[cfg(target_arch = "x86")] => { // SAFETY: standard technique for getting a nanosecond counter on x86 let nanos = unsafe { core::arch::x86::_rdtsc() }; - Duration::from_nanos(nanos) - } else if #[cfg(target_arch = "x86_64")] { + return Duration::from_nanos(nanos); + } + #[cfg(target_arch = "x86_64")] => { // SAFETY: standard technique for getting a nanosecond counter on x86_64 let nanos = unsafe { core::arch::x86_64::_rdtsc() }; - Duration::from_nanos(nanos) - } else if #[cfg(target_arch = "aarch64")] { + return Duration::from_nanos(nanos); + } + #[cfg(target_arch = "aarch64")] => { // SAFETY: standard technique for getting a nanosecond counter of aarch64 let nanos = unsafe { let mut ticks: u64; core::arch::asm!("mrs {}, cntvct_el0", out(reg) ticks); ticks }; - Duration::from_nanos(nanos) - } else { + return Duration::from_nanos(nanos); + } + _ => { panic!("An elapsed time getter has not been provided to `Instant`. Please use `Instant::set_elapsed(...)` before calling `Instant::now()`") } } diff --git a/crates/bevy_platform/src/time/mod.rs b/crates/bevy_platform/src/time/mod.rs index 260d8e4aea..10b9d3d131 100644 --- a/crates/bevy_platform/src/time/mod.rs +++ b/crates/bevy_platform/src/time/mod.rs @@ -2,12 +2,14 @@ pub use time::Instant; -cfg_if::cfg_if! { - if #[cfg(all(target_arch = "wasm32", feature = "web"))] { +crate::cfg::switch! { + crate::cfg::web => { use web_time as time; - } else if #[cfg(feature = "std")] { + } + crate::cfg::std => { use std::time; - } else { + } + _ => { mod fallback; use fallback as time; diff --git a/crates/bevy_reflect/src/array.rs b/crates/bevy_reflect/src/array.rs index 9ad906cfce..8be0110a3e 100644 --- a/crates/bevy_reflect/src/array.rs +++ b/crates/bevy_reflect/src/array.rs @@ -68,12 +68,6 @@ pub trait Array: PartialReflect { /// Drain the elements of this array to get a vector of owned values. fn drain(self: Box) -> Vec>; - /// Clones the list, producing a [`DynamicArray`]. - #[deprecated(since = "0.16.0", note = "use `to_dynamic_array` instead")] - fn clone_dynamic(&self) -> DynamicArray { - self.to_dynamic_array() - } - /// Creates a new [`DynamicArray`] from this array. fn to_dynamic_array(&self) -> DynamicArray { DynamicArray { diff --git a/crates/bevy_reflect/src/enums/dynamic_enum.rs b/crates/bevy_reflect/src/enums/dynamic_enum.rs index 3380921fbe..42c20e1956 100644 --- a/crates/bevy_reflect/src/enums/dynamic_enum.rs +++ b/crates/bevy_reflect/src/enums/dynamic_enum.rs @@ -280,15 +280,6 @@ impl Enum for DynamicEnum { DynamicVariant::Struct(..) => VariantType::Struct, } } - - fn clone_dynamic(&self) -> DynamicEnum { - Self { - represented_type: self.represented_type, - variant_index: self.variant_index, - variant_name: self.variant_name.clone(), - variant: self.variant.clone(), - } - } } impl PartialReflect for DynamicEnum { diff --git a/crates/bevy_reflect/src/enums/enum_trait.rs b/crates/bevy_reflect/src/enums/enum_trait.rs index bcbcb300d5..126c407f23 100644 --- a/crates/bevy_reflect/src/enums/enum_trait.rs +++ b/crates/bevy_reflect/src/enums/enum_trait.rs @@ -124,11 +124,6 @@ pub trait Enum: PartialReflect { fn variant_index(&self) -> usize; /// The type of the current variant. fn variant_type(&self) -> VariantType; - // Clones the enum into a [`DynamicEnum`]. - #[deprecated(since = "0.16.0", note = "use `to_dynamic_enum` instead")] - fn clone_dynamic(&self) -> DynamicEnum { - self.to_dynamic_enum() - } /// Creates a new [`DynamicEnum`] from this enum. fn to_dynamic_enum(&self) -> DynamicEnum { DynamicEnum::from_ref(self) diff --git a/crates/bevy_reflect/src/func/function.rs b/crates/bevy_reflect/src/func/function.rs index eb770e9e50..29fa6bf7cf 100644 --- a/crates/bevy_reflect/src/func/function.rs +++ b/crates/bevy_reflect/src/func/function.rs @@ -63,12 +63,6 @@ pub trait Function: PartialReflect + Debug { /// Call this function with the given arguments. fn reflect_call<'a>(&self, args: ArgList<'a>) -> FunctionResult<'a>; - /// Clone this function into a [`DynamicFunction`]. - #[deprecated(since = "0.16.0", note = "use `to_dynamic_function` instead")] - fn clone_dynamic(&self) -> DynamicFunction<'static> { - self.to_dynamic_function() - } - /// Creates a new [`DynamicFunction`] from this function. fn to_dynamic_function(&self) -> DynamicFunction<'static>; } diff --git a/crates/bevy_reflect/src/impls/std.rs b/crates/bevy_reflect/src/impls/std.rs index 6a752d1877..0115afbf79 100644 --- a/crates/bevy_reflect/src/impls/std.rs +++ b/crates/bevy_reflect/src/impls/std.rs @@ -1319,21 +1319,6 @@ where result } - fn clone_dynamic(&self) -> DynamicMap { - let mut dynamic_map = DynamicMap::default(); - dynamic_map.set_represented_type(self.get_represented_type_info()); - for (k, v) in self { - let key = K::from_reflect(k).unwrap_or_else(|| { - panic!( - "Attempted to clone invalid key of type {}.", - k.reflect_type_path() - ) - }); - dynamic_map.insert_boxed(Box::new(key), v.to_dynamic()); - } - dynamic_map - } - fn insert_boxed( &mut self, key: Box, diff --git a/crates/bevy_reflect/src/impls/uuid.rs b/crates/bevy_reflect/src/impls/uuid.rs index 7385304e28..502b64c898 100644 --- a/crates/bevy_reflect/src/impls/uuid.rs +++ b/crates/bevy_reflect/src/impls/uuid.rs @@ -10,3 +10,12 @@ impl_reflect_opaque!(::uuid::Uuid( PartialEq, Hash )); + +impl_reflect_opaque!(::uuid::NonNilUuid( + Serialize, + Deserialize, + Clone, + Debug, + PartialEq, + Hash +)); diff --git a/crates/bevy_reflect/src/list.rs b/crates/bevy_reflect/src/list.rs index 2e1c085676..7e768b8f1b 100644 --- a/crates/bevy_reflect/src/list.rs +++ b/crates/bevy_reflect/src/list.rs @@ -103,12 +103,6 @@ pub trait List: PartialReflect { /// [`Vec`] will match the order of items in `self`. fn drain(&mut self) -> Vec>; - /// Clones the list, producing a [`DynamicList`]. - #[deprecated(since = "0.16.0", note = "use `to_dynamic_list` instead")] - fn clone_dynamic(&self) -> DynamicList { - self.to_dynamic_list() - } - /// Creates a new [`DynamicList`] from this list. fn to_dynamic_list(&self) -> DynamicList { DynamicList { diff --git a/crates/bevy_reflect/src/map.rs b/crates/bevy_reflect/src/map.rs index 0a1c0b689a..e96537e67d 100644 --- a/crates/bevy_reflect/src/map.rs +++ b/crates/bevy_reflect/src/map.rs @@ -81,12 +81,6 @@ pub trait Map: PartialReflect { /// After calling this function, `self` will be empty. fn drain(&mut self) -> Vec<(Box, Box)>; - /// Clones the map, producing a [`DynamicMap`]. - #[deprecated(since = "0.16.0", note = "use `to_dynamic_map` instead")] - fn clone_dynamic(&self) -> DynamicMap { - self.to_dynamic_map() - } - /// Creates a new [`DynamicMap`] from this map. fn to_dynamic_map(&self) -> DynamicMap { let mut map = DynamicMap::default(); diff --git a/crates/bevy_reflect/src/reflect.rs b/crates/bevy_reflect/src/reflect.rs index 4918179e12..2adfb6db6c 100644 --- a/crates/bevy_reflect/src/reflect.rs +++ b/crates/bevy_reflect/src/reflect.rs @@ -218,42 +218,6 @@ where /// See [`ReflectOwned`]. fn reflect_owned(self: Box) -> ReflectOwned; - /// Clones `Self` into its dynamic representation. - /// - /// For value types or types marked with `#[reflect_value]`, - /// this will simply return a clone of `Self`. - /// - /// Otherwise the associated dynamic type will be returned. - /// - /// For example, a [`List`] type will invoke [`List::clone_dynamic`], returning [`DynamicList`]. - /// A [`Struct`] type will invoke [`Struct::clone_dynamic`], returning [`DynamicStruct`]. - /// And so on. - /// - /// If the dynamic behavior is not desired, a concrete clone can be obtained using [`PartialReflect::reflect_clone`]. - /// - /// # Example - /// - /// ``` - /// # use bevy_reflect::{PartialReflect}; - /// let value = (1, true, 3.14); - /// let cloned = value.clone_value(); - /// assert!(cloned.is_dynamic()) - /// ``` - /// - /// [`List`]: crate::List - /// [`List::clone_dynamic`]: crate::List::clone_dynamic - /// [`DynamicList`]: crate::DynamicList - /// [`Struct`]: crate::Struct - /// [`Struct::clone_dynamic`]: crate::Struct::clone_dynamic - /// [`DynamicStruct`]: crate::DynamicStruct - #[deprecated( - since = "0.16.0", - note = "to clone reflected values, prefer using `reflect_clone`. To convert reflected values to dynamic ones, use `to_dynamic`." - )] - fn clone_value(&self) -> Box { - self.to_dynamic() - } - /// Converts this reflected value into its dynamic representation based on its [kind]. /// /// For example, a [`List`] type will internally invoke [`List::to_dynamic_list`], returning [`DynamicList`]. diff --git a/crates/bevy_reflect/src/set.rs b/crates/bevy_reflect/src/set.rs index 753662b603..b1b9147e4e 100644 --- a/crates/bevy_reflect/src/set.rs +++ b/crates/bevy_reflect/src/set.rs @@ -67,12 +67,6 @@ pub trait Set: PartialReflect { /// After calling this function, `self` will be empty. fn drain(&mut self) -> Vec>; - /// Clones the set, producing a [`DynamicSet`]. - #[deprecated(since = "0.16.0", note = "use `to_dynamic_set` instead")] - fn clone_dynamic(&self) -> DynamicSet { - self.to_dynamic_set() - } - /// Creates a new [`DynamicSet`] from this set. fn to_dynamic_set(&self) -> DynamicSet { let mut set = DynamicSet::default(); diff --git a/crates/bevy_reflect/src/struct_trait.rs b/crates/bevy_reflect/src/struct_trait.rs index 9146e9aece..b6284a8d79 100644 --- a/crates/bevy_reflect/src/struct_trait.rs +++ b/crates/bevy_reflect/src/struct_trait.rs @@ -71,12 +71,6 @@ pub trait Struct: PartialReflect { /// Returns an iterator over the values of the reflectable fields for this struct. fn iter_fields(&self) -> FieldIter; - /// Clones the struct into a [`DynamicStruct`]. - #[deprecated(since = "0.16.0", note = "use `to_dynamic_struct` instead")] - fn clone_dynamic(&self) -> DynamicStruct { - self.to_dynamic_struct() - } - fn to_dynamic_struct(&self) -> DynamicStruct { let mut dynamic_struct = DynamicStruct::default(); dynamic_struct.set_represented_type(self.get_represented_type_info()); diff --git a/crates/bevy_reflect/src/tuple.rs b/crates/bevy_reflect/src/tuple.rs index 31ad67fdcf..9f81d274ae 100644 --- a/crates/bevy_reflect/src/tuple.rs +++ b/crates/bevy_reflect/src/tuple.rs @@ -55,12 +55,6 @@ pub trait Tuple: PartialReflect { /// Drain the fields of this tuple to get a vector of owned values. fn drain(self: Box) -> Vec>; - /// Clones the tuple into a [`DynamicTuple`]. - #[deprecated(since = "0.16.0", note = "use `to_dynamic_tuple` instead")] - fn clone_dynamic(&self) -> DynamicTuple { - self.to_dynamic_tuple() - } - /// Creates a new [`DynamicTuple`] from this tuple. fn to_dynamic_tuple(&self) -> DynamicTuple { DynamicTuple { diff --git a/crates/bevy_reflect/src/tuple_struct.rs b/crates/bevy_reflect/src/tuple_struct.rs index 09d2819807..410a794f68 100644 --- a/crates/bevy_reflect/src/tuple_struct.rs +++ b/crates/bevy_reflect/src/tuple_struct.rs @@ -55,12 +55,6 @@ pub trait TupleStruct: PartialReflect { /// Returns an iterator over the values of the tuple struct's fields. fn iter_fields(&self) -> TupleStructFieldIter; - /// Clones the struct into a [`DynamicTupleStruct`]. - #[deprecated(since = "0.16.0", note = "use `to_dynamic_tuple_struct` instead")] - fn clone_dynamic(&self) -> DynamicTupleStruct { - self.to_dynamic_tuple_struct() - } - /// Creates a new [`DynamicTupleStruct`] from this tuple struct. fn to_dynamic_tuple_struct(&self) -> DynamicTupleStruct { DynamicTupleStruct { diff --git a/crates/bevy_remote/src/builtin_methods.rs b/crates/bevy_remote/src/builtin_methods.rs index ce5fa259a1..b59f08604b 100644 --- a/crates/bevy_remote/src/builtin_methods.rs +++ b/crates/bevy_remote/src/builtin_methods.rs @@ -1490,13 +1490,14 @@ mod tests { "Deserialized value does not match original" ); } + use super::*; #[test] fn serialization_tests() { test_serialize_deserialize(BrpQueryRow { components: Default::default(), - entity: Entity::from_raw(0), + entity: Entity::from_raw_u32(0).unwrap(), has: Default::default(), }); test_serialize_deserialize(BrpListWatchingResponse::default()); @@ -1510,7 +1511,7 @@ mod tests { ..Default::default() }); test_serialize_deserialize(BrpListParams { - entity: Entity::from_raw(0), + entity: Entity::from_raw_u32(0).unwrap(), }); } } diff --git a/crates/bevy_remote/src/lib.rs b/crates/bevy_remote/src/lib.rs index b21fb97bbb..97b2e453e7 100644 --- a/crates/bevy_remote/src/lib.rs +++ b/crates/bevy_remote/src/lib.rs @@ -543,15 +543,15 @@ impl Plugin for RemotePlugin { .add_systems(PreStartup, setup_mailbox_channel) .configure_sets( RemoteLast, - (RemoteSet::ProcessRequests, RemoteSet::Cleanup).chain(), + (RemoteSystems::ProcessRequests, RemoteSystems::Cleanup).chain(), ) .add_systems( RemoteLast, ( (process_remote_requests, process_ongoing_watching_requests) .chain() - .in_set(RemoteSet::ProcessRequests), - remove_closed_watching_requests.in_set(RemoteSet::Cleanup), + .in_set(RemoteSystems::ProcessRequests), + remove_closed_watching_requests.in_set(RemoteSystems::Cleanup), ), ); } @@ -565,13 +565,17 @@ pub struct RemoteLast; /// /// These can be useful for ordering. #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] -pub enum RemoteSet { +pub enum RemoteSystems { /// Processing of remote requests. ProcessRequests, /// Cleanup (remove closed watchers etc) Cleanup, } +/// Deprecated alias for [`RemoteSystems`]. +#[deprecated(since = "0.17.0", note = "Renamed to `RemoteSystems`.")] +pub type RemoteSet = RemoteSystems; + /// A type to hold the allowed types of systems to be used as method handlers. #[derive(Debug)] pub enum RemoteMethodHandler { diff --git a/crates/bevy_render/Cargo.toml b/crates/bevy_render/Cargo.toml index 5da61a57dd..aa6b6e239c 100644 --- a/crates/bevy_render/Cargo.toml +++ b/crates/bevy_render/Cargo.toml @@ -97,7 +97,7 @@ downcast-rs = { version = "2", default-features = false, features = ["std"] } thiserror = { version = "2", default-features = false } derive_more = { version = "1", default-features = false, features = ["from"] } futures-lite = "2.0.1" -ktx2 = { version = "0.3.0", optional = true } +ktx2 = { version = "0.4.0", optional = true } encase = { version = "0.10", features = ["glam"] } # For wgpu profiling using tracing. Use `RUST_LOG=info` to also capture the wgpu spans. profiling = { version = "1", features = [ diff --git a/crates/bevy_render/src/batching/gpu_preprocessing.rs b/crates/bevy_render/src/batching/gpu_preprocessing.rs index 7de55ee022..ea5970431a 100644 --- a/crates/bevy_render/src/batching/gpu_preprocessing.rs +++ b/crates/bevy_render/src/batching/gpu_preprocessing.rs @@ -36,7 +36,7 @@ use crate::{ renderer::{RenderAdapter, RenderDevice, RenderQueue}, sync_world::MainEntity, view::{ExtractedView, NoIndirectDrawing, RetainedViewEntity}, - Render, RenderApp, RenderDebugFlags, RenderSet, + Render, RenderApp, RenderDebugFlags, RenderSystems, }; use super::{BatchMeta, GetBatchData, GetFullBatchData}; @@ -60,11 +60,11 @@ impl Plugin for BatchingPlugin { )) .add_systems( Render, - write_indirect_parameters_buffers.in_set(RenderSet::PrepareResourcesFlush), + write_indirect_parameters_buffers.in_set(RenderSystems::PrepareResourcesFlush), ) .add_systems( Render, - clear_indirect_parameters_buffers.in_set(RenderSet::ManageViews), + clear_indirect_parameters_buffers.in_set(RenderSystems::ManageViews), ); } @@ -392,6 +392,10 @@ where } /// The buffer of GPU preprocessing work items for a single view. +#[expect( + clippy::large_enum_variant, + reason = "See https://github.com/bevyengine/bevy/issues/19220" +)] pub enum PreprocessWorkItemBuffers { /// The work items we use if we aren't using indirect drawing. /// diff --git a/crates/bevy_render/src/camera/mod.rs b/crates/bevy_render/src/camera/mod.rs index 4c77021bad..a2470a7660 100644 --- a/crates/bevy_render/src/camera/mod.rs +++ b/crates/bevy_render/src/camera/mod.rs @@ -12,7 +12,7 @@ pub use projection::*; use crate::{ extract_component::ExtractComponentPlugin, extract_resource::ExtractResourcePlugin, - render_graph::RenderGraph, ExtractSchedule, Render, RenderApp, RenderSet, + render_graph::RenderGraph, ExtractSchedule, Render, RenderApp, RenderSystems, }; use bevy_app::{App, Plugin}; use bevy_ecs::schedule::IntoScheduleConfigs; @@ -42,7 +42,7 @@ impl Plugin for CameraPlugin { render_app .init_resource::() .add_systems(ExtractSchedule, extract_cameras) - .add_systems(Render, sort_cameras.in_set(RenderSet::ManageViews)); + .add_systems(Render, sort_cameras.in_set(RenderSystems::ManageViews)); let camera_driver_node = CameraDriverNode::new(render_app.world_mut()); let mut render_graph = render_app.world_mut().resource_mut::(); render_graph.add_node(crate::graph::CameraDriverLabel, camera_driver_node); diff --git a/crates/bevy_render/src/camera/projection.rs b/crates/bevy_render/src/camera/projection.rs index e3f95cb036..a7796a1d1a 100644 --- a/crates/bevy_render/src/camera/projection.rs +++ b/crates/bevy_render/src/camera/projection.rs @@ -2,12 +2,12 @@ use core::fmt::Debug; use crate::{primitives::Frustum, view::VisibilitySystems}; use bevy_app::{App, Plugin, PostStartup, PostUpdate}; -use bevy_asset::AssetEvents; +use bevy_asset::AssetEventSystems; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::prelude::*; use bevy_math::{ops, AspectRatio, Mat4, Rect, Vec2, Vec3A, Vec4}; use bevy_reflect::{std_traits::ReflectDefault, Reflect, ReflectDeserialize, ReflectSerialize}; -use bevy_transform::{components::GlobalTransform, TransformSystem}; +use bevy_transform::{components::GlobalTransform, TransformSystems}; use derive_more::derive::From; use serde::{Deserialize, Serialize}; @@ -25,18 +25,18 @@ impl Plugin for CameraProjectionPlugin { .register_type::() .add_systems( PostStartup, - crate::camera::camera_system.in_set(CameraUpdateSystem), + crate::camera::camera_system.in_set(CameraUpdateSystems), ) .add_systems( PostUpdate, ( crate::camera::camera_system - .in_set(CameraUpdateSystem) - .before(AssetEvents), + .in_set(CameraUpdateSystems) + .before(AssetEventSystems), crate::view::update_frusta .in_set(VisibilitySystems::UpdateFrusta) .after(crate::camera::camera_system) - .after(TransformSystem::TransformPropagate), + .after(TransformSystems::Propagate), ), ); } @@ -46,7 +46,11 @@ impl Plugin for CameraProjectionPlugin { /// /// [`camera_system`]: crate::camera::camera_system #[derive(SystemSet, Clone, Eq, PartialEq, Hash, Debug)] -pub struct CameraUpdateSystem; +pub struct CameraUpdateSystems; + +/// Deprecated alias for [`CameraUpdateSystems`]. +#[deprecated(since = "0.17.0", note = "Renamed to `CameraUpdateSystems`.")] +pub type CameraUpdateSystem = CameraUpdateSystems; /// Describes a type that can generate a projection matrix, allowing it to be added to a /// [`Camera`]'s [`Projection`] component. diff --git a/crates/bevy_render/src/extract_component.rs b/crates/bevy_render/src/extract_component.rs index 19d15a2b86..e1f528d6ab 100644 --- a/crates/bevy_render/src/extract_component.rs +++ b/crates/bevy_render/src/extract_component.rs @@ -4,7 +4,7 @@ use crate::{ sync_component::SyncComponentPlugin, sync_world::RenderEntity, view::ViewVisibility, - Extract, ExtractSchedule, Render, RenderApp, RenderSet, + Extract, ExtractSchedule, Render, RenderApp, RenderSystems, }; use bevy_app::{App, Plugin}; use bevy_ecs::{ @@ -70,7 +70,7 @@ pub trait ExtractComponent: Component { /// For referencing the newly created uniforms a [`DynamicUniformIndex`] is inserted /// for every processed entity. /// -/// Therefore it sets up the [`RenderSet::Prepare`] step +/// Therefore it sets up the [`RenderSystems::Prepare`] step /// for the specified [`ExtractComponent`]. pub struct UniformComponentPlugin(PhantomData C>); @@ -87,7 +87,7 @@ impl Plugin for UniformComponentP .insert_resource(ComponentUniforms::::default()) .add_systems( Render, - prepare_uniform_components::.in_set(RenderSet::PrepareResources), + prepare_uniform_components::.in_set(RenderSystems::PrepareResources), ); } } diff --git a/crates/bevy_render/src/globals.rs b/crates/bevy_render/src/globals.rs index c05d96c4c9..49755f4098 100644 --- a/crates/bevy_render/src/globals.rs +++ b/crates/bevy_render/src/globals.rs @@ -3,7 +3,7 @@ use crate::{ prelude::Shader, render_resource::{ShaderType, UniformBuffer}, renderer::{RenderDevice, RenderQueue}, - Extract, ExtractSchedule, Render, RenderApp, RenderSet, + Extract, ExtractSchedule, Render, RenderApp, RenderSystems, }; use bevy_app::{App, Plugin}; use bevy_asset::{load_internal_asset, weak_handle, Handle}; @@ -29,7 +29,7 @@ impl Plugin for GlobalsPlugin { .add_systems(ExtractSchedule, (extract_frame_count, extract_time)) .add_systems( Render, - prepare_globals_buffer.in_set(RenderSet::PrepareResources), + prepare_globals_buffer.in_set(RenderSystems::PrepareResources), ); } } diff --git a/crates/bevy_render/src/gpu_component_array_buffer.rs b/crates/bevy_render/src/gpu_component_array_buffer.rs index b3f78f5bfb..b4eb56197f 100644 --- a/crates/bevy_render/src/gpu_component_array_buffer.rs +++ b/crates/bevy_render/src/gpu_component_array_buffer.rs @@ -1,7 +1,7 @@ use crate::{ render_resource::{GpuArrayBuffer, GpuArrayBufferable}, renderer::{RenderDevice, RenderQueue}, - Render, RenderApp, RenderSet, + Render, RenderApp, RenderSystems, }; use bevy_app::{App, Plugin}; use bevy_ecs::{ @@ -20,7 +20,7 @@ impl Plugin for GpuComponentArrayBufferPlugin if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app.add_systems( Render, - prepare_gpu_component_array_buffers::.in_set(RenderSet::PrepareResources), + prepare_gpu_component_array_buffers::.in_set(RenderSystems::PrepareResources), ); } } diff --git a/crates/bevy_render/src/gpu_readback.rs b/crates/bevy_render/src/gpu_readback.rs index 02f0c2d1db..c05861f3da 100644 --- a/crates/bevy_render/src/gpu_readback.rs +++ b/crates/bevy_render/src/gpu_readback.rs @@ -9,7 +9,7 @@ use crate::{ storage::{GpuShaderStorageBuffer, ShaderStorageBuffer}, sync_world::MainEntity, texture::GpuImage, - ExtractSchedule, MainWorld, Render, RenderApp, RenderSet, + ExtractSchedule, MainWorld, Render, RenderApp, RenderSystems, }; use async_channel::{Receiver, Sender}; use bevy_app::{App, Plugin}; @@ -60,8 +60,10 @@ impl Plugin for GpuReadbackPlugin { .add_systems( Render, ( - prepare_buffers.in_set(RenderSet::PrepareResources), - map_buffers.after(render_system).in_set(RenderSet::Render), + prepare_buffers.in_set(RenderSystems::PrepareResources), + map_buffers + .after(render_system) + .in_set(RenderSystems::Render), ), ); } diff --git a/crates/bevy_render/src/lib.rs b/crates/bevy_render/src/lib.rs index 843bb68284..bad447bffe 100644 --- a/crates/bevy_render/src/lib.rs +++ b/crates/bevy_render/src/lib.rs @@ -72,6 +72,12 @@ pub mod prelude { }; } use batching::gpu_preprocessing::BatchingPlugin; + +#[doc(hidden)] +pub mod _macro { + pub use bevy_asset; +} + use bevy_ecs::schedule::ScheduleBuildSettings; use bevy_utils::prelude::default; pub use extract_param::Extract; @@ -102,13 +108,31 @@ use crate::{ }; use alloc::sync::Arc; use bevy_app::{App, AppLabel, Plugin, SubApp}; -use bevy_asset::{load_internal_asset, weak_handle, AssetApp, AssetServer, Handle}; +use bevy_asset::{AssetApp, AssetServer}; use bevy_ecs::{prelude::*, schedule::ScheduleLabel}; use bitflags::bitflags; use core::ops::{Deref, DerefMut}; use std::sync::Mutex; use tracing::debug; +/// Inline shader as an `embedded_asset` and load it permanently. +/// +/// This works around a limitation of the shader loader not properly loading +/// dependencies of shaders. +#[macro_export] +macro_rules! load_shader_library { + ($asset_server_provider: expr, $path: literal $(, $settings: expr)?) => { + $crate::_macro::bevy_asset::embedded_asset!($asset_server_provider, $path); + let handle: $crate::_macro::bevy_asset::prelude::Handle<$crate::prelude::Shader> = + $crate::_macro::bevy_asset::load_embedded_asset!( + $asset_server_provider, + $path + $(,$settings)? + ); + core::mem::forget(handle); + } +} + /// Contains the default Bevy rendering backend based on wgpu. /// /// Rendering is done in a [`SubApp`], which exchanges data with the main app @@ -144,7 +168,7 @@ bitflags! { /// /// These can be useful for ordering, but you almost never want to add your systems to these sets. #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] -pub enum RenderSet { +pub enum RenderSystems { /// This is used for applying the commands from the [`ExtractSchedule`] ExtractCommands, /// Prepare assets that have been created/modified/removed this frame. @@ -156,9 +180,9 @@ pub enum RenderSet { /// Queue drawable entities as phase items in render phases ready for /// sorting (if necessary) Queue, - /// A sub-set within [`Queue`](RenderSet::Queue) where mesh entity queue systems are executed. Ensures `prepare_assets::` is completed. + /// A sub-set within [`Queue`](RenderSystems::Queue) where mesh entity queue systems are executed. Ensures `prepare_assets::` is completed. QueueMeshes, - /// A sub-set within [`Queue`](RenderSet::Queue) where meshes that have + /// A sub-set within [`Queue`](RenderSystems::Queue) where meshes that have /// become invisible or changed phases are removed from the bins. QueueSweep, // TODO: This could probably be moved in favor of a system ordering @@ -169,14 +193,14 @@ pub enum RenderSet { /// Prepare render resources from extracted data for the GPU based on their sorted order. /// Create [`BindGroups`](render_resource::BindGroup) that depend on those data. Prepare, - /// A sub-set within [`Prepare`](RenderSet::Prepare) for initializing buffers, textures and uniforms for use in bind groups. + /// A sub-set within [`Prepare`](RenderSystems::Prepare) for initializing buffers, textures and uniforms for use in bind groups. PrepareResources, /// Collect phase buffers after - /// [`PrepareResources`](RenderSet::PrepareResources) has run. + /// [`PrepareResources`](RenderSystems::PrepareResources) has run. PrepareResourcesCollectPhaseBuffers, - /// Flush buffers after [`PrepareResources`](RenderSet::PrepareResources), but before [`PrepareBindGroups`](RenderSet::PrepareBindGroups). + /// Flush buffers after [`PrepareResources`](RenderSystems::PrepareResources), but before [`PrepareBindGroups`](RenderSystems::PrepareBindGroups). PrepareResourcesFlush, - /// A sub-set within [`Prepare`](RenderSet::Prepare) for constructing bind groups, or other data that relies on render resources prepared in [`PrepareResources`](RenderSet::PrepareResources). + /// A sub-set within [`Prepare`](RenderSystems::Prepare) for constructing bind groups, or other data that relies on render resources prepared in [`PrepareResources`](RenderSystems::PrepareResources). PrepareBindGroups, /// Actual rendering happens here. /// In most cases, only the render backend should insert resources here. @@ -185,10 +209,14 @@ pub enum RenderSet { Cleanup, /// Final cleanup occurs: all entities will be despawned. /// - /// Runs after [`Cleanup`](RenderSet::Cleanup). + /// Runs after [`Cleanup`](RenderSystems::Cleanup). PostCleanup, } +/// Deprecated alias for [`RenderSystems`]. +#[deprecated(since = "0.17.0", note = "Renamed to `RenderSystems`.")] +pub type RenderSet = RenderSystems; + /// The main render schedule. #[derive(ScheduleLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] pub struct Render; @@ -198,7 +226,7 @@ impl Render { /// /// The sets defined in this enum are configured to run in order. pub fn base_schedule() -> Schedule { - use RenderSet::*; + use RenderSystems::*; let mut schedule = Schedule::new(Self); @@ -285,15 +313,8 @@ struct FutureRenderResources(Arc>>); #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, AppLabel)] pub struct RenderApp; -pub const MATHS_SHADER_HANDLE: Handle = - weak_handle!("d94d70d4-746d-49c4-bfc3-27d63f2acda0"); -pub const COLOR_OPERATIONS_SHADER_HANDLE: Handle = - weak_handle!("33a80b2f-aaf7-4c86-b828-e7ae83b72f1a"); -pub const BINDLESS_SHADER_HANDLE: Handle = - weak_handle!("13f1baaa-41bf-448e-929e-258f9307a522"); - impl Plugin for RenderPlugin { - /// Initializes the renderer, sets up the [`RenderSet`] and creates the rendering sub-app. + /// Initializes the renderer, sets up the [`RenderSystems`] and creates the rendering sub-app. fn build(&self, app: &mut App) { app.init_asset::() .init_asset_loader::(); @@ -417,7 +438,7 @@ impl Plugin for RenderPlugin { .add_systems(ExtractSchedule, extract_render_asset_bytes_per_frame) .add_systems( Render, - reset_render_asset_bytes_per_frame.in_set(RenderSet::Cleanup), + reset_render_asset_bytes_per_frame.in_set(RenderSystems::Cleanup), ); } @@ -439,19 +460,9 @@ impl Plugin for RenderPlugin { } fn finish(&self, app: &mut App) { - load_internal_asset!(app, MATHS_SHADER_HANDLE, "maths.wgsl", Shader::from_wgsl); - load_internal_asset!( - app, - COLOR_OPERATIONS_SHADER_HANDLE, - "color_operations.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - BINDLESS_SHADER_HANDLE, - "bindless.wgsl", - Shader::from_wgsl - ); + load_shader_library!(app, "maths.wgsl"); + load_shader_library!(app, "color_operations.wgsl"); + load_shader_library!(app, "bindless.wgsl"); if let Some(future_render_resources) = app.world_mut().remove_resource::() { @@ -528,11 +539,11 @@ unsafe fn initialize_render_app(app: &mut App) { ( // This set applies the commands from the extract schedule while the render schedule // is running in parallel with the main app. - apply_extract_commands.in_set(RenderSet::ExtractCommands), + apply_extract_commands.in_set(RenderSystems::ExtractCommands), (PipelineCache::process_pipeline_queue_system, render_system) .chain() - .in_set(RenderSet::Render), - despawn_temporary_render_entities.in_set(RenderSet::PostCleanup), + .in_set(RenderSystems::Render), + despawn_temporary_render_entities.in_set(RenderSystems::PostCleanup), ), ); diff --git a/crates/bevy_render/src/mesh/allocator.rs b/crates/bevy_render/src/mesh/allocator.rs index bc638859ed..eb2d4de626 100644 --- a/crates/bevy_render/src/mesh/allocator.rs +++ b/crates/bevy_render/src/mesh/allocator.rs @@ -30,7 +30,7 @@ use crate::{ render_asset::{prepare_assets, ExtractedAssets}, render_resource::Buffer, renderer::{RenderAdapter, RenderDevice, RenderQueue}, - Render, RenderApp, RenderSet, + Render, RenderApp, RenderSystems, }; /// A plugin that manages GPU memory for mesh data. @@ -158,6 +158,10 @@ pub struct MeshBufferSlice<'a> { pub struct SlabId(pub NonMaxU32); /// Data for a single slab. +#[expect( + clippy::large_enum_variant, + reason = "See https://github.com/bevyengine/bevy/issues/19220" +)] enum Slab { /// A slab that can contain multiple objects. General(GeneralSlab), @@ -311,7 +315,7 @@ impl Plugin for MeshAllocatorPlugin { .add_systems( Render, allocate_and_free_meshes - .in_set(RenderSet::PrepareAssets) + .in_set(RenderSystems::PrepareAssets) .before(prepare_assets::), ); } diff --git a/crates/bevy_render/src/mesh/mod.rs b/crates/bevy_render/src/mesh/mod.rs index fbd530c14d..d15468376f 100644 --- a/crates/bevy_render/src/mesh/mod.rs +++ b/crates/bevy_render/src/mesh/mod.rs @@ -13,7 +13,7 @@ use crate::{ }; use allocator::MeshAllocatorPlugin; use bevy_app::{App, Plugin, PostUpdate}; -use bevy_asset::{AssetApp, AssetEvents, AssetId, RenderAssetUsages}; +use bevy_asset::{AssetApp, AssetEventSystems, AssetId, RenderAssetUsages}; use bevy_ecs::{ prelude::*, system::{ @@ -73,7 +73,7 @@ impl Plugin for MeshPlugin { PostUpdate, mark_3d_meshes_as_changed_if_their_assets_changed .ambiguous_with(VisibilitySystems::CalculateBounds) - .before(AssetEvents), + .before(AssetEventSystems), ); let Some(render_app) = app.get_sub_app_mut(RenderApp) else { diff --git a/crates/bevy_render/src/pipelined_rendering.rs b/crates/bevy_render/src/pipelined_rendering.rs index fb665e469d..efe728e7c7 100644 --- a/crates/bevy_render/src/pipelined_rendering.rs +++ b/crates/bevy_render/src/pipelined_rendering.rs @@ -97,7 +97,7 @@ impl Drop for RenderAppChannels { /// This is run on the main app's thread. /// - On the render thread, we first apply the `extract commands`. This is not run during extract, so the /// main schedule can start sooner. -/// - Then the `rendering schedule` is run. See [`RenderSet`](crate::RenderSet) for the standard steps in this process. +/// - Then the `rendering schedule` is run. See [`RenderSystems`](crate::RenderSystems) for the standard steps in this process. /// - In parallel to the rendering thread the [`RenderExtractApp`] schedule runs. By /// default, this schedule is empty. But it is useful if you need something to run before I/O processing. /// - Next all the `winit events` are processed. diff --git a/crates/bevy_render/src/render_asset.rs b/crates/bevy_render/src/render_asset.rs index 6626cb7797..1fa5758ca3 100644 --- a/crates/bevy_render/src/render_asset.rs +++ b/crates/bevy_render/src/render_asset.rs @@ -1,6 +1,6 @@ use crate::{ render_resource::AsBindGroupError, Extract, ExtractSchedule, MainWorld, Render, RenderApp, - RenderSet, Res, + RenderSystems, Res, }; use bevy_app::{App, Plugin, SubApp}; pub use bevy_asset::RenderAssetUsages; @@ -27,14 +27,18 @@ pub enum PrepareAssetError { /// The system set during which we extract modified assets to the render world. #[derive(SystemSet, Clone, PartialEq, Eq, Debug, Hash)] -pub struct ExtractAssetsSet; +pub struct AssetExtractionSystems; + +/// Deprecated alias for [`AssetExtractionSystems`]. +#[deprecated(since = "0.17.0", note = "Renamed to `AssetExtractionSystems`.")] +pub type ExtractAssetsSet = AssetExtractionSystems; /// Describes how an asset gets extracted and prepared for rendering. /// /// In the [`ExtractSchedule`] step the [`RenderAsset::SourceAsset`] is transferred /// from the "main world" into the "render world". /// -/// After that in the [`RenderSet::PrepareAssets`] step the extracted asset +/// After that in the [`RenderSystems::PrepareAssets`] step the extracted asset /// is transformed into its GPU-representation of type [`RenderAsset`]. pub trait RenderAsset: Send + Sync + 'static + Sized { /// The representation of the asset in the "main world". @@ -88,7 +92,7 @@ pub trait RenderAsset: Send + Sync + 'static + Sized { /// and prepares them for the GPU. They can then be accessed from the [`RenderAssets`] resource. /// /// Therefore it sets up the [`ExtractSchedule`] and -/// [`RenderSet::PrepareAssets`] steps for the specified [`RenderAsset`]. +/// [`RenderSystems::PrepareAssets`] steps for the specified [`RenderAsset`]. /// /// The `AFTER` generic parameter can be used to specify that `A::prepare_asset` should not be run until /// `prepare_assets::` has completed. This allows the `prepare_asset` function to depend on another @@ -120,11 +124,11 @@ impl Plugin .init_resource::>() .add_systems( ExtractSchedule, - extract_render_asset::.in_set(ExtractAssetsSet), + extract_render_asset::.in_set(AssetExtractionSystems), ); AFTER::register_system( render_app, - prepare_assets::.in_set(RenderSet::PrepareAssets), + prepare_assets::.in_set(RenderSystems::PrepareAssets), ); } } diff --git a/crates/bevy_render/src/render_phase/mod.rs b/crates/bevy_render/src/render_phase/mod.rs index a4eb4a944f..272418f67f 100644 --- a/crates/bevy_render/src/render_phase/mod.rs +++ b/crates/bevy_render/src/render_phase/mod.rs @@ -10,10 +10,10 @@ //! //! To draw an entity, a corresponding [`PhaseItem`] has to be added to one or multiple of these //! render phases for each view that it is visible in. -//! This must be done in the [`RenderSet::Queue`]. -//! After that the render phase sorts them in the [`RenderSet::PhaseSort`]. +//! This must be done in the [`RenderSystems::Queue`]. +//! After that the render phase sorts them in the [`RenderSystems::PhaseSort`]. //! Finally the items are rendered using a single [`TrackedRenderPass`], during -//! the [`RenderSet::Render`]. +//! the [`RenderSystems::Render`]. //! //! Therefore each phase item is assigned a [`Draw`] function. //! These set up the state of the [`TrackedRenderPass`] (i.e. select the @@ -59,7 +59,7 @@ use crate::{ GetFullBatchData, }, render_resource::{CachedRenderPipelineId, GpuArrayBufferIndex, PipelineCache}, - Render, RenderApp, RenderSet, + Render, RenderApp, RenderSystems, }; use bevy_ecs::{ prelude::*, @@ -1134,7 +1134,7 @@ where .add_systems( Render, ( - batching::sort_binned_render_phase::.in_set(RenderSet::PhaseSort), + batching::sort_binned_render_phase::.in_set(RenderSystems::PhaseSort), ( no_gpu_preprocessing::batch_and_prepare_binned_render_phase:: .run_if(resource_exists::>), @@ -1145,15 +1145,15 @@ where >, ), ) - .in_set(RenderSet::PrepareResources), - sweep_old_entities::.in_set(RenderSet::QueueSweep), + .in_set(RenderSystems::PrepareResources), + sweep_old_entities::.in_set(RenderSystems::QueueSweep), gpu_preprocessing::collect_buffers_for_phase:: .run_if( resource_exists::< BatchedInstanceBuffers, >, ) - .in_set(RenderSet::PrepareResourcesCollectPhaseBuffers), + .in_set(RenderSystems::PrepareResourcesCollectPhaseBuffers), ), ); } @@ -1250,14 +1250,14 @@ where >, ), ) - .in_set(RenderSet::PrepareResources), + .in_set(RenderSystems::PrepareResources), gpu_preprocessing::collect_buffers_for_phase:: .run_if( resource_exists::< BatchedInstanceBuffers, >, ) - .in_set(RenderSet::PrepareResourcesCollectPhaseBuffers), + .in_set(RenderSystems::PrepareResourcesCollectPhaseBuffers), ), ); } @@ -1465,10 +1465,10 @@ where /// /// The data required for rendering an entity is extracted from the main world in the /// [`ExtractSchedule`](crate::ExtractSchedule). -/// Then it has to be queued up for rendering during the [`RenderSet::Queue`], +/// Then it has to be queued up for rendering during the [`RenderSystems::Queue`], /// by adding a corresponding phase item to a render phase. /// Afterwards it will be possibly sorted and rendered automatically in the -/// [`RenderSet::PhaseSort`] and [`RenderSet::Render`], respectively. +/// [`RenderSystems::PhaseSort`] and [`RenderSystems::Render`], respectively. /// /// `PhaseItem`s come in two flavors: [`BinnedPhaseItem`]s and /// [`SortedPhaseItem`]s. diff --git a/crates/bevy_render/src/render_resource/pipeline_cache.rs b/crates/bevy_render/src/render_resource/pipeline_cache.rs index 653ae70b1c..7c54b0f406 100644 --- a/crates/bevy_render/src/render_resource/pipeline_cache.rs +++ b/crates/bevy_render/src/render_resource/pipeline_cache.rs @@ -80,6 +80,10 @@ pub struct CachedPipeline { } /// State of a cached pipeline inserted into a [`PipelineCache`]. +#[expect( + clippy::large_enum_variant, + reason = "See https://github.com/bevyengine/bevy/issues/19220" +)] #[derive(Debug)] pub enum CachedPipelineState { /// The pipeline GPU object is queued for creation. @@ -135,7 +139,7 @@ struct ShaderCache { composer: naga_oil::compose::Composer, } -#[derive(Clone, PartialEq, Eq, Debug, Hash)] +#[derive(serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq, Debug, Hash)] pub enum ShaderDefVal { Bool(String, bool), Int(String, i32), @@ -189,33 +193,42 @@ impl ShaderCache { } } + #[expect( + clippy::result_large_err, + reason = "See https://github.com/bevyengine/bevy/issues/19220" + )] fn add_import_to_composer( composer: &mut naga_oil::compose::Composer, import_path_shaders: &HashMap>, shaders: &HashMap, Shader>, import: &ShaderImport, ) -> Result<(), PipelineCacheError> { - if !composer.contains_module(&import.module_name()) { - if let Some(shader_handle) = import_path_shaders.get(import) { - if let Some(shader) = shaders.get(shader_handle) { - for import in &shader.imports { - Self::add_import_to_composer( - composer, - import_path_shaders, - shaders, - import, - )?; - } - - composer.add_composable_module(shader.into())?; - } - } - // if we fail to add a module the composer will tell us what is missing + // Early out if we've already imported this module + if composer.contains_module(&import.module_name()) { + return Ok(()); } + // Check if the import is available (this handles the recursive import case) + let shader = import_path_shaders + .get(import) + .and_then(|handle| shaders.get(handle)) + .ok_or(PipelineCacheError::ShaderImportNotYetAvailable)?; + + // Recurse down to ensure all import dependencies are met + for import in &shader.imports { + Self::add_import_to_composer(composer, import_path_shaders, shaders, import)?; + } + + composer.add_composable_module(shader.into())?; + // if we fail to add a module the composer will tell us what is missing + Ok(()) } + #[expect( + clippy::result_large_err, + reason = "See https://github.com/bevyengine/bevy/issues/19220" + )] fn get( &mut self, render_device: &RenderDevice, @@ -555,13 +568,13 @@ impl LayoutCache { /// The cache stores existing render and compute pipelines allocated on the GPU, as well as /// pending creation. Pipelines inserted into the cache are identified by a unique ID, which /// can be used to retrieve the actual GPU object once it's ready. The creation of the GPU -/// pipeline object is deferred to the [`RenderSet::Render`] step, just before the render +/// pipeline object is deferred to the [`RenderSystems::Render`] step, just before the render /// graph starts being processed, as this requires access to the GPU. /// /// Note that the cache does not perform automatic deduplication of identical pipelines. It is /// up to the user not to insert the same pipeline twice to avoid wasting GPU resources. /// -/// [`RenderSet::Render`]: crate::RenderSet::Render +/// [`RenderSystems::Render`]: crate::RenderSystems::Render #[derive(Resource)] pub struct PipelineCache { layout_cache: Arc>, @@ -959,10 +972,10 @@ impl PipelineCache { /// Process the pipeline queue and create all pending pipelines if possible. /// - /// This is generally called automatically during the [`RenderSet::Render`] step, but can + /// This is generally called automatically during the [`RenderSystems::Render`] step, but can /// be called manually to force creation at a different time. /// - /// [`RenderSet::Render`]: crate::RenderSet::Render + /// [`RenderSystems::Render`]: crate::RenderSystems::Render pub fn process_queue(&mut self) { let mut waiting_pipelines = mem::take(&mut self.waiting_pipelines); let mut pipelines = mem::take(&mut self.pipelines); @@ -1090,6 +1103,10 @@ fn create_pipeline_task( target_os = "macos", not(feature = "multi_threaded") ))] +#[expect( + clippy::large_enum_variant, + reason = "See https://github.com/bevyengine/bevy/issues/19220" +)] fn create_pipeline_task( task: impl Future> + Send + 'static, _sync: bool, @@ -1101,6 +1118,10 @@ fn create_pipeline_task( } /// Type of error returned by a [`PipelineCache`] when the creation of a GPU pipeline object failed. +#[expect( + clippy::large_enum_variant, + reason = "See https://github.com/bevyengine/bevy/issues/19220" +)] #[derive(Error, Debug)] pub enum PipelineCacheError { #[error( diff --git a/crates/bevy_render/src/render_resource/shader.rs b/crates/bevy_render/src/render_resource/shader.rs index 005fb07c05..ff8430b951 100644 --- a/crates/bevy_render/src/render_resource/shader.rs +++ b/crates/bevy_render/src/render_resource/shader.rs @@ -324,14 +324,21 @@ pub enum ShaderLoaderError { Parse(#[from] alloc::string::FromUtf8Error), } +/// Settings for loading shaders. +#[derive(serde::Serialize, serde::Deserialize, Debug, Default)] +pub struct ShaderSettings { + /// The `#define` specified for this shader. + pub shader_defs: Vec, +} + impl AssetLoader for ShaderLoader { type Asset = Shader; - type Settings = (); + type Settings = ShaderSettings; type Error = ShaderLoaderError; async fn load( &self, reader: &mut dyn Reader, - _settings: &Self::Settings, + settings: &Self::Settings, load_context: &mut LoadContext<'_>, ) -> Result { let ext = load_context.path().extension().unwrap().to_str().unwrap(); @@ -341,9 +348,19 @@ impl AssetLoader for ShaderLoader { let path = path.replace(std::path::MAIN_SEPARATOR, "/"); let mut bytes = Vec::new(); reader.read_to_end(&mut bytes).await?; + if ext != "wgsl" && !settings.shader_defs.is_empty() { + tracing::warn!( + "Tried to load a non-wgsl shader with shader defs, this isn't supported: \ + The shader defs will be ignored." + ); + } let mut shader = match ext { "spv" => Shader::from_spirv(bytes, load_context.path().to_string_lossy()), - "wgsl" => Shader::from_wgsl(String::from_utf8(bytes)?, path), + "wgsl" => Shader::from_wgsl_with_defs( + String::from_utf8(bytes)?, + path, + settings.shader_defs.clone(), + ), "vert" => Shader::from_glsl(String::from_utf8(bytes)?, naga::ShaderStage::Vertex, path), "frag" => { Shader::from_glsl(String::from_utf8(bytes)?, naga::ShaderStage::Fragment, path) diff --git a/crates/bevy_render/src/settings.rs b/crates/bevy_render/src/settings.rs index d4eb9b7680..ab50fc81b1 100644 --- a/crates/bevy_render/src/settings.rs +++ b/crates/bevy_render/src/settings.rs @@ -152,6 +152,10 @@ pub struct RenderResources( ); /// An enum describing how the renderer will initialize resources. This is used when creating the [`RenderPlugin`](crate::RenderPlugin). +#[expect( + clippy::large_enum_variant, + reason = "See https://github.com/bevyengine/bevy/issues/19220" +)] pub enum RenderCreation { /// Allows renderer resource initialization to happen outside of the rendering plugin. Manual(RenderResources), diff --git a/crates/bevy_render/src/sync_world.rs b/crates/bevy_render/src/sync_world.rs index ce04088333..f15f3c4003 100644 --- a/crates/bevy_render/src/sync_world.rs +++ b/crates/bevy_render/src/sync_world.rs @@ -1,6 +1,5 @@ use bevy_app::Plugin; use bevy_derive::{Deref, DerefMut}; -use bevy_ecs::component::{ComponentCloneBehavior, Mutable, StorageType}; use bevy_ecs::entity::EntityHash; use bevy_ecs::{ component::Component, @@ -127,7 +126,8 @@ pub struct SyncToRenderWorld; /// Component added on the main world entities that are synced to the Render World in order to keep track of the corresponding render world entity. /// /// Can also be used as a newtype wrapper for render world entities. -#[derive(Deref, Copy, Clone, Debug, Eq, Hash, PartialEq)] +#[derive(Deref, Copy, Clone, Debug, Eq, Hash, PartialEq, Component)] +#[component(clone_behavior = Ignore)] pub struct RenderEntity(Entity); impl RenderEntity { #[inline] @@ -136,16 +136,6 @@ impl RenderEntity { } } -impl Component for RenderEntity { - const STORAGE_TYPE: StorageType = StorageType::Table; - - type Mutability = Mutable; - - fn clone_behavior() -> ComponentCloneBehavior { - ComponentCloneBehavior::Ignore - } -} - impl From for RenderEntity { fn from(entity: Entity) -> Self { RenderEntity(entity) diff --git a/crates/bevy_render/src/texture/mod.rs b/crates/bevy_render/src/texture/mod.rs index 6955de7ff4..de5361aad8 100644 --- a/crates/bevy_render/src/texture/mod.rs +++ b/crates/bevy_render/src/texture/mod.rs @@ -15,7 +15,7 @@ pub use texture_attachment::*; pub use texture_cache::*; use crate::{ - render_asset::RenderAssetPlugin, renderer::RenderDevice, Render, RenderApp, RenderSet, + render_asset::RenderAssetPlugin, renderer::RenderDevice, Render, RenderApp, RenderSystems, }; use bevy_app::{App, Plugin}; use bevy_asset::{weak_handle, AssetApp, Assets, Handle}; @@ -100,7 +100,7 @@ impl Plugin for ImagePlugin { if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app.init_resource::().add_systems( Render, - update_texture_cache_system.in_set(RenderSet::Cleanup), + update_texture_cache_system.in_set(RenderSystems::Cleanup), ); } diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index c392dcaaeb..2f80e5f94b 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -24,7 +24,7 @@ use crate::{ CachedTexture, ColorAttachment, DepthAttachment, GpuImage, OutputColorAttachment, TextureCache, }, - Render, RenderApp, RenderSet, + Render, RenderApp, RenderSystems, }; use alloc::sync::Arc; use bevy_app::{App, Plugin}; @@ -126,18 +126,18 @@ impl Plugin for ViewPlugin { ( // `TextureView`s need to be dropped before reconfiguring window surfaces. clear_view_attachments - .in_set(RenderSet::ManageViews) + .in_set(RenderSystems::ManageViews) .before(create_surfaces), prepare_view_attachments - .in_set(RenderSet::ManageViews) + .in_set(RenderSystems::ManageViews) .before(prepare_view_targets) .after(prepare_windows), prepare_view_targets - .in_set(RenderSet::ManageViews) + .in_set(RenderSystems::ManageViews) .after(prepare_windows) .after(crate::render_asset::prepare_assets::) .ambiguous_with(crate::camera::sort_cameras), // doesn't use `sorted_camera_index_for_target` - prepare_view_uniforms.in_set(RenderSet::PrepareResources), + prepare_view_uniforms.in_set(RenderSystems::PrepareResources), ), ); } diff --git a/crates/bevy_render/src/view/visibility/mod.rs b/crates/bevy_render/src/view/visibility/mod.rs index 63c931a8b0..3a0772b687 100644 --- a/crates/bevy_render/src/view/visibility/mod.rs +++ b/crates/bevy_render/src/view/visibility/mod.rs @@ -14,7 +14,7 @@ use bevy_app::{Plugin, PostUpdate}; use bevy_asset::Assets; use bevy_ecs::{hierarchy::validate_parent_has_component, prelude::*}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; -use bevy_transform::{components::GlobalTransform, TransformSystem}; +use bevy_transform::{components::GlobalTransform, TransformSystems}; use bevy_utils::{Parallel, TypeIdMap}; use smallvec::SmallVec; @@ -342,7 +342,7 @@ impl Plugin for VisibilityPlugin { PostUpdate, (CalculateBounds, UpdateFrusta, VisibilityPropagate) .before(CheckVisibility) - .after(TransformSystem::TransformPropagate), + .after(TransformSystems::Propagate), ) .configure_sets( PostUpdate, diff --git a/crates/bevy_render/src/view/visibility/range.rs b/crates/bevy_render/src/view/visibility/range.rs index 4c264e0778..2559d3b8d2 100644 --- a/crates/bevy_render/src/view/visibility/range.rs +++ b/crates/bevy_render/src/view/visibility/range.rs @@ -32,7 +32,7 @@ use crate::{ primitives::Aabb, render_resource::BufferVec, renderer::{RenderDevice, RenderQueue}, - Extract, ExtractSchedule, Render, RenderApp, RenderSet, + Extract, ExtractSchedule, Render, RenderApp, RenderSystems, }; /// We need at least 4 storage buffer bindings available to enable the @@ -72,7 +72,7 @@ impl Plugin for VisibilityRangePlugin { .add_systems(ExtractSchedule, extract_visibility_ranges) .add_systems( Render, - write_render_visibility_ranges.in_set(RenderSet::PrepareResourcesFlush), + write_render_visibility_ranges.in_set(RenderSystems::PrepareResourcesFlush), ); } } diff --git a/crates/bevy_render/src/view/window/mod.rs b/crates/bevy_render/src/view/window/mod.rs index c3fc6e5516..4c8d86d040 100644 --- a/crates/bevy_render/src/view/window/mod.rs +++ b/crates/bevy_render/src/view/window/mod.rs @@ -1,7 +1,7 @@ use crate::{ render_resource::{SurfaceTexture, TextureView}, renderer::{RenderAdapter, RenderDevice, RenderInstance}, - Extract, ExtractSchedule, Render, RenderApp, RenderSet, WgpuWrapper, + Extract, ExtractSchedule, Render, RenderApp, RenderSystems, WgpuWrapper, }; use bevy_app::{App, Plugin}; use bevy_ecs::{entity::EntityHashMap, prelude::*}; @@ -21,7 +21,7 @@ use wgpu::{ pub mod screenshot; -use screenshot::{ScreenshotPlugin, ScreenshotToScreenPipeline}; +use screenshot::ScreenshotPlugin; pub struct WindowRenderPlugin; @@ -40,13 +40,7 @@ impl Plugin for WindowRenderPlugin { .run_if(need_surface_configuration) .before(prepare_windows), ) - .add_systems(Render, prepare_windows.in_set(RenderSet::ManageViews)); - } - } - - fn finish(&self, app: &mut App) { - if let Some(render_app) = app.get_sub_app_mut(RenderApp) { - render_app.init_resource::(); + .add_systems(Render, prepare_windows.in_set(RenderSystems::ManageViews)); } } } diff --git a/crates/bevy_render/src/view/window/screenshot.rs b/crates/bevy_render/src/view/window/screenshot.rs index 6e223eedaf..854a6bc064 100644 --- a/crates/bevy_render/src/view/window/screenshot.rs +++ b/crates/bevy_render/src/view/window/screenshot.rs @@ -13,7 +13,7 @@ use crate::{ renderer::RenderDevice, texture::{GpuImage, OutputColorAttachment}, view::{prepare_view_attachments, prepare_view_targets, ViewTargetAttachments, WindowSurfaces}, - ExtractSchedule, MainWorld, Render, RenderApp, RenderSet, + ExtractSchedule, MainWorld, Render, RenderApp, RenderSystems, }; use alloc::{borrow::Cow, sync::Arc}; use bevy_app::{First, Plugin, Update}; @@ -425,13 +425,14 @@ impl Plugin for ScreenshotPlugin { .init_resource::() .init_resource::() .init_resource::>() + .init_resource::() .add_systems(ExtractSchedule, extract_screenshots.ambiguous_with_all()) .add_systems( Render, prepare_screenshots .after(prepare_view_attachments) .before(prepare_view_targets) - .in_set(RenderSet::ManageViews), + .in_set(RenderSystems::ManageViews), ); } } diff --git a/crates/bevy_scene/src/dynamic_scene_builder.rs b/crates/bevy_scene/src/dynamic_scene_builder.rs index 5deac09aaa..c9e594107e 100644 --- a/crates/bevy_scene/src/dynamic_scene_builder.rs +++ b/crates/bevy_scene/src/dynamic_scene_builder.rs @@ -509,10 +509,10 @@ mod tests { let mut entities = builder.build().entities.into_iter(); // Assert entities are ordered - assert_eq!(entity_a, entities.next().map(|e| e.entity).unwrap()); - assert_eq!(entity_b, entities.next().map(|e| e.entity).unwrap()); - assert_eq!(entity_c, entities.next().map(|e| e.entity).unwrap()); assert_eq!(entity_d, entities.next().map(|e| e.entity).unwrap()); + assert_eq!(entity_c, entities.next().map(|e| e.entity).unwrap()); + assert_eq!(entity_b, entities.next().map(|e| e.entity).unwrap()); + assert_eq!(entity_a, entities.next().map(|e| e.entity).unwrap()); } #[test] @@ -539,7 +539,7 @@ mod tests { assert_eq!(scene.entities.len(), 2); let mut scene_entities = vec![scene.entities[0].entity, scene.entities[1].entity]; scene_entities.sort(); - assert_eq!(scene_entities, [entity_a_b, entity_a]); + assert_eq!(scene_entities, [entity_a, entity_a_b]); } #[test] @@ -621,9 +621,9 @@ mod tests { .build(); assert_eq!(scene.entities.len(), 3); - assert!(scene.entities[0].components[0].represents::()); + assert!(scene.entities[2].components[0].represents::()); assert!(scene.entities[1].components[0].represents::()); - assert_eq!(scene.entities[2].components.len(), 0); + assert_eq!(scene.entities[0].components.len(), 0); } #[test] diff --git a/crates/bevy_scene/src/scene_spawner.rs b/crates/bevy_scene/src/scene_spawner.rs index dce5ad971e..3bf8ca9f64 100644 --- a/crates/bevy_scene/src/scene_spawner.rs +++ b/crates/bevy_scene/src/scene_spawner.rs @@ -912,7 +912,7 @@ mod tests { app.update(); let world = app.world_mut(); - let spawned_root = world.entity(spawned).get::().unwrap()[0]; + let spawned_root = world.entity(spawned).get::().unwrap()[1]; let children = world.entity(spawned_root).get::().unwrap(); assert_eq!(children.len(), 3); assert!(world.entity(children[0]).get::().is_some()); diff --git a/crates/bevy_scene/src/serde.rs b/crates/bevy_scene/src/serde.rs index ead8933a49..cb8206d3dd 100644 --- a/crates/bevy_scene/src/serde.rs +++ b/crates/bevy_scene/src/serde.rs @@ -638,24 +638,24 @@ mod tests { ), }, entities: { - 4294967296: ( - components: { - "bevy_scene::serde::tests::Foo": (123), - }, - ), - 4294967297: ( - components: { - "bevy_scene::serde::tests::Bar": (345), - "bevy_scene::serde::tests::Foo": (123), - }, - ), - 4294967298: ( + 4294967293: ( components: { "bevy_scene::serde::tests::Bar": (345), "bevy_scene::serde::tests::Baz": (789), "bevy_scene::serde::tests::Foo": (123), }, ), + 4294967294: ( + components: { + "bevy_scene::serde::tests::Bar": (345), + "bevy_scene::serde::tests::Foo": (123), + }, + ), + 4294967295: ( + components: { + "bevy_scene::serde::tests::Foo": (123), + }, + ), }, )"#; let output = scene @@ -675,18 +675,18 @@ mod tests { ), }, entities: { - 4294967296: ( + 8589934591: ( components: { "bevy_scene::serde::tests::Foo": (123), }, ), - 4294967297: ( + 8589934590: ( components: { "bevy_scene::serde::tests::Foo": (123), "bevy_scene::serde::tests::Bar": (345), }, ), - 4294967298: ( + 8589934589: ( components: { "bevy_scene::serde::tests::Foo": (123), "bevy_scene::serde::tests::Bar": (345), @@ -815,7 +815,7 @@ mod tests { assert_eq!( vec![ - 0, 1, 128, 128, 128, 128, 16, 1, 37, 98, 101, 118, 121, 95, 115, 99, 101, 110, 101, + 0, 1, 255, 255, 255, 255, 15, 1, 37, 98, 101, 118, 121, 95, 115, 99, 101, 110, 101, 58, 58, 115, 101, 114, 100, 101, 58, 58, 116, 101, 115, 116, 115, 58, 58, 77, 121, 67, 111, 109, 112, 111, 110, 101, 110, 116, 1, 2, 3, 102, 102, 166, 63, 205, 204, 108, 64, 1, 12, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33 @@ -856,8 +856,8 @@ mod tests { assert_eq!( vec![ - 146, 128, 129, 207, 0, 0, 0, 1, 0, 0, 0, 0, 145, 129, 217, 37, 98, 101, 118, 121, - 95, 115, 99, 101, 110, 101, 58, 58, 115, 101, 114, 100, 101, 58, 58, 116, 101, 115, + 146, 128, 129, 206, 255, 255, 255, 255, 145, 129, 217, 37, 98, 101, 118, 121, 95, + 115, 99, 101, 110, 101, 58, 58, 115, 101, 114, 100, 101, 58, 58, 116, 101, 115, 116, 115, 58, 58, 77, 121, 67, 111, 109, 112, 111, 110, 101, 110, 116, 147, 147, 1, 2, 3, 146, 202, 63, 166, 102, 102, 202, 64, 108, 204, 205, 129, 165, 84, 117, 112, 108, 101, 172, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33 @@ -899,12 +899,13 @@ mod tests { assert_eq!( vec![ - 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, - 0, 0, 0, 0, 37, 0, 0, 0, 0, 0, 0, 0, 98, 101, 118, 121, 95, 115, 99, 101, 110, 101, - 58, 58, 115, 101, 114, 100, 101, 58, 58, 116, 101, 115, 116, 115, 58, 58, 77, 121, - 67, 111, 109, 112, 111, 110, 101, 110, 116, 1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, - 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 102, 102, 166, 63, 205, 204, 108, 64, 1, 0, 0, 0, - 12, 0, 0, 0, 0, 0, 0, 0, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33 + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 0, 1, + 0, 0, 0, 0, 0, 0, 0, 37, 0, 0, 0, 0, 0, 0, 0, 98, 101, 118, 121, 95, 115, 99, 101, + 110, 101, 58, 58, 115, 101, 114, 100, 101, 58, 58, 116, 101, 115, 116, 115, 58, 58, + 77, 121, 67, 111, 109, 112, 111, 110, 101, 110, 116, 1, 0, 0, 0, 0, 0, 0, 0, 2, 0, + 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 102, 102, 166, 63, 205, 204, 108, 64, 1, + 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, + 100, 33 ], serialized_scene ); diff --git a/crates/bevy_sprite/src/lib.rs b/crates/bevy_sprite/src/lib.rs index 37d4d2d6e4..4b2d206420 100644 --- a/crates/bevy_sprite/src/lib.rs +++ b/crates/bevy_sprite/src/lib.rs @@ -42,7 +42,7 @@ pub use sprite::*; pub use texture_slice::*; use bevy_app::prelude::*; -use bevy_asset::{load_internal_asset, weak_handle, AssetEvents, Assets, Handle}; +use bevy_asset::{load_internal_asset, weak_handle, AssetEventSystems, Assets, Handle}; use bevy_core_pipeline::core_2d::{AlphaMask2d, Opaque2d, Transparent2d}; use bevy_ecs::prelude::*; use bevy_image::{prelude::*, TextureAtlasPlugin}; @@ -53,7 +53,7 @@ use bevy_render::{ render_phase::AddRenderCommand, render_resource::{Shader, SpecializedRenderPipelines}, view::{NoFrustumCulling, VisibilitySystems}, - ExtractSchedule, Render, RenderApp, RenderSet, + ExtractSchedule, Render, RenderApp, RenderSystems, }; /// Adds support for 2D sprite rendering. @@ -67,11 +67,15 @@ pub const SPRITE_VIEW_BINDINGS_SHADER_HANDLE: Handle = /// System set for sprite rendering. #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] -pub enum SpriteSystem { +pub enum SpriteSystems { ExtractSprites, ComputeSlices, } +/// Deprecated alias for [`SpriteSystems`]. +#[deprecated(since = "0.17.0", note = "Renamed to `SpriteSystems`.")] +pub type SpriteSystem = SpriteSystems; + impl Plugin for SpritePlugin { fn build(&self, app: &mut App) { load_internal_asset!( @@ -102,10 +106,10 @@ impl Plugin for SpritePlugin { ( calculate_bounds_2d.in_set(VisibilitySystems::CalculateBounds), ( - compute_slices_on_asset_event.before(AssetEvents), + compute_slices_on_asset_event.before(AssetEventSystems), compute_slices_on_sprite_change, ) - .in_set(SpriteSystem::ComputeSlices), + .in_set(SpriteSystems::ComputeSlices), ), ); @@ -124,7 +128,7 @@ impl Plugin for SpritePlugin { .add_systems( ExtractSchedule, ( - extract_sprites.in_set(SpriteSystem::ExtractSprites), + extract_sprites.in_set(SpriteSystems::ExtractSprites), extract_sprite_events, ), ) @@ -132,12 +136,12 @@ impl Plugin for SpritePlugin { Render, ( queue_sprites - .in_set(RenderSet::Queue) + .in_set(RenderSystems::Queue) .ambiguous_with(queue_material2d_meshes::), - prepare_sprite_image_bind_groups.in_set(RenderSet::PrepareBindGroups), - prepare_sprite_view_bind_groups.in_set(RenderSet::PrepareBindGroups), - sort_binned_render_phase::.in_set(RenderSet::PhaseSort), - sort_binned_render_phase::.in_set(RenderSet::PhaseSort), + prepare_sprite_image_bind_groups.in_set(RenderSystems::PrepareBindGroups), + prepare_sprite_view_bind_groups.in_set(RenderSystems::PrepareBindGroups), + sort_binned_render_phase::.in_set(RenderSystems::PhaseSort), + sort_binned_render_phase::.in_set(RenderSystems::PhaseSort), ), ); }; diff --git a/crates/bevy_sprite/src/mesh2d/material.rs b/crates/bevy_sprite/src/mesh2d/material.rs index e34595f138..3f76b516cd 100644 --- a/crates/bevy_sprite/src/mesh2d/material.rs +++ b/crates/bevy_sprite/src/mesh2d/material.rs @@ -4,7 +4,7 @@ use crate::{ }; use bevy_app::{App, Plugin, PostUpdate}; use bevy_asset::prelude::AssetChanged; -use bevy_asset::{AsAssetId, Asset, AssetApp, AssetEvents, AssetId, AssetServer, Handle}; +use bevy_asset::{AsAssetId, Asset, AssetApp, AssetEventSystems, AssetId, AssetServer, Handle}; use bevy_core_pipeline::{ core_2d::{ AlphaMask2d, AlphaMask2dBinKey, BatchSetKey2d, Opaque2d, Opaque2dBinKey, Transparent2d, @@ -43,7 +43,7 @@ use bevy_render::{ renderer::RenderDevice, sync_world::{MainEntity, MainEntityHashMap}, view::{ExtractedView, ViewVisibility}, - Extract, ExtractSchedule, Render, RenderApp, RenderSet, + Extract, ExtractSchedule, Render, RenderApp, RenderSystems, }; use bevy_utils::Parallel; use core::{hash::Hash, marker::PhantomData}; @@ -273,7 +273,7 @@ where .add_plugins(RenderAssetPlugin::>::default()) .add_systems( PostUpdate, - check_entities_needing_specialization::.after(AssetEvents), + check_entities_needing_specialization::.after(AssetEventSystems), ); if let Some(render_app) = app.get_sub_app_mut(RenderApp) { @@ -296,11 +296,11 @@ where Render, ( specialize_material2d_meshes:: - .in_set(RenderSet::PrepareMeshes) + .in_set(RenderSystems::PrepareMeshes) .after(prepare_assets::>) .after(prepare_assets::), queue_material2d_meshes:: - .in_set(RenderSet::QueueMeshes) + .in_set(RenderSystems::QueueMeshes) .after(prepare_assets::>), ), ); diff --git a/crates/bevy_sprite/src/mesh2d/mesh.rs b/crates/bevy_sprite/src/mesh2d/mesh.rs index 5822d47ed6..3bacc35194 100644 --- a/crates/bevy_sprite/src/mesh2d/mesh.rs +++ b/crates/bevy_sprite/src/mesh2d/mesh.rs @@ -21,7 +21,7 @@ use bevy_image::{BevyDefault, Image, ImageSampler, TextureFormatPixelInfo}; use bevy_math::{Affine3, Vec4}; use bevy_render::mesh::MeshTag; use bevy_render::prelude::Msaa; -use bevy_render::RenderSet::PrepareAssets; +use bevy_render::RenderSystems::PrepareAssets; use bevy_render::{ batching::{ gpu_preprocessing::IndirectParametersCpuMetadata, @@ -48,7 +48,7 @@ use bevy_render::{ view::{ ExtractedView, ViewTarget, ViewUniform, ViewUniformOffset, ViewUniforms, ViewVisibility, }, - Extract, ExtractSchedule, Render, RenderApp, RenderSet, + Extract, ExtractSchedule, Render, RenderApp, RenderSystems, }; use bevy_transform::components::GlobalTransform; use nonmax::NonMaxU32; @@ -119,20 +119,20 @@ impl Plugin for Mesh2dRenderPlugin { sweep_old_entities::, sweep_old_entities::, ) - .in_set(RenderSet::QueueSweep), + .in_set(RenderSystems::QueueSweep), batch_and_prepare_binned_render_phase:: - .in_set(RenderSet::PrepareResources), + .in_set(RenderSystems::PrepareResources), batch_and_prepare_binned_render_phase:: - .in_set(RenderSet::PrepareResources), + .in_set(RenderSystems::PrepareResources), batch_and_prepare_sorted_render_phase:: - .in_set(RenderSet::PrepareResources), + .in_set(RenderSystems::PrepareResources), write_batched_instance_buffer:: - .in_set(RenderSet::PrepareResourcesFlush), - prepare_mesh2d_bind_group.in_set(RenderSet::PrepareBindGroups), - prepare_mesh2d_view_bind_groups.in_set(RenderSet::PrepareBindGroups), + .in_set(RenderSystems::PrepareResourcesFlush), + prepare_mesh2d_bind_group.in_set(RenderSystems::PrepareBindGroups), + prepare_mesh2d_view_bind_groups.in_set(RenderSystems::PrepareBindGroups), no_gpu_preprocessing::clear_batched_cpu_instance_buffers:: - .in_set(RenderSet::Cleanup) - .after(RenderSet::Render), + .in_set(RenderSystems::Cleanup) + .after(RenderSystems::Render), ), ); } diff --git a/crates/bevy_sprite/src/mesh2d/wireframe2d.rs b/crates/bevy_sprite/src/mesh2d/wireframe2d.rs index 63e5280519..468a47f6bb 100644 --- a/crates/bevy_sprite/src/mesh2d/wireframe2d.rs +++ b/crates/bevy_sprite/src/mesh2d/wireframe2d.rs @@ -5,7 +5,7 @@ use crate::{ use bevy_app::{App, Plugin, PostUpdate, Startup, Update}; use bevy_asset::{ load_internal_asset, prelude::AssetChanged, weak_handle, AsAssetId, Asset, AssetApp, - AssetEvents, AssetId, Assets, Handle, UntypedAssetId, + AssetEventSystems, AssetId, Assets, Handle, UntypedAssetId, }; use bevy_color::{Color, ColorToComponents}; use bevy_core_pipeline::core_2d::{ @@ -49,7 +49,7 @@ use bevy_render::{ view::{ ExtractedView, RenderVisibleEntities, RetainedViewEntity, ViewDepthTexture, ViewTarget, }, - Extract, Render, RenderApp, RenderDebugFlags, RenderSet, + Extract, Render, RenderApp, RenderDebugFlags, RenderSystems, }; use core::{hash::Hash, ops::Range}; use tracing::error; @@ -113,7 +113,7 @@ impl Plugin for Wireframe2dPlugin { .add_systems( PostUpdate, check_wireframe_entities_needing_specialization - .after(AssetEvents) + .after(AssetEventSystems) .run_if(resource_exists::), ); @@ -149,11 +149,11 @@ impl Plugin for Wireframe2dPlugin { Render, ( specialize_wireframes - .in_set(RenderSet::PrepareMeshes) + .in_set(RenderSystems::PrepareMeshes) .after(prepare_assets::) .after(prepare_assets::), queue_wireframes - .in_set(RenderSet::QueueMeshes) + .in_set(RenderSystems::QueueMeshes) .after(prepare_assets::), ), ); diff --git a/crates/bevy_sprite/src/picking_backend.rs b/crates/bevy_sprite/src/picking_backend.rs index a029838147..56579c9c0a 100644 --- a/crates/bevy_sprite/src/picking_backend.rs +++ b/crates/bevy_sprite/src/picking_backend.rs @@ -1,6 +1,9 @@ //! A [`bevy_picking`] backend for sprites. Works for simple sprites and sprite atlases. Works for -//! sprites with arbitrary transforms. Picking is done based on sprite bounds, not visible pixels. -//! This means a partially transparent sprite is pickable even in its transparent areas. +//! sprites with arbitrary transforms. +//! +//! By default, picking for sprites is based on pixel opacity. +//! A sprite is picked only when a pointer is over an opaque pixel. +//! Alternatively, you can configure picking to be based on sprite bounds. //! //! ## Implementation Notes //! @@ -45,9 +48,10 @@ pub enum SpritePickingMode { #[reflect(Resource, Default)] pub struct SpritePickingSettings { /// When set to `true` sprite picking will only consider cameras marked with - /// [`SpritePickingCamera`]. + /// [`SpritePickingCamera`]. Defaults to `false`. + /// Regardless of this setting, only sprites marked with [`Pickable`] will be considered. /// - /// This setting is provided to give you fine-grained control over which cameras and entities + /// This setting is provided to give you fine-grained control over which cameras /// should be used by the sprite picking backend at runtime. pub require_markers: bool, /// Should the backend count transparent pixels as part of the sprite for picking purposes or should it use the bounding box of the sprite alone. @@ -75,7 +79,7 @@ impl Plugin for SpritePickingPlugin { .register_type::() .register_type::() .register_type::() - .add_systems(PreUpdate, sprite_picking.in_set(PickSet::Backend)); + .add_systems(PreUpdate, sprite_picking.in_set(PickingSystems::Backend)); } } diff --git a/crates/bevy_state/src/app.rs b/crates/bevy_state/src/app.rs index 46a23c9f9a..903a098137 100644 --- a/crates/bevy_state/src/app.rs +++ b/crates/bevy_state/src/app.rs @@ -6,9 +6,9 @@ use log::warn; use crate::{ state::{ setup_state_transitions_in_world, ComputedStates, FreelyMutableState, NextState, State, - StateTransition, StateTransitionEvent, StateTransitionSteps, States, SubStates, + StateTransition, StateTransitionEvent, StateTransitionSystems, States, SubStates, }, - state_scoped::clear_state_scoped_entities, + state_scoped::{despawn_entities_on_enter_state, despawn_entities_on_exit_state}, }; #[cfg(feature = "bevy_reflect")] @@ -62,7 +62,7 @@ pub trait AppExtStates { /// If the [`States`] trait was derived with the `#[states(scoped_entities)]` attribute, it /// will be called automatically. /// - /// For more information refer to [`StateScoped`](crate::state_scoped::StateScoped). + /// For more information refer to [`crate::state_scoped`]. fn enable_state_scoped_entities(&mut self) -> &mut Self; #[cfg(feature = "bevy_reflect")] @@ -222,11 +222,20 @@ impl AppExtStates for SubApp { let name = core::any::type_name::(); warn!("State scoped entities are enabled for state `{}`, but the state isn't installed in the app!", name); } - // We work with [`StateTransition`] in set [`StateTransitionSteps::ExitSchedules`] as opposed to [`OnExit`], - // because [`OnExit`] only runs for one specific variant of the state. + + // Note: We work with `StateTransition` in set + // `StateTransitionSystems::ExitSchedules` rather than `OnExit`, because + // `OnExit` only runs for one specific variant of the state. self.add_systems( StateTransition, - clear_state_scoped_entities::.in_set(StateTransitionSteps::ExitSchedules), + despawn_entities_on_exit_state::.in_set(StateTransitionSystems::ExitSchedules), + ) + // Note: We work with `StateTransition` in set + // `StateTransitionSystems::EnterSchedules` rather than `OnEnter`, because + // `OnEnter` only runs for one specific variant of the state. + .add_systems( + StateTransition, + despawn_entities_on_enter_state::.in_set(StateTransitionSystems::EnterSchedules), ) } diff --git a/crates/bevy_state/src/lib.rs b/crates/bevy_state/src/lib.rs index b2714b50c5..db40adeeb4 100644 --- a/crates/bevy_state/src/lib.rs +++ b/crates/bevy_state/src/lib.rs @@ -28,6 +28,9 @@ //! - A [`StateTransitionEvent`](crate::state::StateTransitionEvent) that gets fired when a given state changes. //! - The [`in_state`](crate::condition::in_state) and [`state_changed`](crate::condition::state_changed) run conditions - which are used //! to determine whether a system should run based on the current state. +//! +//! Bevy also provides ("state-scoped entities")[`crate::state_scoped`] functionality for managing the lifetime of entities in the context of game states. +//! This, especially in combination with system scheduling, enables a flexible and expressive way to manage spawning and despawning entities. #![cfg_attr( any(docsrs, docsrs_dep), @@ -56,8 +59,7 @@ pub mod condition; /// Provides definitions for the basic traits required by the state system pub mod state; -/// Provides [`StateScoped`](crate::state_scoped::StateScoped) and -/// [`clear_state_scoped_entities`](crate::state_scoped::clear_state_scoped_entities) for managing lifetime of entities. +/// Provides tools for managing the lifetime of entities based on state transitions. pub mod state_scoped; #[cfg(feature = "bevy_app")] /// Provides [`App`](bevy_app::App) and [`SubApp`](bevy_app::SubApp) with methods for registering @@ -89,6 +91,6 @@ pub mod prelude { OnExit, OnTransition, State, StateSet, StateTransition, StateTransitionEvent, States, SubStates, TransitionSchedules, }, - state_scoped::StateScoped, + state_scoped::{DespawnOnEnterState, DespawnOnExitState}, }; } diff --git a/crates/bevy_state/src/state/freely_mutable_state.rs b/crates/bevy_state/src/state/freely_mutable_state.rs index aef72e15fa..8751b3ad29 100644 --- a/crates/bevy_state/src/state/freely_mutable_state.rs +++ b/crates/bevy_state/src/state/freely_mutable_state.rs @@ -17,11 +17,11 @@ pub trait FreelyMutableState: States { fn register_state(schedule: &mut Schedule) { schedule.configure_sets(( ApplyStateTransition::::default() - .in_set(StateTransitionSteps::DependentTransitions), - ExitSchedules::::default().in_set(StateTransitionSteps::ExitSchedules), + .in_set(StateTransitionSystems::DependentTransitions), + ExitSchedules::::default().in_set(StateTransitionSystems::ExitSchedules), TransitionSchedules::::default() - .in_set(StateTransitionSteps::TransitionSchedules), - EnterSchedules::::default().in_set(StateTransitionSteps::EnterSchedules), + .in_set(StateTransitionSystems::TransitionSchedules), + EnterSchedules::::default().in_set(StateTransitionSystems::EnterSchedules), )); schedule diff --git a/crates/bevy_state/src/state/state_set.rs b/crates/bevy_state/src/state/state_set.rs index 5199662027..69a6c41b3d 100644 --- a/crates/bevy_state/src/state/state_set.rs +++ b/crates/bevy_state/src/state/state_set.rs @@ -10,7 +10,7 @@ use self::sealed::StateSetSealed; use super::{ computed_states::ComputedStates, internal_apply_state_transition, last_transition, run_enter, run_exit, run_transition, sub_states::SubStates, take_next_state, ApplyStateTransition, - EnterSchedules, ExitSchedules, NextState, State, StateTransitionEvent, StateTransitionSteps, + EnterSchedules, ExitSchedules, NextState, State, StateTransitionEvent, StateTransitionSystems, States, TransitionSchedules, }; @@ -117,14 +117,14 @@ impl StateSet for S { schedule.configure_sets(( ApplyStateTransition::::default() - .in_set(StateTransitionSteps::DependentTransitions) + .in_set(StateTransitionSystems::DependentTransitions) .after(ApplyStateTransition::::default()), ExitSchedules::::default() - .in_set(StateTransitionSteps::ExitSchedules) + .in_set(StateTransitionSystems::ExitSchedules) .before(ExitSchedules::::default()), - TransitionSchedules::::default().in_set(StateTransitionSteps::TransitionSchedules), + TransitionSchedules::::default().in_set(StateTransitionSystems::TransitionSchedules), EnterSchedules::::default() - .in_set(StateTransitionSteps::EnterSchedules) + .in_set(StateTransitionSystems::EnterSchedules) .after(EnterSchedules::::default()), )); @@ -197,14 +197,14 @@ impl StateSet for S { schedule.configure_sets(( ApplyStateTransition::::default() - .in_set(StateTransitionSteps::DependentTransitions) + .in_set(StateTransitionSystems::DependentTransitions) .after(ApplyStateTransition::::default()), ExitSchedules::::default() - .in_set(StateTransitionSteps::ExitSchedules) + .in_set(StateTransitionSystems::ExitSchedules) .before(ExitSchedules::::default()), - TransitionSchedules::::default().in_set(StateTransitionSteps::TransitionSchedules), + TransitionSchedules::::default().in_set(StateTransitionSystems::TransitionSchedules), EnterSchedules::::default() - .in_set(StateTransitionSteps::EnterSchedules) + .in_set(StateTransitionSystems::EnterSchedules) .after(EnterSchedules::::default()), )); @@ -264,15 +264,15 @@ macro_rules! impl_state_set_sealed_tuples { schedule.configure_sets(( ApplyStateTransition::::default() - .in_set(StateTransitionSteps::DependentTransitions) + .in_set(StateTransitionSystems::DependentTransitions) $(.after(ApplyStateTransition::<$param::RawState>::default()))*, ExitSchedules::::default() - .in_set(StateTransitionSteps::ExitSchedules) + .in_set(StateTransitionSystems::ExitSchedules) $(.before(ExitSchedules::<$param::RawState>::default()))*, TransitionSchedules::::default() - .in_set(StateTransitionSteps::TransitionSchedules), + .in_set(StateTransitionSystems::TransitionSchedules), EnterSchedules::::default() - .in_set(StateTransitionSteps::EnterSchedules) + .in_set(StateTransitionSystems::EnterSchedules) $(.after(EnterSchedules::<$param::RawState>::default()))*, )); @@ -318,15 +318,15 @@ macro_rules! impl_state_set_sealed_tuples { schedule.configure_sets(( ApplyStateTransition::::default() - .in_set(StateTransitionSteps::DependentTransitions) + .in_set(StateTransitionSystems::DependentTransitions) $(.after(ApplyStateTransition::<$param::RawState>::default()))*, ExitSchedules::::default() - .in_set(StateTransitionSteps::ExitSchedules) + .in_set(StateTransitionSystems::ExitSchedules) $(.before(ExitSchedules::<$param::RawState>::default()))*, TransitionSchedules::::default() - .in_set(StateTransitionSteps::TransitionSchedules), + .in_set(StateTransitionSystems::TransitionSchedules), EnterSchedules::::default() - .in_set(StateTransitionSteps::EnterSchedules) + .in_set(StateTransitionSystems::EnterSchedules) $(.after(EnterSchedules::<$param::RawState>::default()))*, )); diff --git a/crates/bevy_state/src/state/states.rs b/crates/bevy_state/src/state/states.rs index 163e689f0a..2bbdd615ba 100644 --- a/crates/bevy_state/src/state/states.rs +++ b/crates/bevy_state/src/state/states.rs @@ -65,7 +65,11 @@ pub trait States: 'static + Send + Sync + Clone + PartialEq + Eq + Hash + Debug /// `ComputedState` dependencies. const DEPENDENCY_DEPTH: usize = 1; - /// Should [`StateScoped`](crate::state_scoped::StateScoped) be enabled for this state? If set to `true`, - /// the `StateScoped` component will be used to remove entities when changing state. + /// Should [state scoping](crate::state_scoped) be enabled for this state? + /// If set to `true`, the + /// [`DespawnOnEnterState`](crate::state_scoped::DespawnOnEnterState) and + /// [`DespawnOnExitState`](crate::state_scoped::DespawnOnEnterState) + /// components are used to remove entities when entering or exiting the + /// state. const SCOPED_ENTITIES_ENABLED: bool = false; } diff --git a/crates/bevy_state/src/state/transitions.rs b/crates/bevy_state/src/state/transitions.rs index be28926054..dfe711f245 100644 --- a/crates/bevy_state/src/state/transitions.rs +++ b/crates/bevy_state/src/state/transitions.rs @@ -71,7 +71,7 @@ pub struct StateTransitionEvent { /// /// These system sets are run sequentially, in the order of the enum variants. #[derive(SystemSet, Clone, Debug, PartialEq, Eq, Hash)] -pub enum StateTransitionSteps { +pub enum StateTransitionSystems { /// States apply their transitions from [`NextState`](super::NextState) /// and compute functions based on their parent states. DependentTransitions, @@ -83,6 +83,10 @@ pub enum StateTransitionSteps { EnterSchedules, } +/// Deprecated alias for [`StateTransitionSystems`]. +#[deprecated(since = "0.17.0", note = "Renamed to `StateTransitionSystems`.")] +pub type StateTransitionSteps = StateTransitionSystems; + #[derive(SystemSet, Clone, Debug, PartialEq, Eq, Hash)] /// System set that runs exit schedule(s) for state `S`. pub struct ExitSchedules(PhantomData); @@ -191,10 +195,10 @@ pub fn setup_state_transitions_in_world(world: &mut World) { let mut schedule = Schedule::new(StateTransition); schedule.configure_sets( ( - StateTransitionSteps::DependentTransitions, - StateTransitionSteps::ExitSchedules, - StateTransitionSteps::TransitionSchedules, - StateTransitionSteps::EnterSchedules, + StateTransitionSystems::DependentTransitions, + StateTransitionSystems::ExitSchedules, + StateTransitionSystems::TransitionSchedules, + StateTransitionSystems::EnterSchedules, ) .chain(), ); diff --git a/crates/bevy_state/src/state_scoped.rs b/crates/bevy_state/src/state_scoped.rs index b58017d6e3..c591d0c108 100644 --- a/crates/bevy_state/src/state_scoped.rs +++ b/crates/bevy_state/src/state_scoped.rs @@ -36,7 +36,7 @@ use crate::state::{StateTransitionEvent, States}; /// /// fn spawn_player(mut commands: Commands) { /// commands.spawn(( -/// StateScoped(GameState::InGame), +/// DespawnOnExitState(GameState::InGame), /// Player /// )); /// } @@ -55,9 +55,9 @@ use crate::state::{StateTransitionEvent, States}; /// ``` #[derive(Component, Clone)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component, Clone))] -pub struct StateScoped(pub S); +pub struct DespawnOnExitState(pub S); -impl Default for StateScoped +impl Default for DespawnOnExitState where S: States + Default, { @@ -66,12 +66,12 @@ where } } -/// Removes entities marked with [`StateScoped`] -/// when their state no longer matches the world state. -pub fn clear_state_scoped_entities( +/// Despawns entities marked with [`DespawnOnExitState`] when their state no +/// longer matches the world state. +pub fn despawn_entities_on_exit_state( mut commands: Commands, mut transitions: EventReader>, - query: Query<(Entity, &StateScoped)>, + query: Query<(Entity, &DespawnOnExitState)>, ) { // We use the latest event, because state machine internals generate at most 1 // transition event (per type) each frame. No event means no change happened @@ -91,3 +91,74 @@ pub fn clear_state_scoped_entities( } } } + +/// Entities marked with this component will be despawned +/// upon entering the given state. +/// +/// To enable this feature remember to configure your application +/// with [`enable_state_scoped_entities`](crate::app::AppExtStates::enable_state_scoped_entities) on your state(s) of choice. +/// +/// ``` +/// use bevy_state::prelude::*; +/// use bevy_ecs::{prelude::*, system::ScheduleSystem}; +/// +/// #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)] +/// enum GameState { +/// #[default] +/// MainMenu, +/// SettingsMenu, +/// InGame, +/// } +/// +/// # #[derive(Component)] +/// # struct Player; +/// +/// fn spawn_player(mut commands: Commands) { +/// commands.spawn(( +/// DespawnOnEnterState(GameState::MainMenu), +/// Player +/// )); +/// } +/// +/// # struct AppMock; +/// # impl AppMock { +/// # fn init_state(&mut self) {} +/// # fn enable_state_scoped_entities(&mut self) {} +/// # fn add_systems(&mut self, schedule: S, systems: impl IntoScheduleConfigs) {} +/// # } +/// # struct Update; +/// # let mut app = AppMock; +/// +/// app.init_state::(); +/// app.enable_state_scoped_entities::(); +/// app.add_systems(OnEnter(GameState::InGame), spawn_player); +/// ``` +#[derive(Component, Clone)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component))] +pub struct DespawnOnEnterState(pub S); + +/// Despawns entities marked with [`DespawnOnEnterState`] when their state +/// matches the world state. +pub fn despawn_entities_on_enter_state( + mut commands: Commands, + mut transitions: EventReader>, + query: Query<(Entity, &DespawnOnEnterState)>, +) { + // We use the latest event, because state machine internals generate at most 1 + // transition event (per type) each frame. No event means no change happened + // and we skip iterating all entities. + let Some(transition) = transitions.read().last() else { + return; + }; + if transition.entered == transition.exited { + return; + } + let Some(entered) = &transition.entered else { + return; + }; + for (entity, binding) in &query { + if binding.0 == *entered { + commands.entity(entity).despawn(); + } + } +} diff --git a/crates/bevy_state/src/state_scoped_events.rs b/crates/bevy_state/src/state_scoped_events.rs index c84f5c60bf..9defa4d888 100644 --- a/crates/bevy_state/src/state_scoped_events.rs +++ b/crates/bevy_state/src/state_scoped_events.rs @@ -89,10 +89,11 @@ fn add_state_scoped_event_impl( pub trait StateScopedEventsAppExt { /// Adds an [`Event`] that is automatically cleaned up when leaving the specified `state`. /// - /// Note that event cleanup is ordered ambiguously relative to [`StateScoped`](crate::prelude::StateScoped) entity + /// Note that event cleanup is ordered ambiguously relative to [`DespawnOnEnterState`](crate::prelude::DespawnOnEnterState) + /// and [`DespawnOnExitState`](crate::prelude::DespawnOnExitState) entity /// cleanup and the [`OnExit`] schedule for the target state. All of these (state scoped /// entities and events cleanup, and `OnExit`) occur within schedule [`StateTransition`](crate::prelude::StateTransition) - /// and system set `StateTransitionSteps::ExitSchedules`. + /// and system set `StateTransitionSystems::ExitSchedules`. fn add_state_scoped_event(&mut self, state: impl FreelyMutableState) -> &mut Self; } diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index 670f793c31..2bc74a1aa7 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -66,15 +66,15 @@ pub mod prelude { }; } -use bevy_app::{prelude::*, Animation}; +use bevy_app::{prelude::*, AnimationSystems}; #[cfg(feature = "default_font")] use bevy_asset::{load_internal_binary_asset, Handle}; -use bevy_asset::{AssetApp, AssetEvents}; +use bevy_asset::{AssetApp, AssetEventSystems}; use bevy_ecs::prelude::*; use bevy_render::{ - camera::CameraUpdateSystem, view::VisibilitySystems, ExtractSchedule, RenderApp, + camera::CameraUpdateSystems, view::VisibilitySystems, ExtractSchedule, RenderApp, }; -use bevy_sprite::SpriteSystem; +use bevy_sprite::SpriteSystems; /// The raw data for the default font used by `bevy_text` #[cfg(feature = "default_font")] @@ -87,21 +87,13 @@ pub const DEFAULT_FONT_DATA: &[u8] = include_bytes!("FiraMono-subset.ttf"); #[derive(Default)] pub struct TextPlugin; -/// Text is rendered for two different view projections; -/// 2-dimensional text ([`Text2d`]) is rendered in "world space" with a `BottomToTop` Y-axis, -/// while UI is rendered with a `TopToBottom` Y-axis. -/// This matters for text because the glyph positioning is different in either layout. -/// For `TopToBottom`, 0 is the top of the text, while for `BottomToTop` 0 is the bottom. -pub enum YAxisOrientation { - /// Top to bottom Y-axis orientation, for UI - TopToBottom, - /// Bottom to top Y-axis orientation, for 2d world space - BottomToTop, -} - /// System set in [`PostUpdate`] where all 2d text update systems are executed. #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] -pub struct Update2dText; +pub struct Text2dUpdateSystems; + +/// Deprecated alias for [`Text2dUpdateSystems`]. +#[deprecated(since = "0.17.0", note = "Renamed to `Text2dUpdateSystems`.")] +pub type Update2dText = Text2dUpdateSystems; impl Plugin for TextPlugin { fn build(&self, app: &mut App) { @@ -110,6 +102,7 @@ impl Plugin for TextPlugin { .register_type::() .register_type::() .register_type::() + .register_type::() .register_type::() .register_type::() .register_type::() @@ -124,26 +117,26 @@ impl Plugin for TextPlugin { .add_systems( PostUpdate, ( - remove_dropped_font_atlas_sets.before(AssetEvents), + remove_dropped_font_atlas_sets.before(AssetEventSystems), detect_text_needs_rerender::, update_text2d_layout // Potential conflict: `Assets` // In practice, they run independently since `bevy_render::camera_update_system` // will only ever observe its own render target, and `update_text2d_layout` // will never modify a pre-existing `Image` asset. - .ambiguous_with(CameraUpdateSystem), + .ambiguous_with(CameraUpdateSystems), calculate_bounds_text2d.in_set(VisibilitySystems::CalculateBounds), ) .chain() - .in_set(Update2dText) - .after(Animation), + .in_set(Text2dUpdateSystems) + .after(AnimationSystems), ) .add_systems(Last, trim_cosmic_cache); if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app.add_systems( ExtractSchedule, - extract_text2d_sprite.after(SpriteSystem::ExtractSprites), + extract_text2d_sprite.after(SpriteSystems::ExtractSprites), ); } diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index f1bdeded91..93ee4907bd 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -9,7 +9,7 @@ use bevy_ecs::{ }; use bevy_image::prelude::*; use bevy_log::{once, warn}; -use bevy_math::{UVec2, Vec2}; +use bevy_math::{Rect, UVec2, Vec2}; use bevy_platform::collections::HashMap; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; @@ -17,7 +17,7 @@ use cosmic_text::{Attrs, Buffer, Family, Metrics, Shaping, Wrap}; use crate::{ error::TextError, ComputedTextBlock, Font, FontAtlasSets, FontSmoothing, JustifyText, - LineBreak, PositionedGlyph, TextBounds, TextEntity, TextFont, TextLayout, YAxisOrientation, + LineBreak, PositionedGlyph, TextBounds, TextEntity, TextFont, TextLayout, }; /// A wrapper resource around a [`cosmic_text::FontSystem`] @@ -228,12 +228,12 @@ impl TextPipeline { font_atlas_sets: &mut FontAtlasSets, texture_atlases: &mut Assets, textures: &mut Assets, - y_axis_orientation: YAxisOrientation, computed: &mut ComputedTextBlock, font_system: &mut CosmicFontSystem, swash_cache: &mut SwashCache, ) -> Result<(), TextError> { layout_info.glyphs.clear(); + layout_info.section_rects.clear(); layout_info.size = Default::default(); // Clear this here at the focal point of text rendering to ensure the field's lifecycle has strong boundaries. @@ -265,11 +265,38 @@ impl TextPipeline { 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.section_rects.push(( + computed.entities[section].entity, + Rect::new( + start, + run.line_top, + end, + run.line_top + run.line_height, + ), + )); + 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 = glyph_info[span_index].0; @@ -320,10 +347,6 @@ impl TextPipeline { let x = glyph_size.x as f32 / 2.0 + left + physical_glyph.x as f32; let y = line_y.round() + physical_glyph.y as f32 - top + glyph_size.y as f32 / 2.0; - let y = match y_axis_orientation { - YAxisOrientation::TopToBottom => y, - YAxisOrientation::BottomToTop => box_size.y - y, - }; let position = Vec2::new(x, y); @@ -339,6 +362,12 @@ impl TextPipeline { layout_info.glyphs.push(pos_glyph); Ok(()) }); + if let Some(section) = current_section { + layout_info.section_rects.push(( + computed.entities[section].entity, + Rect::new(start, run.line_top, end, run.line_top + run.line_height), + )); + } result }); @@ -418,6 +447,9 @@ impl TextPipeline { pub struct TextLayoutInfo { /// Scaled and positioned glyphs in screenspace pub glyphs: Vec, + /// Rects bounding the text block's text sections. + /// A text section spanning more than one line will have multiple bounding rects. + pub section_rects: Vec<(Entity, Rect)>, /// The glyphs resulting size pub size: Vec2, } @@ -495,7 +527,7 @@ fn get_attrs<'a>( face_info: &'a FontFaceInfo, scale_factor: f64, ) -> Attrs<'a> { - let attrs = Attrs::new() + Attrs::new() .metadata(span_index) .family(Family::Name(&face_info.family_name)) .stretch(face_info.stretch) @@ -508,8 +540,7 @@ fn get_attrs<'a>( } .scale(scale_factor as f32), ) - .color(cosmic_text::Color(color.to_linear().as_u32())); - attrs + .color(cosmic_text::Color(color.to_linear().as_u32())) } /// Calculate the size of the text area for the given buffer. diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index faa5d93dc9..e9e78e3ed2 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -407,6 +407,30 @@ impl TextColor { pub const WHITE: Self = TextColor(Color::WHITE); } +/// The background color of the text for this section. +#[derive(Component, Copy, Clone, Debug, Deref, DerefMut, Reflect, PartialEq)] +#[reflect(Component, Default, Debug, PartialEq, Clone)] +pub struct TextBackgroundColor(pub Color); + +impl Default for TextBackgroundColor { + fn default() -> Self { + Self(Color::BLACK) + } +} + +impl> From for TextBackgroundColor { + fn from(color: T) -> Self { + Self(color.into()) + } +} + +impl TextBackgroundColor { + /// Black background + pub const BLACK: Self = TextBackgroundColor(Color::BLACK); + /// White background + pub const WHITE: Self = TextBackgroundColor(Color::WHITE); +} + /// Determines how lines will be broken when preventing text from running out of bounds. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Reflect, Serialize, Deserialize)] #[reflect(Serialize, Deserialize, Clone, PartialEq, Hash, Default)] diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index a9419e89c0..5069804df8 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -2,7 +2,7 @@ use crate::pipeline::CosmicFontSystem; use crate::{ ComputedTextBlock, Font, FontAtlasSets, LineBreak, PositionedGlyph, SwashCache, TextBounds, TextColor, TextError, TextFont, TextLayout, TextLayoutInfo, TextPipeline, TextReader, TextRoot, - TextSpanAccess, TextWriter, YAxisOrientation, + TextSpanAccess, TextWriter, }; use bevy_asset::Assets; use bevy_color::LinearRgba; @@ -182,10 +182,10 @@ pub fn extract_text2d_sprite( text_bounds.width.unwrap_or(text_layout_info.size.x), text_bounds.height.unwrap_or(text_layout_info.size.y), ); - let bottom_left = - -(anchor.as_vec() + 0.5) * size + (size.y - text_layout_info.size.y) * Vec2::Y; + + let top_left = (Anchor::TOP_LEFT.0 - anchor.as_vec()) * size; let transform = - *global_transform * GlobalTransform::from_translation(bottom_left.extend(0.)) * scaling; + *global_transform * GlobalTransform::from_translation(top_left.extend(0.)) * scaling; let mut color = LinearRgba::WHITE; let mut current_span = usize::MAX; @@ -218,7 +218,7 @@ pub fn extract_text2d_sprite( .textures[atlas_info.location.glyph_index] .as_rect(); extracted_slices.slices.push(ExtractedSlice { - offset: *position, + offset: Vec2::new(position.x, -position.y), rect, size: rect.size(), }); @@ -316,7 +316,6 @@ pub fn update_text2d_layout( &mut font_atlas_sets, &mut texture_atlases, &mut textures, - YAxisOrientation::BottomToTop, computed.as_mut(), &mut font_system, &mut swash_cache, diff --git a/crates/bevy_time/src/fixed.rs b/crates/bevy_time/src/fixed.rs index dc8f89fd6a..66d2585e82 100644 --- a/crates/bevy_time/src/fixed.rs +++ b/crates/bevy_time/src/fixed.rs @@ -235,7 +235,7 @@ impl Default for Fixed { /// Runs [`FixedMain`] zero or more times based on delta of /// [`Time`](Virtual) and [`Time::overstep`]. /// You can order your systems relative to this by using -/// [`RunFixedMainLoopSystem`](bevy_app::prelude::RunFixedMainLoopSystem). +/// [`RunFixedMainLoopSystems`](bevy_app::prelude::RunFixedMainLoopSystems). pub(super) fn run_fixed_main_schedule(world: &mut World) { let delta = world.resource::>().delta(); world.resource_mut::>().accumulate(delta); diff --git a/crates/bevy_time/src/lib.rs b/crates/bevy_time/src/lib.rs index 1ac297fafa..858da0d7e4 100644 --- a/crates/bevy_time/src/lib.rs +++ b/crates/bevy_time/src/lib.rs @@ -57,7 +57,11 @@ pub struct TimePlugin; /// Updates the elapsed time. Any system that interacts with [`Time`] component should run after /// this. #[derive(Debug, PartialEq, Eq, Clone, Hash, SystemSet)] -pub struct TimeSystem; +pub struct TimeSystems; + +/// Deprecated alias for [`TimeSystems`]. +#[deprecated(since = "0.17.0", note = "Renamed to `TimeSystems`.")] +pub type TimeSystem = TimeSystems; impl Plugin for TimePlugin { fn build(&self, app: &mut App) { @@ -79,12 +83,12 @@ impl Plugin for TimePlugin { app.add_systems( First, time_system - .in_set(TimeSystem) + .in_set(TimeSystems) .ambiguous_with(event_update_system), ) .add_systems( RunFixedMainLoop, - run_fixed_main_schedule.in_set(RunFixedMainLoopSystem::FixedMainLoop), + run_fixed_main_schedule.in_set(RunFixedMainLoopSystems::FixedMainLoop), ); // Ensure the events are not dropped until `FixedMain` systems can observe them @@ -109,7 +113,7 @@ pub enum TimeUpdateStrategy { /// [`Time`] will be updated to the specified [`Instant`] value each frame. /// In order for time to progress, this value must be manually updated each frame. /// - /// Note that the `Time` resource will not be updated until [`TimeSystem`] runs. + /// Note that the `Time` resource will not be updated until [`TimeSystems`] runs. ManualInstant(Instant), /// [`Time`] will be incremented by the specified [`Duration`] each frame. ManualDuration(Duration), diff --git a/crates/bevy_transform/src/commands.rs b/crates/bevy_transform/src/commands.rs index 2fda216210..aed9ea6b22 100644 --- a/crates/bevy_transform/src/commands.rs +++ b/crates/bevy_transform/src/commands.rs @@ -20,7 +20,7 @@ pub trait BuildChildrenTransformExt { /// Make this entity parentless while preserving this entity's [`GlobalTransform`] /// by updating its [`Transform`] to be equal to its current [`GlobalTransform`]. /// - /// See [`EntityWorldMut::remove_parent`] or [`EntityCommands::remove_parent`] for a method that doesn't update the [`Transform`]. + /// See [`EntityWorldMut::remove::`] or [`EntityCommands::remove::`] for a method that doesn't update the [`Transform`]. /// /// Note that both the hierarchy and transform updates will only execute /// the next time commands are applied diff --git a/crates/bevy_transform/src/components/global_transform.rs b/crates/bevy_transform/src/components/global_transform.rs index f606071fe1..b10d5a9d1a 100644 --- a/crates/bevy_transform/src/components/global_transform.rs +++ b/crates/bevy_transform/src/components/global_transform.rs @@ -31,13 +31,15 @@ use { /// if it doesn't have a [`ChildOf`](bevy_ecs::hierarchy::ChildOf) component. /// /// [`GlobalTransform`] is managed by Bevy; it is computed by successively applying the [`Transform`] of each ancestor -/// entity which has a Transform. This is done automatically by Bevy-internal systems in the system set -/// [`TransformPropagate`](crate::TransformSystem::TransformPropagate). +/// entity which has a Transform. This is done automatically by Bevy-internal systems in the [`TransformSystems::Propagate`] +/// system set. /// /// This system runs during [`PostUpdate`](bevy_app::PostUpdate). If you /// update the [`Transform`] of an entity in this schedule or after, you will notice a 1 frame lag /// before the [`GlobalTransform`] is updated. /// +/// [`TransformSystems::Propagate`]: crate::TransformSystems::Propagate +/// /// # Examples /// /// - [`transform`][transform_example] @@ -159,7 +161,7 @@ impl GlobalTransform { /// /// ``` /// # use bevy_transform::prelude::{GlobalTransform, Transform}; - /// # use bevy_ecs::prelude::{Entity, Query, Component, Commands}; + /// # use bevy_ecs::prelude::{Entity, Query, Component, Commands, ChildOf}; /// #[derive(Component)] /// struct ToReparent { /// new_parent: Entity, @@ -174,7 +176,7 @@ impl GlobalTransform { /// *transform = initial.reparented_to(parent_transform); /// commands.entity(entity) /// .remove::() - /// .set_parent(to_reparent.new_parent); + /// .insert(ChildOf(to_reparent.new_parent)); /// } /// } /// } diff --git a/crates/bevy_transform/src/components/transform.rs b/crates/bevy_transform/src/components/transform.rs index 0ea9e1930f..7873ae743c 100644 --- a/crates/bevy_transform/src/components/transform.rs +++ b/crates/bevy_transform/src/components/transform.rs @@ -49,13 +49,15 @@ fn assert_is_normalized(message: &str, length_squared: f32) { /// /// [`GlobalTransform`] is the position of an entity relative to the reference frame. /// -/// [`GlobalTransform`] is updated from [`Transform`] by systems in the system set -/// [`TransformPropagate`](crate::TransformSystem::TransformPropagate). +/// [`GlobalTransform`] is updated from [`Transform`] in the [`TransformSystems::Propagate`] +/// system set. /// /// This system runs during [`PostUpdate`](bevy_app::PostUpdate). If you /// update the [`Transform`] of an entity during this set or after, you will notice a 1 frame lag /// before the [`GlobalTransform`] is updated. /// +/// [`TransformSystems::Propagate`]: crate::TransformSystems::Propagate +/// /// # Examples /// /// - [`transform`][transform_example] diff --git a/crates/bevy_transform/src/lib.rs b/crates/bevy_transform/src/lib.rs index 9a6538ed53..4d107346f7 100644 --- a/crates/bevy_transform/src/lib.rs +++ b/crates/bevy_transform/src/lib.rs @@ -45,10 +45,10 @@ pub mod prelude { pub use crate::{ commands::BuildChildrenTransformExt, helper::TransformHelper, - plugins::{TransformPlugin, TransformSystem}, + plugins::{TransformPlugin, TransformSystems}, traits::TransformPoint, }; } #[cfg(feature = "bevy-support")] -pub use prelude::{TransformPlugin, TransformPoint, TransformSystem}; +pub use prelude::{TransformPlugin, TransformPoint, TransformSystems}; diff --git a/crates/bevy_transform/src/plugins.rs b/crates/bevy_transform/src/plugins.rs index 3b22aefb14..f70e7e1f1d 100644 --- a/crates/bevy_transform/src/plugins.rs +++ b/crates/bevy_transform/src/plugins.rs @@ -4,11 +4,15 @@ use bevy_ecs::schedule::{IntoScheduleConfigs, SystemSet}; /// Set enum for the systems relating to transform propagation #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] -pub enum TransformSystem { +pub enum TransformSystems { /// Propagates changes in transform to children's [`GlobalTransform`](crate::components::GlobalTransform) - TransformPropagate, + Propagate, } +/// Deprecated alias for [`TransformSystems`]. +#[deprecated(since = "0.17.0", note = "Renamed to `TransformSystems`.")] +pub type TransformSystem = TransformSystems; + /// The base plugin for handling [`Transform`](crate::components::Transform) components #[derive(Default)] pub struct TransformPlugin; @@ -30,7 +34,7 @@ impl Plugin for TransformPlugin { sync_simple_transforms, ) .chain() - .in_set(TransformSystem::TransformPropagate), + .in_set(TransformSystems::Propagate), ) .add_systems( PostUpdate, @@ -41,7 +45,7 @@ impl Plugin for TransformPlugin { sync_simple_transforms, ) .chain() - .in_set(TransformSystem::TransformPropagate), + .in_set(TransformSystems::Propagate), ); } } diff --git a/crates/bevy_transform/src/systems.rs b/crates/bevy_transform/src/systems.rs index ecf6651271..62038b37ed 100644 --- a/crates/bevy_transform/src/systems.rs +++ b/crates/bevy_transform/src/systems.rs @@ -798,8 +798,8 @@ mod test { let translation = vec3(1.0, 0.0, 0.0); // These will be overwritten. - let mut child = Entity::from_raw(0); - let mut grandchild = Entity::from_raw(1); + let mut child = Entity::from_raw_u32(0).unwrap(); + let mut grandchild = Entity::from_raw_u32(1).unwrap(); let parent = app .world_mut() .spawn(Transform::from_translation(translation)) @@ -849,7 +849,7 @@ mod test { ); fn setup_world(world: &mut World) -> (Entity, Entity) { - let mut grandchild = Entity::from_raw(0); + let mut grandchild = Entity::from_raw_u32(0).unwrap(); let child = world .spawn(Transform::IDENTITY) .with_children(|builder| { diff --git a/crates/bevy_ui/Cargo.toml b/crates/bevy_ui/Cargo.toml index eb1e9ed0da..2874d8738b 100644 --- a/crates/bevy_ui/Cargo.toml +++ b/crates/bevy_ui/Cargo.toml @@ -35,6 +35,7 @@ bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-fea # other taffy = { version = "0.7" } serde = { version = "1", features = ["derive"], optional = true } +uuid = { version = "1.1", features = ["v4"], optional = true } bytemuck = { version = "1.5", features = ["derive"] } thiserror = { version = "2", default-features = false } derive_more = { version = "1", default-features = false, features = ["from"] } @@ -51,7 +52,7 @@ serialize = [ "bevy_math/serialize", "bevy_platform/serialize", ] -bevy_ui_picking_backend = ["bevy_picking"] +bevy_ui_picking_backend = ["bevy_picking", "dep:uuid"] bevy_ui_debug = [] # Experimental features diff --git a/crates/bevy_ui/src/accessibility.rs b/crates/bevy_ui/src/accessibility.rs index 3b0e6538f0..637ed943f2 100644 --- a/crates/bevy_ui/src/accessibility.rs +++ b/crates/bevy_ui/src/accessibility.rs @@ -14,7 +14,7 @@ use bevy_ecs::{ world::Ref, }; use bevy_math::Vec3Swizzles; -use bevy_render::camera::CameraUpdateSystem; +use bevy_render::camera::CameraUpdateSystems; use bevy_transform::prelude::GlobalTransform; use accesskit::{Node, Rect, Role}; @@ -151,8 +151,8 @@ impl Plugin for AccessibilityPlugin { PostUpdate, ( calc_bounds - .after(bevy_transform::TransformSystem::TransformPropagate) - .after(CameraUpdateSystem) + .after(bevy_transform::TransformSystems::Propagate) + .after(CameraUpdateSystems) // the listed systems do not affect calculated size .ambiguous_with(crate::ui_stack_system), button_changed, diff --git a/crates/bevy_ui/src/layout/mod.rs b/crates/bevy_ui/src/layout/mod.rs index 1c1c60ea8f..9e6906b1f7 100644 --- a/crates/bevy_ui/src/layout/mod.rs +++ b/crates/bevy_ui/src/layout/mod.rs @@ -1039,7 +1039,7 @@ mod tests { let (mut world, ..) = setup_ui_test_world(); - let root_node_entity = Entity::from_raw(1); + let root_node_entity = Entity::from_raw_u32(1).unwrap(); struct TestSystemParam { root_node_entity: Entity, diff --git a/crates/bevy_ui/src/layout/ui_surface.rs b/crates/bevy_ui/src/layout/ui_surface.rs index a4af6737a7..2df6afa947 100644 --- a/crates/bevy_ui/src/layout/ui_surface.rs +++ b/crates/bevy_ui/src/layout/ui_surface.rs @@ -318,7 +318,7 @@ mod tests { #[test] fn test_upsert() { let mut ui_surface = UiSurface::default(); - let root_node_entity = Entity::from_raw(1); + let root_node_entity = Entity::from_raw_u32(1).unwrap(); let node = Node::default(); // standard upsert @@ -350,7 +350,7 @@ mod tests { #[test] fn test_remove_entities() { let mut ui_surface = UiSurface::default(); - let root_node_entity = Entity::from_raw(1); + let root_node_entity = Entity::from_raw_u32(1).unwrap(); let node = Node::default(); ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &node, None); @@ -366,7 +366,7 @@ mod tests { #[test] fn test_try_update_measure() { let mut ui_surface = UiSurface::default(); - let root_node_entity = Entity::from_raw(1); + let root_node_entity = Entity::from_raw_u32(1).unwrap(); let node = Node::default(); ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &node, None); @@ -381,8 +381,8 @@ mod tests { #[test] fn test_update_children() { let mut ui_surface = UiSurface::default(); - let root_node_entity = Entity::from_raw(1); - let child_entity = Entity::from_raw(2); + let root_node_entity = Entity::from_raw_u32(1).unwrap(); + let child_entity = Entity::from_raw_u32(2).unwrap(); let node = Node::default(); ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &node, None); @@ -402,8 +402,8 @@ mod tests { #[test] fn test_set_camera_children() { let mut ui_surface = UiSurface::default(); - let root_node_entity = Entity::from_raw(1); - let child_entity = Entity::from_raw(2); + let root_node_entity = Entity::from_raw_u32(1).unwrap(); + let child_entity = Entity::from_raw_u32(2).unwrap(); let node = Node::default(); ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &node, None); diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index 56462b6952..ae54ffe607 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -19,6 +19,8 @@ pub mod widget; pub mod picking_backend; use bevy_derive::{Deref, DerefMut}; +#[cfg(feature = "bevy_ui_picking_backend")] +use bevy_picking::PickingSystems; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; mod accessibility; // This module is not re-exported, but is instead made public. @@ -39,7 +41,7 @@ pub use render::*; pub use ui_material::*; pub use ui_node::*; -use widget::{ImageNode, ImageNodeSize}; +use widget::{ImageNode, ImageNodeSize, ViewportNode}; /// The UI prelude. /// @@ -59,19 +61,20 @@ pub mod prelude { geometry::*, ui_material::*, ui_node::*, - widget::{Button, ImageNode, Label, NodeImageMode}, + widget::{Button, ImageNode, Label, NodeImageMode, ViewportNode}, Interaction, MaterialNode, UiMaterialPlugin, UiScale, }, // `bevy_sprite` re-exports for texture slicing bevy_sprite::{BorderRect, SliceScaleMode, SpriteImageMode, TextureSlicer}, + bevy_text::TextBackgroundColor, }; } -use bevy_app::{prelude::*, Animation}; +use bevy_app::{prelude::*, AnimationSystems}; use bevy_ecs::prelude::*; -use bevy_input::InputSystem; -use bevy_render::{camera::CameraUpdateSystem, RenderApp}; -use bevy_transform::TransformSystem; +use bevy_input::InputSystems; +use bevy_render::{camera::CameraUpdateSystems, RenderApp}; +use bevy_transform::TransformSystems; use layout::ui_surface::UiSurface; use stack::ui_stack_system; pub use stack::UiStack; @@ -94,7 +97,7 @@ impl Default for UiPlugin { /// The label enum labeling the types of systems in the Bevy UI #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] -pub enum UiSystem { +pub enum UiSystems { /// After this label, input interactions with UI entities have been updated for this frame. /// /// Runs in [`PreUpdate`]. @@ -107,7 +110,7 @@ pub enum UiSystem { /// /// Runs in [`PostUpdate`]. Layout, - /// UI systems ordered after [`UiSystem::Layout`]. + /// UI systems ordered after [`UiSystems::Layout`]. /// /// Runs in [`PostUpdate`]. PostLayout, @@ -117,6 +120,10 @@ pub enum UiSystem { Stack, } +/// Deprecated alias for [`UiSystems`]. +#[deprecated(since = "0.17.0", note = "Renamed to `UiSystems`.")] +pub type UiSystem = UiSystems; + /// The current scale of the UI. /// /// A multiplier to fixed-sized ui values. @@ -134,10 +141,10 @@ impl Default for UiScale { // Marks systems that can be ambiguous with [`widget::text_system`] if the `bevy_text` feature is enabled. // See https://github.com/bevyengine/bevy/pull/11391 for more details. #[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)] -struct AmbiguousWithTextSystem; +struct AmbiguousWithText; #[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)] -struct AmbiguousWithUpdateText2DLayout; +struct AmbiguousWithUpdateText2dLayout; impl Plugin for UiPlugin { fn build(&self, app: &mut App) { @@ -156,6 +163,7 @@ impl Plugin for UiPlugin { .register_type::() .register_type::() .register_type::() + .register_type::() .register_type::() .register_type::() .register_type::() @@ -172,25 +180,29 @@ impl Plugin for UiPlugin { .configure_sets( PostUpdate, ( - CameraUpdateSystem, - UiSystem::Prepare.after(Animation), - UiSystem::Content, - UiSystem::Layout, - UiSystem::PostLayout, + CameraUpdateSystems, + UiSystems::Prepare.after(AnimationSystems), + UiSystems::Content, + UiSystems::Layout, + UiSystems::PostLayout, ) .chain(), ) .add_systems( PreUpdate, - ui_focus_system.in_set(UiSystem::Focus).after(InputSystem), + ui_focus_system.in_set(UiSystems::Focus).after(InputSystems), ); #[cfg(feature = "bevy_ui_picking_backend")] - app.add_plugins(picking_backend::UiPickingPlugin); + app.add_plugins(picking_backend::UiPickingPlugin) + .add_systems( + First, + widget::viewport_picking.in_set(PickingSystems::PostInput), + ); let ui_layout_system_config = ui_layout_system - .in_set(UiSystem::Layout) - .before(TransformSystem::TransformPropagate); + .in_set(UiSystems::Layout) + .before(TransformSystems::Propagate); let ui_layout_system_config = ui_layout_system_config // Text and Text2D operate on disjoint sets of entities @@ -200,25 +212,34 @@ impl Plugin for UiPlugin { app.add_systems( PostUpdate, ( - update_ui_context_system.in_set(UiSystem::Prepare), + update_ui_context_system.in_set(UiSystems::Prepare), ui_layout_system_config, ui_stack_system - .in_set(UiSystem::Stack) - // the systems don't care about stack index + .in_set(UiSystems::Stack) + // These systems don't care about stack index .ambiguous_with(update_clipping_system) .ambiguous_with(ui_layout_system) - .in_set(AmbiguousWithTextSystem), - update_clipping_system.after(TransformSystem::TransformPropagate), + .ambiguous_with(widget::update_viewport_render_target_size) + .in_set(AmbiguousWithText), + update_clipping_system.after(TransformSystems::Propagate), // Potential conflicts: `Assets` // They run independently since `widget::image_node_system` will only ever observe // its own ImageNode, and `widget::text_system` & `bevy_text::update_text2d_layout` // will never modify a pre-existing `Image` asset. widget::update_image_content_size_system - .in_set(UiSystem::Content) - .in_set(AmbiguousWithTextSystem) - .in_set(AmbiguousWithUpdateText2DLayout), + .in_set(UiSystems::Content) + .in_set(AmbiguousWithText) + .in_set(AmbiguousWithUpdateText2dLayout), + // Potential conflicts: `Assets` + // `widget::text_system` and `bevy_text::update_text2d_layout` run independently + // since this system will only ever update viewport images. + widget::update_viewport_render_target_size + .in_set(UiSystems::PostLayout) + .in_set(AmbiguousWithText) + .in_set(AmbiguousWithUpdateText2dLayout), ), ); + build_text_interop(app); if !self.enable_rendering { @@ -261,7 +282,7 @@ fn build_text_interop(app: &mut App) { widget::measure_text_system, ) .chain() - .in_set(UiSystem::Content) + .in_set(UiSystems::Content) // Text and Text2d are independent. .ambiguous_with(bevy_text::detect_text_needs_rerender::) // Potential conflict: `Assets` @@ -272,9 +293,9 @@ fn build_text_interop(app: &mut App) { // FIXME: Add an archetype invariant for this https://github.com/bevyengine/bevy/issues/1481. .ambiguous_with(widget::update_image_content_size_system), widget::text_system - .in_set(UiSystem::PostLayout) + .in_set(UiSystems::PostLayout) .after(bevy_text::remove_dropped_font_atlas_sets) - .before(bevy_asset::AssetEvents) + .before(bevy_asset::AssetEventSystems) // Text2d and bevy_ui text are entirely on separate entities .ambiguous_with(bevy_text::detect_text_needs_rerender::) .ambiguous_with(bevy_text::update_text2d_layout) @@ -286,11 +307,11 @@ fn build_text_interop(app: &mut App) { app.configure_sets( PostUpdate, - AmbiguousWithTextSystem.ambiguous_with(widget::text_system), + AmbiguousWithText.ambiguous_with(widget::text_system), ); app.configure_sets( PostUpdate, - AmbiguousWithUpdateText2DLayout.ambiguous_with(bevy_text::update_text2d_layout), + AmbiguousWithUpdateText2dLayout.ambiguous_with(bevy_text::update_text2d_layout), ); } diff --git a/crates/bevy_ui/src/picking_backend.rs b/crates/bevy_ui/src/picking_backend.rs index f2d0667368..26b84c6005 100644 --- a/crates/bevy_ui/src/picking_backend.rs +++ b/crates/bevy_ui/src/picking_backend.rs @@ -81,7 +81,7 @@ impl Plugin for UiPickingPlugin { fn build(&self, app: &mut App) { app.init_resource::() .register_type::<(UiPickingCamera, UiPickingSettings)>() - .add_systems(PreUpdate, ui_picking.in_set(PickSet::Backend)); + .add_systems(PreUpdate, ui_picking.in_set(PickingSystems::Backend)); } } diff --git a/crates/bevy_ui/src/render/box_shadow.rs b/crates/bevy_ui/src/render/box_shadow.rs index 7ed9855038..94d306e7eb 100644 --- a/crates/bevy_ui/src/render/box_shadow.rs +++ b/crates/bevy_ui/src/render/box_shadow.rs @@ -3,7 +3,7 @@ use core::{hash::Hash, ops::Range}; use crate::{ - BoxShadow, BoxShadowSamples, CalculatedClip, ComputedNode, ComputedNodeTarget, RenderUiSystem, + BoxShadow, BoxShadowSamples, CalculatedClip, ComputedNode, ComputedNodeTarget, RenderUiSystems, ResolvedBorderRadius, TransparentUi, Val, }; use bevy_app::prelude::*; @@ -27,7 +27,7 @@ use bevy_render::{ renderer::{RenderDevice, RenderQueue}, sync_world::TemporaryRenderEntity, view::*, - Extract, ExtractSchedule, Render, RenderSet, + Extract, ExtractSchedule, Render, RenderSystems, }; use bevy_transform::prelude::GlobalTransform; use bytemuck::{Pod, Zeroable}; @@ -57,13 +57,13 @@ impl Plugin for BoxShadowPlugin { .init_resource::>() .add_systems( ExtractSchedule, - extract_shadows.in_set(RenderUiSystem::ExtractBoxShadows), + extract_shadows.in_set(RenderUiSystems::ExtractBoxShadows), ) .add_systems( Render, ( - queue_shadows.in_set(RenderSet::Queue), - prepare_shadows.in_set(RenderSet::PrepareBindGroups), + queue_shadows.in_set(RenderSystems::Queue), + prepare_shadows.in_set(RenderSystems::PrepareBindGroups), ), ); } diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index 8cb61cde21..e811cfe362 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -7,7 +7,7 @@ pub mod ui_texture_slice_pipeline; #[cfg(feature = "bevy_ui_debug")] mod debug_overlay; -use crate::widget::ImageNode; +use crate::widget::{ImageNode, ViewportNode}; use crate::{ BackgroundColor, BorderColor, BoxShadowSamples, CalculatedClip, ComputedNode, ComputedNodeTarget, Outline, ResolvedBorderRadius, TextShadow, UiAntiAlias, @@ -36,7 +36,7 @@ use bevy_render::{ render_resource::*, renderer::{RenderDevice, RenderQueue}, view::{ExtractedView, ViewUniforms}, - Extract, RenderApp, RenderSet, + Extract, RenderApp, RenderSystems, }; use bevy_render::{ render_phase::{PhaseItem, PhaseItemExtraIndex}, @@ -51,7 +51,9 @@ pub use debug_overlay::UiDebugOptions; use crate::{Display, Node}; use bevy_platform::collections::{HashMap, HashSet}; -use bevy_text::{ComputedTextBlock, PositionedGlyph, TextColor, TextLayoutInfo}; +use bevy_text::{ + ComputedTextBlock, PositionedGlyph, TextBackgroundColor, TextColor, TextLayoutInfo, +}; use bevy_transform::components::GlobalTransform; use box_shadow::BoxShadowPlugin; use bytemuck::{Pod, Zeroable}; @@ -98,18 +100,24 @@ pub mod stack_z_offsets { pub const UI_SHADER_HANDLE: Handle = weak_handle!("7d190d05-545b-42f5-bd85-22a0da85b0f6"); #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] -pub enum RenderUiSystem { +pub enum RenderUiSystems { ExtractCameraViews, ExtractBoxShadows, ExtractBackgrounds, ExtractImages, ExtractTextureSlice, ExtractBorders, + ExtractViewportNodes, + ExtractTextBackgrounds, ExtractTextShadows, ExtractText, ExtractDebug, } +/// Deprecated alias for [`RenderUiSystems`]. +#[deprecated(since = "0.17.0", note = "Renamed to `RenderUiSystems`.")] +pub type RenderUiSystem = RenderUiSystems; + pub fn build_ui_render(app: &mut App) { load_internal_asset!(app, UI_SHADER_HANDLE, "ui.wgsl", Shader::from_wgsl); @@ -129,37 +137,40 @@ pub fn build_ui_render(app: &mut App) { .configure_sets( ExtractSchedule, ( - RenderUiSystem::ExtractCameraViews, - RenderUiSystem::ExtractBoxShadows, - RenderUiSystem::ExtractBackgrounds, - RenderUiSystem::ExtractImages, - RenderUiSystem::ExtractTextureSlice, - RenderUiSystem::ExtractBorders, - RenderUiSystem::ExtractTextShadows, - RenderUiSystem::ExtractText, - RenderUiSystem::ExtractDebug, + RenderUiSystems::ExtractCameraViews, + RenderUiSystems::ExtractBoxShadows, + RenderUiSystems::ExtractBackgrounds, + RenderUiSystems::ExtractImages, + RenderUiSystems::ExtractTextureSlice, + RenderUiSystems::ExtractBorders, + RenderUiSystems::ExtractTextBackgrounds, + RenderUiSystems::ExtractTextShadows, + RenderUiSystems::ExtractText, + RenderUiSystems::ExtractDebug, ) .chain(), ) .add_systems( ExtractSchedule, ( - extract_ui_camera_view.in_set(RenderUiSystem::ExtractCameraViews), - extract_uinode_background_colors.in_set(RenderUiSystem::ExtractBackgrounds), - extract_uinode_images.in_set(RenderUiSystem::ExtractImages), - extract_uinode_borders.in_set(RenderUiSystem::ExtractBorders), - extract_text_shadows.in_set(RenderUiSystem::ExtractTextShadows), - extract_text_sections.in_set(RenderUiSystem::ExtractText), + extract_ui_camera_view.in_set(RenderUiSystems::ExtractCameraViews), + extract_uinode_background_colors.in_set(RenderUiSystems::ExtractBackgrounds), + extract_uinode_images.in_set(RenderUiSystems::ExtractImages), + extract_uinode_borders.in_set(RenderUiSystems::ExtractBorders), + extract_viewport_nodes.in_set(RenderUiSystems::ExtractViewportNodes), + extract_text_background_colors.in_set(RenderUiSystems::ExtractTextBackgrounds), + extract_text_shadows.in_set(RenderUiSystems::ExtractTextShadows), + extract_text_sections.in_set(RenderUiSystems::ExtractText), #[cfg(feature = "bevy_ui_debug")] - debug_overlay::extract_debug_overlay.in_set(RenderUiSystem::ExtractDebug), + debug_overlay::extract_debug_overlay.in_set(RenderUiSystems::ExtractDebug), ), ) .add_systems( Render, ( - queue_uinodes.in_set(RenderSet::Queue), - sort_phase_system::.in_set(RenderSet::PhaseSort), - prepare_uinodes.in_set(RenderSet::PrepareBindGroups), + queue_uinodes.in_set(RenderSystems::Queue), + sort_phase_system::.in_set(RenderSystems::PhaseSort), + prepare_uinodes.in_set(RenderSystems::PrepareBindGroups), ), ); @@ -692,6 +703,69 @@ pub fn extract_ui_camera_view( transparent_render_phases.retain(|entity, _| live_entities.contains(entity)); } +pub fn extract_viewport_nodes( + mut commands: Commands, + mut extracted_uinodes: ResMut, + camera_query: Extract>, + uinode_query: Extract< + Query<( + Entity, + &ComputedNode, + &GlobalTransform, + &InheritedVisibility, + Option<&CalculatedClip>, + &ComputedNodeTarget, + &ViewportNode, + )>, + >, + camera_map: Extract, +) { + let mut camera_mapper = camera_map.get_mapper(); + for (entity, uinode, transform, inherited_visibility, clip, camera, viewport_node) in + &uinode_query + { + // Skip invisible images + if !inherited_visibility.get() || uinode.is_empty() { + continue; + } + + let Some(extracted_camera_entity) = camera_mapper.map(camera) else { + continue; + }; + + let Some(image) = camera_query + .get(viewport_node.camera) + .ok() + .and_then(|camera| camera.target.as_image()) + else { + continue; + }; + + extracted_uinodes.uinodes.push(ExtractedUiNode { + render_entity: commands.spawn(TemporaryRenderEntity).id(), + stack_index: uinode.stack_index, + color: LinearRgba::WHITE, + rect: Rect { + min: Vec2::ZERO, + max: uinode.size, + }, + clip: clip.map(|clip| clip.clip), + image: image.id(), + extracted_camera_entity, + item: ExtractedUiItem::Node { + atlas_scaling: None, + transform: transform.compute_matrix(), + flip_x: false, + flip_y: false, + border: uinode.border(), + border_radius: uinode.border_radius(), + node_type: NodeType::Rect, + }, + main_entity: entity.into(), + }); + } +} + pub fn extract_text_sections( mut commands: Commands, mut extracted_uinodes: ResMut, @@ -879,6 +953,70 @@ pub fn extract_text_shadows( } } +pub fn extract_text_background_colors( + mut commands: Commands, + mut extracted_uinodes: ResMut, + uinode_query: Extract< + Query<( + Entity, + &ComputedNode, + &GlobalTransform, + &InheritedVisibility, + Option<&CalculatedClip>, + &ComputedNodeTarget, + &TextLayoutInfo, + )>, + >, + text_background_colors_query: Extract>, + camera_map: Extract, +) { + let mut camera_mapper = camera_map.get_mapper(); + for (entity, uinode, global_transform, inherited_visibility, clip, camera, text_layout_info) in + &uinode_query + { + // Skip if not visible or if size is set to zero (e.g. when a parent is set to `Display::None`) + if !inherited_visibility.get() || uinode.is_empty() { + continue; + } + + let Some(extracted_camera_entity) = camera_mapper.map(camera) else { + continue; + }; + + let transform = global_transform.affine() + * bevy_math::Affine3A::from_translation(-0.5 * uinode.size().extend(0.)); + + for &(section_entity, rect) in text_layout_info.section_rects.iter() { + let Ok(text_background_color) = text_background_colors_query.get(section_entity) else { + continue; + }; + + extracted_uinodes.uinodes.push(ExtractedUiNode { + render_entity: commands.spawn(TemporaryRenderEntity).id(), + stack_index: uinode.stack_index, + color: text_background_color.0.to_linear(), + rect: Rect { + min: Vec2::ZERO, + max: rect.size(), + }, + clip: clip.map(|clip| clip.clip), + image: AssetId::default(), + extracted_camera_entity, + item: ExtractedUiItem::Node { + atlas_scaling: None, + transform: transform * Mat4::from_translation(rect.center().extend(0.)), + flip_x: false, + flip_y: false, + border: uinode.border(), + border_radius: uinode.border_radius(), + node_type: NodeType::Rect, + }, + main_entity: entity.into(), + }); + } + } +} + #[repr(C)] #[derive(Copy, Clone, Pod, Zeroable)] struct UiVertex { diff --git a/crates/bevy_ui/src/render/ui_material_pipeline.rs b/crates/bevy_ui/src/render/ui_material_pipeline.rs index fb893b390e..84eb163e4a 100644 --- a/crates/bevy_ui/src/render/ui_material_pipeline.rs +++ b/crates/bevy_ui/src/render/ui_material_pipeline.rs @@ -21,7 +21,7 @@ use bevy_render::{ render_resource::{binding_types::uniform_buffer, *}, renderer::{RenderDevice, RenderQueue}, view::*, - Extract, ExtractSchedule, Render, RenderSet, + Extract, ExtractSchedule, Render, RenderSystems, }; use bevy_sprite::BorderRect; use bevy_transform::prelude::GlobalTransform; @@ -75,13 +75,13 @@ where .init_resource::>>() .add_systems( ExtractSchedule, - extract_ui_material_nodes::.in_set(RenderUiSystem::ExtractBackgrounds), + extract_ui_material_nodes::.in_set(RenderUiSystems::ExtractBackgrounds), ) .add_systems( Render, ( - queue_ui_material_nodes::.in_set(RenderSet::Queue), - prepare_uimaterial_nodes::.in_set(RenderSet::PrepareBindGroups), + queue_ui_material_nodes::.in_set(RenderSystems::Queue), + prepare_uimaterial_nodes::.in_set(RenderSystems::PrepareBindGroups), ), ); } diff --git a/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs b/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs index d8da926709..7d0fdb6a42 100644 --- a/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs +++ b/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs @@ -22,7 +22,7 @@ use bevy_render::{ sync_world::TemporaryRenderEntity, texture::{GpuImage, TRANSPARENT_IMAGE_HANDLE}, view::*, - Extract, ExtractSchedule, Render, RenderSet, + Extract, ExtractSchedule, Render, RenderSystems, }; use bevy_sprite::{SliceScaleMode, SpriteAssetEvents, SpriteImageMode, TextureSlicer}; use bevy_transform::prelude::GlobalTransform; @@ -53,13 +53,13 @@ impl Plugin for UiTextureSlicerPlugin { .init_resource::>() .add_systems( ExtractSchedule, - extract_ui_texture_slices.in_set(RenderUiSystem::ExtractTextureSlice), + extract_ui_texture_slices.in_set(RenderUiSystems::ExtractTextureSlice), ) .add_systems( Render, ( - queue_ui_slices.in_set(RenderSet::Queue), - prepare_ui_slices.in_set(RenderSet::PrepareBindGroups), + queue_ui_slices.in_set(RenderSystems::Queue), + prepare_ui_slices.in_set(RenderSystems::PrepareBindGroups), ), ); } diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index 2486296bac..c95859624e 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -31,7 +31,7 @@ pub struct ComputedNode { /// The order of the node in the UI layout. /// Nodes with a higher stack index are drawn on top of and receive interactions before nodes with lower stack indices. /// - /// Automatically calculated in [`super::UiSystem::Stack`]. + /// Automatically calculated in [`super::UiSystems::Stack`]. pub stack_index: u32, /// The size of the node as width and height in physical pixels. /// @@ -1178,7 +1178,7 @@ pub struct OverflowClipMargin { impl OverflowClipMargin { pub const DEFAULT: Self = Self { - visual_box: OverflowClipBox::ContentBox, + visual_box: OverflowClipBox::PaddingBox, margin: 0., }; @@ -1224,9 +1224,9 @@ impl OverflowClipMargin { )] pub enum OverflowClipBox { /// Clip any content that overflows outside the content box - #[default] ContentBox, /// Clip any content that overflows outside the padding box + #[default] PaddingBox, /// Clip any content that overflows outside the border box BorderBox, diff --git a/crates/bevy_ui/src/widget/mod.rs b/crates/bevy_ui/src/widget/mod.rs index 9be6a7673d..bbd319e986 100644 --- a/crates/bevy_ui/src/widget/mod.rs +++ b/crates/bevy_ui/src/widget/mod.rs @@ -3,11 +3,11 @@ mod button; mod image; mod label; - mod text; +mod viewport; pub use button::*; pub use image::*; pub use label::*; - pub use text::*; +pub use viewport::*; diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index 0153fa954c..785040c1e9 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -20,7 +20,7 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_text::{ scale_value, ComputedTextBlock, CosmicFontSystem, Font, FontAtlasSets, LineBreak, SwashCache, TextBounds, TextColor, TextError, TextFont, TextLayout, TextLayoutInfo, TextMeasureInfo, - TextPipeline, TextReader, TextRoot, TextSpanAccess, TextWriter, YAxisOrientation, + TextPipeline, TextReader, TextRoot, TextSpanAccess, TextWriter, }; use taffy::style::AvailableSpace; use tracing::error; @@ -328,7 +328,6 @@ fn queue_text( font_atlas_sets, texture_atlases, textures, - YAxisOrientation::TopToBottom, computed, font_system, swash_cache, diff --git a/crates/bevy_ui/src/widget/viewport.rs b/crates/bevy_ui/src/widget/viewport.rs new file mode 100644 index 0000000000..9cdc348da5 --- /dev/null +++ b/crates/bevy_ui/src/widget/viewport.rs @@ -0,0 +1,176 @@ +use bevy_asset::Assets; +use bevy_ecs::{ + component::Component, + entity::Entity, + event::EventReader, + query::{Changed, Or}, + reflect::ReflectComponent, + system::{Commands, Query, Res, ResMut}, +}; +use bevy_image::Image; +use bevy_math::Rect; +#[cfg(feature = "bevy_ui_picking_backend")] +use bevy_picking::{ + events::PointerState, + hover::HoverMap, + pointer::{Location, PointerId, PointerInput, PointerLocation}, +}; +use bevy_platform::collections::HashMap; +use bevy_reflect::Reflect; +use bevy_render::{ + camera::{Camera, NormalizedRenderTarget}, + render_resource::Extent3d, +}; +use bevy_transform::components::GlobalTransform; +use bevy_utils::default; +#[cfg(feature = "bevy_ui_picking_backend")] +use uuid::Uuid; + +use crate::{ComputedNode, Node}; + +/// Component used to render a [`Camera::target`] to a node. +/// +/// # See Also +/// +/// [`update_viewport_render_target_size`] +#[derive(Component, Debug, Clone, Copy, Reflect)] +#[reflect(Component, Debug)] +#[require(Node)] +#[cfg_attr( + feature = "bevy_ui_picking_backend", + require(PointerId::Custom(Uuid::new_v4())) +)] +pub struct ViewportNode { + /// The entity representing the [`Camera`] associated with this viewport. + /// + /// Note that removing the [`ViewportNode`] component will not despawn this entity. + pub camera: Entity, +} + +impl ViewportNode { + /// Creates a new [`ViewportNode`] with a given `camera`. + pub fn new(camera: Entity) -> Self { + Self { camera } + } +} + +#[cfg(feature = "bevy_ui_picking_backend")] +/// Handles viewport picking logic. +/// +/// Viewport entities that are being hovered or dragged will have all pointer inputs sent to them. +pub fn viewport_picking( + mut commands: Commands, + mut viewport_query: Query<( + Entity, + &ViewportNode, + &PointerId, + &mut PointerLocation, + &ComputedNode, + &GlobalTransform, + )>, + camera_query: Query<&Camera>, + hover_map: Res, + pointer_state: Res, + mut pointer_inputs: EventReader, +) { + // Handle hovered entities. + let mut viewport_picks: HashMap = hover_map + .iter() + .flat_map(|(hover_pointer_id, hits)| { + hits.iter() + .filter(|(entity, _)| viewport_query.contains(**entity)) + .map(|(entity, _)| (*entity, *hover_pointer_id)) + }) + .collect(); + + // Handle dragged entities, which need to be considered for dragging in and out of viewports. + for ((pointer_id, _), pointer_state) in pointer_state.pointer_buttons.iter() { + for &target in pointer_state + .dragging + .keys() + .filter(|&entity| viewport_query.contains(*entity)) + { + viewport_picks.insert(target, *pointer_id); + } + } + + for ( + viewport_entity, + &viewport, + &viewport_pointer_id, + mut viewport_pointer_location, + computed_node, + global_transform, + ) in &mut viewport_query + { + let Some(pick_pointer_id) = viewport_picks.get(&viewport_entity) else { + // Lift the viewport pointer if it's not being used. + viewport_pointer_location.location = None; + continue; + }; + let Ok(camera) = camera_query.get(viewport.camera) else { + continue; + }; + let Some(cam_viewport_size) = camera.logical_viewport_size() else { + continue; + }; + + // Create a `Rect` in *physical* coordinates centered at the node's GlobalTransform + let node_rect = Rect::from_center_size( + global_transform.translation().truncate(), + computed_node.size(), + ); + // Location::position uses *logical* coordinates + let top_left = node_rect.min * computed_node.inverse_scale_factor(); + let logical_size = computed_node.size() * computed_node.inverse_scale_factor(); + + let Some(target) = camera.target.as_image() else { + continue; + }; + + for input in pointer_inputs + .read() + .filter(|input| &input.pointer_id == pick_pointer_id) + { + let local_position = (input.location.position - top_left) / logical_size; + let position = local_position * cam_viewport_size; + + let location = Location { + position, + target: NormalizedRenderTarget::Image(target.clone().into()), + }; + viewport_pointer_location.location = Some(location.clone()); + + commands.send_event(PointerInput { + location, + pointer_id: viewport_pointer_id, + action: input.action, + }); + } + } +} + +/// Updates the size of the associated render target for viewports when the node size changes. +pub fn update_viewport_render_target_size( + viewport_query: Query< + (&ViewportNode, &ComputedNode), + Or<(Changed, Changed)>, + >, + camera_query: Query<&Camera>, + mut images: ResMut>, +) { + for (viewport, computed_node) in &viewport_query { + let camera = camera_query.get(viewport.camera).unwrap(); + let size = computed_node.size(); + + let Some(image_handle) = camera.target.as_image() else { + continue; + }; + let size = Extent3d { + width: u32::max(1, size.x as u32), + height: u32::max(1, size.y as u32), + ..default() + }; + images.get_mut(image_handle).unwrap().resize(size); + } +} diff --git a/crates/bevy_window/src/window.rs b/crates/bevy_window/src/window.rs index e09e254d40..31ff212ebe 100644 --- a/crates/bevy_window/src/window.rs +++ b/crates/bevy_window/src/window.rs @@ -443,6 +443,17 @@ pub struct Window { /// /// [`WindowAttributesExtIOS::with_prefers_status_bar_hidden`]: https://docs.rs/winit/latest/x86_64-apple-darwin/winit/platform/ios/trait.WindowAttributesExtIOS.html#tymethod.with_prefers_status_bar_hidden pub prefers_status_bar_hidden: bool, + /// Sets screen edges for which you want your gestures to take precedence + /// over the system gestures. + /// + /// Corresponds to [`WindowAttributesExtIOS::with_preferred_screen_edges_deferring_system_gestures`]. + /// + /// # Platform-specific + /// + /// - Only used on iOS. + /// + /// [`WindowAttributesExtIOS::with_preferred_screen_edges_deferring_system_gestures`]: https://docs.rs/winit/latest/x86_64-apple-darwin/winit/platform/ios/trait.WindowAttributesExtIOS.html#tymethod.with_preferred_screen_edges_deferring_system_gestures + pub preferred_screen_edges_deferring_system_gestures: ScreenEdge, } impl Default for Window { @@ -487,6 +498,7 @@ impl Default for Window { titlebar_show_buttons: true, prefers_home_indicator_hidden: false, prefers_status_bar_hidden: false, + preferred_screen_edges_deferring_system_gestures: Default::default(), } } } @@ -1444,6 +1456,31 @@ impl Default for EnabledButtons { #[derive(Component, Default)] pub struct ClosingWindow; +/// The edges of a screen. Corresponds to [`winit::platform::ios::ScreenEdge`]. +/// +/// # Platform-specific +/// +/// - Only used on iOS. +/// +/// [`winit::platform::ios::ScreenEdge`]: https://docs.rs/winit/latest/x86_64-apple-darwin/winit/platform/ios/struct.ScreenEdge.html +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +pub enum ScreenEdge { + #[default] + /// No edge. + None, + /// The top edge of the screen. + Top, + /// The left edge of the screen. + Left, + /// The bottom edge of the screen. + Bottom, + /// The right edge of the screen. + Right, + /// All edges of the screen. + All, +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/bevy_winit/src/accessibility.rs b/crates/bevy_winit/src/accessibility.rs index ec15258167..b14fd4f57f 100644 --- a/crates/bevy_winit/src/accessibility.rs +++ b/crates/bevy_winit/src/accessibility.rs @@ -2,6 +2,7 @@ use alloc::{collections::VecDeque, sync::Arc}; use bevy_input_focus::InputFocus; +use core::cell::RefCell; use std::sync::Mutex; use winit::event_loop::ActiveEventLoop; @@ -11,18 +12,31 @@ use accesskit::{ }; use accesskit_winit::Adapter; use bevy_a11y::{ - AccessibilityNode, AccessibilityRequested, AccessibilitySystem, + AccessibilityNode, AccessibilityRequested, AccessibilitySystems, ActionRequest as ActionRequestWrapper, ManageAccessibilityUpdates, }; use bevy_app::{App, Plugin, PostUpdate}; use bevy_derive::{Deref, DerefMut}; -use bevy_ecs::{entity::EntityHashMap, prelude::*}; +use bevy_ecs::{entity::EntityHashMap, prelude::*, system::NonSendMarker}; use bevy_window::{PrimaryWindow, Window, WindowClosed}; +thread_local! { + /// Temporary storage of access kit adapter data to replace usage of `!Send` resources. This will be replaced with proper + /// storage of `!Send` data after issue #17667 is complete. + pub static ACCESS_KIT_ADAPTERS: RefCell = const { RefCell::new(AccessKitAdapters::new()) }; +} + /// Maps window entities to their `AccessKit` [`Adapter`]s. #[derive(Default, Deref, DerefMut)] pub struct AccessKitAdapters(pub EntityHashMap); +impl AccessKitAdapters { + /// Creates a new empty `AccessKitAdapters`. + pub const fn new() -> Self { + Self(EntityHashMap::new()) + } +} + /// Maps window entities to their respective [`ActionRequest`]s. #[derive(Resource, Default, Deref, DerefMut)] pub struct WinitActionRequestHandlers(pub EntityHashMap>>); @@ -144,14 +158,16 @@ pub(crate) fn prepare_accessibility_for_window( } fn window_closed( - mut adapters: NonSendMut, mut handlers: ResMut, mut events: EventReader, + _non_send_marker: NonSendMarker, ) { - for WindowClosed { window, .. } in events.read() { - adapters.remove(window); - handlers.remove(window); - } + ACCESS_KIT_ADAPTERS.with_borrow_mut(|adapters| { + for WindowClosed { window, .. } in events.read() { + adapters.remove(window); + handlers.remove(window); + } + }); } fn poll_receivers( @@ -174,7 +190,6 @@ fn should_update_accessibility_nodes( } fn update_accessibility_nodes( - mut adapters: NonSendMut, focus: Option>, primary_window: Query<(Entity, &Window), With>, nodes: Query<( @@ -184,35 +199,38 @@ fn update_accessibility_nodes( Option<&ChildOf>, )>, node_entities: Query>, + _non_send_marker: NonSendMarker, ) { - let Ok((primary_window_id, primary_window)) = primary_window.single() else { - return; - }; - let Some(adapter) = adapters.get_mut(&primary_window_id) else { - return; - }; - let Some(focus) = focus else { - return; - }; - if focus.is_changed() || !nodes.is_empty() { - // Don't panic if the focused entity does not currently exist - // It's probably waiting to be spawned - if let Some(focused_entity) = focus.0 { - if !node_entities.contains(focused_entity) { - return; + ACCESS_KIT_ADAPTERS.with_borrow_mut(|adapters| { + let Ok((primary_window_id, primary_window)) = primary_window.single() else { + return; + }; + let Some(adapter) = adapters.get_mut(&primary_window_id) else { + return; + }; + let Some(focus) = focus else { + return; + }; + if focus.is_changed() || !nodes.is_empty() { + // Don't panic if the focused entity does not currently exist + // It's probably waiting to be spawned + if let Some(focused_entity) = focus.0 { + if !node_entities.contains(focused_entity) { + return; + } } - } - adapter.update_if_active(|| { - update_adapter( - nodes, - node_entities, - primary_window, - primary_window_id, - focus, - ) - }); - } + adapter.update_if_active(|| { + update_adapter( + nodes, + node_entities, + primary_window, + primary_window_id, + focus, + ) + }); + } + }); } fn update_adapter( @@ -290,8 +308,7 @@ pub struct AccessKitPlugin; impl Plugin for AccessKitPlugin { fn build(&self, app: &mut App) { - app.init_non_send_resource::() - .init_resource::() + app.init_resource::() .add_event::() .add_systems( PostUpdate, @@ -302,7 +319,7 @@ impl Plugin for AccessKitPlugin { .before(poll_receivers) .before(update_accessibility_nodes), ) - .in_set(AccessibilitySystem::Update), + .in_set(AccessibilitySystems::Update), ); } } diff --git a/crates/bevy_winit/src/converters.rs b/crates/bevy_winit/src/converters.rs index ba41c62534..3de27162a4 100644 --- a/crates/bevy_winit/src/converters.rs +++ b/crates/bevy_winit/src/converters.rs @@ -10,6 +10,9 @@ use bevy_window::SystemCursorIcon; use bevy_window::{EnabledButtons, WindowLevel, WindowTheme}; use winit::keyboard::{Key, NamedKey, NativeKey}; +#[cfg(target_os = "ios")] +use bevy_window::ScreenEdge; + pub fn convert_keyboard_input( keyboard_input: &winit::event::KeyEvent, window: Entity, @@ -718,3 +721,16 @@ pub fn convert_resize_direction(resize_direction: CompassOctant) -> winit::windo CompassOctant::SouthEast => winit::window::ResizeDirection::SouthEast, } } + +#[cfg(target_os = "ios")] +/// Converts a [`bevy_window::ScreenEdge`] to a [`winit::platform::ios::ScreenEdge`]. +pub(crate) fn convert_screen_edge(edge: ScreenEdge) -> winit::platform::ios::ScreenEdge { + match edge { + ScreenEdge::None => winit::platform::ios::ScreenEdge::NONE, + ScreenEdge::Top => winit::platform::ios::ScreenEdge::TOP, + ScreenEdge::Bottom => winit::platform::ios::ScreenEdge::BOTTOM, + ScreenEdge::Left => winit::platform::ios::ScreenEdge::LEFT, + ScreenEdge::Right => winit::platform::ios::ScreenEdge::RIGHT, + ScreenEdge::All => winit::platform::ios::ScreenEdge::ALL, + } +} diff --git a/crates/bevy_winit/src/cursor.rs b/crates/bevy_winit/src/cursor.rs index f45b7f00d6..bdca3f8585 100644 --- a/crates/bevy_winit/src/cursor.rs +++ b/crates/bevy_winit/src/cursor.rs @@ -39,6 +39,13 @@ use tracing::warn; #[cfg(feature = "custom_cursor")] pub use crate::custom_cursor::{CustomCursor, CustomCursorImage}; +#[cfg(all( + feature = "custom_cursor", + target_family = "wasm", + target_os = "unknown" +))] +pub use crate::custom_cursor::CustomCursorUrl; + pub(crate) struct CursorPlugin; impl Plugin for CursorPlugin { diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index 97943cc14a..d7c880a9b9 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -18,6 +18,7 @@ use bevy_derive::Deref; use bevy_reflect::prelude::ReflectDefault; use bevy_reflect::Reflect; use bevy_window::{RawHandleWrapperHolder, WindowEvent}; +use core::cell::RefCell; use core::marker::PhantomData; use winit::{event_loop::EventLoop, window::WindowId}; @@ -37,7 +38,7 @@ pub use winit_config::*; pub use winit_windows::*; use crate::{ - accessibility::{AccessKitAdapters, AccessKitPlugin, WinitActionRequestHandlers}, + accessibility::{AccessKitPlugin, WinitActionRequestHandlers}, state::winit_runner, winit_monitors::WinitMonitors, }; @@ -53,6 +54,10 @@ mod winit_config; mod winit_monitors; mod winit_windows; +thread_local! { + static WINIT_WINDOWS: RefCell = const { RefCell::new(WinitWindows::new()) }; +} + /// A [`Plugin`] that uses `winit` to create and manage windows, and receive window and input /// events. /// @@ -124,8 +129,7 @@ impl Plugin for WinitPlugin { .build() .expect("Failed to build event loop"); - app.init_non_send_resource::() - .init_resource::() + app.init_resource::() .init_resource::() .insert_resource(DisplayHandleWrapper(event_loop.owned_display_handle())) .add_event::() @@ -210,8 +214,6 @@ pub type CreateWindowParams<'w, 's, F = ()> = ( F, >, EventWriter<'w, WindowCreated>, - NonSendMut<'w, WinitWindows>, - NonSendMut<'w, AccessKitAdapters>, ResMut<'w, WinitActionRequestHandlers>, Res<'w, AccessibilityRequested>, Res<'w, WinitMonitors>, diff --git a/crates/bevy_winit/src/state.rs b/crates/bevy_winit/src/state.rs index 33ad693c5a..1855539cc5 100644 --- a/crates/bevy_winit/src/state.rs +++ b/crates/bevy_winit/src/state.rs @@ -3,7 +3,7 @@ use bevy_app::{App, AppExit, PluginsState}; #[cfg(feature = "custom_cursor")] use bevy_asset::AssetId; use bevy_ecs::{ - change_detection::{DetectChanges, NonSendMut, Res}, + change_detection::{DetectChanges, Res}, entity::Entity, event::{EventCursor, EventWriter}, prelude::*, @@ -49,11 +49,11 @@ use bevy_window::{ use bevy_window::{PrimaryWindow, RawHandleWrapper}; use crate::{ - accessibility::AccessKitAdapters, + accessibility::ACCESS_KIT_ADAPTERS, converters, create_windows, system::{create_monitors, CachedWindow, WinitWindowPressedKeys}, AppSendEvent, CreateMonitorParams, CreateWindowParams, EventLoopProxyWrapper, - RawWinitWindowEvent, UpdateMode, WinitSettings, WinitWindows, + RawWinitWindowEvent, UpdateMode, WinitSettings, WINIT_WINDOWS, }; /// Persistent state that is used to run the [`App`] according to the current @@ -94,7 +94,6 @@ struct WinitAppRunnerState { EventWriter<'static, WindowResized>, EventWriter<'static, WindowBackendScaleFactorChanged>, EventWriter<'static, WindowScaleFactorChanged>, - NonSend<'static, WinitWindows>, Query< 'static, 'static, @@ -104,7 +103,6 @@ struct WinitAppRunnerState { &'static mut WinitWindowPressedKeys, ), >, - NonSendMut<'static, AccessKitAdapters>, )>, } @@ -117,9 +115,7 @@ impl WinitAppRunnerState { EventWriter, EventWriter, EventWriter, - NonSend, Query<(&mut Window, &mut CachedWindow, &mut WinitWindowPressedKeys)>, - NonSendMut, )> = SystemState::new(app.world_mut()); Self { @@ -255,219 +251,236 @@ impl ApplicationHandler for WinitAppRunnerState { ) { self.window_event_received = true; - let ( - mut window_resized, - mut window_backend_scale_factor_changed, - mut window_scale_factor_changed, - winit_windows, - mut windows, - mut access_kit_adapters, - ) = self.event_writer_system_state.get_mut(self.app.world_mut()); + #[cfg_attr( + not(target_os = "windows"), + expect(unused_mut, reason = "only needs to be mut on windows for now") + )] + let mut manual_run_redraw_requested = false; - let Some(window) = winit_windows.get_window_entity(window_id) else { - warn!("Skipped event {event:?} for unknown winit Window Id {window_id:?}"); - return; - }; + WINIT_WINDOWS.with_borrow(|winit_windows| { + ACCESS_KIT_ADAPTERS.with_borrow_mut(|access_kit_adapters| { + let ( + mut window_resized, + mut window_backend_scale_factor_changed, + mut window_scale_factor_changed, + mut windows, + ) = self.event_writer_system_state.get_mut(self.app.world_mut()); - let Ok((mut win, _, mut pressed_keys)) = windows.get_mut(window) else { - warn!("Window {window:?} is missing `Window` component, skipping event {event:?}"); - return; - }; + let Some(window) = winit_windows.get_window_entity(window_id) else { + warn!("Skipped event {event:?} for unknown winit Window Id {window_id:?}"); + return; + }; - // Store a copy of the event to send to an EventWriter later. - self.raw_winit_events.push(RawWinitWindowEvent { - window_id, - event: event.clone(), - }); + let Ok((mut win, _, mut pressed_keys)) = windows.get_mut(window) else { + warn!( + "Window {window:?} is missing `Window` component, skipping event {event:?}" + ); + return; + }; - // Allow AccessKit to respond to `WindowEvent`s before they reach - // the engine. - if let Some(adapter) = access_kit_adapters.get_mut(&window) { - if let Some(winit_window) = winit_windows.get_window(window) { - adapter.process_event(winit_window, &event); - } - } - - match event { - WindowEvent::Resized(size) => { - react_to_resize(window, &mut win, size, &mut window_resized); - } - WindowEvent::ScaleFactorChanged { scale_factor, .. } => { - react_to_scale_factor_change( - window, - &mut win, - scale_factor, - &mut window_backend_scale_factor_changed, - &mut window_scale_factor_changed, - ); - } - WindowEvent::CloseRequested => self - .bevy_window_events - .send(WindowCloseRequested { window }), - WindowEvent::KeyboardInput { - ref event, - // On some platforms, winit sends "synthetic" key press events when the window - // gains or loses focus. These are not implemented on every platform, so we ignore - // winit's synthetic key pressed and implement the same mechanism ourselves. - // (See the `WinitWindowPressedKeys` component) - is_synthetic: false, - .. - } => { - let keyboard_input = converters::convert_keyboard_input(event, window); - if event.state.is_pressed() { - pressed_keys - .0 - .insert(keyboard_input.key_code, keyboard_input.logical_key.clone()); - } else { - pressed_keys.0.remove(&keyboard_input.key_code); - } - self.bevy_window_events.send(keyboard_input); - } - WindowEvent::CursorMoved { position, .. } => { - let physical_position = DVec2::new(position.x, position.y); - - let last_position = win.physical_cursor_position(); - let delta = last_position.map(|last_pos| { - (physical_position.as_vec2() - last_pos) / win.resolution.scale_factor() + // Store a copy of the event to send to an EventWriter later. + self.raw_winit_events.push(RawWinitWindowEvent { + window_id, + event: event.clone(), }); - win.set_physical_cursor_position(Some(physical_position)); - let position = (physical_position / win.resolution.scale_factor() as f64).as_vec2(); - self.bevy_window_events.send(CursorMoved { - window, - position, - delta, - }); - } - WindowEvent::CursorEntered { .. } => { - self.bevy_window_events.send(CursorEntered { window }); - } - WindowEvent::CursorLeft { .. } => { - win.set_physical_cursor_position(None); - self.bevy_window_events.send(CursorLeft { window }); - } - WindowEvent::MouseInput { state, button, .. } => { - self.bevy_window_events.send(MouseButtonInput { - button: converters::convert_mouse_button(button), - state: converters::convert_element_state(state), - window, - }); - } - WindowEvent::PinchGesture { delta, .. } => { - self.bevy_window_events.send(PinchGesture(delta as f32)); - } - WindowEvent::RotationGesture { delta, .. } => { - self.bevy_window_events.send(RotationGesture(delta)); - } - WindowEvent::DoubleTapGesture { .. } => { - self.bevy_window_events.send(DoubleTapGesture); - } - WindowEvent::PanGesture { delta, .. } => { - self.bevy_window_events.send(PanGesture(Vec2 { - x: delta.x, - y: delta.y, - })); - } - WindowEvent::MouseWheel { delta, .. } => match delta { - event::MouseScrollDelta::LineDelta(x, y) => { - self.bevy_window_events.send(MouseWheel { - unit: MouseScrollUnit::Line, - x, - y, - window, - }); - } - event::MouseScrollDelta::PixelDelta(p) => { - self.bevy_window_events.send(MouseWheel { - unit: MouseScrollUnit::Pixel, - x: p.x as f32, - y: p.y as f32, - window, - }); - } - }, - WindowEvent::Touch(touch) => { - let location = touch - .location - .to_logical(win.resolution.scale_factor() as f64); - self.bevy_window_events - .send(converters::convert_touch_input(touch, location, window)); - } - WindowEvent::Focused(focused) => { - win.focused = focused; - self.bevy_window_events - .send(WindowFocused { window, focused }); - } - WindowEvent::Occluded(occluded) => { - self.bevy_window_events - .send(WindowOccluded { window, occluded }); - } - WindowEvent::DroppedFile(path_buf) => { - self.bevy_window_events - .send(FileDragAndDrop::DroppedFile { window, path_buf }); - } - WindowEvent::HoveredFile(path_buf) => { - self.bevy_window_events - .send(FileDragAndDrop::HoveredFile { window, path_buf }); - } - WindowEvent::HoveredFileCancelled => { - self.bevy_window_events - .send(FileDragAndDrop::HoveredFileCanceled { window }); - } - WindowEvent::Moved(position) => { - let position = ivec2(position.x, position.y); - win.position.set(position); - self.bevy_window_events - .send(WindowMoved { window, position }); - } - WindowEvent::Ime(event) => match event { - event::Ime::Preedit(value, cursor) => { - self.bevy_window_events.send(Ime::Preedit { - window, - value, - cursor, - }); - } - event::Ime::Commit(value) => { - self.bevy_window_events.send(Ime::Commit { window, value }); - } - event::Ime::Enabled => { - self.bevy_window_events.send(Ime::Enabled { window }); - } - event::Ime::Disabled => { - self.bevy_window_events.send(Ime::Disabled { window }); - } - }, - WindowEvent::ThemeChanged(theme) => { - self.bevy_window_events.send(WindowThemeChanged { - window, - theme: converters::convert_winit_theme(theme), - }); - } - WindowEvent::Destroyed => { - self.bevy_window_events.send(WindowDestroyed { window }); - } - WindowEvent::RedrawRequested => { - self.ran_update_since_last_redraw = false; - - // https://github.com/bevyengine/bevy/issues/17488 - #[cfg(target_os = "windows")] - { - // Have the startup behavior run in about_to_wait, which prevents issues with - // invisible window creation. https://github.com/bevyengine/bevy/issues/18027 - if self.startup_forced_updates == 0 { - self.redraw_requested(_event_loop); + // Allow AccessKit to respond to `WindowEvent`s before they reach + // the engine. + if let Some(adapter) = access_kit_adapters.get_mut(&window) { + if let Some(winit_window) = winit_windows.get_window(window) { + adapter.process_event(winit_window, &event); } } - } - _ => {} - } - let mut windows = self.world_mut().query::<(&mut Window, &mut CachedWindow)>(); - if let Ok((window_component, mut cache)) = windows.get_mut(self.world_mut(), window) { - if window_component.is_changed() { - cache.window = window_component.clone(); - } + match event { + WindowEvent::Resized(size) => { + react_to_resize(window, &mut win, size, &mut window_resized); + } + WindowEvent::ScaleFactorChanged { scale_factor, .. } => { + react_to_scale_factor_change( + window, + &mut win, + scale_factor, + &mut window_backend_scale_factor_changed, + &mut window_scale_factor_changed, + ); + } + WindowEvent::CloseRequested => self + .bevy_window_events + .send(WindowCloseRequested { window }), + WindowEvent::KeyboardInput { + ref event, + // On some platforms, winit sends "synthetic" key press events when the window + // gains or loses focus. These are not implemented on every platform, so we ignore + // winit's synthetic key pressed and implement the same mechanism ourselves. + // (See the `WinitWindowPressedKeys` component) + is_synthetic: false, + .. + } => { + let keyboard_input = converters::convert_keyboard_input(event, window); + if event.state.is_pressed() { + pressed_keys.0.insert( + keyboard_input.key_code, + keyboard_input.logical_key.clone(), + ); + } else { + pressed_keys.0.remove(&keyboard_input.key_code); + } + self.bevy_window_events.send(keyboard_input); + } + WindowEvent::CursorMoved { position, .. } => { + let physical_position = DVec2::new(position.x, position.y); + + let last_position = win.physical_cursor_position(); + let delta = last_position.map(|last_pos| { + (physical_position.as_vec2() - last_pos) / win.resolution.scale_factor() + }); + + win.set_physical_cursor_position(Some(physical_position)); + let position = + (physical_position / win.resolution.scale_factor() as f64).as_vec2(); + self.bevy_window_events.send(CursorMoved { + window, + position, + delta, + }); + } + WindowEvent::CursorEntered { .. } => { + self.bevy_window_events.send(CursorEntered { window }); + } + WindowEvent::CursorLeft { .. } => { + win.set_physical_cursor_position(None); + self.bevy_window_events.send(CursorLeft { window }); + } + WindowEvent::MouseInput { state, button, .. } => { + self.bevy_window_events.send(MouseButtonInput { + button: converters::convert_mouse_button(button), + state: converters::convert_element_state(state), + window, + }); + } + WindowEvent::PinchGesture { delta, .. } => { + self.bevy_window_events.send(PinchGesture(delta as f32)); + } + WindowEvent::RotationGesture { delta, .. } => { + self.bevy_window_events.send(RotationGesture(delta)); + } + WindowEvent::DoubleTapGesture { .. } => { + self.bevy_window_events.send(DoubleTapGesture); + } + WindowEvent::PanGesture { delta, .. } => { + self.bevy_window_events.send(PanGesture(Vec2 { + x: delta.x, + y: delta.y, + })); + } + WindowEvent::MouseWheel { delta, .. } => match delta { + event::MouseScrollDelta::LineDelta(x, y) => { + self.bevy_window_events.send(MouseWheel { + unit: MouseScrollUnit::Line, + x, + y, + window, + }); + } + event::MouseScrollDelta::PixelDelta(p) => { + self.bevy_window_events.send(MouseWheel { + unit: MouseScrollUnit::Pixel, + x: p.x as f32, + y: p.y as f32, + window, + }); + } + }, + WindowEvent::Touch(touch) => { + let location = touch + .location + .to_logical(win.resolution.scale_factor() as f64); + self.bevy_window_events + .send(converters::convert_touch_input(touch, location, window)); + } + WindowEvent::Focused(focused) => { + win.focused = focused; + self.bevy_window_events + .send(WindowFocused { window, focused }); + } + WindowEvent::Occluded(occluded) => { + self.bevy_window_events + .send(WindowOccluded { window, occluded }); + } + WindowEvent::DroppedFile(path_buf) => { + self.bevy_window_events + .send(FileDragAndDrop::DroppedFile { window, path_buf }); + } + WindowEvent::HoveredFile(path_buf) => { + self.bevy_window_events + .send(FileDragAndDrop::HoveredFile { window, path_buf }); + } + WindowEvent::HoveredFileCancelled => { + self.bevy_window_events + .send(FileDragAndDrop::HoveredFileCanceled { window }); + } + WindowEvent::Moved(position) => { + let position = ivec2(position.x, position.y); + win.position.set(position); + self.bevy_window_events + .send(WindowMoved { window, position }); + } + WindowEvent::Ime(event) => match event { + event::Ime::Preedit(value, cursor) => { + self.bevy_window_events.send(Ime::Preedit { + window, + value, + cursor, + }); + } + event::Ime::Commit(value) => { + self.bevy_window_events.send(Ime::Commit { window, value }); + } + event::Ime::Enabled => { + self.bevy_window_events.send(Ime::Enabled { window }); + } + event::Ime::Disabled => { + self.bevy_window_events.send(Ime::Disabled { window }); + } + }, + WindowEvent::ThemeChanged(theme) => { + self.bevy_window_events.send(WindowThemeChanged { + window, + theme: converters::convert_winit_theme(theme), + }); + } + WindowEvent::Destroyed => { + self.bevy_window_events.send(WindowDestroyed { window }); + } + WindowEvent::RedrawRequested => { + self.ran_update_since_last_redraw = false; + + // https://github.com/bevyengine/bevy/issues/17488 + #[cfg(target_os = "windows")] + { + // Have the startup behavior run in about_to_wait, which prevents issues with + // invisible window creation. https://github.com/bevyengine/bevy/issues/18027 + if self.startup_forced_updates == 0 { + manual_run_redraw_requested = true; + } + } + } + _ => {} + } + + let mut windows = self.world_mut().query::<(&mut Window, &mut CachedWindow)>(); + if let Ok((window_component, mut cache)) = windows.get_mut(self.world_mut(), window) + { + if window_component.is_changed() { + cache.window = window_component.clone(); + } + } + }); + }); + + if manual_run_redraw_requested { + self.redraw_requested(_event_loop); } } @@ -506,19 +519,20 @@ impl ApplicationHandler for WinitAppRunnerState { // invisible window creation. https://github.com/bevyengine/bevy/issues/18027 #[cfg(target_os = "windows")] { - let winit_windows = self.world().non_send_resource::(); - let headless = winit_windows.windows.is_empty(); - let exiting = self.app_exit.is_some(); - let reactive = matches!(self.update_mode, UpdateMode::Reactive { .. }); - let all_invisible = winit_windows - .windows - .iter() - .all(|(_, w)| !w.is_visible().unwrap_or(false)); - if !exiting - && (self.startup_forced_updates > 0 || headless || all_invisible || reactive) - { - self.redraw_requested(event_loop); - } + WINIT_WINDOWS.with_borrow(|winit_windows| { + let headless = winit_windows.windows.is_empty(); + let exiting = self.app_exit.is_some(); + let reactive = matches!(self.update_mode, UpdateMode::Reactive { .. }); + let all_invisible = winit_windows + .windows + .iter() + .all(|(_, w)| !w.is_visible().unwrap_or(false)); + if !exiting + && (self.startup_forced_updates > 0 || headless || all_invisible || reactive) + { + self.redraw_requested(event_loop); + } + }); } } @@ -591,35 +605,33 @@ impl WinitAppRunnerState { // Get windows that are cached but without raw handles. Those window were already created, but got their // handle wrapper removed when the app was suspended. let mut query = self.world_mut() - .query_filtered::<(Entity, &Window), (With, Without)>(); + .query_filtered::<(Entity, &Window), (With, Without)>(); if let Ok((entity, window)) = query.single(&self.world()) { let window = window.clone(); - let mut create_window = - SystemState::::from_world(self.world_mut()); + WINIT_WINDOWS.with_borrow_mut(|winit_windows| { + ACCESS_KIT_ADAPTERS.with_borrow_mut(|adapters| { + let mut create_window = + SystemState::::from_world(self.world_mut()); - let ( - .., - mut winit_windows, - mut adapters, - mut handlers, - accessibility_requested, - monitors, - ) = create_window.get_mut(self.world_mut()); + let (.., mut handlers, accessibility_requested, monitors) = + create_window.get_mut(self.world_mut()); - let winit_window = winit_windows.create_window( - event_loop, - entity, - &window, - &mut adapters, - &mut handlers, - &accessibility_requested, - &monitors, - ); + let winit_window = winit_windows.create_window( + event_loop, + entity, + &window, + adapters, + &mut handlers, + &accessibility_requested, + &monitors, + ); - let wrapper = RawHandleWrapper::new(winit_window).unwrap(); + let wrapper = RawHandleWrapper::new(winit_window).unwrap(); - self.world_mut().entity_mut(entity).insert(wrapper); + self.world_mut().entity_mut(entity).insert(wrapper); + }); + }); } } } @@ -684,9 +696,10 @@ impl WinitAppRunnerState { all(target_os = "linux", any(feature = "x11", feature = "wayland")) )))] { - let winit_windows = self.world().non_send_resource::(); - let visible = winit_windows.windows.iter().any(|(_, w)| { - w.is_visible().unwrap_or(false) + let visible = WINIT_WINDOWS.with_borrow(|winit_windows| { + winit_windows.windows.iter().any(|(_, w)| { + w.is_visible().unwrap_or(false) + }) }); event_loop.set_control_flow(if visible { @@ -716,10 +729,11 @@ impl WinitAppRunnerState { } if self.redraw_requested && self.lifecycle != AppLifecycle::Suspended { - let winit_windows = self.world().non_send_resource::(); - for window in winit_windows.windows.values() { - window.request_redraw(); - } + WINIT_WINDOWS.with_borrow(|winit_windows| { + for window in winit_windows.windows.values() { + window.request_redraw(); + } + }); self.redraw_requested = false; } @@ -871,48 +885,47 @@ impl WinitAppRunnerState { fn update_cursors(&mut self, #[cfg(feature = "custom_cursor")] event_loop: &ActiveEventLoop) { #[cfg(feature = "custom_cursor")] let mut windows_state: SystemState<( - NonSendMut, ResMut, Query<(Entity, &mut PendingCursor), Changed>, )> = SystemState::new(self.world_mut()); #[cfg(feature = "custom_cursor")] - let (winit_windows, mut cursor_cache, mut windows) = - windows_state.get_mut(self.world_mut()); + let (mut cursor_cache, mut windows) = windows_state.get_mut(self.world_mut()); #[cfg(not(feature = "custom_cursor"))] let mut windows_state: SystemState<( - NonSendMut, Query<(Entity, &mut PendingCursor), Changed>, )> = SystemState::new(self.world_mut()); #[cfg(not(feature = "custom_cursor"))] - let (winit_windows, mut windows) = windows_state.get_mut(self.world_mut()); + let (mut windows,) = windows_state.get_mut(self.world_mut()); - for (entity, mut pending_cursor) in windows.iter_mut() { - let Some(winit_window) = winit_windows.get_window(entity) else { - continue; - }; - let Some(pending_cursor) = pending_cursor.0.take() else { - continue; - }; + WINIT_WINDOWS.with_borrow(|winit_windows| { + for (entity, mut pending_cursor) in windows.iter_mut() { + let Some(winit_window) = winit_windows.get_window(entity) else { + continue; + }; + let Some(pending_cursor) = pending_cursor.0.take() else { + continue; + }; - let final_cursor: winit::window::Cursor = match pending_cursor { - #[cfg(feature = "custom_cursor")] - CursorSource::CustomCached(cache_key) => { - let Some(cached_cursor) = cursor_cache.0.get(&cache_key) else { - error!("Cursor should have been cached, but was not found"); - continue; - }; - cached_cursor.clone().into() - } - #[cfg(feature = "custom_cursor")] - CursorSource::Custom((cache_key, cursor)) => { - let custom_cursor = event_loop.create_custom_cursor(cursor); - cursor_cache.0.insert(cache_key, custom_cursor.clone()); - custom_cursor.into() - } - CursorSource::System(system_cursor) => system_cursor.into(), - }; - winit_window.set_cursor(final_cursor); - } + let final_cursor: winit::window::Cursor = match pending_cursor { + #[cfg(feature = "custom_cursor")] + CursorSource::CustomCached(cache_key) => { + let Some(cached_cursor) = cursor_cache.0.get(&cache_key) else { + error!("Cursor should have been cached, but was not found"); + continue; + }; + cached_cursor.clone().into() + } + #[cfg(feature = "custom_cursor")] + CursorSource::Custom((cache_key, cursor)) => { + let custom_cursor = event_loop.create_custom_cursor(cursor); + cursor_cache.0.insert(cache_key, custom_cursor.clone()); + custom_cursor.into() + } + CursorSource::System(system_cursor) => system_cursor.into(), + }; + winit_window.set_cursor(final_cursor); + } + }); } } diff --git a/crates/bevy_winit/src/system.rs b/crates/bevy_winit/src/system.rs index f4ed1a59a3..97483c7358 100644 --- a/crates/bevy_winit/src/system.rs +++ b/crates/bevy_winit/src/system.rs @@ -6,7 +6,7 @@ use bevy_ecs::{ prelude::{Changed, Component}, query::QueryFilter, removal_detection::RemovedComponents, - system::{Local, NonSendMut, Query, SystemParamItem}, + system::{Local, NonSendMarker, Query, SystemParamItem}, }; use bevy_input::keyboard::{Key, KeyCode, KeyboardFocusLost, KeyboardInput}; use bevy_window::{ @@ -30,6 +30,7 @@ use winit::platform::ios::WindowExtIOS; use winit::platform::web::WindowExtWebSys; use crate::{ + accessibility::ACCESS_KIT_ADAPTERS, converters::{ convert_enabled_buttons, convert_resize_direction, convert_window_level, convert_window_theme, convert_winit_theme, @@ -37,7 +38,7 @@ use crate::{ get_selected_videomode, select_monitor, state::react_to_resize, winit_monitors::WinitMonitors, - CreateMonitorParams, CreateWindowParams, WinitWindows, + CreateMonitorParams, CreateWindowParams, WINIT_WINDOWS, }; /// Creates new windows on the [`winit`] backend for each entity with a newly-added @@ -51,78 +52,80 @@ pub fn create_windows( mut commands, mut created_windows, mut window_created_events, - mut winit_windows, - mut adapters, mut handlers, accessibility_requested, monitors, ): SystemParamItem>, ) { - for (entity, mut window, handle_holder) in &mut created_windows { - if winit_windows.get_window(entity).is_some() { - continue; - } + WINIT_WINDOWS.with_borrow_mut(|winit_windows| { + ACCESS_KIT_ADAPTERS.with_borrow_mut(|adapters| { + for (entity, mut window, handle_holder) in &mut created_windows { + if winit_windows.get_window(entity).is_some() { + continue; + } - info!("Creating new window {} ({})", window.title.as_str(), entity); + info!("Creating new window {} ({})", window.title.as_str(), entity); - let winit_window = winit_windows.create_window( - event_loop, - entity, - &window, - &mut adapters, - &mut handlers, - &accessibility_requested, - &monitors, - ); + let winit_window = winit_windows.create_window( + event_loop, + entity, + &window, + adapters, + &mut handlers, + &accessibility_requested, + &monitors, + ); - if let Some(theme) = winit_window.theme() { - window.window_theme = Some(convert_winit_theme(theme)); - } + if let Some(theme) = winit_window.theme() { + window.window_theme = Some(convert_winit_theme(theme)); + } - window - .resolution - .set_scale_factor_and_apply_to_physical_size(winit_window.scale_factor() as f32); + window + .resolution + .set_scale_factor_and_apply_to_physical_size(winit_window.scale_factor() as f32); - commands.entity(entity).insert(( - CachedWindow { - window: window.clone(), - }, - WinitWindowPressedKeys::default(), - )); + commands.entity(entity).insert(( + CachedWindow { + window: window.clone(), + }, + WinitWindowPressedKeys::default(), + )); - if let Ok(handle_wrapper) = RawHandleWrapper::new(winit_window) { - commands.entity(entity).insert(handle_wrapper.clone()); - if let Some(handle_holder) = handle_holder { - *handle_holder.0.lock().unwrap() = Some(handle_wrapper); + if let Ok(handle_wrapper) = RawHandleWrapper::new(winit_window) { + commands.entity(entity).insert(handle_wrapper.clone()); + if let Some(handle_holder) = handle_holder { + *handle_holder.0.lock().unwrap() = Some(handle_wrapper); + } + } + + #[cfg(target_arch = "wasm32")] + { + if window.fit_canvas_to_parent { + let canvas = winit_window + .canvas() + .expect("window.canvas() can only be called in main thread."); + let style = canvas.style(); + style.set_property("width", "100%").unwrap(); + style.set_property("height", "100%").unwrap(); + } + } + + #[cfg(target_os = "ios")] + { + winit_window.recognize_pinch_gesture(window.recognize_pinch_gesture); + winit_window.recognize_rotation_gesture(window.recognize_rotation_gesture); + winit_window.recognize_doubletap_gesture(window.recognize_doubletap_gesture); + if let Some((min, max)) = window.recognize_pan_gesture { + winit_window.recognize_pan_gesture(true, min, max); + } else { + winit_window.recognize_pan_gesture(false, 0, 0); + } + } + + window_created_events.write(WindowCreated { window: entity }); } - } - - #[cfg(target_arch = "wasm32")] - { - if window.fit_canvas_to_parent { - let canvas = winit_window - .canvas() - .expect("window.canvas() can only be called in main thread."); - let style = canvas.style(); - style.set_property("width", "100%").unwrap(); - style.set_property("height", "100%").unwrap(); - } - } - - #[cfg(target_os = "ios")] - { - winit_window.recognize_pinch_gesture(window.recognize_pinch_gesture); - winit_window.recognize_rotation_gesture(window.recognize_rotation_gesture); - winit_window.recognize_doubletap_gesture(window.recognize_doubletap_gesture); - if let Some((min, max)) = window.recognize_pan_gesture { - winit_window.recognize_pan_gesture(true, min, max); - } else { - winit_window.recognize_pan_gesture(false, 0, 0); - } - } - - window_created_events.write(WindowCreated { window: entity }); - } + }); + }); } /// Check whether keyboard focus was lost. This is different from window @@ -239,9 +242,9 @@ pub(crate) fn despawn_windows( window_entities: Query>, mut closing_events: EventWriter, mut closed_events: EventWriter, - mut winit_windows: NonSendMut, mut windows_to_drop: Local>>, mut exit_events: EventReader, + _non_send_marker: NonSendMarker, ) { // Drop all the windows that are waiting to be closed windows_to_drop.clear(); @@ -254,13 +257,15 @@ pub(crate) fn despawn_windows( // rather than having the component added // and removed in the same frame. if !window_entities.contains(window) { - if let Some(window) = winit_windows.remove_window(window) { - // Keeping WindowWrapper that are dropped for one frame - // Otherwise the last `Arc` of the window could be in the rendering thread, and dropped there - // This would hang on macOS - // Keeping the wrapper and dropping it next frame in this system ensure its dropped in the main thread - windows_to_drop.push(window); - } + WINIT_WINDOWS.with_borrow_mut(|winit_windows| { + if let Some(window) = winit_windows.remove_window(window) { + // Keeping WindowWrapper that are dropped for one frame + // Otherwise the last `Arc` of the window could be in the rendering thread, and dropped there + // This would hang on macOS + // Keeping the wrapper and dropping it next frame in this system ensure its dropped in the main thread + windows_to_drop.push(window); + } + }); closed_events.write(WindowClosed { window }); } } @@ -291,286 +296,298 @@ pub struct CachedWindow { /// - [`Window::focused`] cannot be manually changed to `false` after the window is created. pub(crate) fn changed_windows( mut changed_windows: Query<(Entity, &mut Window, &mut CachedWindow), Changed>, - winit_windows: NonSendMut, monitors: Res, mut window_resized: EventWriter, + _non_send_marker: NonSendMarker, ) { - for (entity, mut window, mut cache) in &mut changed_windows { - let Some(winit_window) = winit_windows.get_window(entity) else { - continue; - }; + WINIT_WINDOWS.with_borrow(|winit_windows| { + for (entity, mut window, mut cache) in &mut changed_windows { + let Some(winit_window) = winit_windows.get_window(entity) else { + continue; + }; - if window.title != cache.window.title { - winit_window.set_title(window.title.as_str()); - } + if window.title != cache.window.title { + winit_window.set_title(window.title.as_str()); + } - if window.mode != cache.window.mode { - let new_mode = match window.mode { - WindowMode::BorderlessFullscreen(monitor_selection) => { - Some(Some(winit::window::Fullscreen::Borderless(select_monitor( - &monitors, - winit_window.primary_monitor(), - winit_window.current_monitor(), - &monitor_selection, - )))) - } - WindowMode::Fullscreen(monitor_selection, video_mode_selection) => { - let monitor = &select_monitor( - &monitors, - winit_window.primary_monitor(), - winit_window.current_monitor(), - &monitor_selection, - ) - .unwrap_or_else(|| { - panic!("Could not find monitor for {:?}", monitor_selection) - }); - - if let Some(video_mode) = get_selected_videomode(monitor, &video_mode_selection) - { - Some(Some(winit::window::Fullscreen::Exclusive(video_mode))) - } else { - warn!( - "Could not find valid fullscreen video mode for {:?} {:?}", - monitor_selection, video_mode_selection - ); - None + if window.mode != cache.window.mode { + let new_mode = match window.mode { + WindowMode::BorderlessFullscreen(monitor_selection) => { + Some(Some(winit::window::Fullscreen::Borderless(select_monitor( + &monitors, + winit_window.primary_monitor(), + winit_window.current_monitor(), + &monitor_selection, + )))) } - } - WindowMode::Windowed => Some(None), - }; + WindowMode::Fullscreen(monitor_selection, video_mode_selection) => { + let monitor = &select_monitor( + &monitors, + winit_window.primary_monitor(), + winit_window.current_monitor(), + &monitor_selection, + ) + .unwrap_or_else(|| { + panic!("Could not find monitor for {:?}", monitor_selection) + }); - if let Some(new_mode) = new_mode { - if winit_window.fullscreen() != new_mode { - winit_window.set_fullscreen(new_mode); - } - } - } - - if window.resolution != cache.window.resolution { - let mut physical_size = PhysicalSize::new( - window.resolution.physical_width(), - window.resolution.physical_height(), - ); - - let cached_physical_size = PhysicalSize::new( - cache.window.physical_width(), - cache.window.physical_height(), - ); - - let base_scale_factor = window.resolution.base_scale_factor(); - - // Note: this may be different from `winit`'s base scale factor if - // `scale_factor_override` is set to Some(f32) - let scale_factor = window.scale_factor(); - let cached_scale_factor = cache.window.scale_factor(); - - // Check and update `winit`'s physical size only if the window is not maximized - if scale_factor != cached_scale_factor && !winit_window.is_maximized() { - let logical_size = - if let Some(cached_factor) = cache.window.resolution.scale_factor_override() { - physical_size.to_logical::(cached_factor as f64) - } else { - physical_size.to_logical::(base_scale_factor as f64) - }; - - // Scale factor changed, updating physical and logical size - if let Some(forced_factor) = window.resolution.scale_factor_override() { - // This window is overriding the OS-suggested DPI, so its physical size - // should be set based on the overriding value. Its logical size already - // incorporates any resize constraints. - physical_size = logical_size.to_physical::(forced_factor as f64); - } else { - physical_size = logical_size.to_physical::(base_scale_factor as f64); - } - } - - if physical_size != cached_physical_size { - if let Some(new_physical_size) = winit_window.request_inner_size(physical_size) { - react_to_resize(entity, &mut window, new_physical_size, &mut window_resized); - } - } - } - - if window.physical_cursor_position() != cache.window.physical_cursor_position() { - if let Some(physical_position) = window.physical_cursor_position() { - let position = PhysicalPosition::new(physical_position.x, physical_position.y); - - if let Err(err) = winit_window.set_cursor_position(position) { - error!("could not set cursor position: {}", err); - } - } - } - - if window.cursor_options.grab_mode != cache.window.cursor_options.grab_mode - && crate::winit_windows::attempt_grab(winit_window, window.cursor_options.grab_mode) - .is_err() - { - window.cursor_options.grab_mode = cache.window.cursor_options.grab_mode; - } - - if window.cursor_options.visible != cache.window.cursor_options.visible { - winit_window.set_cursor_visible(window.cursor_options.visible); - } - - if window.cursor_options.hit_test != cache.window.cursor_options.hit_test { - if let Err(err) = winit_window.set_cursor_hittest(window.cursor_options.hit_test) { - window.cursor_options.hit_test = cache.window.cursor_options.hit_test; - warn!( - "Could not set cursor hit test for window {}: {}", - window.title, err - ); - } - } - - if window.decorations != cache.window.decorations - && window.decorations != winit_window.is_decorated() - { - winit_window.set_decorations(window.decorations); - } - - if window.resizable != cache.window.resizable - && window.resizable != winit_window.is_resizable() - { - winit_window.set_resizable(window.resizable); - } - - if window.enabled_buttons != cache.window.enabled_buttons { - winit_window.set_enabled_buttons(convert_enabled_buttons(window.enabled_buttons)); - } - - if window.resize_constraints != cache.window.resize_constraints { - let constraints = window.resize_constraints.check_constraints(); - let min_inner_size = LogicalSize { - width: constraints.min_width, - height: constraints.min_height, - }; - let max_inner_size = LogicalSize { - width: constraints.max_width, - height: constraints.max_height, - }; - - winit_window.set_min_inner_size(Some(min_inner_size)); - if constraints.max_width.is_finite() && constraints.max_height.is_finite() { - winit_window.set_max_inner_size(Some(max_inner_size)); - } - } - - if window.position != cache.window.position { - if let Some(position) = crate::winit_window_position( - &window.position, - &window.resolution, - &monitors, - winit_window.primary_monitor(), - winit_window.current_monitor(), - ) { - let should_set = match winit_window.outer_position() { - Ok(current_position) => current_position != position, - _ => true, + if let Some(video_mode) = get_selected_videomode(monitor, &video_mode_selection) + { + Some(Some(winit::window::Fullscreen::Exclusive(video_mode))) + } else { + warn!( + "Could not find valid fullscreen video mode for {:?} {:?}", + monitor_selection, video_mode_selection + ); + None + } + } + WindowMode::Windowed => Some(None), }; - if should_set { - winit_window.set_outer_position(position); - } - } - } - - if let Some(maximized) = window.internal.take_maximize_request() { - winit_window.set_maximized(maximized); - } - - if let Some(minimized) = window.internal.take_minimize_request() { - winit_window.set_minimized(minimized); - } - - if window.internal.take_move_request() { - if let Err(e) = winit_window.drag_window() { - warn!("Winit returned an error while attempting to drag the window: {e}"); - } - } - - if let Some(resize_direction) = window.internal.take_resize_request() { - if let Err(e) = - winit_window.drag_resize_window(convert_resize_direction(resize_direction)) - { - warn!("Winit returned an error while attempting to drag resize the window: {e}"); - } - } - - if window.focused != cache.window.focused && window.focused { - winit_window.focus_window(); - } - - if window.window_level != cache.window.window_level { - winit_window.set_window_level(convert_window_level(window.window_level)); - } - - // Currently unsupported changes - if window.transparent != cache.window.transparent { - window.transparent = cache.window.transparent; - warn!("Winit does not currently support updating transparency after window creation."); - } - - #[cfg(target_arch = "wasm32")] - if window.canvas != cache.window.canvas { - window.canvas.clone_from(&cache.window.canvas); - warn!( - "Bevy currently doesn't support modifying the window canvas after initialization." - ); - } - - if window.ime_enabled != cache.window.ime_enabled { - winit_window.set_ime_allowed(window.ime_enabled); - } - - if window.ime_position != cache.window.ime_position { - winit_window.set_ime_cursor_area( - LogicalPosition::new(window.ime_position.x, window.ime_position.y), - PhysicalSize::new(10, 10), - ); - } - - if window.window_theme != cache.window.window_theme { - winit_window.set_theme(window.window_theme.map(convert_window_theme)); - } - - if window.visible != cache.window.visible { - winit_window.set_visible(window.visible); - } - - #[cfg(target_os = "ios")] - { - if window.recognize_pinch_gesture != cache.window.recognize_pinch_gesture { - winit_window.recognize_pinch_gesture(window.recognize_pinch_gesture); - } - if window.recognize_rotation_gesture != cache.window.recognize_rotation_gesture { - winit_window.recognize_rotation_gesture(window.recognize_rotation_gesture); - } - if window.recognize_doubletap_gesture != cache.window.recognize_doubletap_gesture { - winit_window.recognize_doubletap_gesture(window.recognize_doubletap_gesture); - } - if window.recognize_pan_gesture != cache.window.recognize_pan_gesture { - match ( - window.recognize_pan_gesture, - cache.window.recognize_pan_gesture, - ) { - (Some(_), Some(_)) => { - warn!("Bevy currently doesn't support modifying PanGesture number of fingers recognition. Please disable it before re-enabling it with the new number of fingers"); + if let Some(new_mode) = new_mode { + if winit_window.fullscreen() != new_mode { + winit_window.set_fullscreen(new_mode); } - (Some((min, max)), _) => winit_window.recognize_pan_gesture(true, min, max), - _ => winit_window.recognize_pan_gesture(false, 0, 0), } } - if window.prefers_home_indicator_hidden != cache.window.prefers_home_indicator_hidden { - winit_window - .set_prefers_home_indicator_hidden(window.prefers_home_indicator_hidden); + if window.resolution != cache.window.resolution { + let mut physical_size = PhysicalSize::new( + window.resolution.physical_width(), + window.resolution.physical_height(), + ); + + let cached_physical_size = PhysicalSize::new( + cache.window.physical_width(), + cache.window.physical_height(), + ); + + let base_scale_factor = window.resolution.base_scale_factor(); + + // Note: this may be different from `winit`'s base scale factor if + // `scale_factor_override` is set to Some(f32) + let scale_factor = window.scale_factor(); + let cached_scale_factor = cache.window.scale_factor(); + + // Check and update `winit`'s physical size only if the window is not maximized + if scale_factor != cached_scale_factor && !winit_window.is_maximized() { + let logical_size = + if let Some(cached_factor) = cache.window.resolution.scale_factor_override() { + physical_size.to_logical::(cached_factor as f64) + } else { + physical_size.to_logical::(base_scale_factor as f64) + }; + + // Scale factor changed, updating physical and logical size + if let Some(forced_factor) = window.resolution.scale_factor_override() { + // This window is overriding the OS-suggested DPI, so its physical size + // should be set based on the overriding value. Its logical size already + // incorporates any resize constraints. + physical_size = logical_size.to_physical::(forced_factor as f64); + } else { + physical_size = logical_size.to_physical::(base_scale_factor as f64); + } + } + + if physical_size != cached_physical_size { + if let Some(new_physical_size) = winit_window.request_inner_size(physical_size) { + react_to_resize(entity, &mut window, new_physical_size, &mut window_resized); + } + } } - if window.prefers_status_bar_hidden != cache.window.prefers_status_bar_hidden { - winit_window.set_prefers_status_bar_hidden(window.prefers_status_bar_hidden); + + if window.physical_cursor_position() != cache.window.physical_cursor_position() { + if let Some(physical_position) = window.physical_cursor_position() { + let position = PhysicalPosition::new(physical_position.x, physical_position.y); + + if let Err(err) = winit_window.set_cursor_position(position) { + error!("could not set cursor position: {}", err); + } + } } + + if window.cursor_options.grab_mode != cache.window.cursor_options.grab_mode + && crate::winit_windows::attempt_grab(winit_window, window.cursor_options.grab_mode) + .is_err() + { + window.cursor_options.grab_mode = cache.window.cursor_options.grab_mode; + } + + if window.cursor_options.visible != cache.window.cursor_options.visible { + winit_window.set_cursor_visible(window.cursor_options.visible); + } + + if window.cursor_options.hit_test != cache.window.cursor_options.hit_test { + if let Err(err) = winit_window.set_cursor_hittest(window.cursor_options.hit_test) { + window.cursor_options.hit_test = cache.window.cursor_options.hit_test; + warn!( + "Could not set cursor hit test for window {}: {}", + window.title, err + ); + } + } + + if window.decorations != cache.window.decorations + && window.decorations != winit_window.is_decorated() + { + winit_window.set_decorations(window.decorations); + } + + if window.resizable != cache.window.resizable + && window.resizable != winit_window.is_resizable() + { + winit_window.set_resizable(window.resizable); + } + + if window.enabled_buttons != cache.window.enabled_buttons { + winit_window.set_enabled_buttons(convert_enabled_buttons(window.enabled_buttons)); + } + + if window.resize_constraints != cache.window.resize_constraints { + let constraints = window.resize_constraints.check_constraints(); + let min_inner_size = LogicalSize { + width: constraints.min_width, + height: constraints.min_height, + }; + let max_inner_size = LogicalSize { + width: constraints.max_width, + height: constraints.max_height, + }; + + winit_window.set_min_inner_size(Some(min_inner_size)); + if constraints.max_width.is_finite() && constraints.max_height.is_finite() { + winit_window.set_max_inner_size(Some(max_inner_size)); + } + } + + if window.position != cache.window.position { + if let Some(position) = crate::winit_window_position( + &window.position, + &window.resolution, + &monitors, + winit_window.primary_monitor(), + winit_window.current_monitor(), + ) { + let should_set = match winit_window.outer_position() { + Ok(current_position) => current_position != position, + _ => true, + }; + + if should_set { + winit_window.set_outer_position(position); + } + } + } + + if let Some(maximized) = window.internal.take_maximize_request() { + winit_window.set_maximized(maximized); + } + + if let Some(minimized) = window.internal.take_minimize_request() { + winit_window.set_minimized(minimized); + } + + if window.internal.take_move_request() { + if let Err(e) = winit_window.drag_window() { + warn!("Winit returned an error while attempting to drag the window: {e}"); + } + } + + if let Some(resize_direction) = window.internal.take_resize_request() { + if let Err(e) = + winit_window.drag_resize_window(convert_resize_direction(resize_direction)) + { + warn!("Winit returned an error while attempting to drag resize the window: {e}"); + } + } + + if window.focused != cache.window.focused && window.focused { + winit_window.focus_window(); + } + + if window.window_level != cache.window.window_level { + winit_window.set_window_level(convert_window_level(window.window_level)); + } + + // Currently unsupported changes + if window.transparent != cache.window.transparent { + window.transparent = cache.window.transparent; + warn!("Winit does not currently support updating transparency after window creation."); + } + + #[cfg(target_arch = "wasm32")] + if window.canvas != cache.window.canvas { + window.canvas.clone_from(&cache.window.canvas); + warn!( + "Bevy currently doesn't support modifying the window canvas after initialization." + ); + } + + if window.ime_enabled != cache.window.ime_enabled { + winit_window.set_ime_allowed(window.ime_enabled); + } + + if window.ime_position != cache.window.ime_position { + winit_window.set_ime_cursor_area( + LogicalPosition::new(window.ime_position.x, window.ime_position.y), + PhysicalSize::new(10, 10), + ); + } + + if window.window_theme != cache.window.window_theme { + winit_window.set_theme(window.window_theme.map(convert_window_theme)); + } + + if window.visible != cache.window.visible { + winit_window.set_visible(window.visible); + } + + #[cfg(target_os = "ios")] + { + if window.recognize_pinch_gesture != cache.window.recognize_pinch_gesture { + winit_window.recognize_pinch_gesture(window.recognize_pinch_gesture); + } + if window.recognize_rotation_gesture != cache.window.recognize_rotation_gesture { + winit_window.recognize_rotation_gesture(window.recognize_rotation_gesture); + } + if window.recognize_doubletap_gesture != cache.window.recognize_doubletap_gesture { + winit_window.recognize_doubletap_gesture(window.recognize_doubletap_gesture); + } + if window.recognize_pan_gesture != cache.window.recognize_pan_gesture { + match ( + window.recognize_pan_gesture, + cache.window.recognize_pan_gesture, + ) { + (Some(_), Some(_)) => { + warn!("Bevy currently doesn't support modifying PanGesture number of fingers recognition. Please disable it before re-enabling it with the new number of fingers"); + } + (Some((min, max)), _) => winit_window.recognize_pan_gesture(true, min, max), + _ => winit_window.recognize_pan_gesture(false, 0, 0), + } + } + + if window.prefers_home_indicator_hidden != cache.window.prefers_home_indicator_hidden { + winit_window + .set_prefers_home_indicator_hidden(window.prefers_home_indicator_hidden); + } + if window.prefers_status_bar_hidden != cache.window.prefers_status_bar_hidden { + winit_window.set_prefers_status_bar_hidden(window.prefers_status_bar_hidden); + } + if window.preferred_screen_edges_deferring_system_gestures + != cache + .window + .preferred_screen_edges_deferring_system_gestures + { + use crate::converters::convert_screen_edge; + let preferred_edge = + convert_screen_edge(window.preferred_screen_edges_deferring_system_gestures); + winit_window.set_preferred_screen_edges_deferring_system_gestures(preferred_edge); + } + } + cache.window = window.clone(); } - cache.window = window.clone(); - } + }); } /// This keeps track of which keys are pressed on each window. diff --git a/crates/bevy_winit/src/winit_windows.rs b/crates/bevy_winit/src/winit_windows.rs index 119da10fe1..d666491311 100644 --- a/crates/bevy_winit/src/winit_windows.rs +++ b/crates/bevy_winit/src/winit_windows.rs @@ -42,6 +42,16 @@ pub struct WinitWindows { } impl WinitWindows { + /// Creates a new instance of `WinitWindows`. + pub const fn new() -> Self { + Self { + windows: HashMap::new(), + entity_to_winit: EntityHashMap::new(), + winit_to_entity: HashMap::new(), + _not_send_sync: core::marker::PhantomData, + } + } + /// Creates a `winit` window and associates it with our entity. pub fn create_window( &mut self, @@ -145,7 +155,14 @@ impl WinitWindows { #[cfg(target_os = "ios")] { + use crate::converters::convert_screen_edge; use winit::platform::ios::WindowAttributesExtIOS; + + let preferred_edge = + convert_screen_edge(window.preferred_screen_edges_deferring_system_gestures); + + winit_window_attributes = winit_window_attributes + .with_preferred_screen_edges_deferring_system_gestures(preferred_edge); winit_window_attributes = winit_window_attributes .with_prefers_home_indicator_hidden(window.prefers_home_indicator_hidden); winit_window_attributes = winit_window_attributes diff --git a/deny.toml b/deny.toml index 7d76c70de0..d22efdf153 100644 --- a/deny.toml +++ b/deny.toml @@ -8,6 +8,9 @@ ignore = [ # See: https://rustsec.org/advisories/RUSTSEC-2024-0436 # Bevy relies on this in multiple indirect ways, so ignoring it is the only feasible current solution "RUSTSEC-2024-0436", + # unmaintained: postcard -> heapless -> atomic-polyfill + # See https://github.com/jamesmunns/postcard/issues/223 + "RUSTSEC-2023-0089", ] [licenses] diff --git a/docs/cargo_features.md b/docs/cargo_features.md index f15fa1c4c6..1a1cb68fda 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -70,7 +70,6 @@ The default feature set enables most of the expected features of a game engine, |bevy_remote|Enable the Bevy Remote Protocol| |bevy_ui_debug|Provides a debug overlay for bevy UI| |bmp|BMP image format support| -|configurable_error_handler|Use the configurable global error handler as the default error handler.| |critical-section|`critical-section` provides the building blocks for synchronization primitives on all platforms, including `no_std`.| |dds|DDS compressed texture support| |debug_glam_assert|Enable assertions in debug builds to check the validity of parameters passed to glam| diff --git a/examples/2d/2d_viewport_to_world.rs b/examples/2d/2d_viewport_to_world.rs index 9e58816cee..ce611bd4e9 100644 --- a/examples/2d/2d_viewport_to_world.rs +++ b/examples/2d/2d_viewport_to_world.rs @@ -15,10 +15,7 @@ fn main() { .add_plugins(DefaultPlugins) .add_systems(Startup, setup) .add_systems(FixedUpdate, controls) - .add_systems( - PostUpdate, - draw_cursor.after(TransformSystem::TransformPropagate), - ) + .add_systems(PostUpdate, draw_cursor.after(TransformSystems::Propagate)) .run(); } diff --git a/examples/2d/mesh2d_manual.rs b/examples/2d/mesh2d_manual.rs index 82da9392c7..6354aa468c 100644 --- a/examples/2d/mesh2d_manual.rs +++ b/examples/2d/mesh2d_manual.rs @@ -28,7 +28,7 @@ use bevy::{ sync_component::SyncComponentPlugin, sync_world::{MainEntityHashMap, RenderEntity}, view::{ExtractedView, RenderVisibleEntities, ViewTarget}, - Extract, Render, RenderApp, RenderSet, + Extract, Render, RenderApp, RenderSystems, }, sprite::{ extract_mesh2d, DrawMesh2d, Material2dBindGroupId, Mesh2dPipeline, Mesh2dPipelineKey, @@ -313,7 +313,10 @@ impl Plugin for ColoredMesh2dPlugin { ExtractSchedule, extract_colored_mesh2d.after(extract_mesh2d), ) - .add_systems(Render, queue_colored_mesh2d.in_set(RenderSet::QueueMeshes)); + .add_systems( + Render, + queue_colored_mesh2d.in_set(RenderSystems::QueueMeshes), + ); } fn finish(&self, app: &mut App) { diff --git a/examples/3d/occlusion_culling.rs b/examples/3d/occlusion_culling.rs index 4c69db0a4a..ecc14ca369 100644 --- a/examples/3d/occlusion_culling.rs +++ b/examples/3d/occlusion_culling.rs @@ -32,9 +32,9 @@ use bevy::{ experimental::occlusion_culling::OcclusionCulling, render_graph::{self, NodeRunError, RenderGraphApp, RenderGraphContext, RenderLabel}, render_resource::{Buffer, BufferDescriptor, BufferUsages, MapMode}, - renderer::{RenderAdapter, RenderContext, RenderDevice}, + renderer::{RenderContext, RenderDevice}, settings::WgpuFeatures, - Render, RenderApp, RenderDebugFlags, RenderPlugin, RenderSet, + Render, RenderApp, RenderDebugFlags, RenderPlugin, RenderSystems, }, }; use bytemuck::Pod; @@ -140,7 +140,7 @@ struct SavedIndirectParametersData { impl FromWorld for SavedIndirectParameters { fn from_world(world: &mut World) -> SavedIndirectParameters { - let render_adapter = world.resource::(); + let render_device = world.resource::(); SavedIndirectParameters(Arc::new(Mutex::new(SavedIndirectParametersData { data: vec![], count: 0, @@ -152,7 +152,7 @@ impl FromWorld for SavedIndirectParameters { // supports `multi_draw_indirect_count`. So, if we don't have that // feature, then we don't bother to display how many meshes were // culled. - occlusion_culling_introspection_supported: render_adapter + occlusion_culling_introspection_supported: render_device .features() .contains(WgpuFeatures::MULTI_DRAW_INDIRECT_COUNT), }))) @@ -220,7 +220,8 @@ impl Plugin for ReadbackIndirectParametersPlugin { .add_systems(ExtractSchedule, readback_indirect_parameters) .add_systems( Render, - create_indirect_parameters_staging_buffers.in_set(RenderSet::PrepareResourcesFlush), + create_indirect_parameters_staging_buffers + .in_set(RenderSystems::PrepareResourcesFlush), ) // Add the node that allows us to read the indirect parameters back // from the GPU to the CPU, which allows us to determine how many diff --git a/examples/3d/tonemapping.rs b/examples/3d/tonemapping.rs index 2fb671c0f6..66b7d76ce9 100644 --- a/examples/3d/tonemapping.rs +++ b/examples/3d/tonemapping.rs @@ -1,6 +1,7 @@ //! This examples compares Tonemapping options use bevy::{ + asset::UnapprovedPathMode, core_pipeline::tonemapping::Tonemapping, pbr::CascadeShadowConfigBuilder, platform::collections::HashMap, @@ -19,7 +20,12 @@ const SHADER_ASSET_PATH: &str = "shaders/tonemapping_test_patterns.wgsl"; fn main() { App::new() .add_plugins(( - DefaultPlugins, + DefaultPlugins.set(AssetPlugin { + // We enable loading assets from arbitrary filesystem paths as this example allows + // drag and dropping a local image for color grading + unapproved_path_mode: UnapprovedPathMode::Allow, + ..default() + }), MaterialPlugin::::default(), )) .insert_resource(CameraTransform( diff --git a/examples/README.md b/examples/README.md index d0e33d957f..060683f96d 100644 --- a/examples/README.md +++ b/examples/README.md @@ -329,6 +329,7 @@ Example | Description [Run Conditions](../examples/ecs/run_conditions.rs) | Run systems only when one or multiple conditions are met [Send and receive events](../examples/ecs/send_and_receive_events.rs) | Demonstrates how to send and receive events of the same type in a single system [Startup System](../examples/ecs/startup_system.rs) | Demonstrates a startup system (one that runs once when the app starts up) +[State Scoped](../examples/ecs/state_scoped.rs) | Shows how to spawn entities that are automatically despawned either when entering or exiting specific game states. [System Closure](../examples/ecs/system_closure.rs) | Show how to use closures as systems, and how to configure `Local` variables by capturing external state [System Parameter](../examples/ecs/system_param.rs) | Illustrates creating custom system parameters with `SystemParam` [System Piping](../examples/ecs/system_piping.rs) | Pipe the output of one system into a second, allowing you to handle any errors gracefully @@ -555,6 +556,7 @@ Example | Description [Size Constraints](../examples/ui/size_constraints.rs) | Demonstrates how the to use the size constraints to control the size of a UI node. [Tab Navigation](../examples/ui/tab_navigation.rs) | Demonstration of Tab Navigation between UI elements [Text](../examples/ui/text.rs) | Illustrates creating and updating text +[Text Background Colors](../examples/ui/text_background_colors.rs) | Demonstrates text background colors [Text Debug](../examples/ui/text_debug.rs) | An example for debugging text layout [Text Wrap Debug](../examples/ui/text_wrap_debug.rs) | Demonstrates text wrapping [Transparency UI](../examples/ui/transparency_ui.rs) | Demonstrates transparency for UI @@ -566,6 +568,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 Z-Index](../examples/ui/z_index.rs) | Demonstrates how to control the relative depth (z-position) of UI elements [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 [Window Fallthrough](../examples/ui/window_fallthrough.rs) | Illustrates how to access `winit::window::Window`'s `hittest` functionality. ## Window diff --git a/examples/app/headless_renderer.rs b/examples/app/headless_renderer.rs index 5d5b2f7d0d..2b6b23b706 100644 --- a/examples/app/headless_renderer.rs +++ b/examples/app/headless_renderer.rs @@ -3,7 +3,7 @@ //! It follows this steps: //! 1. Render from camera to gpu-image render target //! 2. Copy from gpu image to buffer using `ImageCopyDriver` node in `RenderGraph` -//! 3. Copy from buffer to channel using `receive_image_from_buffer` after `RenderSet::Render` +//! 3. Copy from buffer to channel using `receive_image_from_buffer` after `RenderSystems::Render` //! 4. Save from channel to random named file using `scene::update` at `PostUpdate` in `MainWorld` //! 5. Exit if `single_image` setting is set @@ -22,7 +22,7 @@ use bevy::{ TextureUsages, }, renderer::{RenderContext, RenderDevice, RenderQueue}, - Extract, Render, RenderApp, RenderSet, + Extract, Render, RenderApp, RenderSystems, }, winit::WinitPlugin, }; @@ -217,7 +217,10 @@ impl Plugin for ImageCopyPlugin { .add_systems(ExtractSchedule, image_copy_extract) // Receives image data from buffer to channel // so we need to run it after the render graph is done - .add_systems(Render, receive_image_from_buffer.after(RenderSet::Render)); + .add_systems( + Render, + receive_image_from_buffer.after(RenderSystems::Render), + ); } } diff --git a/examples/app/log_layers.rs b/examples/app/log_layers.rs index 558d5b2ba5..8f454e9ee0 100644 --- a/examples/app/log_layers.rs +++ b/examples/app/log_layers.rs @@ -4,7 +4,7 @@ use bevy::{ log::{ tracing::{self, Subscriber}, tracing_subscriber::Layer, - BoxedLayer, + BoxedFmtLayer, BoxedLayer, }, prelude::*, }; @@ -36,10 +36,24 @@ fn custom_layer(_app: &mut App) -> Option { ])) } +// While `custom_layer` allows you to add _additional_ layers, it won't allow you to override the +// default `tracing_subscriber::fmt::Layer` added by `LogPlugin`. To do that, you can use the +// `fmt_layer` option. +// +// In this example, we're disabling the timestamp in the log output. +fn fmt_layer(_app: &mut App) -> Option { + Some(Box::new( + bevy::log::tracing_subscriber::fmt::Layer::default() + .without_time() + .with_writer(std::io::stderr), + )) +} + fn main() { App::new() .add_plugins(DefaultPlugins.set(bevy::log::LogPlugin { custom_layer, + fmt_layer, ..default() })) diff --git a/examples/app/log_layers_ecs.rs b/examples/app/log_layers_ecs.rs index f62198ad32..ec66da9b5f 100644 --- a/examples/app/log_layers_ecs.rs +++ b/examples/app/log_layers_ecs.rs @@ -30,6 +30,7 @@ fn main() { level: Level::TRACE, filter: "warn,log_layers_ecs=trace".to_string(), custom_layer, + ..default() })) .add_systems(Startup, (log_system, setup)) .add_systems(Update, print_logs) diff --git a/examples/camera/custom_projection.rs b/examples/camera/custom_projection.rs index c3bdb49bcd..9e20c48eeb 100644 --- a/examples/camera/custom_projection.rs +++ b/examples/camera/custom_projection.rs @@ -27,7 +27,7 @@ impl CameraProjection for ObliquePerspectiveProjection { mat } - fn get_clip_from_view_for_sub(&self, sub_view: &bevy_render::camera::SubCameraView) -> Mat4 { + fn get_clip_from_view_for_sub(&self, sub_view: &bevy::render::camera::SubCameraView) -> Mat4 { let mut mat = self.perspective.get_clip_from_view_for_sub(sub_view); mat.col_mut(2)[0] = self.horizontal_obliqueness; mat.col_mut(2)[1] = self.vertical_obliqueness; diff --git a/examples/diagnostics/log_diagnostics.rs b/examples/diagnostics/log_diagnostics.rs index fd9be8c04e..f487a87133 100644 --- a/examples/diagnostics/log_diagnostics.rs +++ b/examples/diagnostics/log_diagnostics.rs @@ -21,7 +21,7 @@ fn main() { bevy::diagnostic::SystemInformationDiagnosticsPlugin, // Forwards various diagnostics from the render app to the main app. // These are pretty verbose but can be useful to pinpoint performance issues. - bevy_render::diagnostic::RenderDiagnosticsPlugin, + bevy::render::diagnostic::RenderDiagnosticsPlugin, )) // No rendering diagnostics are emitted unless something is drawn to the screen, // so we spawn a small scene. diff --git a/examples/ecs/ecs_guide.rs b/examples/ecs/ecs_guide.rs index 2b2fdb8148..7d518c0a7d 100644 --- a/examples/ecs/ecs_guide.rs +++ b/examples/ecs/ecs_guide.rs @@ -283,7 +283,7 @@ fn print_at_end_round(mut counter: Local) { /// A group of related system sets, used for controlling the order of systems. Systems can be /// added to any number of sets. #[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)] -enum MySet { +enum MySystems { BeforeRound, Round, AfterRound, @@ -330,7 +330,12 @@ fn main() { .configure_sets( Update, // chain() will ensure sets run in the order they are listed - (MySet::BeforeRound, MySet::Round, MySet::AfterRound).chain(), + ( + MySystems::BeforeRound, + MySystems::Round, + MySystems::AfterRound, + ) + .chain(), ) // The add_systems function is powerful. You can define complex system configurations with ease! .add_systems( @@ -343,9 +348,9 @@ fn main() { exclusive_player_system, ) // All of the systems in the tuple above will be added to this set - .in_set(MySet::BeforeRound), + .in_set(MySystems::BeforeRound), // This `Round` system will run after the `BeforeRound` systems thanks to the chained set configuration - score_system.in_set(MySet::Round), + score_system.in_set(MySystems::Round), // These `AfterRound` systems will run after the `Round` systems thanks to the chained set configuration ( score_check_system, @@ -353,7 +358,7 @@ fn main() { // with sets! game_over_system.after(score_check_system), ) - .in_set(MySet::AfterRound), + .in_set(MySystems::AfterRound), ), ) // This call to run() starts the app we just built! diff --git a/examples/ecs/error_handling.rs b/examples/ecs/error_handling.rs index b13a018530..31f9838aaa 100644 --- a/examples/ecs/error_handling.rs +++ b/examples/ecs/error_handling.rs @@ -1,13 +1,7 @@ //! Showcases how fallible systems and observers can make use of Rust's powerful result handling //! syntax. -//! -//! Important note: to set the global error handler, the `configurable_error_handler` feature must be -//! enabled. This feature is disabled by default, as it may introduce runtime overhead, especially for commands. -use bevy::ecs::{ - error::{warn, GLOBAL_ERROR_HANDLER}, - world::DeferredWorld, -}; +use bevy::ecs::{error::warn, world::DeferredWorld}; use bevy::math::sampling::UniformMeshSampler; use bevy::prelude::*; @@ -16,17 +10,15 @@ use rand::SeedableRng; use rand_chacha::ChaCha8Rng; fn main() { + let mut app = App::new(); // By default, fallible systems that return an error will panic. // - // We can change this by setting a custom error handler, which applies globally. - // Here we set the global error handler using one of the built-in - // error handlers. Bevy provides built-in handlers for `panic`, `error`, `warn`, `info`, + // We can change this by setting a custom error handler, which applies to the entire app + // (you can also set it for specific `World`s). + // Here we it using one of the built-in error handlers. + // Bevy provides built-in handlers for `panic`, `error`, `warn`, `info`, // `debug`, `trace` and `ignore`. - GLOBAL_ERROR_HANDLER - .set(warn) - .expect("The error handler can only be set once, globally."); - - let mut app = App::new(); + app.set_error_handler(warn); app.add_plugins(DefaultPlugins); @@ -169,7 +161,7 @@ fn failing_system(world: &mut World) -> Result { fn failing_commands(mut commands: Commands) { commands // This entity doesn't exist! - .entity(Entity::from_raw(12345678)) + .entity(Entity::from_raw_u32(12345678).unwrap()) // Normally, this failed command would panic, // but since we've set the global error handler to `warn` // it will log a warning instead. diff --git a/examples/ecs/fallible_params.rs b/examples/ecs/fallible_params.rs index 99eaedf10d..94a8007aec 100644 --- a/examples/ecs/fallible_params.rs +++ b/examples/ecs/fallible_params.rs @@ -2,7 +2,7 @@ //! from running if their acquiry conditions aren't met. //! //! Fallible system parameters include: -//! - [`Res`], [`ResMut`] - Resource has to exist, and the [`GLOBAL_ERROR_HANDLER`] will be called if it doesn't. +//! - [`Res`], [`ResMut`] - Resource has to exist, and the [`World::get_default_error_handler`] will be called if it doesn't. //! - [`Single`] - There must be exactly one matching entity, but the system will be silently skipped otherwise. //! - [`Option>`] - There must be zero or one matching entity. The system will be silently skipped if there are more. //! - [`Populated`] - There must be at least one matching entity, but the system will be silently skipped otherwise. @@ -18,19 +18,13 @@ //! //! [`SystemParamValidationError`]: bevy::ecs::system::SystemParamValidationError //! [`SystemParam::validate_param`]: bevy::ecs::system::SystemParam::validate_param +//! [`default_error_handler`]: bevy::ecs::error::default_error_handler -use bevy::ecs::error::{warn, GLOBAL_ERROR_HANDLER}; +use bevy::ecs::error::warn; use bevy::prelude::*; use rand::Rng; fn main() { - // By default, if a parameter fail to be fetched, - // the `GLOBAL_ERROR_HANDLER` will be used to handle the error, - // which by default is set to panic. - GLOBAL_ERROR_HANDLER - .set(warn) - .expect("The error handler can only be set once, globally."); - println!(); println!("Press 'A' to add enemy ships and 'R' to remove them."); println!("Player ship will wait for enemy ships and track one if it exists,"); @@ -38,6 +32,10 @@ fn main() { println!(); App::new() + // By default, if a parameter fail to be fetched, + // `World::get_default_error_handler` will be used to handle the error, + // which by default is set to panic. + .set_error_handler(warn) .add_plugins(DefaultPlugins) .add_systems(Startup, setup) .add_systems(Update, (user_input, move_targets, track_targets).chain()) @@ -131,13 +129,12 @@ fn move_targets(mut enemies: Populated<(&mut Transform, &mut Enemy)>, time: Res< } /// System that moves the player, causing them to track a single enemy. -/// The player will search for enemies if there are none. -/// If there is one, player will track it. -/// If there are too many enemies, the player will cease all action (the system will not run). +/// If there is exactly one, player will track it. +/// Otherwise, the player will search for enemies. fn track_targets( // `Single` ensures the system runs ONLY when exactly one matching entity exists. mut player: Single<(&mut Transform, &Player)>, - // `Option` ensures that the system runs ONLY when zero or one matching entity exists. + // `Option` never prevents the system from running, but will be `None` if there is not exactly one matching entity. enemy: Option, Without)>>, time: Res