Skip to content
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
042337f
add back all changes
Trashtalk217 Jul 10, 2025
47c20a5
Merge branch 'main' of https://github.com/bevyengine/bevy into resour…
Trashtalk217 Jul 10, 2025
64eac9a
cargo fmt
Trashtalk217 Jul 10, 2025
956c1a6
cargo clippy
Trashtalk217 Jul 10, 2025
073df4b
add entities with resources, auto disabled IsResource
Trashtalk217 Jul 12, 2025
6c8281d
fixed and tests
Trashtalk217 Jul 12, 2025
f9dc9f1
fixed more tests
Trashtalk217 Jul 12, 2025
07723ac
fixed moore tests
Trashtalk217 Jul 12, 2025
1ba7af2
fix ci
Trashtalk217 Jul 12, 2025
b89c042
fix mooore tests (benches)
Trashtalk217 Jul 12, 2025
9b33933
fix docs
Trashtalk217 Jul 12, 2025
c7b4f1d
add migration guide
Trashtalk217 Jul 12, 2025
96aef45
fixed spelling errors to prove I'm not AI
Trashtalk217 Jul 12, 2025
e3ccd1b
Merge branch 'main' of https://github.com/bevyengine/bevy into resour…
Trashtalk217 Jul 12, 2025
5758134
addressed comments
Trashtalk217 Jul 13, 2025
9824c3a
testing robustness
Trashtalk217 Jul 14, 2025
9acf43f
Merge branch 'main' of https://github.com/bevyengine/bevy into resour…
Trashtalk217 Jul 14, 2025
da1d203
merge upstream
Trashtalk217 Jul 23, 2025
4d4e914
cleanup
Trashtalk217 Jul 23, 2025
78a0672
fix more stuff
Trashtalk217 Jul 23, 2025
4a2416d
update migration guides
Trashtalk217 Jul 24, 2025
3942c56
spelling
Trashtalk217 Jul 24, 2025
724a4c4
merge
Trashtalk217 Aug 22, 2025
f442ff6
fix imports
Trashtalk217 Aug 22, 2025
acb539e
second attempt at fixing a test
Trashtalk217 Aug 22, 2025
a0d76c3
merge
Trashtalk217 Aug 30, 2025
c99df9c
Merge branch 'resource_entity_lookup' of github.com:trashtalk217/bevy…
Trashtalk217 Aug 30, 2025
12383a3
Merge branch 'main' of https://github.com/bevyengine/bevy into resour…
Trashtalk217 Sep 2, 2025
d1b9b56
Merge branch 'main' of https://github.com/bevyengine/bevy into resour…
Trashtalk217 Sep 8, 2025
2261f8b
Merge branch 'main' of https://github.com/bevyengine/bevy into resour…
Trashtalk217 Sep 14, 2025
7f224be
address comments
Trashtalk217 Sep 14, 2025
6246539
Update crates/bevy_ecs/src/resource.rs
Trashtalk217 Sep 14, 2025
b83a617
Update crates/bevy_ecs/src/resource.rs
Trashtalk217 Sep 14, 2025
e98bcd8
Update crates/bevy_ecs/src/resource.rs
Trashtalk217 Sep 14, 2025
09e5ff8
Update crates/bevy_ecs/src/name.rs
Trashtalk217 Sep 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions benches/benches/bevy_ecs/world/world_get.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,10 @@ pub fn world_entity(criterion: &mut Criterion) {
for entity_count in RANGE.map(|i| i * 10_000) {
group.bench_function(format!("{entity_count}_entities"), |bencher| {
let world = setup::<Table>(entity_count);
let offset = world.resource_count();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is so fragile: I really don't like this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is exactly correct. I've justified it to myself by noting that creating entities from raw numbers is really fragile, so this is not really that bad in comparison.

It's not great either.

Copy link
Contributor Author

@Trashtalk217 Trashtalk217 Jul 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose we could use the lower level entities.len(), which would be more robust to adding internal entities that are not resources, but I think that the offset stuff is gonna have to stay.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setup calls world.spawn_batch, which returns an iterator that yields the actual Entity values. Would it work to stick those in a Vec<Entity> and iterate that instead of using integers and Entity::from_raw?


bencher.iter(|| {
for i in 0..entity_count {
for i in offset..(entity_count + offset) {
let entity =
// SAFETY: Range is exclusive.
Entity::from_raw(EntityRow::new(unsafe { NonMaxU32::new_unchecked(i) }));
Expand All @@ -74,9 +75,10 @@ pub fn world_get(criterion: &mut Criterion) {
for entity_count in RANGE.map(|i| i * 10_000) {
group.bench_function(format!("{entity_count}_entities_table"), |bencher| {
let world = setup::<Table>(entity_count);
let offset = world.resource_count();

bencher.iter(|| {
for i in 0..entity_count {
for i in offset..(entity_count + offset) {
let entity =
// SAFETY: Range is exclusive.
Entity::from_raw(EntityRow::new(unsafe { NonMaxU32::new_unchecked(i) }));
Expand All @@ -86,9 +88,10 @@ pub fn world_get(criterion: &mut Criterion) {
});
group.bench_function(format!("{entity_count}_entities_sparse"), |bencher| {
let world = setup::<Sparse>(entity_count);
let offset = world.resource_count();

bencher.iter(|| {
for i in 0..entity_count {
for i in offset..(entity_count + offset) {
let entity =
// SAFETY: Range is exclusive.
Entity::from_raw(EntityRow::new(unsafe { NonMaxU32::new_unchecked(i) }));
Expand All @@ -109,10 +112,11 @@ pub fn world_query_get(criterion: &mut Criterion) {
for entity_count in RANGE.map(|i| i * 10_000) {
group.bench_function(format!("{entity_count}_entities_table"), |bencher| {
let mut world = setup::<Table>(entity_count);
let offset = world.resource_count();
let mut query = world.query::<&Table>();

bencher.iter(|| {
for i in 0..entity_count {
for i in offset..(entity_count + offset) {
let entity =
// SAFETY: Range is exclusive.
Entity::from_raw(EntityRow::new(unsafe { NonMaxU32::new_unchecked(i) }));
Expand All @@ -137,9 +141,10 @@ pub fn world_query_get(criterion: &mut Criterion) {
&WideTable<4>,
&WideTable<5>,
)>();
let offset = world.resource_count();

bencher.iter(|| {
for i in 0..entity_count {
for i in offset..(entity_count + offset) {
let entity =
// SAFETY: Range is exclusive.
Entity::from_raw(EntityRow::new(unsafe { NonMaxU32::new_unchecked(i) }));
Expand All @@ -149,10 +154,11 @@ pub fn world_query_get(criterion: &mut Criterion) {
});
group.bench_function(format!("{entity_count}_entities_sparse"), |bencher| {
let mut world = setup::<Sparse>(entity_count);
let offset = world.resource_count();
let mut query = world.query::<&Sparse>();

bencher.iter(|| {
for i in 0..entity_count {
for i in offset..(entity_count + offset) {
let entity =
// SAFETY: Range is exclusive.
Entity::from_raw(EntityRow::new(unsafe { NonMaxU32::new_unchecked(i) }));
Expand All @@ -177,9 +183,10 @@ pub fn world_query_get(criterion: &mut Criterion) {
&WideSparse<4>,
&WideSparse<5>,
)>();
let offset = world.resource_count();

bencher.iter(|| {
for i in 0..entity_count {
for i in offset..(entity_count + offset) {
// SAFETY: Range is exclusive.
let entity =
Entity::from_raw(EntityRow::new(unsafe { NonMaxU32::new_unchecked(i) }));
Expand Down
6 changes: 5 additions & 1 deletion crates/bevy_ecs/src/component/info.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use alloc::{borrow::Cow, vec::Vec};
use bevy_platform::{collections::HashSet, sync::PoisonError};
use bevy_platform::{collections::HashMap, collections::HashSet, sync::PoisonError};
use bevy_ptr::OwningPtr;
#[cfg(feature = "bevy_reflect")]
use bevy_reflect::Reflect;
Expand All @@ -18,6 +18,7 @@ use crate::{
RequiredComponents, StorageType,
},
lifecycle::ComponentHooks,
prelude::Entity,
query::DebugCheckedUnwrap as _,
resource::Resource,
storage::SparseSetIndex,
Expand Down Expand Up @@ -346,6 +347,9 @@ pub struct Components {
pub(super) components: Vec<Option<ComponentInfo>>,
pub(super) indices: TypeIdMap<ComponentId>,
pub(super) resource_indices: TypeIdMap<ComponentId>,
/// A lookup for the entities on which resources are stored.
/// It uses `ComponentId`s instead of `TypeId`s for untyped APIs
pub(crate) resource_entities: HashMap<ComponentId, Entity>,
// This is kept internal and local to verify that no deadlocks can occor.
pub(super) queued: bevy_platform::sync::RwLock<QueuedComponents>,
}
Expand Down
3 changes: 3 additions & 0 deletions crates/bevy_ecs/src/entity_disabling.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
use crate::{
component::{ComponentId, Components, StorageType},
query::FilteredAccess,
resource::IsResource,
world::{FromWorld, World},
};
use bevy_ecs_macros::{Component, Resource};
Expand Down Expand Up @@ -143,6 +144,8 @@ impl FromWorld for DefaultQueryFilters {
let mut filters = DefaultQueryFilters::empty();
let disabled_component_id = world.register_component::<Disabled>();
filters.register_disabling_component(disabled_component_id);
let is_resource_component_id = world.register_component::<IsResource>();
filters.register_disabling_component(is_resource_component_id);
filters
}
}
Expand Down
12 changes: 6 additions & 6 deletions crates/bevy_ecs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -378,9 +378,9 @@ mod tests {
let mut world = World::new();
let e = world.spawn((TableStored("abc"), A(123))).id();
let f = world.spawn((TableStored("def"), A(456))).id();
assert_eq!(world.entities.len(), 2);
assert_eq!(world.entity_count(), 2);
assert!(world.despawn(e));
assert_eq!(world.entities.len(), 1);
assert_eq!(world.entity_count(), 1);
assert!(world.get::<TableStored>(e).is_none());
assert!(world.get::<A>(e).is_none());
assert_eq!(world.get::<TableStored>(f).unwrap().0, "def");
Expand All @@ -393,9 +393,9 @@ mod tests {

let e = world.spawn((TableStored("abc"), SparseStored(123))).id();
let f = world.spawn((TableStored("def"), SparseStored(456))).id();
assert_eq!(world.entities.len(), 2);
assert_eq!(world.entity_count(), 2);
assert!(world.despawn(e));
assert_eq!(world.entities.len(), 1);
assert_eq!(world.entity_count(), 1);
assert!(world.get::<TableStored>(e).is_none());
assert!(world.get::<SparseStored>(e).is_none());
assert_eq!(world.get::<TableStored>(f).unwrap().0, "def");
Expand Down Expand Up @@ -1786,7 +1786,7 @@ mod tests {
fn try_insert_batch() {
let mut world = World::default();
let e0 = world.spawn(A(0)).id();
let e1 = Entity::from_raw_u32(1).unwrap();
let e1 = Entity::from_raw_u32(2).unwrap();

let values = vec![(e0, (A(1), B(0))), (e1, (A(0), B(1)))];

Expand All @@ -1810,7 +1810,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_u32(1).unwrap();
let e1 = Entity::from_raw_u32(2).unwrap();

let values = vec![(e0, (A(1), B(0))), (e1, (A(0), B(1)))];

Expand Down
2 changes: 1 addition & 1 deletion crates/bevy_ecs/src/name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ mod tests {
let mut query = world.query::<NameOrEntity>();
let d1 = query.get(&world, e1).unwrap();
// NameOrEntity Display for entities without a Name should be {index}v{generation}
assert_eq!(d1.to_string(), "0v0");
assert_eq!(d1.to_string(), "1v0");
let d2 = query.get(&world, e2).unwrap();
// NameOrEntity Display for entities with a Name should be the Name
assert_eq!(d2.to_string(), "MyName");
Expand Down
4 changes: 4 additions & 0 deletions crates/bevy_ecs/src/query/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ impl<'w, D: QueryData, F: QueryFilter> QueryBuilder<'w, D, F> {
mod tests {
use crate::{
prelude::*,
resource::IsResource,
world::{EntityMutExcept, EntityRefExcept, FilteredEntityMut, FilteredEntityRef},
};
use std::dbg;
Expand Down Expand Up @@ -332,6 +333,7 @@ mod tests {
#[test]
fn builder_or() {
let mut world = World::new();

world.spawn((A(0), B(0)));
world.spawn(B(0));
world.spawn(C(0));
Expand Down Expand Up @@ -485,6 +487,7 @@ mod tests {

let mut query = QueryBuilder::<(FilteredEntityMut, EntityMutExcept<A>)>::new(&mut world)
.data::<EntityMut>()
.filter::<Without<IsResource>>()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should no longer be necessary now that you have a default query filter, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not here, it probably has something to do with the fact we're querying EntityMutExcept

Copy link
Contributor

@chescock chescock Jul 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh, that's not what I would have expected. ... I see, if a query has read access to a disabled component, then we ignore the filter. So it's not just EntityMutExcept; even ordinary EntityMut is enough to disable the filters.

@alice-i-cecile @NiseVoid Do we expect Query<EntityMut> to ignore all default query filters, or is this a bug? I thought the intention was to ignore filters if the component was "mentioned", but EntityMut has access to all components without really mentioning them.

Edit: Oh, and EntityMutExcept<IsResource> doesn't ignore the default query filter, because it doesn't have read access to IsResource, even though it "mentions" it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, this part of the code is real weird

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alice-i-cecile @NiseVoid Do we expect Query to ignore all default query filters, or is this a bug? I thought the intention was to ignore filters if the component was "mentioned", but EntityMut has access to all components without really mentioning them.

This is a bug :(

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alice-i-cecile @NiseVoid Do we expect Query to ignore all default query filters, or is this a bug? I thought the intention was to ignore filters if the component was "mentioned", but EntityMut has access to all components without really mentioning them.

This is a bug :(

I have an idea on how to fix this, by ignoring unbounded access in Access::contains() but then adding archetypal access to Entity(Ref|Mut)Except. It will get some edge cases like Query<(EntityRef, Option<&Disabled>)> wrong, but I think it works on all reasonable cases. I'll try to push up a PR tomorrow or Wednesday!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix for default query filters on EntityMut is here: #20163

.build();

// Removing `EntityMutExcept<A>` just leaves A
Expand All @@ -496,6 +499,7 @@ mod tests {

let mut query = QueryBuilder::<(FilteredEntityMut, EntityRefExcept<A>)>::new(&mut world)
.data::<EntityMut>()
.filter::<Without<IsResource>>()
.build();

// Removing `EntityRefExcept<A>` just leaves A, plus read access
Expand Down
1 change: 1 addition & 0 deletions crates/bevy_ecs/src/query/iter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2669,6 +2669,7 @@ mod tests {
#[test]
fn query_iter_sorts() {
let mut world = World::new();

for i in 0..100 {
world.spawn(A(i as f32));
world.spawn((A(i as f32), Sparse(i)));
Expand Down
8 changes: 5 additions & 3 deletions crates/bevy_ecs/src/query/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1777,6 +1777,7 @@ mod tests {
component::Component,
entity_disabling::DefaultQueryFilters,
prelude::*,
resource::IsResource,
system::{QueryLens, RunSystemOnce},
world::{FilteredEntityMut, FilteredEntityRef},
};
Expand Down Expand Up @@ -1850,6 +1851,7 @@ mod tests {
#[test]
fn can_transmute_empty_tuple() {
let mut world = World::new();

world.register_component::<A>();
let entity = world.spawn(A(10)).id();

Expand Down Expand Up @@ -2172,9 +2174,7 @@ mod tests {
world.spawn((B(0), C(0)));
world.spawn(C(0));

let mut df = DefaultQueryFilters::empty();
df.register_disabling_component(world.register_component::<C>());
world.insert_resource(df);
world.register_disabling_component::<C>();

// Without<C> only matches the first entity
let mut query = QueryState::<()>::new(&mut world);
Expand Down Expand Up @@ -2219,6 +2219,7 @@ mod tests {
let mut df = DefaultQueryFilters::empty();
df.register_disabling_component(world.register_component::<Sparse>());
world.insert_resource(df);
world.register_disabling_component::<IsResource>();

let mut query = QueryState::<()>::new(&mut world);
// The query doesn't ask for sparse components, but the default filters adds
Expand All @@ -2229,6 +2230,7 @@ mod tests {
let mut df = DefaultQueryFilters::empty();
df.register_disabling_component(world.register_component::<Table>());
world.insert_resource(df);
world.register_disabling_component::<IsResource>();

let mut query = QueryState::<()>::new(&mut world);
// If the filter is instead a table components, the query can still be dense
Expand Down
91 changes: 91 additions & 0 deletions crates/bevy_ecs/src/resource.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
//! Resources are unique, singleton-like data types that can be accessed from systems and stored in the [`World`](crate::world::World).

use crate::prelude::Component;
use crate::prelude::ReflectComponent;
use bevy_reflect::prelude::ReflectDefault;
use bevy_reflect::Reflect;
use core::marker::PhantomData;
// The derive macro for the `Resource` trait
pub use bevy_ecs_macros::Resource;

Expand Down Expand Up @@ -73,3 +78,89 @@ pub use bevy_ecs_macros::Resource;
note = "consider annotating `{Self}` with `#[derive(Resource)]`"
)]
pub trait Resource: Send + Sync + 'static {}

/// A marker component for the entity that stores the resource of type `T`.
///
/// This component is automatically inserted when a resource of type `T` is inserted into the world,
/// and can be used to find the entity that stores a particular resource.
///
/// By contrast, the [`IsResource`] component is used to find all entities that store resources,
/// regardless of the type of resource they store.
///
/// This component comes with a hook that ensures that at most one entity has this component for any given `R`:
/// adding this component to an entity (or spawning an entity with this component) will despawn any other entity with this component.
#[derive(Component, Debug)]
#[require(IsResource)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component, Default))]
pub struct ResourceEntity<R: Resource>(#[reflect(ignore)] PhantomData<R>);

impl<R: Resource> Default for ResourceEntity<R> {
fn default() -> Self {
ResourceEntity(PhantomData)
}
}

/// A marker component for entities which store resources.
///
/// By contrast, the [`ResourceEntity<R>`] component is used to find the entity that stores a particular resource.
/// This component is required by the [`ResourceEntity<R>`] component, and will automatically be added.
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect),
reflect(Component, Default, Debug)
)]
#[derive(Component, Default, Debug)]
pub struct IsResource;

/// Used in conjunction with [`ResourceEntity<R>`], when no type information is available.
/// This is used by [`World::insert_resource_by_id`](crate::world::World).
#[derive(Resource)]
pub(crate) struct TypeErasedResource;

#[cfg(test)]
mod tests {
use crate::change_detection::MaybeLocation;
use crate::ptr::OwningPtr;
use crate::resource::Resource;
use crate::world::World;
use bevy_platform::prelude::String;

#[test]
fn unique_resource_entities() {
#[derive(Default, Resource)]
struct TestResource1;

#[derive(Resource)]
#[expect(dead_code, reason = "field needed for testing")]
struct TestResource2(String);

#[derive(Resource)]
#[expect(dead_code, reason = "field needed for testing")]
struct TestResource3(u8);

let mut world = World::new();
let start = world.entities().len();
world.init_resource::<TestResource1>();
assert_eq!(world.entities().len(), start + 1);
world.insert_resource(TestResource2(String::from("Foo")));
assert_eq!(world.entities().len(), start + 2);
// like component registration, which just makes it known to the world that a component exists,
// registering a resource should not spawn an entity.
let id = world.register_resource::<TestResource3>();
assert_eq!(world.entities().len(), start + 2);
OwningPtr::make(20_u8, |ptr| {
// SAFETY: id was just initialized and corresponds to a resource.
unsafe {
world.insert_resource_by_id(id, ptr, MaybeLocation::caller());
}
});
assert_eq!(world.entities().len(), start + 3);
assert!(world.remove_resource_by_id(id).is_some());
assert_eq!(world.entities().len(), start + 2);
world.remove_resource::<TestResource1>();
assert_eq!(world.entities().len(), start + 1);
// make sure that trying to add a resource twice results, doesn't change the entity count
world.insert_resource(TestResource2(String::from("Bar")));
assert_eq!(world.entities().len(), start + 1);
}
}
4 changes: 2 additions & 2 deletions crates/bevy_ecs/src/system/system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -494,9 +494,9 @@ mod tests {
#[test]
fn command_processing() {
let mut world = World::new();
assert_eq!(world.entities.len(), 0);
assert_eq!(world.entity_count(), 0);
world.run_system_once(spawn_entity).unwrap();
assert_eq!(world.entities.len(), 1);
assert_eq!(world.entity_count(), 1);
}

#[test]
Expand Down
Loading
Loading