-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
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
[Merged by Bors] - drop overwritten component data on double insert #2227
Conversation
9667281
to
79fb468
Compare
#[test] | ||
fn insert_overwrite_drop() { | ||
let (dropck1, dropped1) = DropCk::new_pair(); | ||
let (dropck2, dropped2) = DropCk::new_pair(); | ||
let mut world = World::default(); | ||
world.spawn().insert(dropck1).insert(dropck2); | ||
assert_eq!(dropped1.load(Ordering::Relaxed), 1); | ||
assert_eq!(dropped2.load(Ordering::Relaxed), 0); | ||
drop(world); | ||
assert_eq!(dropped1.load(Ordering::Relaxed), 1); | ||
assert_eq!(dropped2.load(Ordering::Relaxed), 1); | ||
} |
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.
Here is the scenario that previously never dropped the first inserted component.
Nice reduction of |
d1e84fd
to
547fa26
Compare
Just as a note, this will probably start to fail once #2217 gets merged (or the other way around) since |
crates/bevy_ecs/src/component/mod.rs
Outdated
pub struct ComponentTicks { | ||
pub(crate) added: u32, | ||
pub(crate) changed: u32, | ||
pub(crate) changed: Cell<u32>, |
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 think this is actually counterintuitively a reduction in safety. Cell
is !Sync
, and calling set_changed()
from multiple threads at the same time violates that constraint. On main
we only provide mutable access to ComponentTicks when the caller has unique access to the corresponding component across all threads, so the old UnsafeCell wrapper doesn't allow invalid access in userspace. But now callers without unique access can call set_changed
, which allows cross thread mutation of Cell
.
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 think technically userspace is still "safe" right now with this pr because there are no public interfaces that expose &ComponentTicks
directly (ex: Res<T>
has an internal &ComponentTicks
reference). But imo this change makes it much easier to accidentally expose unsafe behavior. Its tempting to look at Res<T>
and think "we should expose set_changed()
here ... its a safe api so we're fine".
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 see. I somehow ignored the part where things get sent across threads, and compiler let me do it because of existing pointer casts. I'll try to work around that and get rid of Cell. I still think I can end up with better placement for UnsafeCell
, as I believe having it over the whole Vec
wasn't correct. Safe alternative would be to use an atomic in place of cell, but It's likely to impact performance, so I'm not doing it right now.
I think at some point we would also think about using GhostCell
once its implementation matures. I believe we would be able to remove a lot of unsafety that way.
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.
Cool cool. Definitely happy to consider alternatives. For now, can you revert to the current UnsafeCell impl so we don't need to block the drop fixes on UnsafeCell changes (which appear to be orthogonal).
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 don't want to revert it outright, as I believe a lot of those changes are still valid and benefitial. I will make it more similar to previous implementation though and will keep UnsafeCell
around.
One reason is that I believe this line inside get_with_ticks
is a sneaky UB, and I want to get rid of it
let ticks = &mut *self.ticks.get();
Because even though we only really use that reference in order to get the ticks at specific index, we do create a unique reference to the vector while possibly holding unique references to its parts (some ticks at other indices). It's totally possible to reallocate that vec by accident through shared reference and invalidate all the live ticks references, which should not ever be possible.
For that reason, I want to use Vec<UnsafeCell<ComponentTicks>>
instead.
…n component overwrite
…f incorrect Cell in multithreaded context
Okay, I believe it should be correct now. No more |
@@ -386,7 +387,7 @@ impl<T: Component> WorldQuery for &mut T { | |||
pub struct WriteFetch<T> { | |||
storage_type: StorageType, | |||
table_components: NonNull<T>, | |||
table_ticks: *mut ComponentTicks, | |||
table_ticks: *const UnsafeCell<ComponentTicks>, |
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.
Pointers are allowed to alias, so this UnsafeCell
is not necessary. You can store the result of UnsafeCell::get
here.
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 can't do that without creating a reference to the wrong element. This pointer is being offset before reading, thus it represents a reference to the whole vec slice. I can't get rid of that UnsafeCell
and cast that to *mut
at the same time. The only correct way to do that right now requires a &UnsafeCell<ComponentTicks>
to exist first. This might be changed once UnsafeCell::raw_get
gets stabilised.
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 see.
@@ -34,7 +34,7 @@ impl TableId { | |||
pub struct Column { | |||
pub(crate) component_id: ComponentId, | |||
pub(crate) data: BlobVec, | |||
pub(crate) ticks: UnsafeCell<Vec<ComponentTicks>>, | |||
pub(crate) ticks: Vec<UnsafeCell<ComponentTicks>>, |
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.
👍
Alrighty it hurts, but I've resolved to merge this despite the additional "for loop" query iteration perf regression (in addition to the previous pr's regression). Correctness needs to come first and we can hopefully reclaim the perf when we move to static component storage. |
bors r+ |
Continuing the work on reducing the safety footguns in the code, I've removed one extra `UnsafeCell` in favour of safe `Cell` usage inisde `ComponentTicks`. That change led to discovery of misbehaving component insert logic, where data wasn't properly dropped when overwritten. Apart from that being fixed, some method names were changed to better convey the "initialize new allocation" and "replace existing allocation" semantic. Depends on #2221, I will rebase this PR after the dependency is merged. For now, review just the last commit. Co-authored-by: Carter Anderson <mcanders1@gmail.com>
Pull request successfully merged into main. Build succeeded: |
Continuing the work on reducing the safety footguns in the code, I've removed one extra `UnsafeCell` in favour of safe `Cell` usage inisde `ComponentTicks`. That change led to discovery of misbehaving component insert logic, where data wasn't properly dropped when overwritten. Apart from that being fixed, some method names were changed to better convey the "initialize new allocation" and "replace existing allocation" semantic. Depends on bevyengine#2221, I will rebase this PR after the dependency is merged. For now, review just the last commit. Co-authored-by: Carter Anderson <mcanders1@gmail.com>
Continuing the work on reducing the safety footguns in the code, I've removed one extra
UnsafeCell
in favour of safeCell
usage inisdeComponentTicks
. That change led to discovery of misbehaving component insert logic, where data wasn't properly dropped when overwritten. Apart from that being fixed, some method names were changed to better convey the "initialize new allocation" and "replace existing allocation" semantic.Depends on #2221, I will rebase this PR after the dependency is merged. For now, review just the last commit.