1
- use std:: { mem:: MaybeUninit , ptr :: NonNull } ;
1
+ use std:: mem:: MaybeUninit ;
2
2
3
3
use bevy_ptr:: { OwningPtr , Unaligned } ;
4
4
5
5
use super :: Command ;
6
6
use crate :: world:: World ;
7
7
8
8
struct CommandMeta {
9
- /// Offset from the start of `CommandQueue.bytes` at which the corresponding command is stored.
10
- offset : usize ,
11
9
/// SAFETY: The `value` must point to a value of type `T: Command`,
12
10
/// 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 ,
14
14
}
15
15
16
- /// A queue of [`Command`]s
16
+ /// Densely and efficiently stores a queue of heterogenous types implementing [`Command`].
17
17
//
18
18
// NOTE: [`CommandQueue`] is implemented via a `Vec<MaybeUninit<u8>>` instead of a `Vec<Box<dyn Command>>`
19
19
// as an optimization. Since commands are used frequently in systems as a way to spawn
@@ -22,12 +22,12 @@ struct CommandMeta {
22
22
// preferred to simplicity of implementation.
23
23
#[ derive( Default ) ]
24
24
pub 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.
26
30
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 > ,
31
31
}
32
32
33
33
// SAFETY: All commands [`Command`] implement [`Send`]
@@ -43,45 +43,50 @@ impl CommandQueue {
43
43
where
44
44
C : Command ,
45
45
{
46
- let old_len = self . bytes . len ( ) ;
46
+ // Stores a command alongside its metadata.
47
+ // `repr(C)` prevents the compiler from reordering the fields,
48
+ // while `repr(packed)` prevents the compiler from inserting padding bytes.
49
+ #[ repr( C , packed) ]
50
+ struct Packed < T : Command > {
51
+ meta : CommandMeta ,
52
+ command : T ,
53
+ }
47
54
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`,
55
+ let meta = CommandMeta {
56
+ apply_command_and_get_size : |command, world| {
57
+ // SAFETY: According to the invariants of `CommandMeta.apply_command_and_get_size`,
55
58
// `command` must point to a value of type `C`.
56
59
let command: C = unsafe { command. read_unaligned ( ) } ;
57
60
command. write ( world) ;
61
+ std:: mem:: size_of :: < C > ( )
58
62
} ,
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) ;
63
+ } ;
64
+
65
+ let old_len = self . bytes . len ( ) ;
66
+
67
+ // Reserve enough bytes for both the metadata and the command itself.
68
+ self . bytes . reserve ( std:: mem:: size_of :: < Packed < C > > ( ) ) ;
69
+
70
+ // Pointer to the bytes at the end of the buffer.
71
+ // SAFETY: We know it is within bounds of the allocation, due to the call to `.reserve()`.
72
+ let ptr = unsafe { self . bytes . as_mut_ptr ( ) . add ( old_len) } ;
73
+
74
+ // Write the metadata into the buffer, followed by the command.
75
+ // We are using a packed struct to write them both as one operation.
76
+ // SAFETY: `ptr` must be non-null, since it is within a non-null buffer.
77
+ // The call to `reserve()` ensures that the buffer has enough space to fit a value of type `C`,
78
+ // and it is valid to write any bit pattern since the underlying buffer is of type `MaybeUninit<u8>`.
79
+ unsafe {
80
+ ptr. cast :: < Packed < C > > ( )
81
+ . write_unaligned ( Packed { meta, command } ) ;
82
+ }
83
+
84
+ // Extend the length of the buffer to include the data we just wrote.
85
+ // SAFETY: The new length is guaranteed to fit in the vector's capacity,
86
+ // due to the call to `.reserve()` above.
87
+ unsafe {
88
+ self . bytes
89
+ . set_len ( old_len + std:: mem:: size_of :: < Packed < C > > ( ) ) ;
85
90
}
86
91
}
87
92
@@ -92,23 +97,43 @@ impl CommandQueue {
92
97
// flush the previously queued entities
93
98
world. flush ( ) ;
94
99
100
+ // Pointer that will iterate over the entries of the buffer.
101
+ let mut cursor = self . bytes . as_mut_ptr ( ) ;
102
+
103
+ // The address of the end of the buffer.
104
+ let end_addr = cursor as usize + self . bytes . len ( ) ;
105
+
95
106
// Reset the buffer, so it can be reused after this function ends.
96
107
// In the loop below, ownership of each command will be transferred into user code.
97
108
// SAFETY: `set_len(0)` is always valid.
98
109
unsafe { self . bytes . set_len ( 0 ) } ;
99
110
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 ) } ;
111
+ while ( cursor as usize ) < end_addr {
112
+ // SAFETY: The cursor is either at the start of the buffer, or just after the previous command.
113
+ // Since we know that the cursor is in bounds, it must point to the start of a new command.
114
+ let meta = unsafe { cursor. cast :: < CommandMeta > ( ) . read_unaligned ( ) } ;
115
+ // Advance to the bytes just after `meta`, which represent a type-erased command.
116
+ // SAFETY: For most types of `Command`, the pointer immediately following the metadata
117
+ // is guaranteed to be in bounds. If the command is a zero-sized type (ZST), then the cursor
118
+ // might be 1 byte past the end of the buffer, which is safe.
119
+ cursor = unsafe { cursor. add ( std:: mem:: size_of :: < CommandMeta > ( ) ) } ;
120
+ // Construct an owned pointer to the command.
104
121
// SAFETY: It is safe to transfer ownership out of `self.bytes`, since the call to `set_len(0)` above
105
122
// guarantees that nothing stored in the buffer will get observed after this function ends.
106
123
// `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
- }
124
+ let cmd = unsafe {
125
+ OwningPtr :: < Unaligned > :: new ( std:: ptr:: NonNull :: new_unchecked ( cursor. cast ( ) ) )
126
+ } ;
127
+ // SAFETY: The data underneath the cursor must correspond to the type erased in metadata,
128
+ // since they were stored next to each other by `.push()`.
129
+ // For ZSTs, the type doesn't matter as long as the pointer is non-null.
130
+ let size = unsafe { ( meta. apply_command_and_get_size ) ( cmd, world) } ;
131
+ // Advance the cursor past the command. For ZSTs, the cursor will not move.
132
+ // At this point, it will either point to the next `CommandMeta`,
133
+ // or the cursor will be out of bounds and the loop will end.
134
+ // SAFETY: The address just past the command is either within the buffer,
135
+ // or 1 byte past the end, so this addition will not overflow the pointer's allocation.
136
+ cursor = unsafe { cursor. add ( size) } ;
112
137
}
113
138
}
114
139
}
@@ -217,7 +242,6 @@ mod test {
217
242
// even though the first command panicking.
218
243
// the `bytes`/`metas` vectors were cleared.
219
244
assert_eq ! ( queue. bytes. len( ) , 0 ) ;
220
- assert_eq ! ( queue. metas. len( ) , 0 ) ;
221
245
222
246
// Even though the first command panicked, it's still ok to push
223
247
// more commands.
0 commit comments