Path: blob/main/crates/bevy_ecs/src/world/command_queue.rs
9353 views
use crate::{1change_detection::MaybeLocation,2system::{Command, SystemBuffer, SystemMeta},3world::{DeferredWorld, World},4};56use alloc::{boxed::Box, vec::Vec};7use bevy_ptr::{OwningPtr, Unaligned};8use core::{9fmt::Debug,10mem::{size_of, MaybeUninit},11panic::AssertUnwindSafe,12ptr::{addr_of_mut, NonNull},13};14use log::warn;1516struct CommandMeta {17/// SAFETY: The `value` must point to a value of type `T: Command`,18/// where `T` is some specific type that was used to produce this metadata.19///20/// `world` is optional to allow this one function pointer to perform double-duty as a drop.21///22/// Advances `cursor` by the size of `T` in bytes.23consume_command_and_get_size:24unsafe fn(value: OwningPtr<Unaligned>, world: Option<NonNull<World>>, cursor: &mut usize),25}2627/// Densely and efficiently stores a queue of heterogenous types implementing [`Command`].28// NOTE: [`CommandQueue`] is implemented via a `Vec<MaybeUninit<u8>>` instead of a `Vec<Box<dyn Command>>`29// as an optimization. Since commands are used frequently in systems as a way to spawn30// entities/components/resources, and it's not currently possible to parallelize these31// due to mutable [`World`] access, maximizing performance for [`CommandQueue`] is32// preferred to simplicity of implementation.33pub struct CommandQueue {34// This buffer densely stores all queued commands.35//36// For each command, one `CommandMeta` is stored, followed by zero or more bytes37// to store the command itself. To interpret these bytes, a pointer must38// be passed to the corresponding `CommandMeta.apply_command_and_get_size` fn pointer.39pub(crate) bytes: Vec<MaybeUninit<u8>>,40pub(crate) cursor: usize,41pub(crate) panic_recovery: Vec<MaybeUninit<u8>>,42pub(crate) caller: MaybeLocation,43}4445impl Default for CommandQueue {46#[track_caller]47fn default() -> Self {48Self {49bytes: Default::default(),50cursor: Default::default(),51panic_recovery: Default::default(),52caller: MaybeLocation::caller(),53}54}55}5657/// Wraps pointers to a [`CommandQueue`], used internally to avoid stacked borrow rules when58/// partially applying the world's command queue recursively59#[derive(Clone)]60pub(crate) struct RawCommandQueue {61pub(crate) bytes: NonNull<Vec<MaybeUninit<u8>>>,62pub(crate) cursor: NonNull<usize>,63pub(crate) panic_recovery: NonNull<Vec<MaybeUninit<u8>>>,64}6566// CommandQueue needs to implement Debug manually, rather than deriving it, because the derived impl just prints67// [core::mem::maybe_uninit::MaybeUninit<u8>, core::mem::maybe_uninit::MaybeUninit<u8>, ..] for every byte in the vec,68// which gets extremely verbose very quickly, while also providing no useful information.69// It is not possible to soundly print the values of the contained bytes, as some of them may be padding or uninitialized (#4863)70// So instead, the manual impl just prints the length of vec.71impl Debug for CommandQueue {72fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {73f.debug_struct("CommandQueue")74.field("len_bytes", &self.bytes.len())75.field("caller", &self.caller)76.finish_non_exhaustive()77}78}7980// SAFETY: All commands [`Command`] implement [`Send`]81unsafe impl Send for CommandQueue {}8283// SAFETY: `&CommandQueue` never gives access to the inner commands.84unsafe impl Sync for CommandQueue {}8586impl CommandQueue {87/// Push a [`Command`] onto the queue.88#[inline]89pub fn push(&mut self, command: impl Command) {90// SAFETY: self is guaranteed to live for the lifetime of this method91unsafe {92self.get_raw().push(command);93}94}9596/// Execute the queued [`Command`]s in the world after applying any commands in the world's internal queue.97/// This clears the queue.98#[inline]99pub fn apply(&mut self, world: &mut World) {100// flush the world's internal queue101world.flush_commands();102103// SAFETY: A reference is always a valid pointer104unsafe {105self.get_raw().apply_or_drop_queued(Some(world.into()));106}107}108109/// Take all commands from `other` and append them to `self`, leaving `other` empty110pub fn append(&mut self, other: &mut CommandQueue) {111self.bytes.append(&mut other.bytes);112}113114/// Returns false if there are any commands in the queue115#[inline]116pub fn is_empty(&self) -> bool {117self.cursor >= self.bytes.len()118}119120/// Returns a [`RawCommandQueue`] instance sharing the underlying command queue.121pub(crate) fn get_raw(&mut self) -> RawCommandQueue {122// SAFETY: self is always valid memory123unsafe {124RawCommandQueue {125bytes: NonNull::new_unchecked(addr_of_mut!(self.bytes)),126cursor: NonNull::new_unchecked(addr_of_mut!(self.cursor)),127panic_recovery: NonNull::new_unchecked(addr_of_mut!(self.panic_recovery)),128}129}130}131}132133impl RawCommandQueue {134/// Returns a new `RawCommandQueue` instance, this must be manually dropped.135pub(crate) fn new() -> Self {136// SAFETY: Pointers returned by `Box::into_raw` are guaranteed to be non null137unsafe {138Self {139bytes: NonNull::new_unchecked(Box::into_raw(Box::default())),140cursor: NonNull::new_unchecked(Box::into_raw(Box::new(0usize))),141panic_recovery: NonNull::new_unchecked(Box::into_raw(Box::default())),142}143}144}145146/// Returns true if the queue is empty.147///148/// # Safety149///150/// * Caller ensures that `bytes` and `cursor` point to valid memory151pub unsafe fn is_empty(&self) -> bool {152// SAFETY: Pointers are guaranteed to be valid by requirements on `.clone_unsafe`153(unsafe { *self.cursor.as_ref() }) >= (unsafe { self.bytes.as_ref() }).len()154}155156/// Push a [`Command`] onto the queue.157///158/// # Safety159///160/// * Caller ensures that `self` has not outlived the underlying queue161#[inline]162pub unsafe fn push<C: Command>(&mut self, command: C) {163// Stores a command alongside its metadata.164// `repr(C)` prevents the compiler from reordering the fields,165// while `repr(packed)` prevents the compiler from inserting padding bytes.166#[repr(C, packed)]167struct Packed<C: Command> {168meta: CommandMeta,169command: C,170}171172let meta = CommandMeta {173consume_command_and_get_size: |command, world, cursor| {174*cursor += size_of::<C>();175176// SAFETY: According to the invariants of `CommandMeta.consume_command_and_get_size`,177// `command` must point to a value of type `C`.178let command: C = unsafe { command.read_unaligned() };179match world {180// Apply command to the provided world...181Some(mut world) => {182// SAFETY: Caller ensures pointer is not null183let world = unsafe { world.as_mut() };184command.apply(world);185// The command may have queued up world commands, which we flush here to ensure they are also picked up.186// If the current command queue already the World Command queue, this will still behave appropriately because the global cursor187// is still at the current `stop`, ensuring only the newly queued Commands will be applied.188world.flush();189}190// ...or discard it.191None => drop(command),192}193},194};195196// SAFETY: There are no outstanding references to self.bytes197let bytes = unsafe { self.bytes.as_mut() };198199let old_len = bytes.len();200201// Reserve enough bytes for both the metadata and the command itself.202bytes.reserve(size_of::<Packed<C>>());203204// Pointer to the bytes at the end of the buffer.205// SAFETY: We know it is within bounds of the allocation, due to the call to `.reserve()`.206let ptr = unsafe { bytes.as_mut_ptr().add(old_len) };207208// Write the metadata into the buffer, followed by the command.209// We are using a packed struct to write them both as one operation.210// SAFETY: `ptr` must be non-null, since it is within a non-null buffer.211// The call to `reserve()` ensures that the buffer has enough space to fit a value of type `C`,212// and it is valid to write any bit pattern since the underlying buffer is of type `MaybeUninit<u8>`.213unsafe {214ptr.cast::<Packed<C>>()215.write_unaligned(Packed { meta, command });216}217218// Extend the length of the buffer to include the data we just wrote.219// SAFETY: The new length is guaranteed to fit in the vector's capacity,220// due to the call to `.reserve()` above.221unsafe {222bytes.set_len(old_len + size_of::<Packed<C>>());223}224}225226/// If `world` is [`Some`], this will apply the queued [commands](`Command`).227/// If `world` is [`None`], this will drop the queued [commands](`Command`) (without applying them).228/// This clears the queue.229///230/// # Safety231///232/// * Caller ensures that `self` has not outlived the underlying queue233#[inline]234pub(crate) unsafe fn apply_or_drop_queued(&mut self, world: Option<NonNull<World>>) {235// SAFETY: If this is the command queue on world, world will not be dropped as we have a mutable reference236// If this is not the command queue on world we have exclusive ownership and self will not be mutated237let start = *self.cursor.as_ref();238let stop = self.bytes.as_ref().len();239let mut local_cursor = start;240// SAFETY: we are setting the global cursor to the current length to prevent the executing commands from applying241// the remaining commands currently in this list. This is safe.242*self.cursor.as_mut() = stop;243244while local_cursor < stop {245// SAFETY: The cursor is either at the start of the buffer, or just after the previous command.246// Since we know that the cursor is in bounds, it must point to the start of a new command.247let meta = unsafe {248self.bytes249.as_mut()250.as_mut_ptr()251.add(local_cursor)252.cast::<CommandMeta>()253.read_unaligned()254};255256// Advance to the bytes just after `meta`, which represent a type-erased command.257local_cursor += size_of::<CommandMeta>();258// Construct an owned pointer to the command.259// SAFETY: It is safe to transfer ownership out of `self.bytes`, since the increment of `cursor` above260// guarantees that nothing stored in the buffer will get observed after this function ends.261// `cmd` points to a valid address of a stored command, so it must be non-null.262let cmd = unsafe {263OwningPtr::<Unaligned>::new(NonNull::new_unchecked(264self.bytes.as_mut().as_mut_ptr().add(local_cursor).cast(),265))266};267let f = AssertUnwindSafe(|| {268// SAFETY: The data underneath the cursor must correspond to the type erased in metadata,269// since they were stored next to each other by `.push()`.270// For ZSTs, the type doesn't matter as long as the pointer is non-null.271// This also advances the cursor past the command. For ZSTs, the cursor will not move.272// At this point, it will either point to the next `CommandMeta`,273// or the cursor will be out of bounds and the loop will end.274unsafe { (meta.consume_command_and_get_size)(cmd, world, &mut local_cursor) };275});276277#[cfg(feature = "std")]278{279let result = std::panic::catch_unwind(f);280281if let Err(payload) = result {282// local_cursor now points to the location _after_ the panicked command.283// Add the remaining commands that _would have_ been applied to the284// panic_recovery queue.285//286// This uses `current_stop` instead of `stop` to account for any commands287// that were queued _during_ this panic.288//289// This is implemented in such a way that if apply_or_drop_queued() are nested recursively in,290// an applied Command, the correct command order will be retained.291let panic_recovery = self.panic_recovery.as_mut();292let bytes = self.bytes.as_mut();293let current_stop = bytes.len();294panic_recovery.extend_from_slice(&bytes[local_cursor..current_stop]);295bytes.set_len(start);296*self.cursor.as_mut() = start;297298// This was the "top of the apply stack". If we are _not_ at the top of the apply stack,299// when we call`resume_unwind" the caller "closer to the top" will catch the unwind and do this check,300// until we reach the top.301if start == 0 {302bytes.append(panic_recovery);303}304std::panic::resume_unwind(payload);305}306}307308#[cfg(not(feature = "std"))]309(f)();310}311312// Reset the buffer: all commands past the original `start` cursor have been applied.313// SAFETY: we are setting the length of bytes to the original length, minus the length of the original314// list of commands being considered. All bytes remaining in the Vec are still valid, unapplied commands.315unsafe {316self.bytes.as_mut().set_len(start);317*self.cursor.as_mut() = start;318};319}320}321322impl Drop for CommandQueue {323fn drop(&mut self) {324if !self.bytes.is_empty() {325if let Some(caller) = self.caller.into_option() {326warn!("CommandQueue has un-applied commands being dropped. Did you forget to call SystemState::apply? caller:{caller:?}");327} else {328warn!("CommandQueue has un-applied commands being dropped. Did you forget to call SystemState::apply?");329}330}331// SAFETY: A reference is always a valid pointer332unsafe { self.get_raw().apply_or_drop_queued(None) };333}334}335336impl SystemBuffer for CommandQueue {337#[inline]338fn apply(&mut self, _system_meta: &SystemMeta, world: &mut World) {339#[cfg(feature = "trace")]340let _span_guard = _system_meta.commands_span.enter();341self.apply(world);342}343344#[inline]345fn queue(&mut self, _system_meta: &SystemMeta, mut world: DeferredWorld) {346world.commands().append(self);347}348}349350#[cfg(test)]351mod test {352use super::*;353use crate::{component::Component, resource::Resource};354use alloc::{borrow::ToOwned, string::String, sync::Arc};355use core::{356panic::AssertUnwindSafe,357sync::atomic::{AtomicU32, Ordering},358};359360#[cfg(miri)]361use alloc::format;362363struct DropCheck(Arc<AtomicU32>);364365impl DropCheck {366fn new() -> (Self, Arc<AtomicU32>) {367let drops = Arc::new(AtomicU32::new(0));368(Self(drops.clone()), drops)369}370}371372impl Drop for DropCheck {373fn drop(&mut self) {374self.0.fetch_add(1, Ordering::Relaxed);375}376}377378impl Command for DropCheck {379fn apply(self, _: &mut World) {}380}381382#[test]383fn test_command_queue_inner_drop() {384let mut queue = CommandQueue::default();385386let (dropcheck_a, drops_a) = DropCheck::new();387let (dropcheck_b, drops_b) = DropCheck::new();388389queue.push(dropcheck_a);390queue.push(dropcheck_b);391392assert_eq!(drops_a.load(Ordering::Relaxed), 0);393assert_eq!(drops_b.load(Ordering::Relaxed), 0);394395let mut world = World::new();396queue.apply(&mut world);397398assert_eq!(drops_a.load(Ordering::Relaxed), 1);399assert_eq!(drops_b.load(Ordering::Relaxed), 1);400}401402/// Asserts that inner [commands](`Command`) are dropped on early drop of [`CommandQueue`].403/// Originally identified as an issue in [#10676](https://github.com/bevyengine/bevy/issues/10676)404#[test]405fn test_command_queue_inner_drop_early() {406let mut queue = CommandQueue::default();407408let (dropcheck_a, drops_a) = DropCheck::new();409let (dropcheck_b, drops_b) = DropCheck::new();410411queue.push(dropcheck_a);412queue.push(dropcheck_b);413414assert_eq!(drops_a.load(Ordering::Relaxed), 0);415assert_eq!(drops_b.load(Ordering::Relaxed), 0);416417drop(queue);418419assert_eq!(drops_a.load(Ordering::Relaxed), 1);420assert_eq!(drops_b.load(Ordering::Relaxed), 1);421}422423#[derive(Component)]424struct A;425426struct SpawnCommand;427428impl Command for SpawnCommand {429fn apply(self, world: &mut World) {430world.spawn(A);431}432}433434#[test]435fn test_command_queue_inner() {436let mut queue = CommandQueue::default();437438queue.push(SpawnCommand);439queue.push(SpawnCommand);440441let mut world = World::new();442queue.apply(&mut world);443444assert_eq!(world.query::<&A>().query(&world).count(), 2);445446// The previous call to `apply` cleared the queue.447// This call should do nothing.448queue.apply(&mut world);449assert_eq!(world.query::<&A>().query(&world).count(), 2);450}451452#[expect(453dead_code,454reason = "The inner string is used to ensure that, when the PanicCommand gets pushed to the queue, some data is written to the `bytes` vector."455)]456struct PanicCommand(String);457impl Command for PanicCommand {458fn apply(self, _: &mut World) {459panic!("command is panicking");460}461}462463#[test]464fn test_command_queue_inner_panic_safe() {465std::panic::set_hook(Box::new(|_| {}));466467let mut queue = CommandQueue::default();468469queue.push(PanicCommand("I panic!".to_owned()));470queue.push(SpawnCommand);471472let mut world = World::new();473474let _ = std::panic::catch_unwind(AssertUnwindSafe(|| {475queue.apply(&mut world);476}));477478// Even though the first command panicked, it's still ok to push479// more commands.480queue.push(SpawnCommand);481queue.push(SpawnCommand);482queue.apply(&mut world);483assert_eq!(world.query::<&A>().query(&world).count(), 3);484}485486#[test]487fn test_command_queue_inner_nested_panic_safe() {488std::panic::set_hook(Box::new(|_| {}));489490#[derive(Resource, Default)]491struct Order(Vec<usize>);492493let mut world = World::new();494world.init_resource::<Order>();495496fn add_index(index: usize) -> impl Command {497move |world: &mut World| world.resource_mut::<Order>().0.push(index)498}499world.commands().queue(add_index(1));500world.commands().queue(|world: &mut World| {501world.commands().queue(add_index(2));502world.commands().queue(PanicCommand("I panic!".to_owned()));503world.commands().queue(add_index(3));504world.flush_commands();505});506world.commands().queue(add_index(4));507508let _ = std::panic::catch_unwind(AssertUnwindSafe(|| {509world.flush_commands();510}));511512world.commands().queue(add_index(5));513world.flush_commands();514assert_eq!(&world.resource::<Order>().0, &[1, 2, 3, 4, 5]);515}516517// NOTE: `CommandQueue` is `Send` because `Command` is send.518// If the `Command` trait gets reworked to be non-send, `CommandQueue`519// should be reworked.520// This test asserts that Command types are send.521fn assert_is_send_impl(_: impl Send) {}522fn assert_is_send(command: impl Command) {523assert_is_send_impl(command);524}525526#[test]527fn test_command_is_send() {528assert_is_send(SpawnCommand);529}530531#[expect(532dead_code,533reason = "This struct is used to test how the CommandQueue reacts to padding added by rust's compiler."534)]535struct CommandWithPadding(u8, u16);536impl Command for CommandWithPadding {537fn apply(self, _: &mut World) {}538}539540#[cfg(miri)]541#[test]542fn test_uninit_bytes() {543let mut queue = CommandQueue::default();544queue.push(CommandWithPadding(0, 0));545let _ = format!("{:?}", queue.bytes);546}547}548549550