• 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

40.34
/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, system::Resource},
10
    log::{trace, warn},
11
    render::{render_resource::*, renderer::RenderDevice},
12
    utils::{default, HashMap},
13
};
14
use bytemuck::cast_slice_mut;
15

16
use super::{buffer_table::BufferTableId, BufferBindingSource};
17
use crate::{
18
    asset::EffectAsset,
19
    render::{
20
        calc_hash, event::GpuChildInfo, GpuEffectMetadata, GpuSpawnerParams, LayoutFlags,
21
        StorageType as _, INDIRECT_INDEX_SIZE,
22
    },
23
    ParticleLayout,
24
};
25

26
/// Describes all particle slices of particles in the particle buffer
27
/// for a single effect.
28
#[derive(Debug, Clone, PartialEq, Eq)]
29
pub struct EffectSlice {
30
    /// Slice into the underlying [`BufferVec`].
31
    ///
32
    /// This is measured in items, not bytes.
33
    pub slice: Range<u32>,
34
    /// Index of the buffer in the [`EffectCache`].
35
    pub buffer_index: u32,
36
    /// Particle layout of the effect.
37
    pub particle_layout: ParticleLayout,
38
}
39

40
impl Ord for EffectSlice {
41
    fn cmp(&self, other: &Self) -> Ordering {
8✔
42
        match self.buffer_index.cmp(&other.buffer_index) {
8✔
43
            Ordering::Equal => self.slice.start.cmp(&other.slice.start),
4✔
44
            ord => ord,
4✔
45
        }
46
    }
47
}
48

49
impl PartialOrd for EffectSlice {
50
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
8✔
51
        Some(self.cmp(other))
8✔
52
    }
53
}
54

55
/// A reference to a slice allocated inside an [`EffectBuffer`].
56
#[derive(Debug, Default, Clone, PartialEq, Eq)]
57
pub struct SliceRef {
58
    /// Range into an [`EffectBuffer`], in item count.
59
    range: Range<u32>,
60
    pub(crate) particle_layout: ParticleLayout,
61
}
62

63
impl SliceRef {
64
    /// The length of the slice, in number of items.
65
    #[allow(dead_code)]
66
    pub fn len(&self) -> u32 {
11✔
67
        self.range.end - self.range.start
11✔
68
    }
69

70
    /// The size in bytes of the slice.
71
    #[allow(dead_code)]
72
    pub fn byte_size(&self) -> usize {
4✔
73
        (self.len() as usize) * (self.particle_layout.min_binding_size().get() as usize)
4✔
74
    }
75

NEW
76
    pub fn range(&self) -> Range<u32> {
×
NEW
77
        self.range.clone()
×
78
    }
79
}
80

81
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
82
struct SimBindGroupKey {
83
    buffer: Option<BufferId>,
84
    offset: u32,
85
    size: u32,
86
}
87

88
impl SimBindGroupKey {
89
    /// Invalid key, often used as placeholder.
90
    pub const INVALID: Self = Self {
91
        buffer: None,
92
        offset: u32::MAX,
93
        size: 0,
94
    };
95
}
96

97
impl From<&BufferBindingSource> for SimBindGroupKey {
NEW
98
    fn from(value: &BufferBindingSource) -> Self {
×
99
        Self {
NEW
100
            buffer: Some(value.buffer.id()),
×
NEW
101
            offset: value.offset,
×
NEW
102
            size: value.size.get(),
×
103
        }
104
    }
105
}
106

107
impl From<Option<&BufferBindingSource>> for SimBindGroupKey {
NEW
108
    fn from(value: Option<&BufferBindingSource>) -> Self {
×
NEW
109
        if let Some(bbs) = value {
×
110
            Self {
111
                buffer: Some(bbs.buffer.id()),
112
                offset: bbs.offset,
113
                size: bbs.size.get(),
114
            }
115
        } else {
NEW
116
            Self::INVALID
×
117
        }
118
    }
119
}
120

121
/// Storage for a single kind of effects, sharing the same buffer(s).
122
///
123
/// Currently only accepts a single unique item size (particle size), fixed at
124
/// creation.
125
///
126
/// Also currently only accepts instances of a unique effect asset, although
127
/// this restriction is purely for convenience and may be relaxed in the future
128
/// to improve batching.
129
#[derive(Debug)]
130
pub struct EffectBuffer {
131
    /// GPU buffer holding all particles for the entire group of effects.
132
    particle_buffer: Buffer,
133
    /// GPU buffer holding the indirection indices for the entire group of
134
    /// effects. This is a triple buffer containing:
135
    /// - the ping-pong alive particles and render indirect indices at offsets 0
136
    ///   and 1
137
    /// - the dead particle indices at offset 2
138
    indirect_index_buffer: Buffer,
139
    /// Layout of particles.
140
    particle_layout: ParticleLayout,
141
    /// Layout of the particle@1 bind group for the render pass.
142
    render_particles_buffer_layout: BindGroupLayout,
143
    /// Total buffer capacity, in number of particles.
144
    capacity: u32,
145
    /// Used buffer size, in number of particles, either from allocated slices
146
    /// or from slices in the free list.
147
    used_size: u32,
148
    /// Array of free slices for new allocations, sorted in increasing order in
149
    /// the buffer.
150
    free_slices: Vec<Range<u32>>,
151
    /// Compute pipeline for the effect update pass.
152
    // pub compute_pipeline: ComputePipeline, // FIXME - ComputePipelineId, to avoid duplicating per
153
    // instance!
154
    /// Handle of all effects common in this buffer. TODO - replace with
155
    /// compatible layout.
156
    asset: Handle<EffectAsset>,
157
    /// Bind group particle@1 of the simulation passes (init and udpate).
158
    sim_bind_group: Option<BindGroup>,
159
    /// Key the `sim_bind_group` was created from.
160
    sim_bind_group_key: SimBindGroupKey,
161
}
162

163
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
164
pub enum BufferState {
165
    /// The buffer is in use, with allocated resources.
166
    Used,
167
    /// Like `Used`, but the buffer was resized, so any bind group is
168
    /// nonetheless invalid.
169
    Resized,
170
    /// The buffer is free (its resources were deallocated).
171
    Free,
172
}
173

174
impl EffectBuffer {
175
    /// Minimum buffer capacity to allocate, in number of particles.
176
    // FIXME - Batching is broken due to binding a single GpuSpawnerParam instead of
177
    // N, and inability for a particle index to tell which Spawner it should
178
    // use. Setting this to 1 effectively ensures that all new buffers just fit
179
    // the effect, so batching never occurs.
180
    pub const MIN_CAPACITY: u32 = 1; // 65536; // at least 64k particles
181

182
    /// Create a new group and a GPU buffer to back it up.
183
    ///
184
    /// The buffer cannot contain less than [`MIN_CAPACITY`] particles. If
185
    /// `capacity` is smaller, it's rounded up to [`MIN_CAPACITY`].
186
    ///
187
    /// [`MIN_CAPACITY`]: EffectBuffer::MIN_CAPACITY
188
    pub fn new(
5✔
189
        buffer_index: u32,
190
        asset: Handle<EffectAsset>,
191
        capacity: u32,
192
        particle_layout: ParticleLayout,
193
        layout_flags: LayoutFlags,
194
        render_device: &RenderDevice,
195
    ) -> Self {
196
        trace!(
5✔
NEW
197
            "EffectBuffer::new(buffer_index={}, capacity={}, particle_layout={:?}, layout_flags={:?}, item_size={}B)",
×
NEW
198
            buffer_index,
×
199
            capacity,
×
200
            particle_layout,
×
201
            layout_flags,
×
202
            particle_layout.min_binding_size().get(),
×
203
        );
204

205
        // Calculate the clamped capacity of the group, in number of particles.
206
        let capacity = capacity.max(Self::MIN_CAPACITY);
5✔
207
        debug_assert!(
5✔
208
            capacity > 0,
5✔
209
            "Attempted to create a zero-sized effect buffer."
×
210
        );
211

212
        // Allocate the particle buffer itself, containing the attributes of each
213
        // particle.
214
        let particle_capacity_bytes: BufferAddress =
5✔
215
            capacity as u64 * particle_layout.min_binding_size().get();
5✔
216
        let particle_label = format!("hanabi:buffer:vfx{buffer_index}_particle");
5✔
217
        let particle_buffer = render_device.create_buffer(&BufferDescriptor {
5✔
218
            label: Some(&particle_label),
5✔
219
            size: particle_capacity_bytes,
5✔
220
            usage: BufferUsages::COPY_DST | BufferUsages::STORAGE,
5✔
221
            mapped_at_creation: false,
5✔
222
        });
223

224
        // Each indirect buffer stores 3 arrays of u32, of length the number of
225
        // particles.
226
        let capacity_bytes: BufferAddress = capacity as u64 * 4 * 3;
5✔
227

228
        let indirect_label = format!("hanabi:buffer:vfx{buffer_index}_indirect");
5✔
229
        let indirect_index_buffer = render_device.create_buffer(&BufferDescriptor {
5✔
230
            label: Some(&indirect_label),
5✔
231
            size: capacity_bytes,
5✔
232
            usage: BufferUsages::COPY_DST | BufferUsages::STORAGE,
5✔
233
            mapped_at_creation: true,
5✔
234
        });
235
        // Set content
236
        {
237
            // Scope get_mapped_range_mut() to force a drop before unmap()
238
            {
239
                let slice: &mut [u8] = &mut indirect_index_buffer
5✔
240
                    .slice(..capacity_bytes)
5✔
241
                    .get_mapped_range_mut();
5✔
242
                let slice: &mut [u32] = cast_slice_mut(slice);
5✔
243
                for index in 0..capacity {
12,294✔
244
                    slice[3 * index as usize + 2] = capacity - 1 - index;
6,147✔
245
                }
246
            }
247
            indirect_index_buffer.unmap();
5✔
248
        }
249

250
        // Create the render layout.
251
        let spawner_params_size = GpuSpawnerParams::aligned_size(
252
            render_device.limits().min_storage_buffer_offset_alignment,
5✔
253
        );
254
        let entries = [
5✔
255
            // @group(1) @binding(0) var<storage, read> particle_buffer : ParticleBuffer;
256
            BindGroupLayoutEntry {
5✔
257
                binding: 0,
5✔
258
                visibility: ShaderStages::VERTEX,
5✔
259
                ty: BindingType::Buffer {
5✔
260
                    ty: BufferBindingType::Storage { read_only: true },
5✔
261
                    has_dynamic_offset: false,
5✔
262
                    min_binding_size: Some(particle_layout.min_binding_size()),
5✔
263
                },
264
                count: None,
5✔
265
            },
266
            // @group(1) @binding(1) var<storage, read> indirect_buffer : IndirectBuffer;
267
            BindGroupLayoutEntry {
5✔
268
                binding: 1,
5✔
269
                visibility: ShaderStages::VERTEX,
5✔
270
                ty: BindingType::Buffer {
5✔
271
                    ty: BufferBindingType::Storage { read_only: true },
5✔
272
                    has_dynamic_offset: false,
5✔
273
                    min_binding_size: Some(NonZeroU64::new(INDIRECT_INDEX_SIZE as u64).unwrap()),
5✔
274
                },
275
                count: None,
5✔
276
            },
277
            // @group(1) @binding(2) var<storage, read> spawner : Spawner;
278
            BindGroupLayoutEntry {
5✔
279
                binding: 2,
5✔
280
                visibility: ShaderStages::VERTEX,
5✔
281
                ty: BindingType::Buffer {
5✔
282
                    ty: BufferBindingType::Storage { read_only: true },
5✔
283
                    has_dynamic_offset: true,
5✔
284
                    min_binding_size: Some(spawner_params_size),
5✔
285
                },
286
                count: None,
5✔
287
            },
288
        ];
289
        let label = format!("hanabi:bind_group_layout:render:particles@1:vfx{buffer_index}");
5✔
290
        trace!(
5✔
NEW
291
            "Creating render layout '{}' with {} entries (flags: {:?})",
×
NEW
292
            label,
×
UNCOV
293
            entries.len(),
×
294
            layout_flags
295
        );
296
        let render_particles_buffer_layout =
5✔
297
            render_device.create_bind_group_layout(&label[..], &entries[..]);
5✔
298

299
        Self {
300
            particle_buffer,
301
            indirect_index_buffer,
302
            particle_layout,
303
            render_particles_buffer_layout,
304
            capacity,
305
            used_size: 0,
306
            free_slices: vec![],
5✔
307
            asset,
308
            sim_bind_group: None,
309
            sim_bind_group_key: SimBindGroupKey::INVALID,
310
        }
311
    }
312

NEW
313
    pub fn render_particles_buffer_layout(&self) -> &BindGroupLayout {
×
NEW
314
        &self.render_particles_buffer_layout
×
315
    }
316

317
    #[inline]
NEW
318
    pub fn particle_buffer(&self) -> &Buffer {
×
NEW
319
        &self.particle_buffer
×
320
    }
321

322
    #[inline]
NEW
323
    pub fn indirect_index_buffer(&self) -> &Buffer {
×
NEW
324
        &self.indirect_index_buffer
×
325
    }
326

327
    #[inline]
NEW
328
    pub fn particle_offset(&self, row: u32) -> u32 {
×
NEW
329
        self.particle_layout.min_binding_size().get() as u32 * row
×
330
    }
331

332
    #[inline]
NEW
333
    pub fn indirect_index_offset(&self, row: u32) -> u32 {
×
NEW
334
        row * 12
×
335
    }
336

337
    /// Return a binding for the entire particle buffer.
338
    pub fn max_binding(&self) -> BindingResource {
×
339
        let capacity_bytes = self.capacity as u64 * self.particle_layout.min_binding_size().get();
×
340
        BindingResource::Buffer(BufferBinding {
×
341
            buffer: &self.particle_buffer,
×
342
            offset: 0,
×
343
            size: Some(NonZeroU64::new(capacity_bytes).unwrap()),
×
344
        })
345
    }
346

347
    /// Return a binding source for the entire particle buffer.
NEW
348
    pub fn max_binding_source(&self) -> BufferBindingSource {
×
NEW
349
        let capacity_bytes = self.capacity * self.particle_layout.min_binding_size32().get();
×
350
        BufferBindingSource {
NEW
351
            buffer: self.particle_buffer.clone(),
×
352
            offset: 0,
NEW
353
            size: NonZeroU32::new(capacity_bytes).unwrap(),
×
354
        }
355
    }
356

357
    /// Return a binding for the entire indirect buffer associated with the
358
    /// current effect buffer.
NEW
359
    pub fn indirect_index_max_binding(&self) -> BindingResource {
×
NEW
360
        let capacity_bytes = self.capacity as u64 * 12;
×
361
        BindingResource::Buffer(BufferBinding {
×
NEW
362
            buffer: &self.indirect_index_buffer,
×
363
            offset: 0,
×
NEW
364
            size: Some(NonZeroU64::new(capacity_bytes).unwrap()),
×
365
        })
366
    }
367

368
    /// Create the "particle" bind group @1 for the init and update passes if
369
    /// needed.
370
    ///
371
    /// The `buffer_index` must be the index of the current [`EffectBuffer`]
372
    /// inside the [`EffectCache`].
NEW
373
    pub fn create_particle_sim_bind_group(
×
374
        &mut self,
375
        layout: &BindGroupLayout,
376
        buffer_index: u32,
377
        render_device: &RenderDevice,
378
        parent_binding_source: Option<&BufferBindingSource>,
379
    ) {
NEW
380
        let key: SimBindGroupKey = parent_binding_source.into();
×
NEW
381
        if self.sim_bind_group.is_some() && self.sim_bind_group_key == key {
×
UNCOV
382
            return;
×
383
        }
384

NEW
385
        let label = format!("hanabi:bind_group:sim:particle@1:vfx{}", buffer_index);
×
386
        let entries: &[BindGroupEntry] =
NEW
387
            if let Some(parent_binding) = parent_binding_source.as_ref().map(|bbs| bbs.binding()) {
×
388
                &[
389
                    BindGroupEntry {
390
                        binding: 0,
391
                        resource: self.max_binding(),
392
                    },
393
                    BindGroupEntry {
394
                        binding: 1,
395
                        resource: self.indirect_index_max_binding(),
396
                    },
397
                    BindGroupEntry {
398
                        binding: 2,
399
                        resource: parent_binding,
400
                    },
401
                ]
402
            } else {
NEW
403
                &[
×
NEW
404
                    BindGroupEntry {
×
NEW
405
                        binding: 0,
×
NEW
406
                        resource: self.max_binding(),
×
407
                    },
NEW
408
                    BindGroupEntry {
×
NEW
409
                        binding: 1,
×
NEW
410
                        resource: self.indirect_index_max_binding(),
×
411
                    },
412
                ]
413
            };
414

415
        trace!(
NEW
416
            "Create particle simulation bind group '{}' with {} entries (has_parent:{})",
×
417
            label,
×
NEW
418
            entries.len(),
×
NEW
419
            parent_binding_source.is_some(),
×
420
        );
NEW
421
        let bind_group = render_device.create_bind_group(Some(&label[..]), layout, entries);
×
NEW
422
        self.sim_bind_group = Some(bind_group);
×
NEW
423
        self.sim_bind_group_key = key;
×
424
    }
425

426
    /// Invalidate any existing simulate bind group.
427
    ///
428
    /// Invalidate any existing bind group previously created by
429
    /// [`create_particle_sim_bind_group()`], generally because a buffer was
430
    /// re-allocated. This forces a re-creation of the bind group
431
    /// next time [`create_particle_sim_bind_group()`] is called.
432
    ///
433
    /// [`create_particle_sim_bind_group()`]: self::EffectBuffer::create_particle_sim_bind_group
434
    #[allow(dead_code)] // FIXME - review this...
NEW
435
    fn invalidate_particle_sim_bind_group(&mut self) {
×
NEW
436
        self.sim_bind_group = None;
×
NEW
437
        self.sim_bind_group_key = SimBindGroupKey::INVALID;
×
438
    }
439

440
    /// Return the cached particle@1 bind group for the simulation (init and
441
    /// update) passes.
442
    ///
443
    /// This is the per-buffer bind group at binding @1 which binds all
444
    /// per-buffer resources shared by all effect instances batched in a single
445
    /// buffer. The bind group is created by
446
    /// [`create_particle_sim_bind_group()`], and cached until a call to
447
    /// [`invalidate_particle_sim_bind_groups()`] clears the
448
    /// cached reference.
449
    ///
450
    /// [`create_particle_sim_bind_group()`]: self::EffectBuffer::create_particle_sim_bind_group
451
    /// [`invalidate_particle_sim_bind_groups()`]: self::EffectBuffer::invalidate_particle_sim_bind_groups
NEW
452
    pub fn particle_sim_bind_group(&self) -> Option<&BindGroup> {
×
NEW
453
        self.sim_bind_group.as_ref()
×
454
    }
455

456
    /// Try to recycle a free slice to store `size` items.
457
    fn pop_free_slice(&mut self, size: u32) -> Option<Range<u32>> {
17✔
458
        if self.free_slices.is_empty() {
17✔
459
            return None;
14✔
460
        }
461

462
        struct BestRange {
463
            range: Range<u32>,
464
            capacity: u32,
465
            index: usize,
466
        }
467

468
        let mut result = BestRange {
469
            range: 0..0, // marker for "invalid"
470
            capacity: u32::MAX,
471
            index: usize::MAX,
472
        };
473
        for (index, slice) in self.free_slices.iter().enumerate() {
3✔
474
            let capacity = slice.end - slice.start;
3✔
475
            if size > capacity {
3✔
476
                continue;
1✔
477
            }
478
            if capacity < result.capacity {
4✔
479
                result = BestRange {
2✔
480
                    range: slice.clone(),
2✔
481
                    capacity,
2✔
482
                    index,
2✔
483
                };
484
            }
485
        }
486
        if !result.range.is_empty() {
3✔
487
            if result.capacity > size {
2✔
488
                // split
489
                let start = result.range.start;
1✔
490
                let used_end = start + size;
1✔
491
                let free_end = result.range.end;
1✔
492
                let range = start..used_end;
1✔
493
                self.free_slices[result.index] = used_end..free_end;
1✔
494
                Some(range)
1✔
495
            } else {
496
                // recycle entirely
497
                self.free_slices.remove(result.index);
1✔
498
                Some(result.range)
1✔
499
            }
500
        } else {
501
            None
1✔
502
        }
503
    }
504

505
    /// Allocate a new slice in the buffer to store the particles of a single
506
    /// effect.
507
    pub fn allocate_slice(
18✔
508
        &mut self,
509
        capacity: u32,
510
        particle_layout: &ParticleLayout,
511
    ) -> Option<SliceRef> {
512
        trace!(
18✔
513
            "EffectBuffer::allocate_slice: capacity={} particle_layout={:?} item_size={}",
×
514
            capacity,
×
515
            particle_layout,
×
516
            particle_layout.min_binding_size().get(),
×
517
        );
518

519
        if capacity > self.capacity {
18✔
520
            return None;
1✔
521
        }
522

523
        let range = if let Some(range) = self.pop_free_slice(capacity) {
17✔
524
            range
525
        } else {
526
            let new_size = self.used_size.checked_add(capacity).unwrap();
15✔
527
            if new_size <= self.capacity {
15✔
528
                let range = self.used_size..new_size;
13✔
529
                self.used_size = new_size;
13✔
530
                range
13✔
531
            } else {
532
                if self.used_size == 0 {
2✔
533
                    warn!(
×
534
                        "Cannot allocate slice of size {} in effect cache buffer of capacity {}.",
×
535
                        capacity, self.capacity
536
                    );
537
                }
538
                return None;
2✔
539
            }
540
        };
541

NEW
542
        trace!("-> allocated slice {:?}", range);
×
543
        Some(SliceRef {
15✔
544
            range,
15✔
545
            particle_layout: particle_layout.clone(),
15✔
546
        })
547
    }
548

549
    /// Free an allocated slice, and if this was the last allocated slice also
550
    /// free the buffer.
551
    pub fn free_slice(&mut self, slice: SliceRef) -> BufferState {
9✔
552
        // If slice is at the end of the buffer, reduce total used size
553
        if slice.range.end == self.used_size {
9✔
554
            self.used_size = slice.range.start;
3✔
555
            // Check other free slices to further reduce used size and drain the free slice
556
            // list
557
            while let Some(free_slice) = self.free_slices.last() {
13✔
558
                if free_slice.end == self.used_size {
5✔
559
                    self.used_size = free_slice.start;
5✔
560
                    self.free_slices.pop();
5✔
561
                } else {
562
                    break;
×
563
                }
564
            }
565
            if self.used_size == 0 {
3✔
566
                assert!(self.free_slices.is_empty());
2✔
567
                // The buffer is not used anymore, free it too
568
                BufferState::Free
2✔
569
            } else {
570
                // There are still some slices used, the last one of which ends at
571
                // self.used_size
572
                BufferState::Used
1✔
573
            }
574
        } else {
575
            // Free slice is not at end; insert it in free list
576
            let range = slice.range;
6✔
577
            match self.free_slices.binary_search_by(|s| {
12✔
578
                if s.end <= range.start {
6✔
579
                    Ordering::Less
6✔
580
                } else if s.start >= range.end {
×
581
                    Ordering::Greater
×
582
                } else {
583
                    Ordering::Equal
×
584
                }
585
            }) {
586
                Ok(_) => warn!("Range {:?} already present in free list!", range),
×
587
                Err(index) => self.free_slices.insert(index, range),
6✔
588
            }
589
            BufferState::Used
6✔
590
        }
591
    }
592

593
    pub fn is_compatible(&self, handle: &Handle<EffectAsset>) -> bool {
2✔
594
        // TODO - replace with check particle layout is compatible to allow tighter
595
        // packing in less buffers, and update in the less dispatch calls
596
        *handle == self.asset
2✔
597
    }
598
}
599

600
/// A single cached effect (all groups) in the [`EffectCache`].
601
#[derive(Debug, Component)]
602
pub(crate) struct CachedEffect {
603
    /// Index into the [`EffectCache::buffers`] of the buffer storing the
604
    /// particles for this effect.
605
    pub buffer_index: u32,
606
    /// The effect slice within that buffer.
607
    pub slice: SliceRef,
608
}
609

610
/// The indices in the indirect dispatch buffers for a single effect, as well as
611
/// that of the metadata buffer.
612
#[derive(Debug, Default, Clone, Copy, Component)]
613
pub(crate) struct DispatchBufferIndices {
614
    /// The index of the [`GpuDispatchIndirect`] in
615
    /// [`EffectsMeta::update_dispatch_indirect_buffer`].
616
    ///
617
    /// [`EffectsMeta::update_dispatch_indirect_buffer`]: super::EffectsMeta::update_dispatch_indirect_buffer
618
    pub(crate) update_dispatch_indirect_buffer_table_id: BufferTableId,
619

620
    /// The index of the [`GpuEffectMetadata`] in
621
    /// [`EffectsMeta::effect_metadata_buffer`].
622
    ///
623
    /// [`EffectsMeta::effect_metadata_buffer`]: super::EffectsMeta::effect_metadata_buffer
624
    pub(crate) effect_metadata_buffer_table_id: BufferTableId,
625
}
626

627
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
628
struct ParticleBindGroupLayoutKey {
629
    pub min_binding_size: NonZeroU32,
630
    pub parent_min_binding_size: Option<NonZeroU32>,
631
}
632

633
/// Cache for effect instances sharing common GPU data structures.
634
#[derive(Resource)]
635
pub struct EffectCache {
636
    /// Render device the GPU resources (buffers) are allocated from.
637
    render_device: RenderDevice,
638
    /// Collection of effect buffers managed by this cache. Some buffers might
639
    /// be `None` if the entry is not used. Since the buffers are referenced
640
    /// by index, we cannot move them once they're allocated.
641
    buffers: Vec<Option<EffectBuffer>>,
642
    /// Cache of bind group layouts for the particle@1 bind groups of the
643
    /// simulation passes (init and update). Since all bindings depend only
644
    /// on buffers managed by the [`EffectCache`], we also cache the layouts
645
    /// here for convenience.
646
    particle_bind_group_layouts: HashMap<ParticleBindGroupLayoutKey, BindGroupLayout>,
647
    /// Cache of bind group layouts for the metadata@3 bind group of the init
648
    /// pass.
649
    metadata_init_bind_group_layout: [Option<BindGroupLayout>; 2],
650
    /// Cache of bind group layouts for the metadata@3 bind group of the
651
    /// updatepass.
652
    metadata_update_bind_group_layouts: HashMap<u32, BindGroupLayout>,
653
}
654

655
impl EffectCache {
656
    pub fn new(device: RenderDevice) -> Self {
1✔
657
        Self {
658
            render_device: device,
659
            buffers: vec![],
1✔
660
            particle_bind_group_layouts: default(),
1✔
661
            metadata_init_bind_group_layout: [None, None],
1✔
662
            metadata_update_bind_group_layouts: default(),
1✔
663
        }
664
    }
665

666
    /// Get all the buffer slots. Unallocated slots are `None`. This can be
667
    /// indexed by the buffer index.
668
    #[allow(dead_code)]
669
    #[inline]
670
    pub fn buffers(&self) -> &[Option<EffectBuffer>] {
7✔
671
        &self.buffers
7✔
672
    }
673

674
    /// Get all the buffer slots. Unallocated slots are `None`. This can be
675
    /// indexed by the buffer index.
676
    #[allow(dead_code)]
677
    #[inline]
678
    pub fn buffers_mut(&mut self) -> &mut [Option<EffectBuffer>] {
×
679
        &mut self.buffers
×
680
    }
681

682
    /// Fetch a specific buffer by index.
683
    #[allow(dead_code)]
684
    #[inline]
685
    pub fn get_buffer(&self, buffer_index: u32) -> Option<&EffectBuffer> {
×
686
        self.buffers.get(buffer_index as usize)?.as_ref()
×
687
    }
688

689
    /// Fetch a specific buffer by index.
690
    #[allow(dead_code)]
691
    #[inline]
692
    pub fn get_buffer_mut(&mut self, buffer_index: u32) -> Option<&mut EffectBuffer> {
×
693
        self.buffers.get_mut(buffer_index as usize)?.as_mut()
×
694
    }
695

696
    /// Invalidate all the particle@1 bind group for all buffers.
697
    ///
698
    /// This iterates over all valid buffers and calls
699
    /// [`EffectBuffer::invalidate_particle_sim_bind_group()`] on each one.
700
    #[allow(dead_code)] // FIXME - review this...
NEW
701
    pub fn invalidate_particle_sim_bind_groups(&mut self) {
×
702
        for buffer in self.buffers.iter_mut().flatten() {
×
NEW
703
            buffer.invalidate_particle_sim_bind_group();
×
704
        }
705
    }
706

707
    /// Insert a new effect instance in the cache.
708
    pub fn insert(
3✔
709
        &mut self,
710
        asset: Handle<EffectAsset>,
711
        capacity: u32,
712
        particle_layout: &ParticleLayout,
713
        layout_flags: LayoutFlags,
714
    ) -> CachedEffect {
715
        trace!("Inserting new effect into cache: capacity={capacity}");
3✔
716
        let (buffer_index, slice) = self
3✔
717
            .buffers
3✔
718
            .iter_mut()
719
            .enumerate()
720
            .find_map(|(buffer_index, buffer)| {
6✔
721
                if let Some(buffer) = buffer {
5✔
722
                    // The buffer must be compatible with the effect layout, to allow the update pass
723
                    // to update all particles at once from all compatible effects in a single dispatch.
724
                    if !buffer.is_compatible(&asset) {
725
                        return None;
×
726
                    }
727

728
                    // Try to allocate a slice into the buffer
729
                    buffer
2✔
730
                        .allocate_slice(capacity, particle_layout)
2✔
731
                        .map(|slice| (buffer_index, slice))
4✔
732
                } else {
733
                    None
1✔
734
                }
735
            })
736
            .unwrap_or_else(|| {
3✔
737
                // Cannot find any suitable buffer; allocate a new one
738
                let buffer_index = self.buffers.iter().position(|buf| buf.is_none()).unwrap_or(self.buffers.len());
8✔
739
                let byte_size = capacity.checked_mul(particle_layout.min_binding_size().get() as u32).unwrap_or_else(|| panic!(
3✔
740
                    "Effect size overflow: capacities={:?} particle_layout={:?} item_size={}",
×
NEW
741
                    capacity, particle_layout, particle_layout.min_binding_size().get()
×
742
                ));
743
                trace!(
3✔
744
                    "Creating new effect buffer #{} for effect {:?} (capacities={:?}, particle_layout={:?} item_size={}, byte_size={})",
×
745
                    buffer_index,
×
746
                    asset,
×
NEW
747
                    capacity,
×
748
                    particle_layout,
×
749
                    particle_layout.min_binding_size().get(),
×
750
                    byte_size
751
                );
752
                let mut buffer = EffectBuffer::new(
3✔
753
                    buffer_index as u32,
3✔
754
                    asset,
3✔
755
                    capacity,
3✔
756
                    particle_layout.clone(),
3✔
757
                    layout_flags,
3✔
758
                    &self.render_device,
3✔
759
                );
760
                let slice_ref = buffer.allocate_slice(capacity, particle_layout).unwrap();
3✔
761
                if buffer_index >= self.buffers.len() {
5✔
762
                    self.buffers.push(Some(buffer));
2✔
763
                } else {
764
                    debug_assert!(self.buffers[buffer_index].is_none());
2✔
765
                    self.buffers[buffer_index] = Some(buffer);
1✔
766
                }
767
                (buffer_index, slice_ref)
3✔
768
            });
769

770
        let slice = SliceRef {
771
            range: slice.range.clone(),
772
            particle_layout: slice.particle_layout,
773
        };
774

775
        trace!(
776
            "Insert effect buffer_index={} slice={}B particle_layout={:?}",
×
777
            buffer_index,
×
NEW
778
            slice.particle_layout.min_binding_size().get(),
×
779
            slice.particle_layout,
780
        );
781
        CachedEffect {
782
            buffer_index: buffer_index as u32,
3✔
783
            slice,
784
        }
785
    }
786

787
    /// Remove an effect from the cache. If this was the last effect, drop the
788
    /// underlying buffer and return the index of the dropped buffer.
789
    pub fn remove(&mut self, cached_effect: &CachedEffect) -> Result<BufferState, ()> {
1✔
790
        // Resolve the buffer by index
791
        let Some(maybe_buffer) = self.buffers.get_mut(cached_effect.buffer_index as usize) else {
2✔
NEW
792
            return Err(());
×
793
        };
794
        let Some(buffer) = maybe_buffer.as_mut() else {
1✔
NEW
795
            return Err(());
×
796
        };
797

798
        // Free the slice inside the resolved buffer
799
        if buffer.free_slice(cached_effect.slice.clone()) == BufferState::Free {
800
            *maybe_buffer = None;
1✔
801
            return Ok(BufferState::Free);
1✔
802
        }
803

NEW
804
        Ok(BufferState::Used)
×
805
    }
806

807
    //
808
    // Bind group layouts
809
    //
810

811
    /// Ensure a bind group layout exists for the bind group @1 ("particles")
812
    /// for use with the given min binding sizes.
NEW
813
    pub fn ensure_particle_bind_group_layout(
×
814
        &mut self,
815
        min_binding_size: NonZeroU32,
816
        parent_min_binding_size: Option<NonZeroU32>,
817
    ) -> &BindGroupLayout {
818
        // FIXME - This "ensure" pattern means we never de-allocate entries. This is
819
        // probably fine, because there's a limited number of realistic combinations,
820
        // but could cause wastes if e.g. loading widely different scenes.
821
        let key = ParticleBindGroupLayoutKey {
822
            min_binding_size,
823
            parent_min_binding_size,
824
        };
NEW
825
        self.particle_bind_group_layouts
×
NEW
826
            .entry(key)
×
NEW
827
            .or_insert_with(|| {
×
NEW
828
                trace!("Creating new particle sim bind group @1 for min_binding_size={} parent_min_binding_size={:?}", min_binding_size, parent_min_binding_size);
×
NEW
829
                create_particle_sim_bind_group_layout(
×
NEW
830
                    &self.render_device,
×
NEW
831
                    min_binding_size,
×
NEW
832
                    parent_min_binding_size,
×
833
                )
834
            })
835
    }
836

837
    /// Get the bind group layout for the bind group @1 ("particles") for use
838
    /// with the given min binding sizes.
NEW
839
    pub fn particle_bind_group_layout(
×
840
        &self,
841
        min_binding_size: NonZeroU32,
842
        parent_min_binding_size: Option<NonZeroU32>,
843
    ) -> Option<&BindGroupLayout> {
844
        let key = ParticleBindGroupLayoutKey {
845
            min_binding_size,
846
            parent_min_binding_size,
847
        };
NEW
848
        self.particle_bind_group_layouts.get(&key)
×
849
    }
850

851
    /// Ensure a bind group layout exists for the metadata@3 bind group of
852
    /// the init pass.
NEW
853
    pub fn ensure_metadata_init_bind_group_layout(&mut self, consume_gpu_spawn_events: bool) {
×
NEW
854
        let layout = &mut self.metadata_init_bind_group_layout[consume_gpu_spawn_events as usize];
×
NEW
855
        if layout.is_none() {
×
NEW
856
            *layout = Some(create_metadata_init_bind_group_layout(
×
NEW
857
                &self.render_device,
×
NEW
858
                consume_gpu_spawn_events,
×
859
            ));
860
        }
861
    }
862

863
    /// Get the bind group layout for the metadata@3 bind group of the init
864
    /// pass.
NEW
865
    pub fn metadata_init_bind_group_layout(
×
866
        &self,
867
        consume_gpu_spawn_events: bool,
868
    ) -> Option<&BindGroupLayout> {
NEW
869
        self.metadata_init_bind_group_layout[consume_gpu_spawn_events as usize].as_ref()
×
870
    }
871

872
    /// Ensure a bind group layout exists for the metadata@3 bind group of
873
    /// the update pass.
NEW
874
    pub fn ensure_metadata_update_bind_group_layout(&mut self, num_event_buffers: u32) {
×
NEW
875
        self.metadata_update_bind_group_layouts
×
NEW
876
            .entry(num_event_buffers)
×
NEW
877
            .or_insert_with(|| {
×
NEW
878
                create_metadata_update_bind_group_layout(&self.render_device, num_event_buffers)
×
879
            });
880
    }
881

882
    /// Get the bind group layout for the metadata@3 bind group of the
883
    /// update pass.
NEW
884
    pub fn metadata_update_bind_group_layout(
×
885
        &self,
886
        num_event_buffers: u32,
887
    ) -> Option<&BindGroupLayout> {
NEW
888
        self.metadata_update_bind_group_layouts
×
NEW
889
            .get(&num_event_buffers)
×
890
    }
891

892
    //
893
    // Bind groups
894
    //
895

896
    /// Get the "particle" bind group for the simulation (init and update)
897
    /// passes a cached effect stored in a given GPU particle buffer.
NEW
898
    pub fn particle_sim_bind_group(&self, buffer_index: u32) -> Option<&BindGroup> {
×
NEW
899
        self.buffers[buffer_index as usize]
×
900
            .as_ref()
NEW
901
            .and_then(|eb| eb.particle_sim_bind_group())
×
902
    }
903

NEW
904
    pub fn create_particle_sim_bind_group(
×
905
        &mut self,
906
        buffer_index: u32,
907
        render_device: &RenderDevice,
908
        min_binding_size: NonZeroU32,
909
        parent_min_binding_size: Option<NonZeroU32>,
910
        parent_binding_source: Option<&BufferBindingSource>,
911
    ) -> Result<(), ()> {
912
        // Create the bind group
NEW
913
        let layout = self
×
NEW
914
            .ensure_particle_bind_group_layout(min_binding_size, parent_min_binding_size)
×
915
            .clone();
NEW
916
        let slot = self.buffers.get_mut(buffer_index as usize).ok_or(())?;
×
NEW
917
        let effect_buffer = slot.as_mut().ok_or(())?;
×
918
        effect_buffer.create_particle_sim_bind_group(
919
            &layout,
920
            buffer_index,
921
            render_device,
922
            parent_binding_source,
923
        );
924
        Ok(())
925
    }
926
}
927

928
/// Create the bind group layout for the "particle" group (@1) of the init and
929
/// update passes.
NEW
930
fn create_particle_sim_bind_group_layout(
×
931
    render_device: &RenderDevice,
932
    particle_layout_min_binding_size: NonZeroU32,
933
    parent_particle_layout_min_binding_size: Option<NonZeroU32>,
934
) -> BindGroupLayout {
NEW
935
    let mut entries = Vec::with_capacity(3);
×
936

937
    // @group(1) @binding(0) var<storage, read_write> particle_buffer :
938
    // ParticleBuffer
NEW
939
    entries.push(BindGroupLayoutEntry {
×
NEW
940
        binding: 0,
×
NEW
941
        visibility: ShaderStages::COMPUTE,
×
NEW
942
        ty: BindingType::Buffer {
×
NEW
943
            ty: BufferBindingType::Storage { read_only: false },
×
NEW
944
            has_dynamic_offset: false,
×
NEW
945
            min_binding_size: Some(particle_layout_min_binding_size.into()),
×
946
        },
NEW
947
        count: None,
×
948
    });
949

950
    // @group(1) @binding(1) var<storage, read_write> indirect_buffer :
951
    // IndirectBuffer
NEW
952
    entries.push(BindGroupLayoutEntry {
×
NEW
953
        binding: 1,
×
NEW
954
        visibility: ShaderStages::COMPUTE,
×
NEW
955
        ty: BindingType::Buffer {
×
NEW
956
            ty: BufferBindingType::Storage { read_only: false },
×
NEW
957
            has_dynamic_offset: false,
×
NEW
958
            min_binding_size: Some(NonZeroU64::new(INDIRECT_INDEX_SIZE as _).unwrap()),
×
959
        },
NEW
960
        count: None,
×
961
    });
962

963
    // @group(1) @binding(2) var<storage, read> parent_particle_buffer :
964
    // ParentParticleBuffer;
NEW
965
    if let Some(min_binding_size) = parent_particle_layout_min_binding_size {
×
966
        entries.push(BindGroupLayoutEntry {
967
            binding: 2,
968
            visibility: ShaderStages::COMPUTE,
969
            ty: BindingType::Buffer {
970
                ty: BufferBindingType::Storage { read_only: true },
971
                has_dynamic_offset: false,
972
                min_binding_size: Some(min_binding_size.into()),
973
            },
974
            count: None,
975
        });
976
    }
977

NEW
978
    let hash = calc_hash(&entries);
×
NEW
979
    let label = format!("hanabi:bind_group_layout:sim:particles_{:016X}", hash);
×
NEW
980
    trace!(
×
NEW
981
        "Creating particle bind group layout '{}' for init pass with {} entries. (parent_buffer:{})",
×
NEW
982
        label,
×
NEW
983
        entries.len(),
×
NEW
984
        parent_particle_layout_min_binding_size.is_some(),
×
985
    );
NEW
986
    render_device.create_bind_group_layout(&label[..], &entries)
×
987
}
988

989
/// Create the bind group layout for the metadata@3 bind group of the init pass.
NEW
990
fn create_metadata_init_bind_group_layout(
×
991
    render_device: &RenderDevice,
992
    consume_gpu_spawn_events: bool,
993
) -> BindGroupLayout {
NEW
994
    let storage_alignment = render_device.limits().min_storage_buffer_offset_alignment;
×
NEW
995
    let effect_metadata_size = GpuEffectMetadata::aligned_size(storage_alignment);
×
996

NEW
997
    let mut entries = Vec::with_capacity(3);
×
998

999
    // @group(3) @binding(0) var<storage, read_write> effect_metadata :
1000
    // EffectMetadata;
NEW
1001
    entries.push(BindGroupLayoutEntry {
×
NEW
1002
        binding: 0,
×
NEW
1003
        visibility: ShaderStages::COMPUTE,
×
NEW
1004
        ty: BindingType::Buffer {
×
NEW
1005
            ty: BufferBindingType::Storage { read_only: false },
×
NEW
1006
            has_dynamic_offset: false,
×
1007
            // This WGSL struct is manually padded, so the Rust type GpuEffectMetadata doesn't
1008
            // reflect its true min size.
NEW
1009
            min_binding_size: Some(effect_metadata_size),
×
1010
        },
NEW
1011
        count: None,
×
1012
    });
1013

NEW
1014
    if consume_gpu_spawn_events {
×
1015
        // @group(3) @binding(1) var<storage, read> child_info_buffer : ChildInfoBuffer;
NEW
1016
        entries.push(BindGroupLayoutEntry {
×
NEW
1017
            binding: 1,
×
NEW
1018
            visibility: ShaderStages::COMPUTE,
×
NEW
1019
            ty: BindingType::Buffer {
×
NEW
1020
                ty: BufferBindingType::Storage { read_only: true },
×
NEW
1021
                has_dynamic_offset: false,
×
NEW
1022
                min_binding_size: Some(GpuChildInfo::min_size()),
×
1023
            },
NEW
1024
            count: None,
×
1025
        });
1026

1027
        // @group(3) @binding(2) var<storage, read> event_buffer : EventBuffer;
NEW
1028
        entries.push(BindGroupLayoutEntry {
×
NEW
1029
            binding: 2,
×
NEW
1030
            visibility: ShaderStages::COMPUTE,
×
NEW
1031
            ty: BindingType::Buffer {
×
NEW
1032
                ty: BufferBindingType::Storage { read_only: true },
×
NEW
1033
                has_dynamic_offset: false,
×
NEW
1034
                min_binding_size: Some(NonZeroU64::new(4).unwrap()),
×
1035
            },
NEW
1036
            count: None,
×
1037
        });
1038
    }
1039

NEW
1040
    let hash = calc_hash(&entries);
×
NEW
1041
    let label = format!(
×
1042
        "hanabi:bind_group_layout:init:metadata@3_{}{:016X}",
NEW
1043
        if consume_gpu_spawn_events {
×
NEW
1044
            "events"
×
1045
        } else {
NEW
1046
            "noevent"
×
1047
        },
1048
        hash
1049
    );
NEW
1050
    trace!(
×
NEW
1051
        "Creating metadata@3 bind group layout '{}' for init pass with {} entries. (consume_gpu_spawn_events:{})",
×
NEW
1052
        label,
×
NEW
1053
        entries.len(),
×
1054
        consume_gpu_spawn_events,
1055
    );
NEW
1056
    render_device.create_bind_group_layout(&label[..], &entries)
×
1057
}
1058

1059
/// Create the bind group layout for the metadata@3 bind group of the update
1060
/// pass.
NEW
1061
fn create_metadata_update_bind_group_layout(
×
1062
    render_device: &RenderDevice,
1063
    num_event_buffers: u32,
1064
) -> BindGroupLayout {
NEW
1065
    let storage_alignment = render_device.limits().min_storage_buffer_offset_alignment;
×
NEW
1066
    let effect_metadata_size = GpuEffectMetadata::aligned_size(storage_alignment);
×
1067

NEW
1068
    let mut entries = Vec::with_capacity(num_event_buffers as usize + 2);
×
1069

1070
    // @group(3) @binding(0) var<storage, read_write> effect_metadata :
1071
    // EffectMetadata;
NEW
1072
    entries.push(BindGroupLayoutEntry {
×
NEW
1073
        binding: 0,
×
NEW
1074
        visibility: ShaderStages::COMPUTE,
×
NEW
1075
        ty: BindingType::Buffer {
×
NEW
1076
            ty: BufferBindingType::Storage { read_only: false },
×
NEW
1077
            has_dynamic_offset: false,
×
1078
            // This WGSL struct is manually padded, so the Rust type GpuEffectMetadata doesn't
1079
            // reflect its true min size.
NEW
1080
            min_binding_size: Some(effect_metadata_size),
×
1081
        },
NEW
1082
        count: None,
×
1083
    });
1084

NEW
1085
    if num_event_buffers > 0 {
×
1086
        // @group(3) @binding(1) var<storage, read_write> child_infos : array<ChildInfo,
1087
        // N>;
NEW
1088
        entries.push(BindGroupLayoutEntry {
×
NEW
1089
            binding: 1,
×
NEW
1090
            visibility: ShaderStages::COMPUTE,
×
NEW
1091
            ty: BindingType::Buffer {
×
NEW
1092
                ty: BufferBindingType::Storage { read_only: false },
×
NEW
1093
                has_dynamic_offset: false,
×
NEW
1094
                min_binding_size: Some(GpuChildInfo::min_size()),
×
1095
            },
NEW
1096
            count: None,
×
1097
        });
1098

NEW
1099
        for i in 0..num_event_buffers {
×
1100
            // @group(3) @binding(2+i) var<storage, read_write> event_buffer_#i :
1101
            // EventBuffer;
NEW
1102
            entries.push(BindGroupLayoutEntry {
×
NEW
1103
                binding: 2 + i,
×
NEW
1104
                visibility: ShaderStages::COMPUTE,
×
NEW
1105
                ty: BindingType::Buffer {
×
NEW
1106
                    ty: BufferBindingType::Storage { read_only: false },
×
NEW
1107
                    has_dynamic_offset: false,
×
NEW
1108
                    min_binding_size: Some(NonZeroU64::new(4).unwrap()),
×
1109
                },
NEW
1110
                count: None,
×
1111
            });
1112
        }
1113
    }
1114

NEW
1115
    let hash = calc_hash(&entries);
×
NEW
1116
    let label = format!("hanabi:bind_group_layout:update:metadata_{:016X}", hash);
×
NEW
1117
    trace!(
×
NEW
1118
        "Creating particle bind group layout '{}' for init update with {} entries. (num_event_buffers:{})",
×
NEW
1119
        label,
×
NEW
1120
        entries.len(),
×
1121
        num_event_buffers,
1122
    );
NEW
1123
    render_device.create_bind_group_layout(&label[..], &entries)
×
1124
}
1125

1126
#[cfg(all(test, feature = "gpu_tests"))]
1127
mod gpu_tests {
1128
    use std::borrow::Cow;
1129

1130
    use bevy::math::Vec4;
1131

1132
    use super::*;
1133
    use crate::{
1134
        graph::{Value, VectorValue},
1135
        test_utils::MockRenderer,
1136
        Attribute, AttributeInner,
1137
    };
1138

1139
    #[test]
1140
    fn effect_slice_ord() {
1141
        let particle_layout = ParticleLayout::new().append(Attribute::POSITION).build();
1142
        let slice1 = EffectSlice {
1143
            slice: 0..32,
1144
            buffer_index: 1,
1145
            particle_layout: particle_layout.clone(),
1146
        };
1147
        let slice2 = EffectSlice {
1148
            slice: 32..64,
1149
            buffer_index: 1,
1150
            particle_layout: particle_layout.clone(),
1151
        };
1152
        assert!(slice1 < slice2);
1153
        assert!(slice1 <= slice2);
1154
        assert!(slice2 > slice1);
1155
        assert!(slice2 >= slice1);
1156

1157
        let slice3 = EffectSlice {
1158
            slice: 0..32,
1159
            buffer_index: 0,
1160
            particle_layout,
1161
        };
1162
        assert!(slice3 < slice1);
1163
        assert!(slice3 < slice2);
1164
        assert!(slice1 > slice3);
1165
        assert!(slice2 > slice3);
1166
    }
1167

1168
    const F4A_INNER: &AttributeInner = &AttributeInner::new(
1169
        Cow::Borrowed("F4A"),
1170
        Value::Vector(VectorValue::new_vec4(Vec4::ONE)),
1171
    );
1172
    const F4B_INNER: &AttributeInner = &AttributeInner::new(
1173
        Cow::Borrowed("F4B"),
1174
        Value::Vector(VectorValue::new_vec4(Vec4::ONE)),
1175
    );
1176
    const F4C_INNER: &AttributeInner = &AttributeInner::new(
1177
        Cow::Borrowed("F4C"),
1178
        Value::Vector(VectorValue::new_vec4(Vec4::ONE)),
1179
    );
1180
    const F4D_INNER: &AttributeInner = &AttributeInner::new(
1181
        Cow::Borrowed("F4D"),
1182
        Value::Vector(VectorValue::new_vec4(Vec4::ONE)),
1183
    );
1184

1185
    const F4A: Attribute = Attribute(F4A_INNER);
1186
    const F4B: Attribute = Attribute(F4B_INNER);
1187
    const F4C: Attribute = Attribute(F4C_INNER);
1188
    const F4D: Attribute = Attribute(F4D_INNER);
1189

1190
    #[test]
1191
    fn slice_ref() {
1192
        let l16 = ParticleLayout::new().append(F4A).build();
1193
        assert_eq!(16, l16.size());
1194
        let l32 = ParticleLayout::new().append(F4A).append(F4B).build();
1195
        assert_eq!(32, l32.size());
1196
        let l48 = ParticleLayout::new()
1197
            .append(F4A)
1198
            .append(F4B)
1199
            .append(F4C)
1200
            .build();
1201
        assert_eq!(48, l48.size());
1202
        for (range, particle_layout, len, byte_size) in [
1203
            (0..0, &l16, 0, 0),
1204
            (0..16, &l16, 16, 16 * 16),
1205
            (0..16, &l32, 16, 16 * 32),
1206
            (240..256, &l48, 16, 16 * 48),
1207
        ] {
1208
            let sr = SliceRef {
1209
                range,
1210
                particle_layout: particle_layout.clone(),
1211
            };
1212
            assert_eq!(sr.len(), len);
1213
            assert_eq!(sr.byte_size(), byte_size);
1214
        }
1215
    }
1216

1217
    #[test]
1218
    fn effect_buffer() {
1219
        let renderer = MockRenderer::new();
1220
        let render_device = renderer.device();
1221

1222
        let l64 = ParticleLayout::new()
1223
            .append(F4A)
1224
            .append(F4B)
1225
            .append(F4C)
1226
            .append(F4D)
1227
            .build();
1228
        assert_eq!(64, l64.size());
1229

1230
        let asset = Handle::<EffectAsset>::default();
1231
        let capacity = 4096;
1232
        let mut buffer = EffectBuffer::new(
1233
            42,
1234
            asset,
1235
            capacity,
1236
            l64.clone(),
1237
            LayoutFlags::NONE,
1238
            &render_device,
1239
        );
1240

1241
        assert_eq!(buffer.capacity, capacity.max(EffectBuffer::MIN_CAPACITY));
1242
        assert_eq!(64, buffer.particle_layout.size());
1243
        assert_eq!(64, buffer.particle_layout.min_binding_size().get());
1244
        assert_eq!(0, buffer.used_size);
1245
        assert!(buffer.free_slices.is_empty());
1246

1247
        assert_eq!(None, buffer.allocate_slice(buffer.capacity + 1, &l64));
1248

1249
        let mut offset = 0;
1250
        let mut slices = vec![];
1251
        for size in [32, 128, 55, 148, 1, 2048, 42] {
1252
            let slice = buffer.allocate_slice(size, &l64);
1253
            assert!(slice.is_some());
1254
            let slice = slice.unwrap();
1255
            assert_eq!(64, slice.particle_layout.size());
1256
            assert_eq!(64, buffer.particle_layout.min_binding_size().get());
1257
            assert_eq!(offset..offset + size, slice.range);
1258
            slices.push(slice);
1259
            offset += size;
1260
        }
1261
        assert_eq!(offset, buffer.used_size);
1262

1263
        assert_eq!(BufferState::Used, buffer.free_slice(slices[2].clone()));
1264
        assert_eq!(1, buffer.free_slices.len());
1265
        let free_slice = &buffer.free_slices[0];
1266
        assert_eq!(160..215, *free_slice);
1267
        assert_eq!(offset, buffer.used_size); // didn't move
1268

1269
        assert_eq!(BufferState::Used, buffer.free_slice(slices[3].clone()));
1270
        assert_eq!(BufferState::Used, buffer.free_slice(slices[4].clone()));
1271
        assert_eq!(BufferState::Used, buffer.free_slice(slices[5].clone()));
1272
        assert_eq!(4, buffer.free_slices.len());
1273
        assert_eq!(offset, buffer.used_size); // didn't move
1274

1275
        // this will collapse all the way to slices[1], the highest allocated
1276
        assert_eq!(BufferState::Used, buffer.free_slice(slices[6].clone()));
1277
        assert_eq!(0, buffer.free_slices.len()); // collapsed
1278
        assert_eq!(160, buffer.used_size); // collapsed
1279

1280
        assert_eq!(BufferState::Used, buffer.free_slice(slices[0].clone()));
1281
        assert_eq!(1, buffer.free_slices.len());
1282
        assert_eq!(160, buffer.used_size); // didn't move
1283

1284
        // collapse all, and free buffer
1285
        assert_eq!(BufferState::Free, buffer.free_slice(slices[1].clone()));
1286
        assert_eq!(0, buffer.free_slices.len());
1287
        assert_eq!(0, buffer.used_size); // collapsed and empty
1288
    }
1289

1290
    #[test]
1291
    fn pop_free_slice() {
1292
        let renderer = MockRenderer::new();
1293
        let render_device = renderer.device();
1294

1295
        let l64 = ParticleLayout::new()
1296
            .append(F4A)
1297
            .append(F4B)
1298
            .append(F4C)
1299
            .append(F4D)
1300
            .build();
1301
        assert_eq!(64, l64.size());
1302

1303
        let asset = Handle::<EffectAsset>::default();
1304
        let capacity = 2048; // EffectBuffer::MIN_CAPACITY;
1305
        assert!(capacity >= 2048); // otherwise the logic below breaks
1306
        let mut buffer = EffectBuffer::new(
1307
            42,
1308
            asset,
1309
            capacity,
1310
            l64.clone(),
1311
            LayoutFlags::NONE,
1312
            &render_device,
1313
        );
1314

1315
        let slice0 = buffer.allocate_slice(32, &l64);
1316
        assert!(slice0.is_some());
1317
        let slice0 = slice0.unwrap();
1318
        assert_eq!(slice0.range, 0..32);
1319
        assert!(buffer.free_slices.is_empty());
1320

1321
        let slice1 = buffer.allocate_slice(1024, &l64);
1322
        assert!(slice1.is_some());
1323
        let slice1 = slice1.unwrap();
1324
        assert_eq!(slice1.range, 32..1056);
1325
        assert!(buffer.free_slices.is_empty());
1326

1327
        let state = buffer.free_slice(slice0);
1328
        assert_eq!(state, BufferState::Used);
1329
        assert_eq!(buffer.free_slices.len(), 1);
1330
        assert_eq!(buffer.free_slices[0], 0..32);
1331

1332
        // Try to allocate a slice larger than slice0, such that slice0 cannot be
1333
        // recycled, and instead the new slice has to be appended after all
1334
        // existing ones.
1335
        let slice2 = buffer.allocate_slice(64, &l64);
1336
        assert!(slice2.is_some());
1337
        let slice2 = slice2.unwrap();
1338
        assert_eq!(slice2.range.start, slice1.range.end); // after slice1
1339
        assert_eq!(slice2.range, 1056..1120);
1340
        assert_eq!(buffer.free_slices.len(), 1);
1341

1342
        // Now allocate a small slice that fits, to recycle (part of) slice0.
1343
        let slice3 = buffer.allocate_slice(16, &l64);
1344
        assert!(slice3.is_some());
1345
        let slice3 = slice3.unwrap();
1346
        assert_eq!(slice3.range, 0..16);
1347
        assert_eq!(buffer.free_slices.len(), 1); // split
1348
        assert_eq!(buffer.free_slices[0], 16..32);
1349

1350
        // Allocate a second small slice that fits exactly the left space, completely
1351
        // recycling
1352
        let slice4 = buffer.allocate_slice(16, &l64);
1353
        assert!(slice4.is_some());
1354
        let slice4 = slice4.unwrap();
1355
        assert_eq!(slice4.range, 16..32);
1356
        assert!(buffer.free_slices.is_empty()); // recycled
1357
    }
1358

1359
    #[test]
1360
    fn effect_cache() {
1361
        let renderer = MockRenderer::new();
1362
        let render_device = renderer.device();
1363

1364
        let l32 = ParticleLayout::new().append(F4A).append(F4B).build();
1365
        assert_eq!(32, l32.size());
1366

1367
        let mut effect_cache = EffectCache::new(render_device);
1368
        assert_eq!(effect_cache.buffers().len(), 0);
1369

1370
        let asset = Handle::<EffectAsset>::default();
1371
        let capacity = EffectBuffer::MIN_CAPACITY;
1372
        let item_size = l32.size();
1373

1374
        // Insert an effect
1375
        let effect1 = effect_cache.insert(asset.clone(), capacity, &l32, LayoutFlags::NONE);
1376
        //assert!(effect1.is_valid());
1377
        let slice1 = &effect1.slice;
1378
        assert_eq!(slice1.len(), capacity);
1379
        assert_eq!(
1380
            slice1.particle_layout.min_binding_size().get() as u32,
1381
            item_size
1382
        );
1383
        assert_eq!(slice1.range, 0..capacity);
1384
        assert_eq!(effect_cache.buffers().len(), 1);
1385

1386
        // Insert a second copy of the same effect
1387
        let effect2 = effect_cache.insert(asset.clone(), capacity, &l32, LayoutFlags::NONE);
1388
        //assert!(effect2.is_valid());
1389
        let slice2 = &effect2.slice;
1390
        assert_eq!(slice2.len(), capacity);
1391
        assert_eq!(
1392
            slice2.particle_layout.min_binding_size().get() as u32,
1393
            item_size
1394
        );
1395
        assert_eq!(slice2.range, 0..capacity);
1396
        assert_eq!(effect_cache.buffers().len(), 2);
1397

1398
        // Remove the first effect instance
1399
        let buffer_state = effect_cache.remove(&effect1).unwrap();
1400
        // Note: currently batching is disabled, so each instance has its own buffer,
1401
        // which becomes unused once the instance is destroyed.
1402
        assert_eq!(buffer_state, BufferState::Free);
1403
        assert_eq!(effect_cache.buffers().len(), 2);
1404
        {
1405
            let buffers = effect_cache.buffers();
1406
            assert!(buffers[0].is_none());
1407
            assert!(buffers[1].is_some()); // id2
1408
        }
1409

1410
        // Regression #60
1411
        let effect3 = effect_cache.insert(asset, capacity, &l32, LayoutFlags::NONE);
1412
        //assert!(effect3.is_valid());
1413
        let slice3 = &effect3.slice;
1414
        assert_eq!(slice3.len(), capacity);
1415
        assert_eq!(
1416
            slice3.particle_layout.min_binding_size().get() as u32,
1417
            item_size
1418
        );
1419
        assert_eq!(slice3.range, 0..capacity);
1420
        // Note: currently batching is disabled, so each instance has its own buffer.
1421
        assert_eq!(effect_cache.buffers().len(), 2);
1422
        {
1423
            let buffers = effect_cache.buffers();
1424
            assert!(buffers[0].is_some()); // id3
1425
            assert!(buffers[1].is_some()); // id2
1426
        }
1427
    }
1428
}
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