1- use std:: { mem:: MaybeUninit , ptr :: NonNull } ;
1+ use std:: mem:: MaybeUninit ;
22
33use bevy_ptr:: { OwningPtr , Unaligned } ;
44
55use super :: Command ;
66use crate :: world:: World ;
77
88struct CommandMeta {
9- /// Offset from the start of `CommandQueue.bytes` at which the corresponding command is stored.
10- offset : usize ,
119 /// SAFETY: The `value` must point to a value of type `T: Command`,
1210 /// where `T` is some specific type that was used to produce this metadata.
13- apply_command : unsafe fn ( value : OwningPtr < Unaligned > , world : & mut World ) ,
11+ ///
12+ /// Returns the size of `T` in bytes.
13+ apply_command_and_get_size : unsafe fn ( value : OwningPtr < Unaligned > , world : & mut World ) -> usize ,
1414}
1515
16- /// A queue of [`Command`]s
16+ /// Densely and efficiently stores a queue of heterogenous types implementing [`Command`].
1717//
1818// NOTE: [`CommandQueue`] is implemented via a `Vec<MaybeUninit<u8>>` instead of a `Vec<Box<dyn Command>>`
1919// as an optimization. Since commands are used frequently in systems as a way to spawn
@@ -22,12 +22,12 @@ struct CommandMeta {
2222// preferred to simplicity of implementation.
2323#[ derive( Default ) ]
2424pub struct CommandQueue {
25- /// Densely stores the data for all commands in the queue.
25+ // This buffer densely stores all queued commands.
26+ //
27+ // For each command, one `CommandMeta` is stored, followed by zero or more bytes
28+ // to store the command itself. To interpret these bytes, a pointer must
29+ // be passed to the corresponding `CommandMeta.apply_command_and_get_size` fn pointer.
2630 bytes : Vec < MaybeUninit < u8 > > ,
27- /// Metadata for each command stored in the queue.
28- /// SAFETY: Each entry must have a corresponding value stored in `bytes`,
29- /// stored at offset `CommandMeta.offset` and with an underlying type matching `CommandMeta.apply_command`.
30- metas : Vec < CommandMeta > ,
3131}
3232
3333// SAFETY: All commands [`Command`] implement [`Send`]
@@ -43,45 +43,47 @@ impl CommandQueue {
4343 where
4444 C : Command ,
4545 {
46- let old_len = self . bytes . len ( ) ;
46+ #[ repr( C , packed) ]
47+ struct Packed < T : Command > {
48+ meta : CommandMeta ,
49+ command : T ,
50+ }
4751
48- // SAFETY: After adding the metadata, we correctly write the corresponding `command`
49- // of type `C` into `self.bytes`. Zero-sized commands do not get written into the buffer,
50- // so we'll just use a dangling pointer, which is valid for zero-sized types.
51- self . metas . push ( CommandMeta {
52- offset : old_len,
53- apply_command : |command, world| {
54- // SAFETY: According to the invariants of `CommandMeta.apply_command`,
52+ let meta = CommandMeta {
53+ apply_command_and_get_size : |command, world| {
54+ // SAFETY: According to the invariants of `CommandMeta.apply_command_and_get_size`,
5555 // `command` must point to a value of type `C`.
5656 let command: C = unsafe { command. read_unaligned ( ) } ;
5757 command. write ( world) ;
58+ std:: mem:: size_of :: < C > ( )
5859 } ,
59- } ) ;
60-
61- let size = std:: mem:: size_of :: < C > ( ) ;
62- if size > 0 {
63- // Ensure that the buffer has enough space at the end to fit a value of type `C`.
64- // Since `C` is non-zero sized, this also guarantees that the buffer is non-null.
65- self . bytes . reserve ( size) ;
66-
67- // SAFETY: The buffer must be at least as long as `old_len`, so this operation
68- // will not overflow the pointer's original allocation.
69- let ptr: * mut C = unsafe { self . bytes . as_mut_ptr ( ) . add ( old_len) . cast ( ) } ;
70-
71- // Transfer ownership of the command into the buffer.
72- // SAFETY: `ptr` must be non-null, since it is within a non-null buffer.
73- // The call to `reserve()` ensures that the buffer has enough space to fit a value of type `C`,
74- // and it is valid to write any bit pattern since the underlying buffer is of type `MaybeUninit<u8>`.
75- unsafe { ptr. write_unaligned ( command) } ;
76-
77- // Grow the vector to include the command we just wrote.
78- // SAFETY: Due to the call to `.reserve(size)` above,
79- // this is guaranteed to fit in the vector's capacity.
80- unsafe { self . bytes . set_len ( old_len + size) } ;
81- } else {
82- // Instead of writing zero-sized types into the buffer, we'll just use a dangling pointer.
83- // We must forget the command so it doesn't get double-dropped when the queue gets applied.
84- std:: mem:: forget ( command) ;
60+ } ;
61+
62+ let old_len = self . bytes . len ( ) ;
63+
64+ // Reserve enough bytes for both the metadata and the command itself.
65+ self . bytes . reserve ( std:: mem:: size_of :: < Packed < C > > ( ) ) ;
66+
67+ // Pointer to the bytes at the end of the buffer.
68+ // SAFETY: We know it is within bounds of the allocation, due to the call to `.reserve()`.
69+ let ptr = unsafe { self . bytes . as_mut_ptr ( ) . add ( old_len) } ;
70+
71+ // Write the metadata into the buffer, followed by the command.
72+ // We are using a packed struct to write them both as one operation.
73+ // SAFETY: `ptr` must be non-null, since it is within a non-null buffer.
74+ // The call to `reserve()` ensures that the buffer has enough space to fit a value of type `C`,
75+ // and it is valid to write any bit pattern since the underlying buffer is of type `MaybeUninit<u8>`.
76+ unsafe {
77+ ptr. cast :: < Packed < C > > ( )
78+ . write_unaligned ( Packed { meta, command } ) ;
79+ }
80+
81+ // Extend the length of the buffer to include the data we just wrote.
82+ // SAFETY: The new length is guaranteed to fit in the vector's capacity,
83+ // due to the call to `.reserve()` above.
84+ unsafe {
85+ self . bytes
86+ . set_len ( old_len + std:: mem:: size_of :: < Packed < C > > ( ) ) ;
8587 }
8688 }
8789
@@ -92,23 +94,40 @@ impl CommandQueue {
9294 // flush the previously queued entities
9395 world. flush ( ) ;
9496
97+ // Pointer that will iterate over the entries of the buffer.
98+ let mut cursor = self . bytes . as_mut_ptr ( ) ;
99+
100+ // The address of the end of the buffer.
101+ let end_addr = cursor as usize + self . bytes . len ( ) ;
102+
95103 // Reset the buffer, so it can be reused after this function ends.
96104 // In the loop below, ownership of each command will be transferred into user code.
97105 // SAFETY: `set_len(0)` is always valid.
98106 unsafe { self . bytes . set_len ( 0 ) } ;
99107
100- for meta in self . metas . drain ( ..) {
101- // SAFETY: `CommandQueue` guarantees that each metadata must have a corresponding value stored in `self.bytes`,
102- // so this addition will not overflow its original allocation.
103- let cmd = unsafe { self . bytes . as_mut_ptr ( ) . add ( meta. offset ) } ;
108+ while ( cursor as usize ) < end_addr {
109+ // SAFETY: The cursor is either at the start of the buffer, or just after the previous command.
110+ // Since we know that the cursor is in bounds, it must point to the start of a new command.
111+ let meta = unsafe { cursor. cast :: < CommandMeta > ( ) . read_unaligned ( ) } ;
112+ // Advance to the bytes just after `meta`, which represent a type-erased command.
113+ // SAFETY: For most types of `Command`, the pointer immediately following the metadata
114+ // is guaranteed to be in bounds. If the command is a zero-sized type (ZST), then the cursor
115+ // might be 1 byte past the end of the buffer, which is safe.
116+ cursor = unsafe { cursor. add ( std:: mem:: size_of :: < CommandMeta > ( ) ) } ;
117+ // Construct an owned pointer to the command.
104118 // SAFETY: It is safe to transfer ownership out of `self.bytes`, since the call to `set_len(0)` above
105119 // guarantees that nothing stored in the buffer will get observed after this function ends.
106120 // `cmd` points to a valid address of a stored command, so it must be non-null.
107- let cmd = unsafe { OwningPtr :: new ( NonNull :: new_unchecked ( cmd. cast ( ) ) ) } ;
108- // SAFETY: The underlying type of `cmd` matches the type expected by `meta.apply_command`.
109- unsafe {
110- ( meta. apply_command ) ( cmd, world) ;
111- }
121+ let cmd = unsafe { OwningPtr :: new ( std:: ptr:: NonNull :: new_unchecked ( cursor. cast ( ) ) ) } ;
122+ // SAFETY: The data underneath the cursor must correspond to the type erased in metadata,
123+ // since they were stored next to each other by `.push()`.
124+ // For ZSTs, the type doesn't matter as long as the pointer is non-null.
125+ let size = unsafe { ( meta. apply_command_and_get_size ) ( cmd, world) } ;
126+ // Advance the cursor past the command.
127+ // For ZSTs, the cursor will not move.
128+ // SAFETY: At this point, it will either point to the next `CommandMeta`,
129+ // or the cursor will be out of bounds and the loop will end.
130+ cursor = unsafe { cursor. add ( size) } ;
112131 }
113132 }
114133}
@@ -217,7 +236,6 @@ mod test {
217236 // even though the first command panicking.
218237 // the `bytes`/`metas` vectors were cleared.
219238 assert_eq ! ( queue. bytes. len( ) , 0 ) ;
220- assert_eq ! ( queue. metas. len( ) , 0 ) ;
221239
222240 // Even though the first command panicked, it's still ok to push
223241 // more commands.
0 commit comments