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

djeedai / bevy_hanabi / 14348872530

09 Apr 2025 04:18AM UTC coverage: 39.38% (-0.7%) from 40.116%
14348872530

Pull #444

github

web-flow
Merge 5f906b79b into 027286d2a
Pull Request #444: Make the number of particles to emit in a GPU event an expression instead of a constant.

0 of 3 new or added lines in 1 file covered. (0.0%)

139 existing lines in 8 files now uncovered.

3022 of 7674 relevant lines covered (39.38%)

17.34 hits per line

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

0.0
/src/render/property.rs
1
use std::{
2
    num::{NonZeroU32, NonZeroU64},
3
    ops::Range,
4
};
5

6
use bevy::{
7
    log::{error, trace},
8
    prelude::{Component, Entity, OnRemove, Query, Res, ResMut, Resource, Trigger},
9
    render::{
10
        render_resource::{BindGroup, BindGroupLayout, Buffer},
11
        renderer::{RenderDevice, RenderQueue},
12
    },
13
    utils::HashMap,
14
};
15
use wgpu::{
16
    BindGroupEntry, BindGroupLayoutEntry, BindingResource, BindingType, BufferBinding,
17
    BufferBindingType, BufferUsages, ShaderStages,
18
};
19

20
use super::{aligned_buffer_vec::HybridAlignedBufferVec, effect_cache::BufferState};
21
use crate::{
22
    render::{GpuSpawnerParams, StorageType},
23
    PropertyLayout,
24
};
25

26
/// Allocation into the [`PropertyCache`] for an effect instance. This component
27
/// is only present on an effect instance if that effect uses properties.
28
#[derive(Debug, Clone, PartialEq, Eq, Component)]
29
pub struct CachedEffectProperties {
30
    /// Index of the [`PropertyBuffer`] inside the [`PropertyCache`].
31
    pub buffer_index: u32,
32
    /// Slice of GPU buffer where the storage for properties is allocated.
33
    pub range: Range<u32>,
34
}
35

36
impl CachedEffectProperties {
37
    /// Convert this allocation into a bind group key, used for bind group
38
    /// re-creation when a change is detected in the key.
39
    pub fn to_key(&self) -> PropertyBindGroupKey {
×
40
        PropertyBindGroupKey {
41
            buffer_index: self.buffer_index,
×
42
            binding_size: self.range.len() as u32,
×
43
        }
44
    }
45
}
46

47
/// Error code for [`PropertyCache::remove_properties()`].
48
#[derive(Debug)]
49
pub enum CachedPropertiesError {
50
    /// The given buffer index is invalid. The [`PropertyCache`] doesn't contain
51
    /// any buffer with such index.
52
    InvalidBufferIndex(u32),
53
    /// The given buffer index corresponds to a [`PropertyCache`] buffer which
54
    /// was already deallocated.
55
    BufferDeallocated(u32),
56
}
57

58
#[derive(Debug)]
59
pub(crate) struct PropertyBuffer {
60
    /// GPU buffer holding the properties of some effect(s).
61
    buffer: HybridAlignedBufferVec,
62
    // Layout of properties of the effect(s), if using properties.
63
    //property_layout: PropertyLayout,
64
}
65

66
impl PropertyBuffer {
67
    pub fn new(align: u32, label: Option<String>) -> Self {
×
68
        let align = NonZeroU64::new(align as u64).unwrap();
×
69
        let label = label.unwrap_or("hanabi:buffer:properties".to_string());
×
70
        Self {
71
            buffer: HybridAlignedBufferVec::new(BufferUsages::STORAGE, Some(align), Some(label)),
×
72
        }
73
    }
74

75
    #[inline]
76
    pub fn buffer(&self) -> Option<&Buffer> {
×
77
        self.buffer.buffer()
×
78
    }
79

80
    #[inline]
81
    pub fn allocate(&mut self, layout: &PropertyLayout) -> Range<u32> {
×
82
        // Note: allocate with min_binding_size() and not cpu_size(), because the buffer
83
        // needs to be large enough to host at least one struct when bound to a shader,
84
        // and in WGSL the struct is padded to its align size.
85
        let size = layout.min_binding_size().get() as usize;
×
86
        // FIXME - allocate(size) instead of push(data) so we don't need to allocate an
87
        // empty vector just to read its size.
88
        self.buffer.push_raw(&vec![0u8; size][..])
×
89
    }
90

91
    #[allow(dead_code)]
92
    #[inline]
93
    pub fn free(&mut self, range: Range<u32>) -> BufferState {
×
94
        let id = self
×
95
            .buffer
×
96
            .buffer()
97
            .map(|buf| {
×
98
                let id: NonZeroU32 = buf.id().into();
×
99
                id.get()
×
100
            })
101
            .unwrap_or(u32::MAX);
×
102
        let size = self.buffer.len();
×
103
        if self.buffer.remove(range) {
×
104
            if self.buffer.is_empty() {
×
105
                BufferState::Free
×
106
            } else if self.buffer.len() != size
×
107
                || self
×
108
                    .buffer
×
109
                    .buffer()
×
110
                    .map(|buf| {
×
111
                        let id: NonZeroU32 = buf.id().into();
×
112
                        id.get()
×
113
                    })
114
                    .unwrap_or(u32::MAX)
×
115
                    != id
×
116
            {
117
                BufferState::Resized
×
118
            } else {
119
                BufferState::Used
×
120
            }
121
        } else {
122
            BufferState::Used
×
123
        }
124
    }
125

126
    pub fn write(&mut self, offset: u32, data: &[u8]) {
×
127
        self.buffer.update(offset, data);
×
128
    }
129

130
    #[inline]
131
    pub fn write_buffer(&mut self, device: &RenderDevice, queue: &RenderQueue) -> bool {
×
132
        self.buffer.write_buffer(device, queue)
×
133
    }
134
}
135

136
/// Cache for effect properties.
137
#[derive(Resource)]
138
pub struct PropertyCache {
139
    /// Render device to allocate GPU buffers and bind group layouts as needed.
140
    device: RenderDevice,
141
    /// Collection of property buffers managed by this cache. Some buffers might
142
    /// be `None` if the entry is not used. Since the buffers are referenced
143
    /// by index, we cannot move them once they're allocated.
144
    buffers: Vec<Option<PropertyBuffer>>,
145
    /// Map from a binding size in bytes to its bind group layout. The binding
146
    /// size zero is valid, and corresponds to the variant without properties,
147
    /// which by abuse is stored here even though it's not related to properties
148
    /// (contains only the spawner binding).
149
    bind_group_layouts: HashMap<u32, BindGroupLayout>,
150
}
151

152
impl PropertyCache {
153
    pub fn new(device: RenderDevice) -> Self {
×
154
        let spawner_min_binding_size =
×
155
            GpuSpawnerParams::aligned_size(device.limits().min_storage_buffer_offset_alignment);
×
156
        let bgl = device.create_bind_group_layout(
×
157
            "hanabi:bind_group_layout:no_property",
158
            // @group(2) @binding(0) var<storage, read> spawner: Spawner;
159
            &[BindGroupLayoutEntry {
×
160
                binding: 0,
×
161
                visibility: ShaderStages::COMPUTE,
×
162
                ty: BindingType::Buffer {
×
163
                    ty: BufferBindingType::Storage { read_only: true },
×
164
                    has_dynamic_offset: true,
×
165
                    min_binding_size: Some(spawner_min_binding_size),
×
166
                },
167
                count: None,
×
168
            }],
169
        );
170
        trace!(
×
171
            "-> created bind group layout #{:?} for no-property variant",
×
172
            bgl.id()
×
173
        );
174
        let mut bind_group_layouts = HashMap::with_capacity(1);
×
175
        bind_group_layouts.insert(0, bgl);
×
176

177
        Self {
178
            device,
179
            buffers: vec![],
×
180
            bind_group_layouts,
181
        }
182
    }
183

184
    #[allow(dead_code)]
185
    #[inline]
186
    pub fn buffers(&self) -> &[Option<PropertyBuffer>] {
×
187
        &self.buffers
×
188
    }
189

190
    #[allow(dead_code)]
191
    #[inline]
192
    pub fn buffers_mut(&mut self) -> &mut [Option<PropertyBuffer>] {
×
193
        &mut self.buffers
×
194
    }
195

196
    pub fn bind_group_layout(
×
197
        &self,
198
        min_binding_size: Option<NonZeroU64>,
199
    ) -> Option<&BindGroupLayout> {
200
        let key = min_binding_size.map(NonZeroU64::get).unwrap_or(0) as u32;
×
201
        self.bind_group_layouts.get(&key)
×
202
    }
203

204
    pub fn insert(&mut self, property_layout: &PropertyLayout) -> CachedEffectProperties {
×
205
        assert!(!property_layout.is_empty());
×
206

207
        // Ensure there's a bind group layout for the property variant with that binding
208
        // size
209
        let properties_min_binding_size = property_layout.min_binding_size();
×
210
        let spawner_min_binding_size = GpuSpawnerParams::aligned_size(
211
            self.device.limits().min_storage_buffer_offset_alignment,
×
212
        );
213
        self.bind_group_layouts
×
214
            .entry(properties_min_binding_size.get() as u32)
×
215
            .or_insert_with(|| {
×
216
                let label = format!(
×
217
                    "hanabi:bind_group_layout:property_size{}",
×
218
                    properties_min_binding_size.get()
×
219
                );
220
                trace!(
×
221
                    "Create new property bind group layout '{}' for binding size {} bytes.",
×
222
                    label,
×
223
                    properties_min_binding_size.get()
×
224
                );
225
                let bgl = self.device.create_bind_group_layout(
×
226
                    Some(&label[..]),
×
227
                    &[
×
228
                        // @group(2) @binding(0) var<storage, read> spawner: Spawner;
229
                        BindGroupLayoutEntry {
×
230
                            binding: 0,
×
231
                            visibility: ShaderStages::COMPUTE,
×
232
                            ty: BindingType::Buffer {
×
233
                                ty: BufferBindingType::Storage { read_only: true },
×
234
                                has_dynamic_offset: true,
×
235
                                min_binding_size: Some(spawner_min_binding_size),
×
236
                            },
237
                            count: None,
×
238
                        },
239
                        // @group(2) @binding(1) var<storage, read> properties : Properties;
240
                        BindGroupLayoutEntry {
×
241
                            binding: 1,
×
242
                            visibility: ShaderStages::COMPUTE,
×
243
                            ty: BindingType::Buffer {
×
244
                                ty: BufferBindingType::Storage { read_only: true },
×
245
                                has_dynamic_offset: true,
×
246
                                min_binding_size: Some(properties_min_binding_size),
×
247
                            },
248
                            count: None,
×
249
                        },
250
                    ],
251
                );
252
                trace!("-> created bind group layout #{:?}", bgl.id());
×
253
                bgl
×
254
            });
255

256
        self.buffers
257
            .iter_mut()
258
            .enumerate()
259
            .find_map(|(buffer_index, buffer)| {
×
260
                if let Some(buffer) = buffer {
×
261
                    // Try to allocate a slice into the buffer
262
                    // FIXME - Currently PropertyBuffer::allocate() always succeeds and
263
                    // grows indefinitely
264
                    let range = buffer.allocate(property_layout);
265
                    trace!("Allocate new slice in property buffer #{buffer_index} for layout {property_layout:?}: range={range:?}");
×
266
                    Some(CachedEffectProperties {
267
                        buffer_index: buffer_index as u32,
268
                        range,
269
                    })
270
                } else {
271
                    None
×
272
                }
273
            })
274
            .unwrap_or_else(|| {
×
275
                // Cannot find any suitable buffer; allocate a new one
276
                let buffer_index = self
×
277
                    .buffers
×
278
                    .iter()
×
279
                    .position(|buf| buf.is_none())
×
280
                    .unwrap_or(self.buffers.len());
×
281
                let label = format!("hanabi:buffer:properties{buffer_index}");
×
282
                trace!("Creating new property buffer #{buffer_index} '{label}'");
×
283
                let align = self.device.limits().min_storage_buffer_offset_alignment;
×
284
                let mut buffer = PropertyBuffer::new(align, Some(label));
×
285
                // FIXME - Currently PropertyBuffer::allocate() always succeeds and grows
286
                // indefinitely
287
                let range = buffer.allocate(property_layout);
×
288
                if buffer_index >= self.buffers.len() {
×
289
                    self.buffers.push(Some(buffer));
×
290
                } else {
291
                    debug_assert!(self.buffers[buffer_index].is_none());
×
292
                    self.buffers[buffer_index] = Some(buffer);
×
293
                }
294
                CachedEffectProperties {
×
295
                    buffer_index: buffer_index as u32,
×
296
                    range,
×
297
                }
298
            })
299
    }
300

301
    pub fn get_buffer(&self, buffer_index: u32) -> Option<&Buffer> {
×
302
        self.buffers
×
303
            .get(buffer_index as usize)
×
304
            .and_then(|opt_pb| opt_pb.as_ref().map(|pb| pb.buffer()))
×
305
            .flatten()
306
    }
307

308
    /// Deallocated and remove properties from the cache.
309
    pub fn remove_properties(
×
310
        &mut self,
311
        cached_effect_properties: &CachedEffectProperties,
312
    ) -> Result<BufferState, CachedPropertiesError> {
313
        trace!(
×
314
            "Removing cached properties {:?} from cache.",
×
315
            cached_effect_properties
316
        );
317
        let entry = self
×
318
            .buffers
×
319
            .get_mut(cached_effect_properties.buffer_index as usize)
×
320
            .ok_or(CachedPropertiesError::InvalidBufferIndex(
×
321
                cached_effect_properties.buffer_index,
×
322
            ))?;
323
        let buffer = entry
×
324
            .as_mut()
325
            .ok_or(CachedPropertiesError::BufferDeallocated(
326
                cached_effect_properties.buffer_index,
327
            ))?;
328
        Ok(buffer.free(cached_effect_properties.range.clone()))
329
    }
330
}
331

332
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
333
pub struct PropertyBindGroupKey {
334
    pub buffer_index: u32,
335
    pub binding_size: u32,
336
}
337

338
#[derive(Default, Resource)]
339
pub struct PropertyBindGroups {
340
    /// Map from a [`PropertyBuffer`] index and a binding size to the
341
    /// corresponding bind group.
342
    property_bind_groups: HashMap<PropertyBindGroupKey, BindGroup>,
343
    /// Bind group for the variant without any property.
344
    no_property_bind_group: Option<BindGroup>,
345
}
346

347
impl PropertyBindGroups {
348
    /// Clear all bind groups.
349
    ///
350
    /// If `with_no_property` is `true`, also clear the no-property bind group,
351
    /// which doesn't depend on any property buffer.
352
    pub fn clear(&mut self, with_no_property: bool) {
×
353
        self.property_bind_groups.clear();
×
354
        if with_no_property {
×
355
            self.no_property_bind_group = None;
×
356
        }
357
    }
358

359
    /// Ensure the bind group for the given key exists, creating it if needed.
360
    pub fn ensure_exists(
×
361
        &mut self,
362
        property_key: &PropertyBindGroupKey,
363
        property_cache: &PropertyCache,
364
        spawner_buffer: &Buffer,
365
        spawner_buffer_binding_size: NonZeroU64,
366
        render_device: &RenderDevice,
367
    ) -> Result<(), ()> {
368
        let Some(property_buffer) = property_cache.get_buffer(property_key.buffer_index) else {
×
369
            error!(
×
370
                "Missing property buffer #{}, referenced by effect batch.",
×
371
                property_key.buffer_index,
372
            );
373
            return Err(());
×
374
        };
375

376
        // This should always be non-zero if the property key is Some().
377
        let property_binding_size = NonZeroU64::new(property_key.binding_size as u64).unwrap();
378
        let Some(layout) = property_cache.bind_group_layout(Some(property_binding_size)) else {
×
379
            error!(
×
380
                "Missing property bind group layout for binding size {}, referenced by effect batch.",
×
381
                property_binding_size.get(),
×
382
            );
383
            return Err(());
×
384
        };
385

386
        self.property_bind_groups
387
            .entry(*property_key)
388
            .or_insert_with(|| {
×
389
                trace!(
×
390
                    "Creating new spawner@2 bind group for property buffer #{} and binding size {}",
×
391
                    property_key.buffer_index,
392
                    property_key.binding_size
393
                );
394
                render_device.create_bind_group(
×
395
                    Some(
×
396
                        &format!(
×
397
                            "hanabi:bind_group:spawner@2:property{}_size{}",
×
398
                            property_key.buffer_index, property_key.binding_size
×
399
                        )[..],
×
400
                    ),
401
                    layout,
×
402
                    &[
×
403
                        BindGroupEntry {
×
404
                            binding: 0,
×
405
                            resource: BindingResource::Buffer(BufferBinding {
×
406
                                buffer: spawner_buffer,
×
407
                                offset: 0,
×
408
                                size: Some(spawner_buffer_binding_size),
×
409
                            }),
410
                        },
411
                        BindGroupEntry {
×
412
                            binding: 1,
×
413
                            resource: BindingResource::Buffer(BufferBinding {
×
414
                                buffer: property_buffer,
×
415
                                offset: 0,
×
416
                                size: Some(property_binding_size),
×
417
                            }),
418
                        },
419
                    ],
420
                )
421
            });
422
        Ok(())
423
    }
424

425
    /// Ensure the bind group for the given key exists, creating it if needed.
426
    pub fn ensure_exists_no_property(
×
427
        &mut self,
428
        property_cache: &PropertyCache,
429
        spawner_buffer: &Buffer,
430
        spawner_buffer_binding_size: NonZeroU64,
431
        render_device: &RenderDevice,
432
    ) -> Result<(), ()> {
433
        let Some(layout) = property_cache.bind_group_layout(None) else {
×
434
            error!(
×
435
                "Missing property bind group layout for no-property variant, referenced by effect batch.",
×
436
            );
437
            return Err(());
×
438
        };
439

440
        if self.no_property_bind_group.is_none() {
441
            trace!("Creating new spawner@2 bind group for no-property variant");
×
442
            self.no_property_bind_group = Some(render_device.create_bind_group(
×
443
                Some("hanabi:bind_group:spawner@2:no-property"),
×
444
                layout,
×
445
                &[BindGroupEntry {
×
446
                    binding: 0,
×
447
                    resource: BindingResource::Buffer(BufferBinding {
×
448
                        buffer: spawner_buffer,
×
449
                        offset: 0,
×
450
                        size: Some(spawner_buffer_binding_size),
×
451
                    }),
452
                }],
453
            ));
454
        }
455

456
        Ok(())
457
    }
458

459
    /// Get the bind group for the given key.
460
    pub fn get(&self, key: Option<&PropertyBindGroupKey>) -> Option<&BindGroup> {
×
461
        if let Some(key) = key {
×
462
            self.property_bind_groups.get(key)
463
        } else {
464
            self.no_property_bind_group.as_ref()
×
465
        }
466
    }
467
}
468

469
/// Observer raised when the [`CachedEffectProperties`] component is removed,
470
/// which indicates that the effect doesn't use properties anymore (including,
471
/// when the effect itself is despawned).
472
pub(crate) fn on_remove_cached_properties(
×
473
    trigger: Trigger<OnRemove, CachedEffectProperties>,
474
    query: Query<(Entity, &CachedEffectProperties)>,
475
    mut property_cache: ResMut<PropertyCache>,
476
    mut property_bind_groups: ResMut<PropertyBindGroups>,
477
) {
478
    // FIXME - review this Observer pattern; this triggers for each event one by
479
    // one, which could kill performance if many effects are removed.
480

481
    let Ok((render_entity, cached_effect_properties)) = query.get(trigger.entity()) else {
×
482
        return;
×
483
    };
484

485
    match property_cache.remove_properties(cached_effect_properties) {
486
        Err(err) => match err {
×
487
            CachedPropertiesError::InvalidBufferIndex(buffer_index)
×
488
                => error!("Failed to remove cached properties of render entity {render_entity:?} from buffer #{buffer_index}: the index is invalid."),
×
489
            CachedPropertiesError::BufferDeallocated(buffer_index)
×
490
                => error!("Failed to remove cached properties of render entity {render_entity:?} from buffer #{buffer_index}: the buffer is not allocated."),
×
491
        }
492
        Ok(buffer_state) => if buffer_state != BufferState::Used {
×
493
            // The entire buffer was deallocated, or it was resized; destroy all bind groups referencing it
494
            let key = cached_effect_properties.to_key();
×
495
            trace!("Destroying property bind group for key {key:?} due to property buffer deallocated.");
×
496
            property_bind_groups
×
497
                .property_bind_groups
×
498
                .retain(|&k, _| k.buffer_index != key.buffer_index);
×
499
        }
500
    }
501
}
502

503
/// Prepare GPU buffers storing effect properties.
504
///
505
/// This system runs after the new effects have been registered by
506
/// [`add_effects()`], and all effects using properties are known for this
507
/// frame. It (re-)allocate any property buffer, and schedule buffer writes to
508
/// them, in anticipation of [`prepare_bind_groups()`] referencing those buffers
509
/// to create bind groups.
510
pub(crate) fn prepare_property_buffers(
×
511
    render_device: Res<RenderDevice>,
512
    render_queue: Res<RenderQueue>,
513
    mut cache: ResMut<PropertyCache>,
514
    mut bind_groups: ResMut<PropertyBindGroups>,
515
) {
516
    // Allocate all the property buffer(s) as needed, before we move to the next
517
    // step which will need those buffers to schedule data copies from CPU.
518
    for (buffer_index, buffer_slot) in cache.buffers_mut().iter_mut().enumerate() {
×
519
        let Some(property_buffer) = buffer_slot.as_mut() else {
×
520
            continue;
×
521
        };
UNCOV
522
        let changed = property_buffer.write_buffer(&render_device, &render_queue);
×
UNCOV
523
        if changed {
×
524
            trace!("Destroying all bind groups for property buffer #{buffer_index}");
×
525
            bind_groups
×
526
                .property_bind_groups
×
527
                .retain(|&k, _| k.buffer_index != buffer_index as u32);
×
528
        }
529
    }
530
}
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