Path: blob/main/crates/bevy_ecs/src/world/command_queue.rs
6604 views
use crate::{1system::{Command, SystemBuffer, SystemMeta},2world::{DeferredWorld, World},3};4use alloc::{boxed::Box, vec::Vec};5use bevy_ptr::{OwningPtr, Unaligned};6use core::{7fmt::Debug,8mem::{size_of, MaybeUninit},9panic::AssertUnwindSafe,10ptr::{addr_of_mut, NonNull},11};12use log::warn;1314struct CommandMeta {15/// SAFETY: The `value` must point to a value of type `T: Command`,16/// where `T` is some specific type that was used to produce this metadata.17///18/// `world` is optional to allow this one function pointer to perform double-duty as a drop.19///20/// Advances `cursor` by the size of `T` in bytes.21consume_command_and_get_size:22unsafe fn(value: OwningPtr<Unaligned>, world: Option<NonNull<World>>, cursor: &mut usize),23}2425/// Densely and efficiently stores a queue of heterogenous types implementing [`Command`].26// NOTE: [`CommandQueue`] is implemented via a `Vec<MaybeUninit<u8>>` instead of a `Vec<Box<dyn Command>>`27// as an optimization. Since commands are used frequently in systems as a way to spawn28// entities/components/resources, and it's not currently possible to parallelize these29// due to mutable [`World`] access, maximizing performance for [`CommandQueue`] is30// preferred to simplicity of implementation.31#[derive(Default)]32pub struct CommandQueue {33// This buffer densely stores all queued commands.34//35// For each command, one `CommandMeta` is stored, followed by zero or more bytes36// to store the command itself. To interpret these bytes, a pointer must37// be passed to the corresponding `CommandMeta.apply_command_and_get_size` fn pointer.38pub(crate) bytes: Vec<MaybeUninit<u8>>,39pub(crate) cursor: usize,40pub(crate) panic_recovery: Vec<MaybeUninit<u8>>,41}4243/// Wraps pointers to a [`CommandQueue`], used internally to avoid stacked borrow rules when44/// partially applying the world's command queue recursively45#[derive(Clone)]46pub(crate) struct RawCommandQueue {47pub(crate) bytes: NonNull<Vec<MaybeUninit<u8>>>,48pub(crate) cursor: NonNull<usize>,49pub(crate) panic_recovery: NonNull<Vec<MaybeUninit<u8>>>,50}5152// CommandQueue needs to implement Debug manually, rather than deriving it, because the derived impl just prints53// [core::mem::maybe_uninit::MaybeUninit<u8>, core::mem::maybe_uninit::MaybeUninit<u8>, ..] for every byte in the vec,54// which gets extremely verbose very quickly, while also providing no useful information.55// It is not possible to soundly print the values of the contained bytes, as some of them may be padding or uninitialized (#4863)56// So instead, the manual impl just prints the length of vec.57impl Debug for CommandQueue {58fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {59f.debug_struct("CommandQueue")60.field("len_bytes", &self.bytes.len())61.finish_non_exhaustive()62}63}6465// SAFETY: All commands [`Command`] implement [`Send`]66unsafe impl Send for CommandQueue {}6768// SAFETY: `&CommandQueue` never gives access to the inner commands.69unsafe impl Sync for CommandQueue {}7071impl CommandQueue {72/// Push a [`Command`] onto the queue.73#[inline]74pub fn push(&mut self, command: impl Command) {75// SAFETY: self is guaranteed to live for the lifetime of this method76unsafe {77self.get_raw().push(command);78}79}8081/// Execute the queued [`Command`]s in the world after applying any commands in the world's internal queue.82/// This clears the queue.83#[inline]84pub fn apply(&mut self, world: &mut World) {85// flush the previously queued entities86world.flush_entities();8788// flush the world's internal queue89world.flush_commands();9091// SAFETY: A reference is always a valid pointer92unsafe {93self.get_raw().apply_or_drop_queued(Some(world.into()));94}95}9697/// Take all commands from `other` and append them to `self`, leaving `other` empty98pub fn append(&mut self, other: &mut CommandQueue) {99self.bytes.append(&mut other.bytes);100}101102/// Returns false if there are any commands in the queue103#[inline]104pub fn is_empty(&self) -> bool {105self.cursor >= self.bytes.len()106}107108/// Returns a [`RawCommandQueue`] instance sharing the underlying command queue.109pub(crate) fn get_raw(&mut self) -> RawCommandQueue {110// SAFETY: self is always valid memory111unsafe {112RawCommandQueue {113bytes: NonNull::new_unchecked(addr_of_mut!(self.bytes)),114cursor: NonNull::new_unchecked(addr_of_mut!(self.cursor)),115panic_recovery: NonNull::new_unchecked(addr_of_mut!(self.panic_recovery)),116}117}118}119}120121impl RawCommandQueue {122/// Returns a new `RawCommandQueue` instance, this must be manually dropped.123pub(crate) fn new() -> Self {124// SAFETY: Pointers returned by `Box::into_raw` are guaranteed to be non null125unsafe {126Self {127bytes: NonNull::new_unchecked(Box::into_raw(Box::default())),128cursor: NonNull::new_unchecked(Box::into_raw(Box::new(0usize))),129panic_recovery: NonNull::new_unchecked(Box::into_raw(Box::default())),130}131}132}133134/// Returns true if the queue is empty.135///136/// # Safety137///138/// * Caller ensures that `bytes` and `cursor` point to valid memory139pub unsafe fn is_empty(&self) -> bool {140// SAFETY: Pointers are guaranteed to be valid by requirements on `.clone_unsafe`141(unsafe { *self.cursor.as_ref() }) >= (unsafe { self.bytes.as_ref() }).len()142}143144/// Push a [`Command`] onto the queue.145///146/// # Safety147///148/// * Caller ensures that `self` has not outlived the underlying queue149#[inline]150pub unsafe fn push<C: Command>(&mut self, command: C) {151// Stores a command alongside its metadata.152// `repr(C)` prevents the compiler from reordering the fields,153// while `repr(packed)` prevents the compiler from inserting padding bytes.154#[repr(C, packed)]155struct Packed<C: Command> {156meta: CommandMeta,157command: C,158}159160let meta = CommandMeta {161consume_command_and_get_size: |command, world, cursor| {162*cursor += size_of::<C>();163164// SAFETY: According to the invariants of `CommandMeta.consume_command_and_get_size`,165// `command` must point to a value of type `C`.166let command: C = unsafe { command.read_unaligned() };167match world {168// Apply command to the provided world...169Some(mut world) => {170// SAFETY: Caller ensures pointer is not null171let world = unsafe { world.as_mut() };172command.apply(world);173// The command may have queued up world commands, which we flush here to ensure they are also picked up.174// If the current command queue already the World Command queue, this will still behave appropriately because the global cursor175// is still at the current `stop`, ensuring only the newly queued Commands will be applied.176world.flush();177}178// ...or discard it.179None => drop(command),180}181},182};183184// SAFETY: There are no outstanding references to self.bytes185let bytes = unsafe { self.bytes.as_mut() };186187let old_len = bytes.len();188189// Reserve enough bytes for both the metadata and the command itself.190bytes.reserve(size_of::<Packed<C>>());191192// Pointer to the bytes at the end of the buffer.193// SAFETY: We know it is within bounds of the allocation, due to the call to `.reserve()`.194let ptr = unsafe { bytes.as_mut_ptr().add(old_len) };195196// Write the metadata into the buffer, followed by the command.197// We are using a packed struct to write them both as one operation.198// SAFETY: `ptr` must be non-null, since it is within a non-null buffer.199// The call to `reserve()` ensures that the buffer has enough space to fit a value of type `C`,200// and it is valid to write any bit pattern since the underlying buffer is of type `MaybeUninit<u8>`.201unsafe {202ptr.cast::<Packed<C>>()203.write_unaligned(Packed { meta, command });204}205206// Extend the length of the buffer to include the data we just wrote.207// SAFETY: The new length is guaranteed to fit in the vector's capacity,208// due to the call to `.reserve()` above.209unsafe {210bytes.set_len(old_len + size_of::<Packed<C>>());211}212}213214/// If `world` is [`Some`], this will apply the queued [commands](`Command`).215/// If `world` is [`None`], this will drop the queued [commands](`Command`) (without applying them).216/// This clears the queue.217///218/// # Safety219///220/// * Caller ensures that `self` has not outlived the underlying queue221#[inline]222pub(crate) unsafe fn apply_or_drop_queued(&mut self, world: Option<NonNull<World>>) {223// SAFETY: If this is the command queue on world, world will not be dropped as we have a mutable reference224// If this is not the command queue on world we have exclusive ownership and self will not be mutated225let start = *self.cursor.as_ref();226let stop = self.bytes.as_ref().len();227let mut local_cursor = start;228// SAFETY: we are setting the global cursor to the current length to prevent the executing commands from applying229// the remaining commands currently in this list. This is safe.230*self.cursor.as_mut() = stop;231232while local_cursor < stop {233// SAFETY: The cursor is either at the start of the buffer, or just after the previous command.234// Since we know that the cursor is in bounds, it must point to the start of a new command.235let meta = unsafe {236self.bytes237.as_mut()238.as_mut_ptr()239.add(local_cursor)240.cast::<CommandMeta>()241.read_unaligned()242};243244// Advance to the bytes just after `meta`, which represent a type-erased command.245local_cursor += size_of::<CommandMeta>();246// Construct an owned pointer to the command.247// SAFETY: It is safe to transfer ownership out of `self.bytes`, since the increment of `cursor` above248// guarantees that nothing stored in the buffer will get observed after this function ends.249// `cmd` points to a valid address of a stored command, so it must be non-null.250let cmd = unsafe {251OwningPtr::<Unaligned>::new(NonNull::new_unchecked(252self.bytes.as_mut().as_mut_ptr().add(local_cursor).cast(),253))254};255let f = AssertUnwindSafe(|| {256// SAFETY: The data underneath the cursor must correspond to the type erased in metadata,257// since they were stored next to each other by `.push()`.258// For ZSTs, the type doesn't matter as long as the pointer is non-null.259// This also advances the cursor past the command. For ZSTs, the cursor will not move.260// At this point, it will either point to the next `CommandMeta`,261// or the cursor will be out of bounds and the loop will end.262unsafe { (meta.consume_command_and_get_size)(cmd, world, &mut local_cursor) };263});264265#[cfg(feature = "std")]266{267let result = std::panic::catch_unwind(f);268269if let Err(payload) = result {270// local_cursor now points to the location _after_ the panicked command.271// Add the remaining commands that _would have_ been applied to the272// panic_recovery queue.273//274// This uses `current_stop` instead of `stop` to account for any commands275// that were queued _during_ this panic.276//277// This is implemented in such a way that if apply_or_drop_queued() are nested recursively in,278// an applied Command, the correct command order will be retained.279let panic_recovery = self.panic_recovery.as_mut();280let bytes = self.bytes.as_mut();281let current_stop = bytes.len();282panic_recovery.extend_from_slice(&bytes[local_cursor..current_stop]);283bytes.set_len(start);284*self.cursor.as_mut() = start;285286// This was the "top of the apply stack". If we are _not_ at the top of the apply stack,287// when we call`resume_unwind" the caller "closer to the top" will catch the unwind and do this check,288// until we reach the top.289if start == 0 {290bytes.append(panic_recovery);291}292std::panic::resume_unwind(payload);293}294}295296#[cfg(not(feature = "std"))]297(f)();298}299300// Reset the buffer: all commands past the original `start` cursor have been applied.301// SAFETY: we are setting the length of bytes to the original length, minus the length of the original302// list of commands being considered. All bytes remaining in the Vec are still valid, unapplied commands.303unsafe {304self.bytes.as_mut().set_len(start);305*self.cursor.as_mut() = start;306};307}308}309310impl Drop for CommandQueue {311fn drop(&mut self) {312if !self.bytes.is_empty() {313warn!("CommandQueue has un-applied commands being dropped. Did you forget to call SystemState::apply?");314}315// SAFETY: A reference is always a valid pointer316unsafe { self.get_raw().apply_or_drop_queued(None) };317}318}319320impl SystemBuffer for CommandQueue {321#[inline]322fn apply(&mut self, _system_meta: &SystemMeta, world: &mut World) {323#[cfg(feature = "trace")]324let _span_guard = _system_meta.commands_span.enter();325self.apply(world);326}327328#[inline]329fn queue(&mut self, _system_meta: &SystemMeta, mut world: DeferredWorld) {330world.commands().append(self);331}332}333334#[cfg(test)]335mod test {336use super::*;337use crate::{component::Component, resource::Resource};338use alloc::{borrow::ToOwned, string::String, sync::Arc};339use core::{340panic::AssertUnwindSafe,341sync::atomic::{AtomicU32, Ordering},342};343344#[cfg(miri)]345use alloc::format;346347struct DropCheck(Arc<AtomicU32>);348349impl DropCheck {350fn new() -> (Self, Arc<AtomicU32>) {351let drops = Arc::new(AtomicU32::new(0));352(Self(drops.clone()), drops)353}354}355356impl Drop for DropCheck {357fn drop(&mut self) {358self.0.fetch_add(1, Ordering::Relaxed);359}360}361362impl Command for DropCheck {363fn apply(self, _: &mut World) {}364}365366#[test]367fn test_command_queue_inner_drop() {368let mut queue = CommandQueue::default();369370let (dropcheck_a, drops_a) = DropCheck::new();371let (dropcheck_b, drops_b) = DropCheck::new();372373queue.push(dropcheck_a);374queue.push(dropcheck_b);375376assert_eq!(drops_a.load(Ordering::Relaxed), 0);377assert_eq!(drops_b.load(Ordering::Relaxed), 0);378379let mut world = World::new();380queue.apply(&mut world);381382assert_eq!(drops_a.load(Ordering::Relaxed), 1);383assert_eq!(drops_b.load(Ordering::Relaxed), 1);384}385386/// Asserts that inner [commands](`Command`) are dropped on early drop of [`CommandQueue`].387/// Originally identified as an issue in [#10676](https://github.com/bevyengine/bevy/issues/10676)388#[test]389fn test_command_queue_inner_drop_early() {390let mut queue = CommandQueue::default();391392let (dropcheck_a, drops_a) = DropCheck::new();393let (dropcheck_b, drops_b) = DropCheck::new();394395queue.push(dropcheck_a);396queue.push(dropcheck_b);397398assert_eq!(drops_a.load(Ordering::Relaxed), 0);399assert_eq!(drops_b.load(Ordering::Relaxed), 0);400401drop(queue);402403assert_eq!(drops_a.load(Ordering::Relaxed), 1);404assert_eq!(drops_b.load(Ordering::Relaxed), 1);405}406407#[derive(Component)]408struct A;409410struct SpawnCommand;411412impl Command for SpawnCommand {413fn apply(self, world: &mut World) {414world.spawn(A);415}416}417418#[test]419fn test_command_queue_inner() {420let mut queue = CommandQueue::default();421422queue.push(SpawnCommand);423queue.push(SpawnCommand);424425let mut world = World::new();426queue.apply(&mut world);427428assert_eq!(world.query::<&A>().query(&world).count(), 2);429430// The previous call to `apply` cleared the queue.431// This call should do nothing.432queue.apply(&mut world);433assert_eq!(world.query::<&A>().query(&world).count(), 2);434}435436#[expect(437dead_code,438reason = "The inner string is used to ensure that, when the PanicCommand gets pushed to the queue, some data is written to the `bytes` vector."439)]440struct PanicCommand(String);441impl Command for PanicCommand {442fn apply(self, _: &mut World) {443panic!("command is panicking");444}445}446447#[test]448fn test_command_queue_inner_panic_safe() {449std::panic::set_hook(Box::new(|_| {}));450451let mut queue = CommandQueue::default();452453queue.push(PanicCommand("I panic!".to_owned()));454queue.push(SpawnCommand);455456let mut world = World::new();457458let _ = std::panic::catch_unwind(AssertUnwindSafe(|| {459queue.apply(&mut world);460}));461462// Even though the first command panicked, it's still ok to push463// more commands.464queue.push(SpawnCommand);465queue.push(SpawnCommand);466queue.apply(&mut world);467assert_eq!(world.query::<&A>().query(&world).count(), 3);468}469470#[test]471fn test_command_queue_inner_nested_panic_safe() {472std::panic::set_hook(Box::new(|_| {}));473474#[derive(Resource, Default)]475struct Order(Vec<usize>);476477let mut world = World::new();478world.init_resource::<Order>();479480fn add_index(index: usize) -> impl Command {481move |world: &mut World| world.resource_mut::<Order>().0.push(index)482}483world.commands().queue(add_index(1));484world.commands().queue(|world: &mut World| {485world.commands().queue(add_index(2));486world.commands().queue(PanicCommand("I panic!".to_owned()));487world.commands().queue(add_index(3));488world.flush_commands();489});490world.commands().queue(add_index(4));491492let _ = std::panic::catch_unwind(AssertUnwindSafe(|| {493world.flush_commands();494}));495496world.commands().queue(add_index(5));497world.flush_commands();498assert_eq!(&world.resource::<Order>().0, &[1, 2, 3, 4, 5]);499}500501// NOTE: `CommandQueue` is `Send` because `Command` is send.502// If the `Command` trait gets reworked to be non-send, `CommandQueue`503// should be reworked.504// This test asserts that Command types are send.505fn assert_is_send_impl(_: impl Send) {}506fn assert_is_send(command: impl Command) {507assert_is_send_impl(command);508}509510#[test]511fn test_command_is_send() {512assert_is_send(SpawnCommand);513}514515#[expect(516dead_code,517reason = "This struct is used to test how the CommandQueue reacts to padding added by rust's compiler."518)]519struct CommandWithPadding(u8, u16);520impl Command for CommandWithPadding {521fn apply(self, _: &mut World) {}522}523524#[cfg(miri)]525#[test]526fn test_uninit_bytes() {527let mut queue = CommandQueue::default();528queue.push(CommandWithPadding(0, 0));529let _ = format!("{:?}", queue.bytes);530}531}532533534