Path: blob/main/crates/bevy_ecs/src/world/command_queue.rs
6849 views
#[cfg(feature = "track_location")]1use crate::change_detection::MaybeLocation;2use crate::{3system::{Command, SystemBuffer, SystemMeta},4world::{DeferredWorld, World},5};67use alloc::{boxed::Box, vec::Vec};8use bevy_ptr::{OwningPtr, Unaligned};9use core::{10fmt::Debug,11mem::{size_of, MaybeUninit},12panic::AssertUnwindSafe,13ptr::{addr_of_mut, NonNull},14};15use log::warn;1617struct CommandMeta {18/// SAFETY: The `value` must point to a value of type `T: Command`,19/// where `T` is some specific type that was used to produce this metadata.20///21/// `world` is optional to allow this one function pointer to perform double-duty as a drop.22///23/// Advances `cursor` by the size of `T` in bytes.24consume_command_and_get_size:25unsafe fn(value: OwningPtr<Unaligned>, world: Option<NonNull<World>>, cursor: &mut usize),26}2728/// Densely and efficiently stores a queue of heterogenous types implementing [`Command`].29// NOTE: [`CommandQueue`] is implemented via a `Vec<MaybeUninit<u8>>` instead of a `Vec<Box<dyn Command>>`30// as an optimization. Since commands are used frequently in systems as a way to spawn31// entities/components/resources, and it's not currently possible to parallelize these32// due to mutable [`World`] access, maximizing performance for [`CommandQueue`] is33// preferred to simplicity of implementation.34pub struct CommandQueue {35// This buffer densely stores all queued commands.36//37// For each command, one `CommandMeta` is stored, followed by zero or more bytes38// to store the command itself. To interpret these bytes, a pointer must39// be passed to the corresponding `CommandMeta.apply_command_and_get_size` fn pointer.40pub(crate) bytes: Vec<MaybeUninit<u8>>,41pub(crate) cursor: usize,42pub(crate) panic_recovery: Vec<MaybeUninit<u8>>,43#[cfg(feature = "track_location")]44pub(crate) caller: MaybeLocation,45}4647impl Default for CommandQueue {48#[track_caller]49fn default() -> Self {50Self {51bytes: Default::default(),52cursor: Default::default(),53panic_recovery: Default::default(),54#[cfg(feature = "track_location")]55caller: MaybeLocation::caller(),56}57}58}5960/// Wraps pointers to a [`CommandQueue`], used internally to avoid stacked borrow rules when61/// partially applying the world's command queue recursively62#[derive(Clone)]63pub(crate) struct RawCommandQueue {64pub(crate) bytes: NonNull<Vec<MaybeUninit<u8>>>,65pub(crate) cursor: NonNull<usize>,66pub(crate) panic_recovery: NonNull<Vec<MaybeUninit<u8>>>,67}6869// CommandQueue needs to implement Debug manually, rather than deriving it, because the derived impl just prints70// [core::mem::maybe_uninit::MaybeUninit<u8>, core::mem::maybe_uninit::MaybeUninit<u8>, ..] for every byte in the vec,71// which gets extremely verbose very quickly, while also providing no useful information.72// It is not possible to soundly print the values of the contained bytes, as some of them may be padding or uninitialized (#4863)73// So instead, the manual impl just prints the length of vec.74impl Debug for CommandQueue {75fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {76let mut binding = f.debug_struct("CommandQueue");77binding.field("len_bytes", &self.bytes.len());7879#[cfg(feature = "track_location")]80binding.field("caller", &self.caller.into_option());8182binding.finish_non_exhaustive()83}84}8586// SAFETY: All commands [`Command`] implement [`Send`]87unsafe impl Send for CommandQueue {}8889// SAFETY: `&CommandQueue` never gives access to the inner commands.90unsafe impl Sync for CommandQueue {}9192impl CommandQueue {93/// Push a [`Command`] onto the queue.94#[inline]95pub fn push(&mut self, command: impl Command) {96// SAFETY: self is guaranteed to live for the lifetime of this method97unsafe {98self.get_raw().push(command);99}100}101102/// Execute the queued [`Command`]s in the world after applying any commands in the world's internal queue.103/// This clears the queue.104#[inline]105pub fn apply(&mut self, world: &mut World) {106// flush the previously queued entities107world.flush_entities();108109// flush the world's internal queue110world.flush_commands();111112// SAFETY: A reference is always a valid pointer113unsafe {114self.get_raw().apply_or_drop_queued(Some(world.into()));115}116}117118/// Take all commands from `other` and append them to `self`, leaving `other` empty119pub fn append(&mut self, other: &mut CommandQueue) {120self.bytes.append(&mut other.bytes);121}122123/// Returns false if there are any commands in the queue124#[inline]125pub fn is_empty(&self) -> bool {126self.cursor >= self.bytes.len()127}128129/// Returns a [`RawCommandQueue`] instance sharing the underlying command queue.130pub(crate) fn get_raw(&mut self) -> RawCommandQueue {131// SAFETY: self is always valid memory132unsafe {133RawCommandQueue {134bytes: NonNull::new_unchecked(addr_of_mut!(self.bytes)),135cursor: NonNull::new_unchecked(addr_of_mut!(self.cursor)),136panic_recovery: NonNull::new_unchecked(addr_of_mut!(self.panic_recovery)),137}138}139}140}141142impl RawCommandQueue {143/// Returns a new `RawCommandQueue` instance, this must be manually dropped.144pub(crate) fn new() -> Self {145// SAFETY: Pointers returned by `Box::into_raw` are guaranteed to be non null146unsafe {147Self {148bytes: NonNull::new_unchecked(Box::into_raw(Box::default())),149cursor: NonNull::new_unchecked(Box::into_raw(Box::new(0usize))),150panic_recovery: NonNull::new_unchecked(Box::into_raw(Box::default())),151}152}153}154155/// Returns true if the queue is empty.156///157/// # Safety158///159/// * Caller ensures that `bytes` and `cursor` point to valid memory160pub unsafe fn is_empty(&self) -> bool {161// SAFETY: Pointers are guaranteed to be valid by requirements on `.clone_unsafe`162(unsafe { *self.cursor.as_ref() }) >= (unsafe { self.bytes.as_ref() }).len()163}164165/// Push a [`Command`] onto the queue.166///167/// # Safety168///169/// * Caller ensures that `self` has not outlived the underlying queue170#[inline]171pub unsafe fn push<C: Command>(&mut self, command: C) {172// Stores a command alongside its metadata.173// `repr(C)` prevents the compiler from reordering the fields,174// while `repr(packed)` prevents the compiler from inserting padding bytes.175#[repr(C, packed)]176struct Packed<C: Command> {177meta: CommandMeta,178command: C,179}180181let meta = CommandMeta {182consume_command_and_get_size: |command, world, cursor| {183*cursor += size_of::<C>();184185// SAFETY: According to the invariants of `CommandMeta.consume_command_and_get_size`,186// `command` must point to a value of type `C`.187let command: C = unsafe { command.read_unaligned() };188match world {189// Apply command to the provided world...190Some(mut world) => {191// SAFETY: Caller ensures pointer is not null192let world = unsafe { world.as_mut() };193command.apply(world);194// The command may have queued up world commands, which we flush here to ensure they are also picked up.195// If the current command queue already the World Command queue, this will still behave appropriately because the global cursor196// is still at the current `stop`, ensuring only the newly queued Commands will be applied.197world.flush();198}199// ...or discard it.200None => drop(command),201}202},203};204205// SAFETY: There are no outstanding references to self.bytes206let bytes = unsafe { self.bytes.as_mut() };207208let old_len = bytes.len();209210// Reserve enough bytes for both the metadata and the command itself.211bytes.reserve(size_of::<Packed<C>>());212213// Pointer to the bytes at the end of the buffer.214// SAFETY: We know it is within bounds of the allocation, due to the call to `.reserve()`.215let ptr = unsafe { bytes.as_mut_ptr().add(old_len) };216217// Write the metadata into the buffer, followed by the command.218// We are using a packed struct to write them both as one operation.219// SAFETY: `ptr` must be non-null, since it is within a non-null buffer.220// The call to `reserve()` ensures that the buffer has enough space to fit a value of type `C`,221// and it is valid to write any bit pattern since the underlying buffer is of type `MaybeUninit<u8>`.222unsafe {223ptr.cast::<Packed<C>>()224.write_unaligned(Packed { meta, command });225}226227// Extend the length of the buffer to include the data we just wrote.228// SAFETY: The new length is guaranteed to fit in the vector's capacity,229// due to the call to `.reserve()` above.230unsafe {231bytes.set_len(old_len + size_of::<Packed<C>>());232}233}234235/// If `world` is [`Some`], this will apply the queued [commands](`Command`).236/// If `world` is [`None`], this will drop the queued [commands](`Command`) (without applying them).237/// This clears the queue.238///239/// # Safety240///241/// * Caller ensures that `self` has not outlived the underlying queue242#[inline]243pub(crate) unsafe fn apply_or_drop_queued(&mut self, world: Option<NonNull<World>>) {244// SAFETY: If this is the command queue on world, world will not be dropped as we have a mutable reference245// If this is not the command queue on world we have exclusive ownership and self will not be mutated246let start = *self.cursor.as_ref();247let stop = self.bytes.as_ref().len();248let mut local_cursor = start;249// SAFETY: we are setting the global cursor to the current length to prevent the executing commands from applying250// the remaining commands currently in this list. This is safe.251*self.cursor.as_mut() = stop;252253while local_cursor < stop {254// SAFETY: The cursor is either at the start of the buffer, or just after the previous command.255// Since we know that the cursor is in bounds, it must point to the start of a new command.256let meta = unsafe {257self.bytes258.as_mut()259.as_mut_ptr()260.add(local_cursor)261.cast::<CommandMeta>()262.read_unaligned()263};264265// Advance to the bytes just after `meta`, which represent a type-erased command.266local_cursor += size_of::<CommandMeta>();267// Construct an owned pointer to the command.268// SAFETY: It is safe to transfer ownership out of `self.bytes`, since the increment of `cursor` above269// guarantees that nothing stored in the buffer will get observed after this function ends.270// `cmd` points to a valid address of a stored command, so it must be non-null.271let cmd = unsafe {272OwningPtr::<Unaligned>::new(NonNull::new_unchecked(273self.bytes.as_mut().as_mut_ptr().add(local_cursor).cast(),274))275};276let f = AssertUnwindSafe(|| {277// SAFETY: The data underneath the cursor must correspond to the type erased in metadata,278// since they were stored next to each other by `.push()`.279// For ZSTs, the type doesn't matter as long as the pointer is non-null.280// This also advances the cursor past the command. For ZSTs, the cursor will not move.281// At this point, it will either point to the next `CommandMeta`,282// or the cursor will be out of bounds and the loop will end.283unsafe { (meta.consume_command_and_get_size)(cmd, world, &mut local_cursor) };284});285286#[cfg(feature = "std")]287{288let result = std::panic::catch_unwind(f);289290if let Err(payload) = result {291// local_cursor now points to the location _after_ the panicked command.292// Add the remaining commands that _would have_ been applied to the293// panic_recovery queue.294//295// This uses `current_stop` instead of `stop` to account for any commands296// that were queued _during_ this panic.297//298// This is implemented in such a way that if apply_or_drop_queued() are nested recursively in,299// an applied Command, the correct command order will be retained.300let panic_recovery = self.panic_recovery.as_mut();301let bytes = self.bytes.as_mut();302let current_stop = bytes.len();303panic_recovery.extend_from_slice(&bytes[local_cursor..current_stop]);304bytes.set_len(start);305*self.cursor.as_mut() = start;306307// This was the "top of the apply stack". If we are _not_ at the top of the apply stack,308// when we call`resume_unwind" the caller "closer to the top" will catch the unwind and do this check,309// until we reach the top.310if start == 0 {311bytes.append(panic_recovery);312}313std::panic::resume_unwind(payload);314}315}316317#[cfg(not(feature = "std"))]318(f)();319}320321// Reset the buffer: all commands past the original `start` cursor have been applied.322// SAFETY: we are setting the length of bytes to the original length, minus the length of the original323// list of commands being considered. All bytes remaining in the Vec are still valid, unapplied commands.324unsafe {325self.bytes.as_mut().set_len(start);326*self.cursor.as_mut() = start;327};328}329}330331impl Drop for CommandQueue {332fn drop(&mut self) {333if !self.bytes.is_empty() {334#[cfg(feature = "track_location")]335warn!("CommandQueue has un-applied commands being dropped. Did you forget to call SystemState::apply? caller:{:?}",self.caller.into_option());336#[cfg(not(feature = "track_location"))]337warn!("CommandQueue has un-applied commands being dropped. Did you forget to call SystemState::apply?");338}339// SAFETY: A reference is always a valid pointer340unsafe { self.get_raw().apply_or_drop_queued(None) };341}342}343344impl SystemBuffer for CommandQueue {345#[inline]346fn apply(&mut self, _system_meta: &SystemMeta, world: &mut World) {347#[cfg(feature = "trace")]348let _span_guard = _system_meta.commands_span.enter();349self.apply(world);350}351352#[inline]353fn queue(&mut self, _system_meta: &SystemMeta, mut world: DeferredWorld) {354world.commands().append(self);355}356}357358#[cfg(test)]359mod test {360use super::*;361use crate::{component::Component, resource::Resource};362use alloc::{borrow::ToOwned, string::String, sync::Arc};363use core::{364panic::AssertUnwindSafe,365sync::atomic::{AtomicU32, Ordering},366};367368#[cfg(miri)]369use alloc::format;370371struct DropCheck(Arc<AtomicU32>);372373impl DropCheck {374fn new() -> (Self, Arc<AtomicU32>) {375let drops = Arc::new(AtomicU32::new(0));376(Self(drops.clone()), drops)377}378}379380impl Drop for DropCheck {381fn drop(&mut self) {382self.0.fetch_add(1, Ordering::Relaxed);383}384}385386impl Command for DropCheck {387fn apply(self, _: &mut World) {}388}389390#[test]391fn test_command_queue_inner_drop() {392let mut queue = CommandQueue::default();393394let (dropcheck_a, drops_a) = DropCheck::new();395let (dropcheck_b, drops_b) = DropCheck::new();396397queue.push(dropcheck_a);398queue.push(dropcheck_b);399400assert_eq!(drops_a.load(Ordering::Relaxed), 0);401assert_eq!(drops_b.load(Ordering::Relaxed), 0);402403let mut world = World::new();404queue.apply(&mut world);405406assert_eq!(drops_a.load(Ordering::Relaxed), 1);407assert_eq!(drops_b.load(Ordering::Relaxed), 1);408}409410/// Asserts that inner [commands](`Command`) are dropped on early drop of [`CommandQueue`].411/// Originally identified as an issue in [#10676](https://github.com/bevyengine/bevy/issues/10676)412#[test]413fn test_command_queue_inner_drop_early() {414let mut queue = CommandQueue::default();415416let (dropcheck_a, drops_a) = DropCheck::new();417let (dropcheck_b, drops_b) = DropCheck::new();418419queue.push(dropcheck_a);420queue.push(dropcheck_b);421422assert_eq!(drops_a.load(Ordering::Relaxed), 0);423assert_eq!(drops_b.load(Ordering::Relaxed), 0);424425drop(queue);426427assert_eq!(drops_a.load(Ordering::Relaxed), 1);428assert_eq!(drops_b.load(Ordering::Relaxed), 1);429}430431#[derive(Component)]432struct A;433434struct SpawnCommand;435436impl Command for SpawnCommand {437fn apply(self, world: &mut World) {438world.spawn(A);439}440}441442#[test]443fn test_command_queue_inner() {444let mut queue = CommandQueue::default();445446queue.push(SpawnCommand);447queue.push(SpawnCommand);448449let mut world = World::new();450queue.apply(&mut world);451452assert_eq!(world.query::<&A>().query(&world).count(), 2);453454// The previous call to `apply` cleared the queue.455// This call should do nothing.456queue.apply(&mut world);457assert_eq!(world.query::<&A>().query(&world).count(), 2);458}459460#[expect(461dead_code,462reason = "The inner string is used to ensure that, when the PanicCommand gets pushed to the queue, some data is written to the `bytes` vector."463)]464struct PanicCommand(String);465impl Command for PanicCommand {466fn apply(self, _: &mut World) {467panic!("command is panicking");468}469}470471#[test]472fn test_command_queue_inner_panic_safe() {473std::panic::set_hook(Box::new(|_| {}));474475let mut queue = CommandQueue::default();476477queue.push(PanicCommand("I panic!".to_owned()));478queue.push(SpawnCommand);479480let mut world = World::new();481482let _ = std::panic::catch_unwind(AssertUnwindSafe(|| {483queue.apply(&mut world);484}));485486// Even though the first command panicked, it's still ok to push487// more commands.488queue.push(SpawnCommand);489queue.push(SpawnCommand);490queue.apply(&mut world);491assert_eq!(world.query::<&A>().query(&world).count(), 3);492}493494#[test]495fn test_command_queue_inner_nested_panic_safe() {496std::panic::set_hook(Box::new(|_| {}));497498#[derive(Resource, Default)]499struct Order(Vec<usize>);500501let mut world = World::new();502world.init_resource::<Order>();503504fn add_index(index: usize) -> impl Command {505move |world: &mut World| world.resource_mut::<Order>().0.push(index)506}507world.commands().queue(add_index(1));508world.commands().queue(|world: &mut World| {509world.commands().queue(add_index(2));510world.commands().queue(PanicCommand("I panic!".to_owned()));511world.commands().queue(add_index(3));512world.flush_commands();513});514world.commands().queue(add_index(4));515516let _ = std::panic::catch_unwind(AssertUnwindSafe(|| {517world.flush_commands();518}));519520world.commands().queue(add_index(5));521world.flush_commands();522assert_eq!(&world.resource::<Order>().0, &[1, 2, 3, 4, 5]);523}524525// NOTE: `CommandQueue` is `Send` because `Command` is send.526// If the `Command` trait gets reworked to be non-send, `CommandQueue`527// should be reworked.528// This test asserts that Command types are send.529fn assert_is_send_impl(_: impl Send) {}530fn assert_is_send(command: impl Command) {531assert_is_send_impl(command);532}533534#[test]535fn test_command_is_send() {536assert_is_send(SpawnCommand);537}538539#[expect(540dead_code,541reason = "This struct is used to test how the CommandQueue reacts to padding added by rust's compiler."542)]543struct CommandWithPadding(u8, u16);544impl Command for CommandWithPadding {545fn apply(self, _: &mut World) {}546}547548#[cfg(miri)]549#[test]550fn test_uninit_bytes() {551let mut queue = CommandQueue::default();552queue.push(CommandWithPadding(0, 0));553let _ = format!("{:?}", queue.bytes);554}555}556557558