• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

djeedai / bevy_hanabi / 13640457354

03 Mar 2025 09:09PM UTC coverage: 40.055% (-6.7%) from 46.757%
13640457354

push

github

web-flow
Hierarchical effects and GPU spawn event (#424)

This change introduces hierarchical effects, the ability of an effect to
be parented to another effect through the `EffectParent` component.
Child effects can inherit attributes from their parent when spawned
during the init pass, but are otherwise independent effects. They
replace the old group system, which is entirely removed. The parent
effect can emit GPU spawn events, which are consumed by the child effect
to spawn particles instead of the traditional CPU spawn count. Those GPU
spawn events currently are just the ID of the parent particles, to allow
read-only access to its attribute in _e.g._ the new
`InheritAttributeModifier`.

The ribbon/trail system is also reworked. The atomic linked list based
on `Attribute::PREV` and `Attribute::NEXT` is abandoned, and replaced
with an explicit sort compute pass which orders particles by
`Attribute::RIBBON_ID` first, and `Attribute::AGE` next. The ribbon ID
is any `u32` value unique to each ribbon/trail. Sorting particles by age
inside a given ribbon/trail allows avoiding the edge case where a
particle in the middle of a trail dies, leaving a gap in the list.

A migration guide is provided from v0.14 to the upcoming v0.15 which
will include this change, due to the large change of behavior and APIs.

409 of 2997 new or added lines in 17 files covered. (13.65%)

53 existing lines in 11 files now uncovered.

3208 of 8009 relevant lines covered (40.05%)

18.67 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/render/gpu_buffer.rs
1
use std::marker::PhantomData;
2

3
use bevy::{
4
    log::trace,
5
    render::{
6
        render_resource::{
7
            BindingResource, Buffer, BufferAddress, BufferBinding, BufferDescriptor, BufferUsages,
8
            ShaderSize, ShaderType,
9
        },
10
        renderer::RenderDevice,
11
    },
12
};
13
use bytemuck::Pod;
14
use wgpu::CommandEncoder;
15

16
struct BufferAndSize {
17
    /// Allocate GPU buffer.
18
    pub buffer: Buffer,
19
    /// Size of the buffer, in number of elements.
20
    pub size: u32,
21
}
22

23
/// GPU-only buffer without CPU-side storage.
24
///
25
/// This is a rather specialized helper to allocate an array on the GPU and
26
/// manage its buffer, depending on the device constraints and the WGSL rules
27
/// for data alignment, and allowing to resize the buffer without losing its
28
/// content (so, scheduling a buffer-to-buffer copy on GPU after reallocatin).
29
///
30
/// The element type `T` needs to implement the following traits:
31
/// - [`Pod`] to prevent user error. This is not strictly necessary, as there's
32
///   no copy from or to CPU, but if the placeholder type is not POD this might
33
///   indicate some user error.
34
/// - [`ShaderSize`] to ensure a fixed footprint, to allow packing multiple
35
///   instances inside a single buffer. This therefore excludes any
36
///   runtime-sized array (T being the element type here; it will itself be part
37
///   of an array).
38
pub struct GpuBuffer<T: Pod + ShaderSize> {
39
    /// GPU buffer if already allocated, or `None` otherwise.
40
    buffer: Option<BufferAndSize>,
41
    /// Previous GPU buffer, pending copy.
42
    old_buffer: Option<BufferAndSize>,
43
    /// GPU buffer usages.
44
    buffer_usage: BufferUsages,
45
    /// Optional GPU buffer name, for debugging.
46
    label: Option<String>,
47
    /// Used size, in element count. Elements past this are all free. Elements
48
    /// with a lower index are either allocated or in the free list.
49
    used_size: u32,
50
    /// Free list.
51
    free_list: Vec<u32>,
52
    _phantom: PhantomData<T>,
53
}
54

55
impl<T: Pod + ShaderType + ShaderSize> Default for GpuBuffer<T> {
NEW
56
    fn default() -> Self {
×
57
        Self {
58
            buffer: None,
59
            old_buffer: None,
NEW
60
            buffer_usage: BufferUsages::all(),
×
61
            label: None,
62
            used_size: 0,
NEW
63
            free_list: vec![],
×
64
            _phantom: PhantomData,
65
        }
66
    }
67
}
68

69
impl<T: Pod + ShaderType + ShaderSize> GpuBuffer<T> {
70
    /// Create a new collection.
71
    ///
72
    /// The buffer usage is always augmented by [`BufferUsages::COPY_SRC`] and
73
    /// [`BufferUsages::COPY_DST`] in order to allow buffer-to-buffer copy when
74
    /// reallocating, to preserve old content.
75
    ///
76
    /// # Panics
77
    ///
78
    /// Panics if `buffer_usage` contains [`BufferUsages::UNIFORM`] and the
79
    /// layout of the element type `T` does not meet the requirements of the
80
    /// uniform address space, as tested by
81
    /// [`ShaderType::assert_uniform_compat()`].
82
    ///
83
    /// [`BufferUsages::UNIFORM`]: bevy::render::render_resource::BufferUsages::UNIFORM
84
    #[allow(dead_code)]
NEW
85
    pub fn new(buffer_usage: BufferUsages, label: Option<String>) -> Self {
×
86
        // GPU-aligned item size, compatible with WGSL rules
NEW
87
        let item_size = <T as ShaderSize>::SHADER_SIZE.get() as usize;
×
NEW
88
        trace!("GpuBuffer: item_size={}", item_size);
×
NEW
89
        if buffer_usage.contains(BufferUsages::UNIFORM) {
×
NEW
90
            <T as ShaderType>::assert_uniform_compat();
×
91
        }
92
        Self {
93
            // We need both COPY_SRC and COPY_DST for copy_buffer_to_buffer() on realloc
NEW
94
            buffer_usage: buffer_usage | BufferUsages::COPY_SRC | BufferUsages::COPY_DST,
×
95
            label,
96
            ..Default::default()
97
        }
98
    }
99

100
    /// Create a new collection from an allocated buffer.
101
    ///
102
    /// The buffer usage must contain [`BufferUsages::COPY_SRC`] and
103
    /// [`BufferUsages::COPY_DST`] in order to allow buffer-to-buffer copy when
104
    /// reallocating, to preserve old content.
105
    ///
106
    /// # Panics
107
    ///
108
    /// Panics if `buffer_usage` doesn't contain [`BufferUsages::COPY_SRC`] or
109
    /// [`BufferUsages::COPY_DST`].
110
    ///
111
    /// Panics if `buffer_usage` contains [`BufferUsages::UNIFORM`] and the
112
    /// layout of the element type `T` does not meet the requirements of the
113
    /// uniform address space, as tested by
114
    /// [`ShaderType::assert_uniform_compat()`].
115
    ///
116
    /// [`BufferUsages::UNIFORM`]: bevy::render::render_resource::BufferUsages::UNIFORM
NEW
117
    pub fn new_allocated(buffer: Buffer, size: u32, label: Option<String>) -> Self {
×
118
        // GPU-aligned item size, compatible with WGSL rules
NEW
119
        let item_size = <T as ShaderSize>::SHADER_SIZE.get() as u32;
×
NEW
120
        let buffer_usage = buffer.usage();
×
NEW
121
        assert!(
×
NEW
122
            buffer_usage.contains(BufferUsages::COPY_SRC | BufferUsages::COPY_DST),
×
NEW
123
            "GpuBuffer requires COPY_SRC and COPY_DST buffer usages to allow copy on reallocation."
×
124
        );
NEW
125
        if buffer_usage.contains(BufferUsages::UNIFORM) {
×
NEW
126
            <T as ShaderType>::assert_uniform_compat();
×
127
        }
NEW
128
        trace!("GpuBuffer: item_size={}", item_size);
×
129
        Self {
NEW
130
            buffer: Some(BufferAndSize { buffer, size }),
×
131
            buffer_usage,
132
            label,
133
            ..Default::default()
134
        }
135
    }
136

137
    /// Clear the buffer.
138
    ///
139
    /// This doesn't de-allocate any GPU buffer.
NEW
140
    pub fn clear(&mut self) {
×
NEW
141
        self.free_list.clear();
×
NEW
142
        self.used_size = 0;
×
143
    }
144

145
    /// Allocate a new entry in the buffer.
146
    ///
147
    /// If the GPU buffer has not enough storage, or is not allocated yet, this
148
    /// schedules a (re-)allocation, which must be applied by calling
149
    /// [`allocate_gpu()`] once a frame after all [`allocate()`] calls were made
150
    /// for that frame.
151
    ///
152
    /// # Returns
153
    ///
154
    /// The index of the allocated entry.
155
    ///
156
    /// [`allocate_gpu()`]: Self::allocate_gpu
157
    /// [`allocate()`]: Self::allocate
NEW
158
    pub fn allocate(&mut self) -> u32 {
×
NEW
159
        if let Some(index) = self.free_list.pop() {
×
NEW
160
            index
×
161
        } else {
162
            // Note: we may return an index past the buffer capacity. This will instruct
163
            // allocate_gpu() to re-allocate the buffer.
NEW
164
            let index = self.used_size;
×
NEW
165
            self.used_size += 1;
×
NEW
166
            index
×
167
        }
168
    }
169

170
    /// Free an existing entry.
171
    ///
172
    /// # Panics
173
    ///
174
    /// In debug only, panics if the entry is not allocated (double-free). In
175
    /// non-debug, the behavior is undefined and will generally lead to bugs.
176
    // Currently we use GpuBuffer in sorting, and re-allocate everything each frame.
177
    #[allow(dead_code)]
NEW
178
    pub fn free(&mut self, index: u32) {
×
NEW
179
        if index < self.used_size {
×
NEW
180
            debug_assert!(
×
NEW
181
                !self.free_list.iter().any(|i| *i == index),
×
NEW
182
                "Double-free in GpuBuffer at index #{}",
×
NEW
183
                index
×
184
            );
NEW
185
            self.free_list.push(index);
×
186
        }
187
    }
188

189
    /// Get the current GPU buffer, if allocated.
190
    #[inline]
NEW
191
    pub fn buffer(&self) -> Option<&Buffer> {
×
NEW
192
        self.buffer.as_ref().map(|b| &b.buffer)
×
193
    }
194

195
    /// Get a binding for the entire GPU buffer, if allocated.
196
    #[inline]
197
    #[allow(dead_code)]
NEW
198
    pub fn binding(&self) -> Option<BindingResource> {
×
NEW
199
        let buffer = self.buffer()?;
×
NEW
200
        Some(BindingResource::Buffer(BufferBinding {
×
NEW
201
            buffer,
×
NEW
202
            offset: 0,
×
NEW
203
            size: None, // entire buffer
×
204
        }))
205
    }
206

207
    /// Get the current buffer capacity, in element count.
208
    ///
209
    /// This is the CPU view of allocations, which counts the number of
210
    /// [`allocate()`] and [`free()`] calls.
211
    ///
212
    /// [`allocate()`]: Self::allocate
213
    /// [`free()`]: Self::allocate_gpu
214
    #[inline]
215
    #[allow(dead_code)]
NEW
216
    pub fn capacity(&self) -> u32 {
×
NEW
217
        debug_assert!(self.used_size >= self.free_list.len() as u32);
×
NEW
218
        self.used_size - self.free_list.len() as u32
×
219
    }
220

221
    /// Get the current GPU buffer capacity, in element count.
222
    ///
223
    /// Note that it is possible for [`allocate()`] to return an index greater
224
    /// than or equal to the value returned by [`capacity()`], at least
225
    /// temporarily until [`allocate_gpu()`] is called.
226
    ///
227
    /// [`allocate()`]: Self::allocate
228
    /// [`gpu_capacity()`]: Self::gpu_capacity
229
    /// [`allocate_gpu()`]: Self::allocate_gpu
230
    #[inline]
NEW
231
    pub fn gpu_capacity(&self) -> u32 {
×
NEW
232
        self.buffer.as_ref().map(|b| b.size).unwrap_or(0)
×
233
    }
234

235
    /// Size in bytes of a single item in the buffer.
236
    ///
237
    /// This is equal to [`ShaderSize::SHADER_SIZE`] for the buffer element `T`.
238
    #[inline]
NEW
239
    pub fn item_size(&self) -> usize {
×
NEW
240
        <T as ShaderSize>::SHADER_SIZE.get() as usize
×
241
    }
242

243
    /// Check if the buffer is empty.
244
    ///
245
    /// The check is based on the CPU representation of the buffer, that is the
246
    /// number of calls to [`allocate()`]. The buffer is considered empty if no
247
    /// [`allocate()`] call was made, or they all have been followed by a
248
    /// corresponding [`free()`] call. This makes no assumption about the GPU
249
    /// buffer.
250
    ///
251
    /// [`allocate()`]: Self::allocate
252
    /// [`free()`]: Self::free
253
    #[inline]
254
    #[allow(dead_code)]
NEW
255
    pub fn is_empty(&self) -> bool {
×
NEW
256
        self.used_size == 0
×
257
    }
258

259
    /// Allocate or reallocate the GPU buffer if needed.
260
    ///
261
    /// This allocates or reallocates a GPU buffer to ensure storage for all
262
    /// previous calls to [`allocate()`]. This is a no-op if a GPU buffer is
263
    /// already allocated and has sufficient storage.
264
    ///
265
    /// This should be called once a frame after any new [`allocate()`] in that
266
    /// frame. After this call, [`buffer()`] is guaranteed to return `Some(..)`.
267
    ///
268
    /// # Returns
269
    ///
270
    /// `true` if the buffer was (re)allocated, or `false` if an existing buffer
271
    /// was reused which already had enough capacity.
272
    ///
273
    /// [`reserve()`]: Self::reserve
274
    /// [`allocate()`]: Self::allocate
275
    /// [`buffer()`]: Self::buffer
NEW
276
    pub fn prepare_buffers(&mut self, render_device: &RenderDevice) -> bool {
×
277
        // Don't do anything if we still have some storage.
NEW
278
        let old_capacity = self.gpu_capacity();
×
NEW
279
        if self.used_size <= old_capacity {
×
NEW
280
            return false;
×
281
        }
282

283
        // Ensure we allocate at least 256 more entries than what we need this frame,
284
        // and round that to make it nicer for the GPU.
NEW
285
        let new_capacity = (self.used_size + 256).next_multiple_of(1024);
×
NEW
286
        if new_capacity <= old_capacity {
×
NEW
287
            return false;
×
288
        }
289

290
        // Save the old buffer, we will need to copy it to the new one later.
NEW
291
        assert!(self.old_buffer.is_none(), "Multiple calls to GpuTable::prepare_buffers() before write_buffers() was called to copy old content.");
×
NEW
292
        self.old_buffer = self.buffer.take();
×
293

294
        // Allocate a new buffer of the appropriate size.
NEW
295
        let byte_size = self.item_size() * new_capacity as usize;
×
NEW
296
        trace!(
×
NEW
297
            "prepare_buffers(): increase capacity from {} to {} elements, new size {} bytes",
×
NEW
298
            old_capacity,
×
NEW
299
            new_capacity,
×
NEW
300
            byte_size
×
301
        );
NEW
302
        let buffer = render_device.create_buffer(&BufferDescriptor {
×
NEW
303
            label: self.label.as_ref().map(|s| &s[..]),
×
NEW
304
            size: byte_size as BufferAddress,
×
NEW
305
            usage: BufferUsages::COPY_DST | self.buffer_usage,
×
NEW
306
            mapped_at_creation: false,
×
307
        });
NEW
308
        self.buffer = Some(BufferAndSize {
×
NEW
309
            buffer,
×
NEW
310
            size: new_capacity,
×
311
        });
312

NEW
313
        true
×
314
    }
315

316
    /// Schedule any pending buffer copy.
317
    ///
318
    /// If a new buffer was (re-)allocated this frame, this schedules a
319
    /// buffer-to-buffer copy from the old buffer to the new one, then releases
320
    /// the old buffer.
321
    ///
322
    /// This should be called once a frame after [`prepare_buffers()`]. This is
323
    /// a no-op if there's no need for a buffer copy.
324
    ///
325
    /// [`prepare_buffers()`]: Self::prepare_buffers
NEW
326
    pub fn write_buffers(&self, command_encoder: &mut CommandEncoder) {
×
NEW
327
        if let Some(old_buffer) = self.old_buffer.as_ref() {
×
NEW
328
            let new_buffer = self.buffer.as_ref().unwrap();
×
NEW
329
            assert!(
×
NEW
330
                new_buffer.size >= old_buffer.size,
×
NEW
331
                "Old buffer is smaller than the new one. This is unexpected."
×
332
            );
NEW
333
            command_encoder.copy_buffer_to_buffer(
×
NEW
334
                &old_buffer.buffer,
×
335
                0,
NEW
336
                &new_buffer.buffer,
×
337
                0,
NEW
338
                old_buffer.size as u64,
×
339
            );
340
        }
341
    }
342

343
    /// Clear any stale buffer used for resize in the previous frame during
344
    /// rendering while the data structure was immutable.
345
    ///
346
    /// This must be called before any new [`allocate()`].
347
    ///
348
    /// [`allocate()`]: Self::allocate
NEW
349
    pub fn clear_previous_frame_resizes(&mut self) {
×
NEW
350
        if let Some(old_buffer) = self.old_buffer.take() {
×
NEW
351
            old_buffer.buffer.destroy();
×
352
        }
353
    }
354
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc