-
-
Notifications
You must be signed in to change notification settings - Fork 3.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
implement EntitySet and iter_many_unique methods #16547
base: main
Are you sure you want to change the base?
Conversation
add EntityBorrow, TrustedEntityBorrow, EntitySet and EntitySetIterator traits add iter_many_unique, iter_many_unique_mut, iter_many_unique_unsafe methods on Query add iter_many_unique, iter_many_unique_mut, iter_many_unique_manual and iter_many_unique_unchecked_manual methods on QueryState add the corresponding QueryManyUniqueIter add UniqueEntityIter
Haven't had a chance to do a review yet, but just a thought:
We could implement impl EntitySet for HashSet<Entity, AGoodHasher> { /* ... */ }
impl EntitySet for HashSet<Entity, AnotherGoodHasher> { /* ... */ }
// ... Or perhaps add another trait, |
However, it is also a question whether/when we want The inconvenient newtyping and order instability leads me to suggest that we add an The true solution is to ask rust-lang to add a defaulted |
Ah ok, apologies for the misunderstanding, I fixated on that point! I'll give the full PR a review at some point this weekend |
/// | ||
/// - [`iter_many_unique_mut`](Self::iter_many_unique_mut) to get mutable query items. | ||
#[inline] | ||
pub fn iter_many_unique<'w, 's, EntityList: EntitySet>( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do agree that these should exist for API symmetry, but ... are the non-mut
versions actually useful? If you're getting the read-only version then iter_many()
already gives you an Iterator
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While the mutable versions are definitely the main feature, these are nice too! Just the guarantee that each entity will only be handled once can be useful, especially in libraries.
There are non-unique query iterators like ancestor and descendant iterators that can be rejected this way.
It also serves as an intermediate step in tandem with iter_many_unique_mut
, if you want to query for some TrustedEntityBorrow
type, you can pass the query directly into iter_many_unique_mut
, or store it in a Vec
so you can pass it later. Mutable access is not needed for either.
/// A trait for entity borrows. | ||
/// | ||
/// This trait can be thought of as `Borrow<Entity>`, but yielding `Entity` directly. | ||
pub trait EntityBorrow { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the advantage of a separate trait here versus unsafe trait TrustedEntityBorrow: Borrow<Entity>
? I assumed it was to avoid the orphan rule somehow, but I don't see what type impls EntityBorrow
that wouldn't impl Borrow<Entity>
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are a few reasons for this trait:
Borrow<T>
is blanket implemented for T
, &T
, &mut T
, Box<T>
, Rc<T>
, Arc<T>
.
These blankets overlap with the "for every &U where U: EntityBorrow
" impls we would like.
Because Borrow<Entity>
can not be guaranteed to be implemented for a reference, you'd need to bound EntitySetIterator
impls to only valid references:
unsafe impl<'a, T: TrustedEntityBorrow> EntitySetIterator for result::Iter<'a, T>
where &'a T: TrustedEntityBorrow {}
vs
unsafe impl<T: TrustedEntityBorrow> EntitySetIterator for result::Iter<'_, T>
In addition, we would now have to manually implement Borrow<Entity>
for all levels of nested impls that we want: &&T
, &&mut T
, &Arc<T>
, &mut SomeBorrowEntityType
These may seem unnecessary, but iterators/iterator adapters can easily introduce more reference levels to a type.
Furthermore, Entity
is Copy
, we do not need the reference returned by Borrow::borrow()
!
I only discovered this after making the PR, but the idea of such a trait has been floated before in #9367
EntityBorrow
also looks nicer than Borrow<Entity>
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was imagining that a few calls to Iterator::copied()
could prevent the deeply-nested types and save a whole pile of impl
blocks. Not a big deal either way!
Hmm, given that you aren't returning a reference anymore, maybe AsEntity
from the linked issue is a better name than EntityBorrow
.
Oh, and that reminds me! Do you want to implement TrustedEntityBorrow
for MainEntity
and RenderEntity
, since those are simple newtypes over Entity
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, and that reminds me! Do you want to implement TrustedEntityBorrow for MainEntity and RenderEntity, since those are simple newtypes over Entity?
Yes! Anything that represents an Entity
and can delegate its comparison trait to Entity
.
There's a handful more of Entity
newtypes in bevy itself as far as I'm aware.
EntityBorrow
could even be made derivable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, given that you aren't returning a reference anymore, maybe AsEntity from the linked issue is a better name than EntityBorrow.
The name EntityBorrow
isn't fully ideal, but there is an important distinction between as
and borrow
in Rust:
borrow
is supposed to preserve equality, as
does not have this restriction. (See Borrow
and AsRef
docs, as
is also often used for lossy integer conversions)
It is important that this is not implemented for something that compares differently to Entity
, if that is really desired, then it can't be bundled with EntityBorrow
, it would have to be separate.
IMO, It's okay to start with the stricter version and introduce a relaxed one later if we want to!
I think we want Would it work to offer a safe method to create a |
I agree that we would like this to work with |
Objective
In current Bevy, it is very inconvenient to mutably retrieve a user-provided list of entities more than one element at a time.
If the list contains any duplicate entities, we risk mutable aliasing.
Users of
Query::iter_many_mut
do not have access toIterator
trait, and thus miss out on common functionality, for instance collecting theirQueryManyIter
.We can circumvent this issue with validation, however that entails checking every entity against all others for inequality, or utilizing an
EntityHashSet
. Even if an entity list remains unchanged, this validation is/would have to be redone every time we wish to fetch with the list.This presents a lot of wasted work, as we often trivially know an entity list to be unique f.e.:
QueryIter
will fetch everyEntity
once and only once.As more things become entities – assets, components, queries – this issue will become more pronounced.
get_many
/many
/iter_many
/par_iter_many
-like functionality is all affected.Solution
The solution this PR proposes is to introduce functionality built around a new trait:
EntitySet
.The goal is to preserve the property of "uniqueness" in a list wherever possible, and then rely on it as a bound within new
*_many_unique
methods to avoid the need for validation.This is achieved using
Iterator
:EntitySet
is blanket implemented for anyT
that implementsIntoIterator<IntoIter: EntitySetIterator>
.EntitySetIterator
is the unsafe trait that actually guarantees an iterator to be "unique" via its safety contract.We define an "Iterator over unique entities" as: "No two entities returned by the iterator may compare equal."
For iterators that cannot return more than 1 element, this is trivially true.
Whether an iterator can satisfy this is up to the
EntitySetIterator
implementor to ensure, hence the unsafe.However, this is not yet a complete solution. Looking at the signature of
iter_many
, we find thatIntoIterator::Item
is notEntity
, but is instead bounded by theBorrow<Entity>
trait. That is because iteration without consuming the collection will often yield us references, not owned items.Borrow<Entity>
presents an issue: TheBorrow
docs state thatx = y
should equalx.borrow() = y.borrow()
, but unsafe cannot rely on this for soundness. We run into similar problems with other trait implementations of anyBorrow<Entity>
type:PartialEq
,Eq
,PartialOrd
,Ord
,Hash
,Clone
,Borrow
, andBorrowMut
.This PR solves this with the unsafe
TrustedEntityBorrow
trait:Any implementor promises that the behavior of the aforementioned traits matches that of the underlying entity.
While
Borrow<Entity>
was the inspiration, we use our own counterpart traitEntityBorrow
as the supertrait toTrustedEntityBorrow
, so we can circumvent the limitations of the existingBorrow<T>
blanket impls.All together, these traits allow us to implement
*_many_unique
functionality with a loneEntitySet
bound.EntitySetIterator
is implemented for all the std iterators and iterator adapters that guarantee or preserve uniqueness, so we can filter, skip, take, step, reverse, ... our unique entity iterators without worry!Sadly, current
HashSet
iterators do not carry the necessary type information with them to determine whether the sourceHashSet
produces logic errors; A maliciousHasher
could compromise aHashSet
.HashSet
iteration is generally discouraged in the first place, so we also exclude the set operation iterators, even though they do carry theHasher
type parameter.BTreeSet
implementsEntitySet
without any problems.If an iterator type cannot guarantee uniqueness at compile time, then a user can still attach
EntitySetIterator
to an individual instance of that type viaUniqueEntityIter::from_iterator_unchecked
.With this, custom types can use
UniqueEntityIter<I>
as theirIntoIterator::IntoIter
type, if necessary.This PR is focused on the base concept, and expansions on it are left for follow-up PRs. See "Potential Future Work" below.
Testing
Doctests on
iter_many_unique
/iter_many_unique_mut
+ 2 tests in entity_set.rs.Showcase
Changelog
EntityBorrow
,TrustedEntityBorrow
,EntitySet
andEntitySetIterator
traitsiter_many_unique
,iter_many_unique_mut
,iter_many_unique_unsafe
methods on Queryiter_many_unique
,iter_many_unique_mut
,iter_many_unique_manual
anditer_many_unique_unchecked_manual
methods onQueryState
QueryManyUniqueIter
UniqueEntityIter
Migration Guide
Any custom type used as a
Borrow<Entity>
entity list item for aniter_many
method now has to implementEntityBorrow
instead. Any type that implementsBorrow<Entity>
can trivially implementEntityBorrow
.Potential Future Work
ToEntitySet
trait for converting any entity iterator into anEntitySetIterator
EntityIndexSet/Map
to tie in hashing withEntitySet
EntityIndexSetSlice/MapSlice
EntityIndexSet/Map
par_iter_many_unique_mut
for parallel mutable iterationpar_iter_many
UniqueEntityVec
to store entity setsUniqueEntitySlice
sUniqueEntityVec
UniqueEntityArray
sUniqueEntitySlice
get_many
/many
methodsUniqueEntityArray
World::entity_unique
to matchWorld::entity
methodsget_many
/many
TrustedEntityBorrow
for theEntityRef
familyUniqueEntityVec