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

djeedai / bevy_hanabi / 22263909674

21 Feb 2026 08:36PM UTC coverage: 57.75% (-0.6%) from 58.351%
22263909674

push

github

web-flow
Clean-up bind groups with Bevy utils (#524)

Use the Bevy utils to shorten the bind group layout and bind group
creation and make the code more readable.

Rename all bind group GPU resources to `bg` and the layout ones to `bgl`
to make them shorter in debugger too.

108 of 181 new or added lines in 6 files covered. (59.67%)

4728 of 8187 relevant lines covered (57.75%)

197.44 hits per line

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

70.52
/src/render/effect_cache.rs
1
use std::{
2
    cmp::Ordering,
3
    num::{NonZeroU32, NonZeroU64},
4
    ops::Range,
5
};
6

7
use bevy::{
8
    asset::Handle,
9
    ecs::{component::Component, resource::Resource},
10
    log::{trace, warn},
11
    platform::collections::HashMap,
12
    render::{
13
        mesh::allocator::MeshBufferSlice,
14
        render_resource::{
15
            binding_types::{storage_buffer_read_only, storage_buffer_read_only_sized},
16
            *,
17
        },
18
        renderer::RenderDevice,
19
    },
20
    utils::default,
21
};
22
use bytemuck::cast_slice_mut;
23

24
use super::{buffer_table::BufferTableId, BufferBindingSource};
25
use crate::{
26
    asset::EffectAsset,
27
    render::{
28
        calc_hash, event::GpuChildInfo, GpuDrawIndexedIndirectArgs, GpuDrawIndirectArgs,
29
        GpuEffectMetadata, GpuIndirectIndex, GpuSpawnerParams, StorageType as _,
30
    },
31
    ParticleLayout,
32
};
33

34
/// Describes all particle slices of particles in the particle buffer
35
/// for a single effect.
36
#[derive(Debug, Clone, PartialEq, Eq)]
37
pub struct EffectSlice {
38
    /// Slice into the underlying [`BufferVec`].
39
    ///
40
    /// This is measured in items, not bytes.
41
    pub slice: Range<u32>,
42
    /// ID of the particle slab in the [`EffectCache`].
43
    pub slab_id: SlabId,
44
    /// Particle layout of the effect.
45
    pub particle_layout: ParticleLayout,
46
}
47

48
impl Ord for EffectSlice {
49
    fn cmp(&self, other: &Self) -> Ordering {
8✔
50
        match self.slab_id.cmp(&other.slab_id) {
16✔
51
            Ordering::Equal => self.slice.start.cmp(&other.slice.start),
4✔
52
            ord => ord,
8✔
53
        }
54
    }
55
}
56

57
impl PartialOrd for EffectSlice {
58
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
8✔
59
        Some(self.cmp(other))
16✔
60
    }
61
}
62

63
/// A reference to a slice allocated inside an [`ParticleSlab`].
64
#[derive(Debug, Default, Clone, PartialEq, Eq)]
65
pub struct SlabSliceRef {
66
    /// Range into a [`ParticleSlab`], in item count.
67
    range: Range<u32>,
68
    /// Particle layout for the effect stored in that slice.
69
    pub(crate) particle_layout: ParticleLayout,
70
}
71

72
impl SlabSliceRef {
73
    /// The length of the slice, in number of items.
74
    #[allow(dead_code)]
75
    pub fn len(&self) -> u32 {
14✔
76
        self.range.end - self.range.start
14✔
77
    }
78

79
    /// The size in bytes of the slice.
80
    #[allow(dead_code)]
81
    pub fn byte_size(&self) -> usize {
4✔
82
        (self.len() as usize) * (self.particle_layout.min_binding_size().get() as usize)
12✔
83
    }
84

85
    pub fn range(&self) -> Range<u32> {
624✔
86
        self.range.clone()
1,248✔
87
    }
88
}
89

90
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
91
struct SimBindGroupKey {
92
    buffer: Option<BufferId>,
93
    offset: u32,
94
    size: u32,
95
}
96

97
impl SimBindGroupKey {
98
    /// Invalid key, often used as placeholder.
99
    pub const INVALID: Self = Self {
100
        buffer: None,
101
        offset: u32::MAX,
102
        size: 0,
103
    };
104
}
105

106
impl From<&BufferBindingSource> for SimBindGroupKey {
107
    fn from(value: &BufferBindingSource) -> Self {
×
108
        Self {
109
            buffer: Some(value.buffer.id()),
×
110
            offset: value.offset,
×
111
            size: value.size.get(),
×
112
        }
113
    }
114
}
115

116
impl From<Option<&BufferBindingSource>> for SimBindGroupKey {
117
    fn from(value: Option<&BufferBindingSource>) -> Self {
312✔
118
        if let Some(bbs) = value {
312✔
119
            Self {
120
                buffer: Some(bbs.buffer.id()),
×
121
                offset: bbs.offset,
×
122
                size: bbs.size.get(),
×
123
            }
124
        } else {
125
            Self::INVALID
312✔
126
        }
127
    }
128
}
129

130
/// State of a [`ParticleSlab`] after an insertion or removal operation.
131
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132
pub enum SlabState {
133
    /// The slab is in use, with allocated resources.
134
    Used,
135
    /// Like `Used`, but the slab was resized, so any bind group is
136
    /// nonetheless invalid.
137
    Resized,
138
    /// The slab is free (its resources were deallocated).
139
    Free,
140
}
141

142
/// ID of a [`ParticleSlab`] inside an [`EffectCache`].
143
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
144
pub struct SlabId(u32);
145

146
impl SlabId {
147
    /// An invalid value, often used as placeholder.
148
    pub const INVALID: SlabId = SlabId(u32::MAX);
149

150
    /// Create a new slab ID from its underlying index.
151
    pub const fn new(index: u32) -> Self {
326✔
152
        assert!(index != u32::MAX);
652✔
153
        Self(index)
326✔
154
    }
155

156
    /// Check if the current ID is valid, that is, is different from
157
    /// [`INVALID`].
158
    ///
159
    /// [`INVALID`]: Self::INVALID
160
    #[inline]
161
    #[allow(dead_code)]
162
    pub const fn is_valid(&self) -> bool {
×
163
        self.0 != Self::INVALID.0
×
164
    }
165

166
    /// Get the raw underlying index.
167
    ///
168
    /// This is mostly used for debugging / logging.
169
    #[inline]
170
    pub const fn index(&self) -> u32 {
1,879✔
171
        self.0
1,879✔
172
    }
173
}
174

175
impl Default for SlabId {
176
    fn default() -> Self {
×
177
        Self::INVALID
×
178
    }
179
}
180

181
/// Storage for the per-particle data of effects sharing compatible layouts.
182
///
183
/// Currently only accepts a single unique particle layout, fixed at creation.
184
/// If an effect has a different particle layout, it needs to be stored in a
185
/// different slab.
186
///
187
/// Also currently only accepts instances of a unique effect asset, although
188
/// this restriction is purely for convenience and may be relaxed in the future
189
/// to improve batching.
190
#[derive(Debug)]
191
pub struct ParticleSlab {
192
    /// GPU buffer storing all particles for the entire slab of effects.
193
    ///
194
    /// Each particle is a collection of attributes arranged according to
195
    /// [`Self::particle_layout`]. The buffer contains storage for exactly
196
    /// [`Self::capacity`] particles.
197
    particle_buffer: Buffer,
198
    /// GPU buffer storing the indirection indices for the entire slab of
199
    /// effects.
200
    ///
201
    /// Each indirection item contains 3 values:
202
    /// - the ping-pong alive particles and render indirect indices at offsets 0
203
    ///   and 1
204
    /// - the dead particle indices at offset 2
205
    ///
206
    /// The buffer contains storage for exactly [`Self::capacity`] items.
207
    indirect_index_buffer: Buffer,
208
    /// Layout of particles.
209
    particle_layout: ParticleLayout,
210
    /// Total slab capacity, in number of particles.
211
    capacity: u32,
212
    /// Used slab size, in number of particles, either from allocated slices
213
    /// or from slices in the free list.
214
    used_size: u32,
215
    /// Array of free slices for new allocations, sorted in increasing order
216
    /// inside the slab buffers.
217
    free_slices: Vec<Range<u32>>,
218

219
    /// Handle of all effects common in this slab. TODO - replace with
220
    /// compatible layout.
221
    asset: Handle<EffectAsset>,
222
    /// Layout of the particle@1 bind group for the render pass.
223
    // TODO - move; this only depends on the particle and spawner layouts, can be shared across
224
    // slabs
225
    render_particles_buffer_layout: BindGroupLayout,
226
    /// Bind group particle@1 of the simulation passes (init and udpate).
227
    sim_bind_group: Option<BindGroup>,
228
    /// Key the `sim_bind_group` was created from.
229
    sim_bind_group_key: SimBindGroupKey,
230
}
231

232
impl ParticleSlab {
233
    /// Minimum buffer capacity to allocate, in number of particles.
234
    pub const MIN_CAPACITY: u32 = 65536; // at least 64k particles
235

236
    /// Create a new slab and the GPU resources to back it up.
237
    ///
238
    /// The slab cannot contain less than [`MIN_CAPACITY`] particles. If the
239
    /// input `capacity` is smaller, it's rounded up to [`MIN_CAPACITY`].
240
    ///
241
    /// # Panics
242
    ///
243
    /// This panics if the `capacity` is zero.
244
    ///
245
    /// [`MIN_CAPACITY`]: Self::MIN_CAPACITY
246
    pub fn new(
8✔
247
        slab_id: SlabId,
248
        asset: Handle<EffectAsset>,
249
        capacity: u32,
250
        particle_layout: ParticleLayout,
251
        render_device: &RenderDevice,
252
    ) -> Self {
253
        trace!(
8✔
254
            "ParticleSlab::new(slab_id={}, capacity={}, particle_layout={:?}, item_size={}B)",
255
            slab_id.0,
256
            capacity,
257
            particle_layout,
258
            particle_layout.min_binding_size().get(),
9✔
259
        );
260

261
        // Calculate the clamped capacity of the group, in number of particles.
262
        let capacity = capacity.max(Self::MIN_CAPACITY);
24✔
263
        assert!(
8✔
264
            capacity > 0,
8✔
265
            "Attempted to create a zero-sized effect buffer."
266
        );
267

268
        // Allocate the particle buffer itself, containing the attributes of each
269
        // particle.
270
        #[cfg(debug_assertions)]
271
        let mapped_at_creation = true;
16✔
272
        #[cfg(not(debug_assertions))]
273
        let mapped_at_creation = false;
274
        let particle_capacity_bytes: BufferAddress =
16✔
275
            capacity as u64 * particle_layout.min_binding_size().get();
24✔
276
        let particle_label = format!("hanabi:buffer:slab{}:particle", slab_id.0);
16✔
277
        let particle_buffer = render_device.create_buffer(&BufferDescriptor {
32✔
278
            label: Some(&particle_label),
16✔
279
            size: particle_capacity_bytes,
16✔
280
            usage: BufferUsages::COPY_DST | BufferUsages::STORAGE,
8✔
281
            mapped_at_creation,
8✔
282
        });
283
        // Set content
284
        #[cfg(debug_assertions)]
285
        {
286
            // Scope get_mapped_range_mut() to force a drop before unmap()
287
            {
288
                let slice: &mut [u8] = &mut particle_buffer
48✔
289
                    .slice(..particle_capacity_bytes)
24✔
290
                    .get_mapped_range_mut();
24✔
291
                let slice: &mut [u32] = cast_slice_mut(slice);
40✔
292
                slice.fill(0xFFFFFFFF);
16✔
293
            }
294
            particle_buffer.unmap();
8✔
295
        }
296

297
        // Each indirect buffer stores 3 arrays of u32, of length the number of
298
        // particles.
299
        let indirect_capacity_bytes: BufferAddress = capacity as u64 * 4 * 3;
24✔
300
        let indirect_label = format!("hanabi:buffer:slab{}:indirect", slab_id.0);
16✔
301
        let indirect_index_buffer = render_device.create_buffer(&BufferDescriptor {
32✔
302
            label: Some(&indirect_label),
16✔
303
            size: indirect_capacity_bytes,
8✔
304
            usage: BufferUsages::COPY_DST | BufferUsages::STORAGE,
8✔
305
            mapped_at_creation: true,
8✔
306
        });
307
        // Set content
308
        {
309
            // Scope get_mapped_range_mut() to force a drop before unmap()
310
            {
311
                let slice: &mut [u8] = &mut indirect_index_buffer
32✔
312
                    .slice(..indirect_capacity_bytes)
8✔
313
                    .get_mapped_range_mut();
8✔
314
                let slice: &mut [u32] = cast_slice_mut(slice);
24✔
315
                for index in 0..capacity {
524,296✔
316
                    slice[3 * index as usize + 2] = index;
317
                }
318
            }
319
            indirect_index_buffer.unmap();
16✔
320
        }
321

322
        // Create the render layout.
323
        // TODO - move; this only depends on the particle and spawner layouts, can be
324
        // shared across slabs
325
        let spawner_params_size = GpuSpawnerParams::aligned_size(
326
            render_device.limits().min_storage_buffer_offset_alignment,
8✔
327
        );
328
        let label = format!("hanabi:bgl:render:particles@1:slab{}", slab_id.0);
16✔
329
        let render_particles_buffer_layout = render_device.create_bind_group_layout(
24✔
330
            &label[..],
8✔
331
            &BindGroupLayoutEntries::sequential(
16✔
332
                ShaderStages::VERTEX,
8✔
333
                (
334
                    // @group(1) @binding(0) var<storage, read> particle_buffer : ParticleBuffer;
335
                    storage_buffer_read_only_sized(false, Some(particle_layout.min_binding_size()))
24✔
336
                        .visibility(ShaderStages::VERTEX_FRAGMENT),
16✔
337
                    // @group(1) @binding(1) var<storage, read> indirect_buffer : IndirectBuffer;
338
                    storage_buffer_read_only::<GpuIndirectIndex>(false),
16✔
339
                    // @group(1) @binding(2) var<storage, read> spawner : Spawner;
340
                    storage_buffer_read_only_sized(true, Some(spawner_params_size)),
8✔
341
                ),
342
            ),
343
        );
344

345
        Self {
346
            particle_buffer,
347
            indirect_index_buffer,
348
            particle_layout,
349
            render_particles_buffer_layout,
350
            capacity,
351
            used_size: 0,
352
            free_slices: vec![],
16✔
353
            asset,
354
            sim_bind_group: None,
355
            sim_bind_group_key: SimBindGroupKey::INVALID,
356
        }
357
    }
358

359
    // TODO - move; this only depends on the particle and spawner layouts, can be
360
    // shared across slabs
361
    pub fn render_particles_buffer_layout(&self) -> &BindGroupLayout {
2✔
362
        &self.render_particles_buffer_layout
2✔
363
    }
364

365
    #[inline]
366
    pub fn particle_buffer(&self) -> &Buffer {
×
367
        &self.particle_buffer
×
368
    }
369

370
    #[inline]
371
    pub fn indirect_index_buffer(&self) -> &Buffer {
×
372
        &self.indirect_index_buffer
×
373
    }
374

375
    /// Return a binding for the entire particle buffer.
376
    pub fn as_entire_binding_particle(&self) -> BindingResource<'_> {
5✔
377
        let capacity_bytes = self.capacity as u64 * self.particle_layout.min_binding_size().get();
20✔
378
        BindingResource::Buffer(BufferBinding {
5✔
379
            buffer: &self.particle_buffer,
10✔
380
            offset: 0,
5✔
381
            size: Some(NonZeroU64::new(capacity_bytes).unwrap()),
10✔
382
        })
383
        //self.particle_buffer.as_entire_binding()
384
    }
385

386
    /// Return a binding source for the entire particle buffer.
387
    pub fn max_binding_source(&self) -> BufferBindingSource {
×
388
        let capacity_bytes = self.capacity * self.particle_layout.min_binding_size32().get();
×
389
        BufferBindingSource {
390
            buffer: self.particle_buffer.clone(),
×
391
            offset: 0,
392
            size: NonZeroU32::new(capacity_bytes).unwrap(),
×
393
        }
394
    }
395

396
    /// Return a binding for the entire indirect buffer associated with the
397
    /// current effect buffer.
398
    pub fn as_entire_binding_indirect(&self) -> BindingResource<'_> {
5✔
399
        let capacity_bytes = self.capacity as u64 * 12;
10✔
400
        BindingResource::Buffer(BufferBinding {
5✔
401
            buffer: &self.indirect_index_buffer,
10✔
402
            offset: 0,
5✔
403
            size: Some(NonZeroU64::new(capacity_bytes).unwrap()),
10✔
404
        })
405
        //self.indirect_index_buffer.as_entire_binding()
406
    }
407

408
    /// Create the "particle" bind group @1 for the init and update passes if
409
    /// needed.
410
    ///
411
    /// The `slab_id` must be the ID of the current [`ParticleSlab`] inside the
412
    /// [`EffectCache`].
413
    pub fn create_particle_sim_bind_group(
312✔
414
        &mut self,
415
        layout: &BindGroupLayout,
416
        slab_id: &SlabId,
417
        render_device: &RenderDevice,
418
        parent_binding_source: Option<&BufferBindingSource>,
419
    ) {
420
        let key: SimBindGroupKey = parent_binding_source.into();
1,248✔
421
        if self.sim_bind_group.is_some() && self.sim_bind_group_key == key {
933✔
422
            return;
309✔
423
        }
424

NEW
425
        let label = format!("hanabi:bg:sim:particle@1:vfx{}", slab_id.index());
×
426
        let entries: &[BindGroupEntry] = if let Some(parent_binding) =
×
427
            parent_binding_source.as_ref().map(|bbs| bbs.as_binding())
×
428
        {
NEW
429
            &BindGroupEntries::sequential((
×
NEW
430
                self.as_entire_binding_particle(),
×
NEW
431
                self.as_entire_binding_indirect(),
×
NEW
432
                parent_binding,
×
433
            ))
434
        } else {
435
            &BindGroupEntries::sequential((
6✔
436
                self.as_entire_binding_particle(),
9✔
437
                self.as_entire_binding_indirect(),
3✔
438
            ))
439
        };
440

441
        trace!(
×
442
            "Create particle simulation bind group '{}' with {} entries (has_parent:{})",
443
            label,
444
            entries.len(),
6✔
445
            parent_binding_source.is_some(),
6✔
446
        );
447
        let bind_group = render_device.create_bind_group(Some(&label[..]), layout, entries);
×
448
        self.sim_bind_group = Some(bind_group);
×
449
        self.sim_bind_group_key = key;
×
450
    }
451

452
    /// Invalidate any existing simulate bind group.
453
    ///
454
    /// Invalidate any existing bind group previously created by
455
    /// [`create_particle_sim_bind_group()`], generally because a buffer was
456
    /// re-allocated. This forces a re-creation of the bind group
457
    /// next time [`create_particle_sim_bind_group()`] is called.
458
    ///
459
    /// [`create_particle_sim_bind_group()`]: self::ParticleSlab::create_particle_sim_bind_group
460
    #[allow(dead_code)] // FIXME - review this...
461
    fn invalidate_particle_sim_bind_group(&mut self) {
×
462
        self.sim_bind_group = None;
×
463
        self.sim_bind_group_key = SimBindGroupKey::INVALID;
×
464
    }
465

466
    /// Return the cached particle@1 bind group for the simulation (init and
467
    /// update) passes.
468
    ///
469
    /// This is the per-buffer bind group at binding @1 which binds all
470
    /// per-buffer resources shared by all effect instances batched in a single
471
    /// buffer. The bind group is created by
472
    /// [`create_particle_sim_bind_group()`], and cached until a call to
473
    /// [`invalidate_particle_sim_bind_groups()`] clears the
474
    /// cached reference.
475
    ///
476
    /// [`create_particle_sim_bind_group()`]: self::ParticleSlab::create_particle_sim_bind_group
477
    /// [`invalidate_particle_sim_bind_groups()`]: self::ParticleSlab::invalidate_particle_sim_bind_groups
478
    pub fn particle_sim_bind_group(&self) -> Option<&BindGroup> {
609✔
479
        self.sim_bind_group.as_ref()
1,218✔
480
    }
481

482
    /// Try to recycle a free slice to store `size` items.
483
    fn pop_free_slice(&mut self, size: u32) -> Option<Range<u32>> {
20✔
484
        if self.free_slices.is_empty() {
40✔
485
            return None;
17✔
486
        }
487

488
        struct BestRange {
489
            range: Range<u32>,
490
            capacity: u32,
491
            index: usize,
492
        }
493

494
        let mut result = BestRange {
495
            range: 0..0, // marker for "invalid"
496
            capacity: u32::MAX,
497
            index: usize::MAX,
498
        };
499
        for (index, slice) in self.free_slices.iter().enumerate() {
3✔
500
            let capacity = slice.end - slice.start;
501
            if size > capacity {
502
                continue;
1✔
503
            }
504
            if capacity < result.capacity {
4✔
505
                result = BestRange {
2✔
506
                    range: slice.clone(),
6✔
507
                    capacity,
2✔
508
                    index,
2✔
509
                };
510
            }
511
        }
512
        if !result.range.is_empty() {
513
            if result.capacity > size {
2✔
514
                // split
515
                let start = result.range.start;
2✔
516
                let used_end = start + size;
2✔
517
                let free_end = result.range.end;
2✔
518
                let range = start..used_end;
2✔
519
                self.free_slices[result.index] = used_end..free_end;
2✔
520
                Some(range)
1✔
521
            } else {
522
                // recycle entirely
523
                self.free_slices.remove(result.index);
1✔
524
                Some(result.range)
525
            }
526
        } else {
527
            None
1✔
528
        }
529
    }
530

531
    /// Allocate a new entry in the slab to store the particles of a single
532
    /// effect.
533
    pub fn allocate(&mut self, capacity: u32) -> Option<SlabSliceRef> {
21✔
534
        trace!("ParticleSlab::allocate(capacity={})", capacity);
21✔
535

536
        if capacity > self.capacity {
21✔
537
            return None;
1✔
538
        }
539

540
        let range = if let Some(range) = self.pop_free_slice(capacity) {
20✔
541
            range
542
        } else {
543
            let new_size = self.used_size.checked_add(capacity).unwrap();
90✔
544
            if new_size <= self.capacity {
18✔
545
                let range = self.used_size..new_size;
32✔
546
                self.used_size = new_size;
16✔
547
                range
16✔
548
            } else {
549
                if self.used_size == 0 {
2✔
550
                    warn!(
×
551
                        "Cannot allocate slice of size {} in particle slab of capacity {}.",
552
                        capacity, self.capacity
553
                    );
554
                }
555
                return None;
556
            }
557
        };
558

559
        trace!("-> allocated slice {:?}", range);
560
        Some(SlabSliceRef {
561
            range,
562
            particle_layout: self.particle_layout.clone(),
563
        })
564
    }
565

566
    /// Free an allocated slice, and if this was the last allocated slice also
567
    /// free the buffer.
568
    pub fn free_slice(&mut self, slice: SlabSliceRef) -> SlabState {
11✔
569
        // If slice is at the end of the buffer, reduce total used size
570
        if slice.range.end == self.used_size {
11✔
571
            self.used_size = slice.range.start;
5✔
572
            // Check other free slices to further reduce used size and drain the free slice
573
            // list
574
            while let Some(free_slice) = self.free_slices.last() {
15✔
575
                if free_slice.end == self.used_size {
5✔
576
                    self.used_size = free_slice.start;
5✔
577
                    self.free_slices.pop();
5✔
578
                } else {
579
                    break;
×
580
                }
581
            }
582
            if self.used_size == 0 {
5✔
583
                assert!(self.free_slices.is_empty());
12✔
584
                // The buffer is not used anymore, free it too
585
                SlabState::Free
4✔
586
            } else {
587
                // There are still some slices used, the last one of which ends at
588
                // self.used_size
589
                SlabState::Used
1✔
590
            }
591
        } else {
592
            // Free slice is not at end; insert it in free list
593
            let range = slice.range;
6✔
594
            match self.free_slices.binary_search_by(|s| {
6✔
595
                if s.end <= range.start {
6✔
596
                    Ordering::Less
6✔
597
                } else if s.start >= range.end {
×
598
                    Ordering::Greater
×
599
                } else {
600
                    Ordering::Equal
×
601
                }
602
            }) {
603
                Ok(_) => warn!("Range {:?} already present in free list!", range),
×
604
                Err(index) => self.free_slices.insert(index, range),
30✔
605
            }
606
            SlabState::Used
607
        }
608
    }
609

610
    /// Check whether this slab is compatible with the given asset.
611
    ///
612
    /// This allows determining whether an instance of the effect can be stored
613
    /// inside this slab.
614
    pub fn is_compatible(
2✔
615
        &self,
616
        handle: &Handle<EffectAsset>,
617
        _particle_layout: &ParticleLayout,
618
    ) -> bool {
619
        // TODO - replace with check particle layout is compatible to allow tighter
620
        // packing in less buffers, and update in the less dispatch calls
621
        *handle == self.asset
2✔
622
    }
623
}
624

625
/// A single cached effect in the [`EffectCache`].
626
#[derive(Debug, Component)]
627
pub(crate) struct CachedEffect {
628
    /// ID of the slab of the slab storing the particles for this effect in the
629
    /// [`EffectCache`].
630
    pub slab_id: SlabId,
631
    /// The allocated effect slice within that slab.
632
    pub slice: SlabSliceRef,
633
}
634

635
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
636
pub(crate) enum AnyDrawIndirectArgs {
637
    /// Args of a non-indexed draw call.
638
    NonIndexed(GpuDrawIndirectArgs),
639
    /// Args of an indexed draw call.
640
    Indexed(GpuDrawIndexedIndirectArgs),
641
}
642

643
impl AnyDrawIndirectArgs {
644
    /// Create from a vertex buffer slice and an optional index buffer one.
645
    pub fn from_slices(
314✔
646
        vertex_slice: &MeshBufferSlice<'_>,
647
        index_slice: Option<&MeshBufferSlice<'_>>,
648
    ) -> Self {
649
        if let Some(index_slice) = index_slice {
628✔
650
            Self::Indexed(GpuDrawIndexedIndirectArgs {
×
651
                index_count: index_slice.range.len() as u32,
×
652
                instance_count: 0,
×
653
                first_index: index_slice.range.start,
×
654
                base_vertex: vertex_slice.range.start as i32,
×
655
                first_instance: 0,
×
656
            })
657
        } else {
658
            Self::NonIndexed(GpuDrawIndirectArgs {
×
659
                vertex_count: vertex_slice.range.len() as u32,
×
660
                instance_count: 0,
×
661
                first_vertex: vertex_slice.range.start,
×
662
                first_instance: 0,
×
663
            })
664
        }
665
    }
666

667
    /// Check if this args are for an indexed draw call.
668
    #[inline(always)]
669
    #[allow(dead_code)]
670
    pub fn is_indexed(&self) -> bool {
×
671
        matches!(*self, Self::Indexed(..))
×
672
    }
673

674
    /// Bit-cast the args to the row entry of the GPU buffer.
675
    ///
676
    /// If non-indexed, this returns an indexed struct bit-cast from the actual
677
    /// non-indexed one, ready for GPU upload.
678
    pub fn bitcast_to_row_entry(&self) -> GpuDrawIndexedIndirectArgs {
2✔
679
        match self {
2✔
680
            AnyDrawIndirectArgs::NonIndexed(args) => GpuDrawIndexedIndirectArgs {
681
                index_count: args.vertex_count,
×
682
                instance_count: args.instance_count,
×
683
                first_index: args.first_vertex,
×
684
                base_vertex: args.first_instance as i32,
×
685
                first_instance: 0,
686
            },
687
            AnyDrawIndirectArgs::Indexed(args) => *args,
4✔
688
        }
689
    }
690
}
691

692
impl From<GpuDrawIndirectArgs> for AnyDrawIndirectArgs {
693
    fn from(args: GpuDrawIndirectArgs) -> Self {
×
694
        Self::NonIndexed(args)
×
695
    }
696
}
697

698
impl From<GpuDrawIndexedIndirectArgs> for AnyDrawIndirectArgs {
699
    fn from(args: GpuDrawIndexedIndirectArgs) -> Self {
×
700
        Self::Indexed(args)
×
701
    }
702
}
703

704
/// Index of a row (entry) into the [`BufferTable`] storing the indirect draw
705
/// args of a single draw call.
706
#[derive(Debug, Clone, Copy, Component)]
707
pub(crate) struct CachedDrawIndirectArgs {
708
    pub row: BufferTableId,
709
    pub args: AnyDrawIndirectArgs,
710
}
711

712
impl Default for CachedDrawIndirectArgs {
713
    fn default() -> Self {
×
714
        Self {
715
            row: BufferTableId::INVALID,
716
            args: AnyDrawIndirectArgs::NonIndexed(default()),
×
717
        }
718
    }
719
}
720

721
impl CachedDrawIndirectArgs {
722
    /// Check if the index is valid.
723
    ///
724
    /// An invalid index doesn't correspond to any allocated args entry. A valid
725
    /// one may, but note that the args entry in the buffer may have been freed
726
    /// already with this index. There's no mechanism to detect reuse either.
727
    #[inline(always)]
728
    #[allow(dead_code)]
729
    pub fn is_valid(&self) -> bool {
×
730
        self.get_row_raw().is_valid()
×
731
    }
732

733
    /// Check if this row index refers to an indexed draw args entry.
734
    #[inline(always)]
735
    #[allow(dead_code)]
736
    pub fn is_indexed(&self) -> bool {
×
737
        self.args.is_indexed()
×
738
    }
739

740
    /// Get the raw index value.
741
    ///
742
    /// Retrieve the raw index value, losing the discriminant between indexed
743
    /// and non-indexed draw. This is useful when storing the index value into a
744
    /// GPU buffer. The rest of the time, prefer retaining the typed enum for
745
    /// safety.
746
    ///
747
    /// # Panics
748
    ///
749
    /// Panics if the index is invalid, whether indexed or non-indexed.
750
    pub fn get_row(&self) -> BufferTableId {
316✔
751
        let idx = self.get_row_raw();
948✔
752
        assert!(idx.is_valid());
948✔
753
        idx
316✔
754
    }
755

756
    #[inline(always)]
757
    fn get_row_raw(&self) -> BufferTableId {
316✔
758
        self.row
316✔
759
    }
760
}
761

762
/// The indices in the indirect dispatch buffers for a single effect, as well as
763
/// that of the metadata buffer.
764
#[derive(Debug, Default, Clone, Copy, Component)]
765
pub(crate) struct DispatchBufferIndices {
766
    /// The index of the [`GpuDispatchIndirect`] row in the GPU buffer
767
    /// [`EffectsMeta::update_dispatch_indirect_buffer`].
768
    ///
769
    /// [`GpuDispatchIndirect`]: super::GpuDispatchIndirect
770
    /// [`EffectsMeta::update_dispatch_indirect_buffer`]: super::EffectsMeta::dispatch_indirect_buffer
771
    pub(crate) update_dispatch_indirect_buffer_row_index: u32,
772
}
773

774
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
775
struct ParticleBindGroupLayoutKey {
776
    pub min_binding_size: NonZeroU32,
777
    pub parent_min_binding_size: Option<NonZeroU32>,
778
}
779

780
/// Cache for effect instances sharing common GPU data structures.
781
#[derive(Resource)]
782
pub struct EffectCache {
783
    /// Render device the GPU resources (buffers) are allocated from.
784
    render_device: RenderDevice,
785
    /// Collection of particle slabs managed by this cache. Some slabs might be
786
    /// `None` if the entry is not used. Since the slabs are referenced
787
    /// by index, we cannot move them once they're allocated.
788
    particle_slabs: Vec<Option<ParticleSlab>>,
789
    /// Cache of bind group layouts for the particle@1 bind groups of the
790
    /// simulation passes (init and update). Since all bindings depend only
791
    /// on buffers managed by the [`EffectCache`], we also cache the layouts
792
    /// here for convenience.
793
    particle_bind_group_layout_descs:
794
        HashMap<ParticleBindGroupLayoutKey, BindGroupLayoutDescriptor>,
795
    /// Cache of bind group layouts for the metadata@3 bind group of the init
796
    /// pass.
797
    metadata_init_bind_group_layout_desc: [Option<BindGroupLayoutDescriptor>; 2],
798
    /// Cache of bind group layouts for the metadata@3 bind group of the
799
    /// updatepass.
800
    metadata_update_bind_group_layout_descs: HashMap<u32, BindGroupLayoutDescriptor>,
801
}
802

803
impl EffectCache {
804
    /// Create a new empty cache.
805
    pub fn new(device: RenderDevice) -> Self {
4✔
806
        Self {
807
            render_device: device,
808
            particle_slabs: vec![],
8✔
809
            particle_bind_group_layout_descs: default(),
8✔
810
            metadata_init_bind_group_layout_desc: [None, None],
4✔
811
            metadata_update_bind_group_layout_descs: default(),
4✔
812
        }
813
    }
814

815
    /// Get all the particle slab slots. Unallocated slots are `None`. This can
816
    /// be indexed by the slab index.
817
    #[allow(dead_code)]
818
    #[inline]
819
    pub fn slabs(&self) -> &[Option<ParticleSlab>] {
319✔
820
        &self.particle_slabs
319✔
821
    }
822

823
    /// Get all the particle slab slots. Unallocated slots are `None`. This can
824
    /// be indexed by the slab ID.
825
    #[allow(dead_code)]
826
    #[inline]
827
    pub fn slabs_mut(&mut self) -> &mut [Option<ParticleSlab>] {
×
828
        &mut self.particle_slabs
×
829
    }
830

831
    /// Fetch a specific slab by ID.
832
    #[inline]
833
    pub fn get_slab(&self, slab_id: &SlabId) -> Option<&ParticleSlab> {
609✔
834
        self.particle_slabs.get(slab_id.0 as usize)?.as_ref()
1,827✔
835
    }
836

837
    /// Fetch a specific buffer by ID.
838
    #[allow(dead_code)]
839
    #[inline]
840
    pub fn get_slab_mut(&mut self, slab_id: &SlabId) -> Option<&mut ParticleSlab> {
×
841
        self.particle_slabs.get_mut(slab_id.0 as usize)?.as_mut()
×
842
    }
843

844
    /// Invalidate all the particle@1 bind group for all buffers.
845
    ///
846
    /// This iterates over all valid buffers and calls
847
    /// [`ParticleSlab::invalidate_particle_sim_bind_group()`] on each one.
848
    #[allow(dead_code)] // FIXME - review this...
849
    pub fn invalidate_particle_sim_bind_groups(&mut self) {
×
850
        for buffer in self.particle_slabs.iter_mut().flatten() {
×
851
            buffer.invalidate_particle_sim_bind_group();
×
852
        }
853
    }
854

855
    /// Insert a new effect instance in the cache.
856
    pub fn insert(
6✔
857
        &mut self,
858
        asset: Handle<EffectAsset>,
859
        capacity: u32,
860
        particle_layout: &ParticleLayout,
861
    ) -> CachedEffect {
862
        trace!("Inserting new effect into cache: capacity={capacity}");
6✔
863
        let (slab_id, slice) = self
18✔
864
            .particle_slabs
6✔
865
            .iter_mut()
866
            .enumerate()
867
            .find_map(|(slab_index, maybe_slab)| {
10✔
868
                // Ignore empty (non-allocated) entries as we're trying to fit the new allocation inside an existing slab.
869
                let Some(slab) = maybe_slab else { return None; };
8✔
870

871
                // The slab must be compatible with the effect's layout, otherwise ignore it.
872
                if !slab.is_compatible(&asset, particle_layout) {
873
                    return None;
×
874
                }
875

876
                // Try to allocate a slice into the slab
877
                slab
2✔
878
                    .allocate(capacity)
4✔
879
                    .map(|slice| (SlabId::new(slab_index as u32), slice))
2✔
880
            })
881
            .unwrap_or_else(|| {
12✔
882
                // Cannot find any suitable slab; allocate a new one
883
                let index = self.particle_slabs.iter().position(|buf| buf.is_none()).unwrap_or(self.particle_slabs.len());
42✔
884
                let byte_size = capacity.checked_mul(particle_layout.min_binding_size().get() as u32).unwrap_or_else(|| panic!(
36✔
885
                    "Effect size overflow: capacity={:?} particle_layout={:?} item_size={}",
886
                    capacity, particle_layout, particle_layout.min_binding_size().get()
×
887
                ));
888
                trace!(
6✔
889
                    "Creating new particle slab #{} for effect {:?} (capacity={:?}, particle_layout={:?} item_size={}, byte_size={})",
890
                    index,
891
                    asset,
892
                    capacity,
893
                    particle_layout,
894
                    particle_layout.min_binding_size().get(),
9✔
895
                    byte_size
896
                );
897
                let slab_id = SlabId::new(index as u32);
18✔
898
                let mut slab = ParticleSlab::new(
12✔
899
                    slab_id,
6✔
900
                    asset,
6✔
901
                    capacity,
6✔
902
                    particle_layout.clone(),
12✔
903
                    &self.render_device,
6✔
904
                );
905
                let slice_ref = slab.allocate(capacity).unwrap();
30✔
906
                if index >= self.particle_slabs.len() {
16✔
907
                    self.particle_slabs.push(Some(slab));
8✔
908
                } else {
909
                    debug_assert!(self.particle_slabs[index].is_none());
2✔
910
                    self.particle_slabs[index] = Some(slab);
4✔
911
                }
912
                (slab_id, slice_ref)
6✔
913
            });
914

915
        let slice = SlabSliceRef {
916
            range: slice.range.clone(),
12✔
917
            particle_layout: slice.particle_layout,
6✔
918
        };
919

920
        trace!(
6✔
921
            "Insert effect slab_id={} slice={}B particle_layout={:?}",
922
            slab_id.0,
923
            slice.particle_layout.min_binding_size().get(),
9✔
924
            slice.particle_layout,
925
        );
926
        CachedEffect { slab_id, slice }
927
    }
928

929
    /// Remove an effect from the cache. If this was the last effect, drop the
930
    /// underlying buffer and return the index of the dropped buffer.
931
    pub fn remove(&mut self, cached_effect: &CachedEffect) -> Result<SlabState, ()> {
3✔
932
        // Resolve the buffer by index
933
        let Some(maybe_buffer) = self
9✔
934
            .particle_slabs
6✔
935
            .get_mut(cached_effect.slab_id.0 as usize)
3✔
936
        else {
937
            return Err(());
×
938
        };
939
        let Some(buffer) = maybe_buffer.as_mut() else {
3✔
940
            return Err(());
×
941
        };
942

943
        // Free the slice inside the resolved buffer
944
        if buffer.free_slice(cached_effect.slice.clone()) == SlabState::Free {
945
            *maybe_buffer = None;
6✔
946
            return Ok(SlabState::Free);
3✔
947
        }
948

949
        Ok(SlabState::Used)
950
    }
951

952
    //
953
    // Bind group layouts
954
    //
955

956
    /// Ensure a bind group layout exists for the bind group @1 ("particles")
957
    /// for use with the given min binding sizes.
958
    pub fn ensure_particle_bind_group_layout_desc(
315✔
959
        &mut self,
960
        min_binding_size: NonZeroU32,
961
        parent_min_binding_size: Option<NonZeroU32>,
962
    ) -> &BindGroupLayoutDescriptor {
963
        // FIXME - This "ensure" pattern means we never de-allocate entries. This is
964
        // probably fine, because there's a limited number of realistic combinations,
965
        // but could cause wastes if e.g. loading widely different scenes.
966
        let key = ParticleBindGroupLayoutKey {
967
            min_binding_size,
968
            parent_min_binding_size,
969
        };
970
        self.particle_bind_group_layout_descs
315✔
971
            .entry(key)
630✔
972
            .or_insert_with(|| {
317✔
973
                trace!("Creating new particle sim bind group @1 for min_binding_size={} parent_min_binding_size={:?}", min_binding_size, parent_min_binding_size);
2✔
974
                create_particle_sim_bind_group_layout_desc(
2✔
975
                    min_binding_size,
2✔
976
                    parent_min_binding_size,
2✔
977
                )
978
            })
979
    }
980

981
    /// Get the bind group layout for the bind group @1 ("particles") for use
982
    /// with the given min binding sizes.
983
    pub fn particle_bind_group_layout_desc(
314✔
984
        &self,
985
        min_binding_size: NonZeroU32,
986
        parent_min_binding_size: Option<NonZeroU32>,
987
    ) -> Option<&BindGroupLayoutDescriptor> {
988
        let key = ParticleBindGroupLayoutKey {
989
            min_binding_size,
990
            parent_min_binding_size,
991
        };
992
        self.particle_bind_group_layout_descs.get(&key)
942✔
993
    }
994

995
    /// Ensure a bind group layout exists for the metadata@3 bind group of
996
    /// the init pass.
997
    pub fn ensure_metadata_init_bind_group_layout_desc(&mut self, consume_gpu_spawn_events: bool) {
3✔
998
        let layout =
3✔
999
            &mut self.metadata_init_bind_group_layout_desc[consume_gpu_spawn_events as usize];
3✔
1000
        if layout.is_none() {
8✔
1001
            *layout = Some(create_metadata_init_bind_group_layout_desc(
6✔
1002
                &self.render_device,
2✔
1003
                consume_gpu_spawn_events,
2✔
1004
            ));
1005
        }
1006
    }
1007

1008
    /// Get the bind group layout for the metadata@3 bind group of the init
1009
    /// pass.
1010
    pub fn metadata_init_bind_group_layout_desc(
314✔
1011
        &self,
1012
        consume_gpu_spawn_events: bool,
1013
    ) -> Option<&BindGroupLayoutDescriptor> {
1014
        self.metadata_init_bind_group_layout_desc[consume_gpu_spawn_events as usize].as_ref()
628✔
1015
    }
1016

1017
    /// Ensure a bind group layout exists for the metadata@3 bind group of
1018
    /// the update pass.
1019
    pub fn ensure_metadata_update_bind_group_layout_desc(&mut self, num_event_buffers: u32) {
2✔
1020
        self.metadata_update_bind_group_layout_descs
2✔
1021
            .entry(num_event_buffers)
4✔
1022
            .or_insert_with(|| {
4✔
1023
                create_metadata_update_bind_group_layout_desc(
2✔
1024
                    &self.render_device,
2✔
1025
                    num_event_buffers,
2✔
1026
                )
1027
            });
1028
    }
1029

1030
    /// Get the bind group layout for the metadata@3 bind group of the
1031
    /// update pass.
1032
    pub fn metadata_update_bind_group_layout_desc(
314✔
1033
        &self,
1034
        num_event_buffers: u32,
1035
    ) -> Option<&BindGroupLayoutDescriptor> {
1036
        self.metadata_update_bind_group_layout_descs
314✔
1037
            .get(&num_event_buffers)
628✔
1038
    }
1039

1040
    //
1041
    // Bind groups
1042
    //
1043

1044
    /// Get the "particle" bind group for the simulation (init and update)
1045
    /// passes a cached effect stored in a given GPU particle buffer.
1046
    pub fn particle_sim_bind_group(&self, slab_id: &SlabId) -> Option<&BindGroup> {
609✔
1047
        self.get_slab(slab_id)
1,827✔
1048
            .and_then(|slab| slab.particle_sim_bind_group())
1,827✔
1049
    }
1050

1051
    pub fn create_particle_sim_bind_group(
312✔
1052
        &mut self,
1053
        slab_id: &SlabId,
1054
        render_device: &RenderDevice,
1055
        min_binding_size: NonZeroU32,
1056
        parent_min_binding_size: Option<NonZeroU32>,
1057
        parent_binding_source: Option<&BufferBindingSource>,
1058
        pipeline_cache: &PipelineCache,
1059
    ) -> Result<(), ()> {
1060
        // Create the bind group
1061
        let layout = self
936✔
1062
            .ensure_particle_bind_group_layout_desc(min_binding_size, parent_min_binding_size)
624✔
1063
            .clone();
1064
        let slot = self
936✔
1065
            .particle_slabs
624✔
1066
            .get_mut(slab_id.index() as usize)
624✔
1067
            .ok_or(())?;
624✔
1068
        let effect_buffer = slot.as_mut().ok_or(())?;
312✔
1069
        effect_buffer.create_particle_sim_bind_group(
×
1070
            &pipeline_cache.get_bind_group_layout(&layout),
×
1071
            slab_id,
×
1072
            render_device,
×
1073
            parent_binding_source,
×
1074
        );
1075
        Ok(())
×
1076
    }
1077
}
1078

1079
/// Create the bind group layout for the "particle" group (@1) of the init and
1080
/// update passes.
1081
fn create_particle_sim_bind_group_layout_desc(
2✔
1082
    particle_layout_min_binding_size: NonZeroU32,
1083
    parent_particle_layout_min_binding_size: Option<NonZeroU32>,
1084
) -> BindGroupLayoutDescriptor {
1085
    let mut entries = Vec::with_capacity(3);
4✔
1086

1087
    // @group(1) @binding(0) var<storage, read_write> particle_buffer :
1088
    // ParticleBuffer
1089
    entries.push(BindGroupLayoutEntry {
6✔
1090
        binding: 0,
2✔
1091
        visibility: ShaderStages::COMPUTE,
2✔
1092
        ty: BindingType::Buffer {
2✔
1093
            ty: BufferBindingType::Storage { read_only: false },
4✔
1094
            has_dynamic_offset: false,
2✔
1095
            min_binding_size: Some(particle_layout_min_binding_size.into()),
2✔
1096
        },
1097
        count: None,
2✔
1098
    });
1099

1100
    // @group(1) @binding(1) var<storage, read_write> indirect_buffer :
1101
    // IndirectBuffer
1102
    entries.push(BindGroupLayoutEntry {
6✔
1103
        binding: 1,
2✔
1104
        visibility: ShaderStages::COMPUTE,
2✔
1105
        ty: BindingType::Buffer {
2✔
1106
            ty: BufferBindingType::Storage { read_only: false },
2✔
1107
            has_dynamic_offset: false,
2✔
1108
            min_binding_size: Some(GpuIndirectIndex::SHADER_SIZE),
2✔
1109
        },
1110
        count: None,
2✔
1111
    });
1112

1113
    // @group(1) @binding(2) var<storage, read> parent_particle_buffer :
1114
    // ParentParticleBuffer;
1115
    if let Some(min_binding_size) = parent_particle_layout_min_binding_size {
2✔
1116
        entries.push(BindGroupLayoutEntry {
×
1117
            binding: 2,
×
1118
            visibility: ShaderStages::COMPUTE,
×
1119
            ty: BindingType::Buffer {
×
1120
                ty: BufferBindingType::Storage { read_only: true },
×
1121
                has_dynamic_offset: false,
×
1122
                min_binding_size: Some(min_binding_size.into()),
×
1123
            },
1124
            count: None,
×
1125
        });
1126
    }
1127

1128
    let hash = calc_hash(&entries);
6✔
1129
    let label = format!("hanabi:bgl:sim:particles_{:016X}", hash);
4✔
1130
    trace!(
2✔
1131
        "Creating particle bind group layout '{}' for init pass with {} entries. (parent_buffer:{})",
1132
        label,
1133
        entries.len(),
4✔
1134
        parent_particle_layout_min_binding_size.is_some(),
4✔
1135
    );
1136
    BindGroupLayoutDescriptor::new(label, &entries)
6✔
1137
}
1138

1139
/// Create the bind group layout for the metadata@3 bind group of the init pass.
1140
fn create_metadata_init_bind_group_layout_desc(
2✔
1141
    render_device: &RenderDevice,
1142
    consume_gpu_spawn_events: bool,
1143
) -> BindGroupLayoutDescriptor {
1144
    let storage_alignment = render_device.limits().min_storage_buffer_offset_alignment;
4✔
1145
    let effect_metadata_size = GpuEffectMetadata::aligned_size(storage_alignment);
6✔
1146

1147
    let mut entries = Vec::with_capacity(3);
4✔
1148

1149
    // @group(3) @binding(0) var<storage, read_write> effect_metadata :
1150
    // EffectMetadata;
1151
    entries.push(BindGroupLayoutEntry {
6✔
1152
        binding: 0,
2✔
1153
        visibility: ShaderStages::COMPUTE,
2✔
1154
        ty: BindingType::Buffer {
2✔
1155
            ty: BufferBindingType::Storage { read_only: false },
2✔
1156
            has_dynamic_offset: false,
2✔
1157
            // This WGSL struct is manually padded, so the Rust type GpuEffectMetadata doesn't
1158
            // reflect its true min size.
1159
            min_binding_size: Some(effect_metadata_size),
2✔
1160
        },
1161
        count: None,
2✔
1162
    });
1163

1164
    if consume_gpu_spawn_events {
2✔
1165
        // @group(3) @binding(1) var<storage, read> child_info_buffer : ChildInfoBuffer;
1166
        entries.push(BindGroupLayoutEntry {
×
1167
            binding: 1,
×
1168
            visibility: ShaderStages::COMPUTE,
×
1169
            ty: BindingType::Buffer {
×
1170
                ty: BufferBindingType::Storage { read_only: true },
×
1171
                has_dynamic_offset: false,
×
1172
                min_binding_size: Some(GpuChildInfo::min_size()),
×
1173
            },
1174
            count: None,
×
1175
        });
1176

1177
        // @group(3) @binding(2) var<storage, read> event_buffer : EventBuffer;
1178
        entries.push(BindGroupLayoutEntry {
×
1179
            binding: 2,
×
1180
            visibility: ShaderStages::COMPUTE,
×
1181
            ty: BindingType::Buffer {
×
1182
                ty: BufferBindingType::Storage { read_only: true },
×
1183
                has_dynamic_offset: false,
×
1184
                min_binding_size: Some(NonZeroU64::new(4).unwrap()),
×
1185
            },
1186
            count: None,
×
1187
        });
1188
    }
1189

1190
    let hash = calc_hash(&entries);
6✔
1191
    let label = format!(
4✔
1192
        "hanabi:bgl:init:metadata@3_{}{:016X}",
1193
        if consume_gpu_spawn_events {
2✔
1194
            "events"
×
1195
        } else {
1196
            "noevent"
2✔
1197
        },
1198
        hash
1199
    );
1200
    trace!(
2✔
1201
        "Creating metadata@3 bind group layout '{}' for init pass with {} entries. (consume_gpu_spawn_events:{})",
1202
        label,
1203
        entries.len(),
4✔
1204
        consume_gpu_spawn_events,
1205
    );
1206
    BindGroupLayoutDescriptor::new(label, &entries)
6✔
1207
}
1208

1209
/// Create the bind group layout for the metadata@3 bind group of the update
1210
/// pass.
1211
fn create_metadata_update_bind_group_layout_desc(
2✔
1212
    render_device: &RenderDevice,
1213
    num_event_buffers: u32,
1214
) -> BindGroupLayoutDescriptor {
1215
    let storage_alignment = render_device.limits().min_storage_buffer_offset_alignment;
4✔
1216
    let effect_metadata_size = GpuEffectMetadata::aligned_size(storage_alignment);
6✔
1217

1218
    let mut entries = Vec::with_capacity(num_event_buffers as usize + 2);
6✔
1219

1220
    // @group(3) @binding(0) var<storage, read_write> effect_metadata :
1221
    // EffectMetadata;
1222
    entries.push(BindGroupLayoutEntry {
6✔
1223
        binding: 0,
2✔
1224
        visibility: ShaderStages::COMPUTE,
2✔
1225
        ty: BindingType::Buffer {
2✔
1226
            ty: BufferBindingType::Storage { read_only: false },
2✔
1227
            has_dynamic_offset: false,
2✔
1228
            // This WGSL struct is manually padded, so the Rust type GpuEffectMetadata doesn't
1229
            // reflect its true min size.
1230
            min_binding_size: Some(effect_metadata_size),
2✔
1231
        },
1232
        count: None,
2✔
1233
    });
1234

1235
    if num_event_buffers > 0 {
2✔
1236
        // @group(3) @binding(1) var<storage, read_write> child_infos : array<ChildInfo,
1237
        // N>;
1238
        entries.push(BindGroupLayoutEntry {
×
1239
            binding: 1,
×
1240
            visibility: ShaderStages::COMPUTE,
×
1241
            ty: BindingType::Buffer {
×
1242
                ty: BufferBindingType::Storage { read_only: false },
×
1243
                has_dynamic_offset: false,
×
1244
                min_binding_size: Some(GpuChildInfo::min_size()),
×
1245
            },
1246
            count: None,
×
1247
        });
1248

1249
        for i in 0..num_event_buffers {
×
1250
            // @group(3) @binding(2+i) var<storage, read_write> event_buffer_#i :
1251
            // EventBuffer;
1252
            entries.push(BindGroupLayoutEntry {
×
1253
                binding: 2 + i,
×
1254
                visibility: ShaderStages::COMPUTE,
×
1255
                ty: BindingType::Buffer {
×
1256
                    ty: BufferBindingType::Storage { read_only: false },
×
1257
                    has_dynamic_offset: false,
×
1258
                    min_binding_size: Some(NonZeroU64::new(4).unwrap()),
×
1259
                },
1260
                count: None,
×
1261
            });
1262
        }
1263
    }
1264

1265
    let hash = calc_hash(&entries);
6✔
1266
    let label = format!("hanabi:bgl:update:metadata_{:016X}", hash);
4✔
1267
    trace!(
2✔
1268
        "Creating particle bind group layout '{}' for init update with {} entries. (num_event_buffers:{})",
1269
        label,
1270
        entries.len(),
4✔
1271
        num_event_buffers,
1272
    );
1273
    BindGroupLayoutDescriptor::new(label, &entries)
6✔
1274
}
1275

1276
#[cfg(all(test, feature = "gpu_tests"))]
1277
mod gpu_tests {
1278
    use std::borrow::Cow;
1279

1280
    use bevy::math::Vec4;
1281

1282
    use super::*;
1283
    use crate::{
1284
        graph::{Value, VectorValue},
1285
        test_utils::MockRenderer,
1286
        Attribute, AttributeInner,
1287
    };
1288

1289
    #[test]
1290
    fn effect_slice_ord() {
1291
        let particle_layout = ParticleLayout::new().append(Attribute::POSITION).build();
1292
        let slice1 = EffectSlice {
1293
            slice: 0..32,
1294
            slab_id: SlabId::new(1),
1295
            particle_layout: particle_layout.clone(),
1296
        };
1297
        let slice2 = EffectSlice {
1298
            slice: 32..64,
1299
            slab_id: SlabId::new(1),
1300
            particle_layout: particle_layout.clone(),
1301
        };
1302
        assert!(slice1 < slice2);
1303
        assert!(slice1 <= slice2);
1304
        assert!(slice2 > slice1);
1305
        assert!(slice2 >= slice1);
1306

1307
        let slice3 = EffectSlice {
1308
            slice: 0..32,
1309
            slab_id: SlabId::new(0),
1310
            particle_layout,
1311
        };
1312
        assert!(slice3 < slice1);
1313
        assert!(slice3 < slice2);
1314
        assert!(slice1 > slice3);
1315
        assert!(slice2 > slice3);
1316
    }
1317

1318
    const F4A_INNER: &AttributeInner = &AttributeInner::new(
1319
        Cow::Borrowed("F4A"),
1320
        Value::Vector(VectorValue::new_vec4(Vec4::ONE)),
1321
    );
1322
    const F4B_INNER: &AttributeInner = &AttributeInner::new(
1323
        Cow::Borrowed("F4B"),
1324
        Value::Vector(VectorValue::new_vec4(Vec4::ONE)),
1325
    );
1326
    const F4C_INNER: &AttributeInner = &AttributeInner::new(
1327
        Cow::Borrowed("F4C"),
1328
        Value::Vector(VectorValue::new_vec4(Vec4::ONE)),
1329
    );
1330
    const F4D_INNER: &AttributeInner = &AttributeInner::new(
1331
        Cow::Borrowed("F4D"),
1332
        Value::Vector(VectorValue::new_vec4(Vec4::ONE)),
1333
    );
1334

1335
    const F4A: Attribute = Attribute(F4A_INNER);
1336
    const F4B: Attribute = Attribute(F4B_INNER);
1337
    const F4C: Attribute = Attribute(F4C_INNER);
1338
    const F4D: Attribute = Attribute(F4D_INNER);
1339

1340
    #[test]
1341
    fn slice_ref() {
1342
        let l16 = ParticleLayout::new().append(F4A).build();
1343
        assert_eq!(16, l16.size());
1344
        let l32 = ParticleLayout::new().append(F4A).append(F4B).build();
1345
        assert_eq!(32, l32.size());
1346
        let l48 = ParticleLayout::new()
1347
            .append(F4A)
1348
            .append(F4B)
1349
            .append(F4C)
1350
            .build();
1351
        assert_eq!(48, l48.size());
1352
        for (range, particle_layout, len, byte_size) in [
1353
            (0..0, &l16, 0, 0),
1354
            (0..16, &l16, 16, 16 * 16),
1355
            (0..16, &l32, 16, 16 * 32),
1356
            (240..256, &l48, 16, 16 * 48),
1357
        ] {
1358
            let sr = SlabSliceRef {
1359
                range,
1360
                particle_layout: particle_layout.clone(),
1361
            };
1362
            assert_eq!(sr.len(), len);
1363
            assert_eq!(sr.byte_size(), byte_size);
1364
        }
1365
    }
1366

1367
    #[test]
1368
    fn effect_buffer() {
1369
        let renderer = MockRenderer::new();
1370
        let render_device = renderer.device();
1371

1372
        let l64 = ParticleLayout::new()
1373
            .append(F4A)
1374
            .append(F4B)
1375
            .append(F4C)
1376
            .append(F4D)
1377
            .build();
1378
        assert_eq!(64, l64.size());
1379

1380
        let asset = Handle::<EffectAsset>::default();
1381
        let capacity = 4096;
1382
        let mut buffer = ParticleSlab::new(
1383
            SlabId::new(42),
1384
            asset,
1385
            capacity,
1386
            l64.clone(),
1387
            &render_device,
1388
        );
1389

1390
        assert_eq!(buffer.capacity, capacity.max(ParticleSlab::MIN_CAPACITY));
1391
        assert_eq!(64, buffer.particle_layout.size());
1392
        assert_eq!(64, buffer.particle_layout.min_binding_size().get());
1393
        assert_eq!(0, buffer.used_size);
1394
        assert!(buffer.free_slices.is_empty());
1395

1396
        assert_eq!(None, buffer.allocate(buffer.capacity + 1));
1397

1398
        let mut offset = 0;
1399
        let mut slices = vec![];
1400
        for size in [32, 128, 55, 148, 1, 2048, 42] {
1401
            let slice = buffer.allocate(size);
1402
            assert!(slice.is_some());
1403
            let slice = slice.unwrap();
1404
            assert_eq!(64, slice.particle_layout.size());
1405
            assert_eq!(64, buffer.particle_layout.min_binding_size().get());
1406
            assert_eq!(offset..offset + size, slice.range);
1407
            slices.push(slice);
1408
            offset += size;
1409
        }
1410
        assert_eq!(offset, buffer.used_size);
1411

1412
        assert_eq!(SlabState::Used, buffer.free_slice(slices[2].clone()));
1413
        assert_eq!(1, buffer.free_slices.len());
1414
        let free_slice = &buffer.free_slices[0];
1415
        assert_eq!(160..215, *free_slice);
1416
        assert_eq!(offset, buffer.used_size); // didn't move
1417

1418
        assert_eq!(SlabState::Used, buffer.free_slice(slices[3].clone()));
1419
        assert_eq!(SlabState::Used, buffer.free_slice(slices[4].clone()));
1420
        assert_eq!(SlabState::Used, buffer.free_slice(slices[5].clone()));
1421
        assert_eq!(4, buffer.free_slices.len());
1422
        assert_eq!(offset, buffer.used_size); // didn't move
1423

1424
        // this will collapse all the way to slices[1], the highest allocated
1425
        assert_eq!(SlabState::Used, buffer.free_slice(slices[6].clone()));
1426
        assert_eq!(0, buffer.free_slices.len()); // collapsed
1427
        assert_eq!(160, buffer.used_size); // collapsed
1428

1429
        assert_eq!(SlabState::Used, buffer.free_slice(slices[0].clone()));
1430
        assert_eq!(1, buffer.free_slices.len());
1431
        assert_eq!(160, buffer.used_size); // didn't move
1432

1433
        // collapse all, and free buffer
1434
        assert_eq!(SlabState::Free, buffer.free_slice(slices[1].clone()));
1435
        assert_eq!(0, buffer.free_slices.len());
1436
        assert_eq!(0, buffer.used_size); // collapsed and empty
1437
    }
1438

1439
    #[test]
1440
    fn pop_free_slice() {
1441
        let renderer = MockRenderer::new();
1442
        let render_device = renderer.device();
1443

1444
        let l64 = ParticleLayout::new()
1445
            .append(F4A)
1446
            .append(F4B)
1447
            .append(F4C)
1448
            .append(F4D)
1449
            .build();
1450
        assert_eq!(64, l64.size());
1451

1452
        let asset = Handle::<EffectAsset>::default();
1453
        let capacity = 2048; // ParticleSlab::MIN_CAPACITY;
1454
        assert!(capacity >= 2048); // otherwise the logic below breaks
1455
        let mut buffer = ParticleSlab::new(
1456
            SlabId::new(42),
1457
            asset,
1458
            capacity,
1459
            l64.clone(),
1460
            &render_device,
1461
        );
1462

1463
        let slice0 = buffer.allocate(32);
1464
        assert!(slice0.is_some());
1465
        let slice0 = slice0.unwrap();
1466
        assert_eq!(slice0.range, 0..32);
1467
        assert!(buffer.free_slices.is_empty());
1468

1469
        let slice1 = buffer.allocate(1024);
1470
        assert!(slice1.is_some());
1471
        let slice1 = slice1.unwrap();
1472
        assert_eq!(slice1.range, 32..1056);
1473
        assert!(buffer.free_slices.is_empty());
1474

1475
        let state = buffer.free_slice(slice0);
1476
        assert_eq!(state, SlabState::Used);
1477
        assert_eq!(buffer.free_slices.len(), 1);
1478
        assert_eq!(buffer.free_slices[0], 0..32);
1479

1480
        // Try to allocate a slice larger than slice0, such that slice0 cannot be
1481
        // recycled, and instead the new slice has to be appended after all
1482
        // existing ones.
1483
        let slice2 = buffer.allocate(64);
1484
        assert!(slice2.is_some());
1485
        let slice2 = slice2.unwrap();
1486
        assert_eq!(slice2.range.start, slice1.range.end); // after slice1
1487
        assert_eq!(slice2.range, 1056..1120);
1488
        assert_eq!(buffer.free_slices.len(), 1);
1489

1490
        // Now allocate a small slice that fits, to recycle (part of) slice0.
1491
        let slice3 = buffer.allocate(16);
1492
        assert!(slice3.is_some());
1493
        let slice3 = slice3.unwrap();
1494
        assert_eq!(slice3.range, 0..16);
1495
        assert_eq!(buffer.free_slices.len(), 1); // split
1496
        assert_eq!(buffer.free_slices[0], 16..32);
1497

1498
        // Allocate a second small slice that fits exactly the left space, completely
1499
        // recycling
1500
        let slice4 = buffer.allocate(16);
1501
        assert!(slice4.is_some());
1502
        let slice4 = slice4.unwrap();
1503
        assert_eq!(slice4.range, 16..32);
1504
        assert!(buffer.free_slices.is_empty()); // recycled
1505
    }
1506

1507
    #[test]
1508
    fn effect_cache() {
1509
        let renderer = MockRenderer::new();
1510
        let render_device = renderer.device();
1511

1512
        let l32 = ParticleLayout::new().append(F4A).append(F4B).build();
1513
        assert_eq!(32, l32.size());
1514

1515
        let mut effect_cache = EffectCache::new(render_device);
1516
        assert_eq!(effect_cache.slabs().len(), 0);
1517

1518
        let asset = Handle::<EffectAsset>::default();
1519
        let capacity = ParticleSlab::MIN_CAPACITY;
1520
        let item_size = l32.size();
1521

1522
        // Insert an effect
1523
        let effect1 = effect_cache.insert(asset.clone(), capacity, &l32);
1524
        //assert!(effect1.is_valid());
1525
        let slice1 = &effect1.slice;
1526
        assert_eq!(slice1.len(), capacity);
1527
        assert_eq!(
1528
            slice1.particle_layout.min_binding_size().get() as u32,
1529
            item_size
1530
        );
1531
        assert_eq!(slice1.range, 0..capacity);
1532
        assert_eq!(effect_cache.slabs().len(), 1);
1533

1534
        // Insert a second copy of the same effect
1535
        let effect2 = effect_cache.insert(asset.clone(), capacity, &l32);
1536
        //assert!(effect2.is_valid());
1537
        let slice2 = &effect2.slice;
1538
        assert_eq!(slice2.len(), capacity);
1539
        assert_eq!(
1540
            slice2.particle_layout.min_binding_size().get() as u32,
1541
            item_size
1542
        );
1543
        assert_eq!(slice2.range, 0..capacity);
1544
        assert_eq!(effect_cache.slabs().len(), 2);
1545

1546
        // Remove the first effect instance
1547
        let buffer_state = effect_cache.remove(&effect1).unwrap();
1548
        // Note: currently batching is disabled, so each instance has its own buffer,
1549
        // which becomes unused once the instance is destroyed.
1550
        assert_eq!(buffer_state, SlabState::Free);
1551
        assert_eq!(effect_cache.slabs().len(), 2);
1552
        {
1553
            let slabs = effect_cache.slabs();
1554
            assert!(slabs[0].is_none());
1555
            assert!(slabs[1].is_some()); // id2
1556
        }
1557

1558
        // Regression #60
1559
        let effect3 = effect_cache.insert(asset, capacity, &l32);
1560
        //assert!(effect3.is_valid());
1561
        let slice3 = &effect3.slice;
1562
        assert_eq!(slice3.len(), capacity);
1563
        assert_eq!(
1564
            slice3.particle_layout.min_binding_size().get() as u32,
1565
            item_size
1566
        );
1567
        assert_eq!(slice3.range, 0..capacity);
1568
        // Note: currently batching is disabled, so each instance has its own buffer.
1569
        assert_eq!(effect_cache.slabs().len(), 2);
1570
        {
1571
            let slabs = effect_cache.slabs();
1572
            assert!(slabs[0].is_some()); // id3
1573
            assert!(slabs[1].is_some()); // id2
1574
        }
1575
    }
1576
}
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