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

djeedai / bevy_hanabi / 12736836671

12 Jan 2025 08:28PM UTC coverage: 46.643% (-0.1%) from 46.755%
12736836671

Pull #415

github

web-flow
Merge 1828509eb into a49c92367
Pull Request #415: Add `DebugSettings` for GPU capture

6 of 34 new or added lines in 4 files covered. (17.65%)

3223 of 6910 relevant lines covered (46.64%)

20.09 hits per line

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

58.88
/src/render/effect_cache.rs
1
use std::{cmp::Ordering, num::NonZeroU64, ops::Range};
2

3
use bevy::{
4
    asset::Handle,
5
    ecs::{component::Component, system::Resource},
6
    log::{trace, warn},
7
    render::{render_resource::*, renderer::RenderDevice},
8
    utils::{default, HashMap},
9
};
10
use bytemuck::cast_slice_mut;
11

12
use super::{buffer_table::BufferTableId, AddedEffectGroup};
13
use crate::{
14
    asset::EffectAsset,
15
    render::{
16
        GpuDispatchIndirect, GpuParticleGroup, GpuSpawnerParams, LayoutFlags, StorageType as _,
17
    },
18
    ParticleLayout,
19
};
20

21
/// Describes all particle groups' slices of particles in the particle buffer
22
/// for a single effect.
23
#[derive(Debug, Clone, PartialEq, Eq)]
24
pub struct EffectSlices {
25
    /// Slices into the underlying [`BufferVec`]` of the group.
26
    ///
27
    /// The length of this vector is the number of particle groups plus one.
28
    /// The range of the first group is (slices[0]..slices[1]), the index of
29
    /// the second group is (slices[1]..slices[2]), etc.
30
    ///
31
    /// This is measured in items, not bytes.
32
    pub slices: Vec<u32>,
33
    /// Index of the buffer in the [`EffectCache`].
34
    pub buffer_index: u32,
35
    /// Particle layout of the effect.
36
    pub particle_layout: ParticleLayout,
37
}
38

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

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

54
/// Describes all particle groups' slices of particles in the particle buffer
55
/// for a single effect, as well as the [`DispatchBufferIndices`].
56
#[derive(Debug)]
57
pub struct SlicesRef {
58
    pub ranges: Vec<u32>,
59
    /// Size of a single item in the slice. Currently equal to the unique size
60
    /// of all items in an [`EffectBuffer`] (no mixed size supported in same
61
    /// buffer), so cached only for convenience.
62
    particle_layout: ParticleLayout,
63
}
64

65
impl SlicesRef {
66
    pub fn group_count(&self) -> u32 {
3✔
67
        debug_assert!(self.ranges.len() >= 2);
6✔
68
        (self.ranges.len() - 1) as u32
3✔
69
    }
70

71
    #[allow(dead_code)]
72
    pub fn group_capacity(&self, group_index: u32) -> u32 {
3✔
73
        assert!(group_index + 1 < self.ranges.len() as u32);
3✔
74
        let start = self.ranges[group_index as usize];
3✔
75
        let end = self.ranges[group_index as usize + 1];
3✔
76
        end - start
3✔
77
    }
78

79
    #[allow(dead_code)]
80
    pub fn total_capacity(&self) -> u32 {
3✔
81
        if self.ranges.is_empty() {
3✔
82
            0
×
83
        } else {
84
            debug_assert!(self.ranges.len() >= 2);
6✔
85
            let start = self.ranges[0];
3✔
86
            let end = self.ranges[self.ranges.len() - 1];
3✔
87
            end - start
3✔
88
        }
89
    }
90

91
    pub fn particle_layout(&self) -> &ParticleLayout {
×
92
        &self.particle_layout
×
93
    }
94
}
95

96
/// A reference to a slice allocated inside an [`EffectBuffer`].
97
#[derive(Debug, Default, Clone, PartialEq, Eq)]
98
pub struct SliceRef {
99
    /// Range into an [`EffectBuffer`], in item count.
100
    range: Range<u32>,
101
    /// Size of a single item in the slice. Currently equal to the unique size
102
    /// of all items in an [`EffectBuffer`] (no mixed size supported in same
103
    /// buffer), so cached only for convenience.
104
    particle_layout: ParticleLayout,
105
}
106

107
impl SliceRef {
108
    /// The length of the slice, in number of items.
109
    #[allow(dead_code)]
110
    pub fn len(&self) -> u32 {
8✔
111
        self.range.end - self.range.start
8✔
112
    }
113

114
    /// The size in bytes of the slice.
115
    #[allow(dead_code)]
116
    pub fn byte_size(&self) -> usize {
4✔
117
        (self.len() as usize) * (self.particle_layout.min_binding_size().get() as usize)
4✔
118
    }
119
}
120

121
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
122
struct SimulateBindGroupKey {
123
    buffer: Option<BufferId>,
124
    offset: u32,
125
    size: u32,
126
}
127

128
impl SimulateBindGroupKey {
129
    /// Invalid key, often used as placeholder.
130
    pub const INVALID: Self = Self {
131
        buffer: None,
132
        offset: u32::MAX,
133
        size: 0,
134
    };
135
}
136

137
/// Storage for a single kind of effects, sharing the same buffer(s).
138
///
139
/// Currently only accepts a single unique item size (particle size), fixed at
140
/// creation. Also currently only accepts instances of a unique effect asset,
141
/// although this restriction is purely for convenience and may be relaxed in
142
/// the future to improve batching.
143
#[derive(Debug)]
144
pub struct EffectBuffer {
145
    /// GPU buffer holding all particles for the entire group of effects.
146
    particle_buffer: Buffer,
147
    /// GPU buffer holding the indirection indices for the entire group of
148
    /// effects. This is a triple buffer containing:
149
    /// - the ping-pong alive particles and render indirect indices at offsets 0
150
    ///   and 1
151
    /// - the dead particle indices at offset 2
152
    indirect_buffer: Buffer,
153
    /// Layout of particles.
154
    particle_layout: ParticleLayout,
155
    /// Flags
156
    layout_flags: LayoutFlags,
157
    /// -
158
    particles_buffer_layout_sim: BindGroupLayout,
159
    /// -
160
    particles_buffer_layout_with_dispatch: BindGroupLayout,
161
    /// Total buffer capacity, in number of particles.
162
    capacity: u32,
163
    /// Used buffer size, in number of particles, either from allocated slices
164
    /// or from slices in the free list.
165
    used_size: u32,
166
    /// Array of free slices for new allocations, sorted in increasing order in
167
    /// the buffer.
168
    free_slices: Vec<Range<u32>>,
169
    /// Compute pipeline for the effect update pass.
170
    // pub compute_pipeline: ComputePipeline, // FIXME - ComputePipelineId, to avoid duplicating per
171
    // instance!
172
    /// Handle of all effects common in this buffer. TODO - replace with
173
    /// compatible layout.
174
    asset: Handle<EffectAsset>,
175
    /// Bind group for the per-buffer data (group @1) of the init and update
176
    /// passes.
177
    simulate_bind_group: Option<BindGroup>,
178
    /// Key the `simulate_bind_group` was created from.
179
    simulate_bind_group_key: SimulateBindGroupKey,
180
}
181

182
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
183
pub enum BufferState {
184
    /// The buffer is in use, with allocated resources.
185
    Used,
186
    /// Like `Used`, but the buffer was resized, so any bind group is
187
    /// nonetheless invalid.
188
    Resized,
189
    /// The buffer is free (its resources were deallocated).
190
    Free,
191
}
192

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

201
    /// Create a new group and a GPU buffer to back it up.
202
    ///
203
    /// The buffer cannot contain less than [`MIN_CAPACITY`] particles. If
204
    /// `capacity` is smaller, it's rounded up to [`MIN_CAPACITY`].
205
    ///
206
    /// [`MIN_CAPACITY`]: EffectBuffer::MIN_CAPACITY
207
    pub fn new(
5✔
208
        asset: Handle<EffectAsset>,
209
        capacity: u32,
210
        particle_layout: ParticleLayout,
211
        layout_flags: LayoutFlags,
212
        render_device: &RenderDevice,
213
        label: Option<&str>,
214
    ) -> Self {
215
        trace!(
5✔
216
            "EffectBuffer::new(capacity={}, particle_layout={:?}, layout_flags={:?}, item_size={}B)",
×
217
            capacity,
×
218
            particle_layout,
×
219
            layout_flags,
×
220
            particle_layout.min_binding_size().get(),
×
221
        );
222

223
        // Calculate the clamped capacity of the group, in number of particles.
224
        let capacity = capacity.max(Self::MIN_CAPACITY);
5✔
225
        debug_assert!(
5✔
226
            capacity > 0,
5✔
227
            "Attempted to create a zero-sized effect buffer."
×
228
        );
229

230
        // Allocate the particle buffer itself, containing the attributes of each
231
        // particle.
232
        let particle_capacity_bytes: BufferAddress =
5✔
233
            capacity as u64 * particle_layout.min_binding_size().get();
5✔
234
        let particle_label = label
5✔
235
            .map(|s| format!("hanabi:buffer:effect{s}_particle"))
10✔
236
            .unwrap_or("hanabi:buffer:effect_particle".to_owned());
237
        let particle_buffer = render_device.create_buffer(&BufferDescriptor {
238
            label: Some(&particle_label),
239
            size: particle_capacity_bytes,
240
            usage: BufferUsages::COPY_DST | BufferUsages::STORAGE,
241
            mapped_at_creation: false,
242
        });
243

244
        let capacity_bytes: BufferAddress = capacity as u64 * 4;
245

246
        let indirect_label = label
247
            .map(|s| format!("hanabi:buffer:effect{s}_indirect"))
5✔
248
            .unwrap_or("hanabi:buffer:effect_indirect".to_owned());
249
        let indirect_buffer = render_device.create_buffer(&BufferDescriptor {
250
            label: Some(&indirect_label),
251
            size: capacity_bytes * 3, // ping-pong + deadlist
252
            usage: BufferUsages::COPY_DST | BufferUsages::STORAGE,
253
            mapped_at_creation: true,
254
        });
255
        // Set content
256
        {
257
            // Scope get_mapped_range_mut() to force a drop before unmap()
258
            {
259
                let slice = &mut indirect_buffer.slice(..).get_mapped_range_mut()
260
                    [..capacity_bytes as usize * 3];
261
                let slice: &mut [u32] = cast_slice_mut(slice);
262
                for index in 0..capacity {
12,294✔
263
                    slice[3 * index as usize + 2] = capacity - 1 - index;
6,147✔
264
                }
265
            }
266
            indirect_buffer.unmap();
5✔
267
        }
268

269
        // TODO - Cache particle_layout and associated bind group layout, instead of
270
        // creating one bind group layout per buffer using that layout...
271
        // FIXME - the layout is duplicated in ParticlesInitPipeline and
272
        // ParticlesUpdatePipeline.
273
        let particle_group_size = GpuParticleGroup::aligned_size(
274
            render_device.limits().min_storage_buffer_offset_alignment,
5✔
275
        );
276
        let entries = [
5✔
277
            // @binding(0) var<storage, read_write> particle_buffer : ParticleBuffer
278
            BindGroupLayoutEntry {
5✔
279
                binding: 0,
5✔
280
                visibility: ShaderStages::COMPUTE,
5✔
281
                ty: BindingType::Buffer {
5✔
282
                    ty: BufferBindingType::Storage { read_only: false },
5✔
283
                    has_dynamic_offset: false,
5✔
284
                    min_binding_size: Some(particle_layout.min_binding_size()),
5✔
285
                },
286
                count: None,
5✔
287
            },
288
            // @binding(1) var<storage, read_write> indirect_buffer : IndirectBuffer
289
            BindGroupLayoutEntry {
5✔
290
                binding: 1,
5✔
291
                visibility: ShaderStages::COMPUTE,
5✔
292
                ty: BindingType::Buffer {
5✔
293
                    ty: BufferBindingType::Storage { read_only: false },
5✔
294
                    has_dynamic_offset: false,
5✔
295
                    min_binding_size: Some(NonZeroU64::new(12).unwrap()),
5✔
296
                },
297
                count: None,
5✔
298
            },
299
            // @binding(2) var<storage, read> particle_groups : array<ParticleGroup>
300
            BindGroupLayoutEntry {
5✔
301
                binding: 2,
5✔
302
                visibility: ShaderStages::COMPUTE,
5✔
303
                ty: BindingType::Buffer {
5✔
304
                    ty: BufferBindingType::Storage { read_only: true },
5✔
305
                    has_dynamic_offset: false,
5✔
306
                    // Despite no dynamic offset, we do bind a non-zero offset sometimes,
307
                    // so keep this aligned
308
                    min_binding_size: Some(particle_group_size),
5✔
309
                },
310
                count: None,
5✔
311
            },
312
        ];
313
        let bgl_label = label
5✔
314
            .map(|s| format!("hanabi:bind_group_layout:effect{s}"))
15✔
315
            .unwrap_or("hanabi:bind_group_layout:effect".to_owned());
5✔
316
        trace!(
5✔
317
            "Creating particle bind group layout '{}' for simulation passes with {} entries.",
×
318
            bgl_label,
×
319
            entries.len(),
×
320
        );
321
        let particles_buffer_layout_sim =
5✔
322
            render_device.create_bind_group_layout(Some(&bgl_label[..]), &entries);
5✔
323

324
        // Create the render layout.
325
        let dispatch_indirect_size = GpuDispatchIndirect::aligned_size(
326
            render_device.limits().min_storage_buffer_offset_alignment,
5✔
327
        );
328
        let mut entries = vec![
5✔
329
            // @group(1) @binding(0) var<storage, read> particle_buffer : ParticleBuffer;
330
            BindGroupLayoutEntry {
5✔
331
                binding: 0,
5✔
332
                visibility: ShaderStages::VERTEX,
5✔
333
                ty: BindingType::Buffer {
5✔
334
                    ty: BufferBindingType::Storage { read_only: true },
5✔
335
                    has_dynamic_offset: false,
5✔
336
                    min_binding_size: Some(particle_layout.min_binding_size()),
5✔
337
                },
338
                count: None,
5✔
339
            },
340
            // @group(1) @binding(1) var<storage, read> indirect_buffer : IndirectBuffer;
341
            BindGroupLayoutEntry {
5✔
342
                binding: 1,
5✔
343
                visibility: ShaderStages::VERTEX,
5✔
344
                ty: BindingType::Buffer {
5✔
345
                    ty: BufferBindingType::Storage { read_only: true },
5✔
346
                    has_dynamic_offset: false,
5✔
347
                    min_binding_size: BufferSize::new(std::mem::size_of::<u32>() as u64),
5✔
348
                },
349
                count: None,
5✔
350
            },
351
            // @group(1) @binding(2) var<storage, read> dispatch_indirect : DispatchIndirect;
352
            BindGroupLayoutEntry {
5✔
353
                binding: 2,
5✔
354
                visibility: ShaderStages::VERTEX,
5✔
355
                ty: BindingType::Buffer {
5✔
356
                    ty: BufferBindingType::Storage { read_only: true },
5✔
357
                    has_dynamic_offset: true,
5✔
358
                    min_binding_size: Some(dispatch_indirect_size),
5✔
359
                },
360
                count: None,
5✔
361
            },
362
        ];
363
        if layout_flags.contains(LayoutFlags::LOCAL_SPACE_SIMULATION) {
5✔
364
            // @group(1) @binding(3) var<storage, read> spawner : Spawner;
365
            entries.push(BindGroupLayoutEntry {
×
366
                binding: 3,
×
367
                visibility: ShaderStages::VERTEX,
×
368
                ty: BindingType::Buffer {
×
369
                    ty: BufferBindingType::Storage { read_only: true },
×
370
                    has_dynamic_offset: true,
×
371
                    min_binding_size: Some(GpuSpawnerParams::min_size()), // TODO - array
×
372
                },
373
                count: None,
×
374
            });
375
        }
376
        trace!(
377
            "Creating render layout with {} entries (flags: {:?})",
×
378
            entries.len(),
×
379
            layout_flags
380
        );
381
        let particles_buffer_layout_with_dispatch =
5✔
382
            render_device.create_bind_group_layout("hanabi:buffer_layout_render", &entries);
5✔
383

384
        Self {
385
            particle_buffer,
386
            indirect_buffer,
387
            particle_layout,
388
            layout_flags,
389
            particles_buffer_layout_sim,
390
            particles_buffer_layout_with_dispatch,
391
            capacity,
392
            used_size: 0,
393
            free_slices: vec![],
5✔
394
            asset,
395
            simulate_bind_group: None,
396
            simulate_bind_group_key: SimulateBindGroupKey::INVALID,
397
        }
398
    }
399

400
    pub fn particle_layout(&self) -> &ParticleLayout {
×
401
        &self.particle_layout
×
402
    }
403

404
    pub fn layout_flags(&self) -> LayoutFlags {
×
405
        self.layout_flags
×
406
    }
407

408
    pub fn particle_layout_bind_group_sim(&self) -> &BindGroupLayout {
×
409
        &self.particles_buffer_layout_sim
×
410
    }
411

412
    pub fn particle_layout_bind_group_with_dispatch(&self) -> &BindGroupLayout {
×
413
        &self.particles_buffer_layout_with_dispatch
×
414
    }
415

416
    /// Return a binding for the entire particle buffer.
417
    pub fn max_binding(&self) -> BindingResource {
×
418
        let capacity_bytes = self.capacity as u64 * self.particle_layout.min_binding_size().get();
×
419
        BindingResource::Buffer(BufferBinding {
×
420
            buffer: &self.particle_buffer,
×
421
            offset: 0,
×
422
            size: Some(NonZeroU64::new(capacity_bytes).unwrap()),
×
423
        })
424
    }
425

426
    /// Return a binding of the buffer for a starting range of a given size (in
427
    /// bytes).
428
    #[allow(dead_code)]
429
    pub fn binding(&self, size: u32) -> BindingResource {
×
430
        BindingResource::Buffer(BufferBinding {
×
431
            buffer: &self.particle_buffer,
×
432
            offset: 0,
×
433
            size: Some(NonZeroU64::new(size as u64).unwrap()),
×
434
        })
435
    }
436

437
    /// Return a binding for the entire indirect buffer associated with the
438
    /// current effect buffer.
439
    pub fn indirect_max_binding(&self) -> BindingResource {
×
440
        let capacity_bytes = self.capacity as u64 * 4;
×
441
        BindingResource::Buffer(BufferBinding {
×
442
            buffer: &self.indirect_buffer,
×
443
            offset: 0,
×
444
            size: Some(NonZeroU64::new(capacity_bytes * 3).unwrap()),
×
445
        })
446
    }
447

448
    /// Create the bind group for the init and update passes if needed.
449
    ///
450
    /// The `buffer_index` must be the index of the current [`EffectBuffer`]
451
    /// inside the [`EffectCache`]. The `group_binding` is the binding resource
452
    /// for the particle groups of this buffer.
453
    pub fn create_sim_bind_group(
×
454
        &mut self,
455
        buffer_index: u32,
456
        render_device: &RenderDevice,
457
        particle_group_buffer: &Buffer,
458
        particle_group_offset: u64,
459
        particle_group_size: NonZeroU64,
460
    ) {
461
        let key = SimulateBindGroupKey {
462
            buffer: Some(particle_group_buffer.id()),
×
463
            offset: particle_group_offset as u32,
×
464
            size: particle_group_size.get() as u32,
×
465
        };
466

467
        if self.simulate_bind_group.is_some() && self.simulate_bind_group_key == key {
×
468
            return;
×
469
        }
470

471
        let group_binding = BufferBinding {
472
            buffer: particle_group_buffer,
473
            offset: particle_group_offset,
474
            size: Some(particle_group_size),
×
475
        };
476

477
        let layout = self.particle_layout_bind_group_sim();
×
478
        let label = format!("hanabi:bind_group_sim_batch{}", buffer_index);
×
479
        let bindings = [
×
480
            BindGroupEntry {
×
481
                binding: 0,
×
482
                resource: self.max_binding(),
×
483
            },
484
            BindGroupEntry {
×
485
                binding: 1,
×
486
                resource: self.indirect_max_binding(),
×
487
            },
488
            BindGroupEntry {
×
489
                binding: 2,
×
490
                resource: BindingResource::Buffer(group_binding),
×
491
            },
492
        ];
493
        trace!(
×
494
            "Create simulate bind group '{}' with {} entries",
×
495
            label,
×
496
            bindings.len()
×
497
        );
498
        let bind_group = render_device.create_bind_group(Some(&label[..]), layout, &bindings);
×
499
        self.simulate_bind_group = Some(bind_group);
×
500
        self.simulate_bind_group_key = key;
×
501
    }
502

503
    /// Invalidate any existing simulate bind group.
504
    ///
505
    /// Invalidate any existing bind group previously created by
506
    /// [`create_sim_bind_group()`], generally because a buffer was
507
    /// re-allocated. This forces a re-creation of the bind group
508
    /// next time [`create_sim_bind_group()`] is called.
509
    ///
510
    /// [`create_sim_bind_group()`]: self::EffectBuffer::create_sim_bind_group
511
    fn invalidate_sim_bind_group(&mut self) {
×
512
        self.simulate_bind_group = None;
×
NEW
513
        self.simulate_bind_group_key = SimulateBindGroupKey::INVALID;
×
514
    }
515

516
    /// Return the cached bind group for the init and update passes.
517
    ///
518
    /// This is the per-buffer bind group at binding @1 which binds all
519
    /// per-buffer resources shared by all effect instances batched in a single
520
    /// buffer. The bind group is created by [`create_sim_bind_group()`], and
521
    /// cached until a call to [`invalidate_sim_bind_group()`] clears the cached
522
    /// reference.
523
    ///
524
    /// [`create_sim_bind_group()`]: self::EffectBuffer::create_sim_bind_group
525
    /// [`invalidate_sim_bind_group()`]: self::EffectBuffer::invalidate_sim_bind_group
526
    pub fn sim_bind_group(&self) -> Option<&BindGroup> {
×
527
        self.simulate_bind_group.as_ref()
×
528
    }
529

530
    /// Try to recycle a free slice to store `size` items.
531
    fn pop_free_slice(&mut self, size: u32) -> Option<Range<u32>> {
17✔
532
        if self.free_slices.is_empty() {
17✔
533
            return None;
14✔
534
        }
535

536
        struct BestRange {
537
            range: Range<u32>,
538
            capacity: u32,
539
            index: usize,
540
        }
541

542
        let mut result = BestRange {
543
            range: 0..0, // marker for "invalid"
544
            capacity: u32::MAX,
545
            index: usize::MAX,
546
        };
547
        for (index, slice) in self.free_slices.iter().enumerate() {
3✔
548
            let capacity = slice.end - slice.start;
3✔
549
            if size > capacity {
3✔
550
                continue;
1✔
551
            }
552
            if capacity < result.capacity {
4✔
553
                result = BestRange {
2✔
554
                    range: slice.clone(),
2✔
555
                    capacity,
2✔
556
                    index,
2✔
557
                };
558
            }
559
        }
560
        if !result.range.is_empty() {
3✔
561
            if result.capacity > size {
2✔
562
                // split
563
                let start = result.range.start;
1✔
564
                let used_end = start + size;
1✔
565
                let free_end = result.range.end;
1✔
566
                let range = start..used_end;
1✔
567
                self.free_slices[result.index] = used_end..free_end;
1✔
568
                Some(range)
1✔
569
            } else {
570
                // recycle entirely
571
                self.free_slices.remove(result.index);
1✔
572
                Some(result.range)
1✔
573
            }
574
        } else {
575
            None
1✔
576
        }
577
    }
578

579
    /// Allocate a new slice in the buffer to store the particles of a single
580
    /// effect.
581
    pub fn allocate_slice(
18✔
582
        &mut self,
583
        capacity: u32,
584
        particle_layout: &ParticleLayout,
585
    ) -> Option<SliceRef> {
586
        trace!(
18✔
587
            "EffectBuffer::allocate_slice: capacity={} particle_layout={:?} item_size={}",
×
588
            capacity,
×
589
            particle_layout,
×
590
            particle_layout.min_binding_size().get(),
×
591
        );
592

593
        if capacity > self.capacity {
18✔
594
            return None;
1✔
595
        }
596

597
        let range = if let Some(range) = self.pop_free_slice(capacity) {
17✔
598
            range
2✔
599
        } else {
600
            let new_size = self.used_size.checked_add(capacity).unwrap();
15✔
601
            if new_size <= self.capacity {
15✔
602
                let range = self.used_size..new_size;
13✔
603
                self.used_size = new_size;
13✔
604
                range
13✔
605
            } else {
606
                if self.used_size == 0 {
2✔
607
                    warn!(
×
608
                        "Cannot allocate slice of size {} in effect cache buffer of capacity {}.",
×
609
                        capacity, self.capacity
610
                    );
611
                }
612
                return None;
2✔
613
            }
614
        };
615

616
        Some(SliceRef {
617
            range,
618
            particle_layout: particle_layout.clone(),
619
        })
620
    }
621

622
    /// Free an allocated slice, and if this was the last allocated slice also
623
    /// free the buffer.
624
    pub fn free_slice(&mut self, slice: SliceRef) -> BufferState {
9✔
625
        // If slice is at the end of the buffer, reduce total used size
626
        if slice.range.end == self.used_size {
9✔
627
            self.used_size = slice.range.start;
3✔
628
            // Check other free slices to further reduce used size and drain the free slice
629
            // list
630
            while let Some(free_slice) = self.free_slices.last() {
13✔
631
                if free_slice.end == self.used_size {
5✔
632
                    self.used_size = free_slice.start;
5✔
633
                    self.free_slices.pop();
5✔
634
                } else {
635
                    break;
×
636
                }
637
            }
638
            if self.used_size == 0 {
3✔
639
                assert!(self.free_slices.is_empty());
2✔
640
                // The buffer is not used anymore, free it too
641
                BufferState::Free
2✔
642
            } else {
643
                // There are still some slices used, the last one of which ends at
644
                // self.used_size
645
                BufferState::Used
1✔
646
            }
647
        } else {
648
            // Free slice is not at end; insert it in free list
649
            let range = slice.range;
6✔
650
            match self.free_slices.binary_search_by(|s| {
12✔
651
                if s.end <= range.start {
6✔
652
                    Ordering::Less
6✔
653
                } else if s.start >= range.end {
×
654
                    Ordering::Greater
×
655
                } else {
656
                    Ordering::Equal
×
657
                }
658
            }) {
659
                Ok(_) => warn!("Range {:?} already present in free list!", range),
×
660
                Err(index) => self.free_slices.insert(index, range),
6✔
661
            }
662
            BufferState::Used
6✔
663
        }
664
    }
665

666
    pub fn is_compatible(&self, handle: &Handle<EffectAsset>) -> bool {
2✔
667
        // TODO - replace with check particle layout is compatible to allow tighter
668
        // packing in less buffers, and update in the less dispatch calls
669
        *handle == self.asset
2✔
670
    }
671
}
672

673
/// Identifier referencing an effect cached in an internal effect cache.
674
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
675
pub(crate) struct EffectCacheId {
676
    pub index: u32,
677
}
678

679
impl EffectCacheId {
680
    /// An invalid handle, corresponding to nothing.
681
    pub const INVALID: Self = Self { index: u32::MAX };
682

683
    /// Generate a new valid effect cache identifier.
684
    #[allow(dead_code)]
685
    pub fn new(index: u32) -> Self {
×
686
        Self { index }
687
    }
688

689
    /// Check if the ID is valid.
690
    #[allow(dead_code)]
691
    pub fn is_valid(&self) -> bool {
×
692
        *self != Self::INVALID
×
693
    }
694
}
695

696
/// Stores various data, including the buffer index and slice boundaries within
697
/// the buffer for all groups in a single effect.
698
#[derive(Debug, Component)]
699
pub(crate) struct CachedEffect {
700
    /// The index of the [`EffectBuffer`].
701
    pub(crate) buffer_index: u32,
702
    /// The slices within that buffer.
703
    pub(crate) slices: SlicesRef,
704
    /// The order in which we evaluate groups.
705
    pub(crate) group_order: Vec<u32>,
706
}
707

708
#[derive(Debug, Clone, Copy)]
709
pub(crate) struct PendingEffectGroup {
710
    pub capacity: u32,
711
    pub src_group_index_if_trail: Option<u32>,
712
}
713

714
impl From<&AddedEffectGroup> for PendingEffectGroup {
715
    fn from(value: &AddedEffectGroup) -> Self {
×
716
        Self {
717
            capacity: value.capacity,
×
718
            src_group_index_if_trail: value.src_group_index_if_trail,
×
719
        }
720
    }
721
}
722

723
#[derive(Debug, Clone)]
724
pub(crate) enum RenderGroupDispatchIndices {
725
    Pending {
726
        groups: Box<[PendingEffectGroup]>,
727
    },
728
    Allocated {
729
        /// The index of the first render group indirect dispatch buffer.
730
        ///
731
        /// There will be one such dispatch buffer for each particle group.
732
        first_render_group_dispatch_buffer_index: BufferTableId,
733
        /// Map from a group index to its source and destination rows into the
734
        /// render group dispatch buffer.
735
        trail_dispatch_buffer_indices: HashMap<u32, TrailDispatchBufferIndices>,
736
    },
737
}
738

739
impl Default for RenderGroupDispatchIndices {
740
    fn default() -> Self {
×
741
        Self::Pending {
742
            groups: Box::new([]),
×
743
        }
744
    }
745
}
746

747
/// The indices in the indirect dispatch buffers for a single effect, as well as
748
/// that of the metadata buffer.
749
#[derive(Debug, Clone, Component)]
750
pub(crate) struct DispatchBufferIndices {
751
    /// The index of the first update group indirect dispatch buffer.
752
    ///
753
    /// There will be one such dispatch buffer for each particle group.
754
    pub(crate) first_update_group_dispatch_buffer_index: BufferTableId,
755
    /// The index of the render indirect metadata buffer.
756
    pub(crate) render_effect_metadata_buffer_table_id: BufferTableId,
757
    /// Render group dispatch indirect indices for all groups of the effect.
758
    pub(crate) render_group_dispatch_indices: RenderGroupDispatchIndices,
759
}
760

761
#[derive(Debug, Clone, Copy)]
762
pub(crate) struct TrailDispatchBufferIndices {
763
    pub(crate) dest: BufferTableId,
764
    pub(crate) src: BufferTableId,
765
}
766

767
impl Default for DispatchBufferIndices {
768
    // For testing purposes only.
769
    fn default() -> Self {
×
770
        DispatchBufferIndices {
771
            first_update_group_dispatch_buffer_index: BufferTableId::INVALID,
772
            render_effect_metadata_buffer_table_id: BufferTableId::INVALID,
773
            render_group_dispatch_indices: default(),
×
774
        }
775
    }
776
}
777

778
/// Cache for effect instances sharing common GPU data structures.
779
#[derive(Resource)]
780
pub struct EffectCache {
781
    /// Render device the GPU resources (buffers) are allocated from.
782
    device: RenderDevice,
783
    /// Collection of effect buffers managed by this cache. Some buffers might
784
    /// be `None` if the entry is not used. Since the buffers are referenced
785
    /// by index, we cannot move them once they're allocated.
786
    buffers: Vec<Option<EffectBuffer>>,
787
}
788

789
impl EffectCache {
790
    pub fn new(device: RenderDevice) -> Self {
1✔
791
        Self {
792
            device,
793
            buffers: vec![],
1✔
794
        }
795
    }
796

797
    #[allow(dead_code)]
798
    #[inline]
799
    pub fn buffers(&self) -> &[Option<EffectBuffer>] {
7✔
800
        &self.buffers
7✔
801
    }
802

803
    #[allow(dead_code)]
804
    #[inline]
805
    pub fn buffers_mut(&mut self) -> &mut [Option<EffectBuffer>] {
×
806
        &mut self.buffers
×
807
    }
808

809
    #[allow(dead_code)]
810
    #[inline]
811
    pub fn get_buffer(&self, buffer_index: u32) -> Option<&EffectBuffer> {
×
812
        self.buffers.get(buffer_index as usize)?.as_ref()
×
813
    }
814

815
    #[allow(dead_code)]
816
    #[inline]
817
    pub fn get_buffer_mut(&mut self, buffer_index: u32) -> Option<&mut EffectBuffer> {
×
818
        self.buffers.get_mut(buffer_index as usize)?.as_mut()
×
819
    }
820

821
    /// Invalidate all the "simulation" bind groups for all buffers.
822
    ///
823
    /// This iterates over all valid buffers and calls
824
    /// [`EffectBuffer::invalidate_sim_bind_group()`] on each one.
825
    pub fn invalidate_sim_bind_groups(&mut self) {
×
826
        for buffer in self.buffers.iter_mut().flatten() {
×
827
            buffer.invalidate_sim_bind_group();
×
828
        }
829
    }
830

831
    pub fn insert(
3✔
832
        &mut self,
833
        asset: Handle<EffectAsset>,
834
        capacities: Vec<u32>,
835
        particle_layout: &ParticleLayout,
836
        layout_flags: LayoutFlags,
837
        group_order: Vec<u32>,
838
    ) -> CachedEffect {
839
        let total_capacity = capacities.iter().cloned().sum();
3✔
840
        let (buffer_index, slice) = self
3✔
841
            .buffers
3✔
842
            .iter_mut()
843
            .enumerate()
844
            .find_map(|(buffer_index, buffer)| {
6✔
845
                if let Some(buffer) = buffer {
5✔
846
                    // The buffer must be compatible with the effect layout, to allow the update pass
847
                    // to update all particles at once from all compatible effects in a single dispatch.
848
                    if !buffer.is_compatible(&asset) {
849
                        return None;
×
850
                    }
851

852
                    // Try to allocate a slice into the buffer
853
                    buffer
2✔
854
                        .allocate_slice(total_capacity, particle_layout)
2✔
855
                        .map(|slice| (buffer_index, slice))
4✔
856
                } else {
857
                    None
1✔
858
                }
859
            })
860
            .unwrap_or_else(|| {
6✔
861
                // Cannot find any suitable buffer; allocate a new one
862
                let buffer_index = self.buffers.iter().position(|buf| buf.is_none()).unwrap_or(self.buffers.len());
8✔
863
                let byte_size = total_capacity.checked_mul(particle_layout.min_binding_size().get() as u32).unwrap_or_else(|| panic!(
3✔
864
                    "Effect size overflow: capacities={:?} particle_layout={:?} item_size={}",
×
865
                    capacities, particle_layout, particle_layout.min_binding_size().get()
×
866
                ));
867
                trace!(
3✔
868
                    "Creating new effect buffer #{} for effect {:?} (capacities={:?}, particle_layout={:?} item_size={}, byte_size={})",
×
869
                    buffer_index,
×
870
                    asset,
×
871
                    capacities,
×
872
                    particle_layout,
×
873
                    particle_layout.min_binding_size().get(),
×
874
                    byte_size
875
                );
876
                let mut buffer = EffectBuffer::new(
3✔
877
                    asset,
3✔
878
                    total_capacity,
3✔
879
                    particle_layout.clone(),
3✔
880
                    layout_flags,
3✔
881
                    &self.device,
3✔
882
                    Some(&format!("{buffer_index}")),
3✔
883
                );
884
                let slice_ref = buffer.allocate_slice(total_capacity, particle_layout).unwrap();
3✔
885
                if buffer_index >= self.buffers.len() {
5✔
886
                    self.buffers.push(Some(buffer));
2✔
887
                } else {
888
                    debug_assert!(self.buffers[buffer_index].is_none());
2✔
889
                    self.buffers[buffer_index] = Some(buffer);
1✔
890
                }
891
                (buffer_index, slice_ref)
3✔
892
            });
893

894
        let mut ranges = vec![slice.range.start];
3✔
895
        let group_count = capacities.len();
3✔
896
        for capacity in capacities {
12✔
897
            let start_index = ranges.last().unwrap();
3✔
898
            ranges.push(start_index + capacity);
3✔
899
        }
900
        debug_assert_eq!(ranges.len(), group_count + 1);
6✔
901

902
        let slices = SlicesRef {
903
            ranges,
904
            particle_layout: slice.particle_layout,
3✔
905
        };
906

907
        trace!(
3✔
908
            "Insert effect buffer_index={} slice={}B particle_layout={:?}",
×
909
            buffer_index,
×
910
            slices.particle_layout.min_binding_size().get(),
×
911
            slices.particle_layout,
912
        );
913
        CachedEffect {
914
            buffer_index: buffer_index as u32,
3✔
915
            slices,
916
            group_order,
917
        }
918
    }
919

920
    /// Get the init bind group for a cached effect.
921
    pub fn init_bind_group(&self, buffer_index: u32) -> Option<&BindGroup> {
×
922
        self.buffers[buffer_index as usize]
×
923
            .as_ref()
924
            .and_then(|eb| eb.sim_bind_group())
×
925
    }
926

927
    /// Get the update bind group for a cached effect.
928
    #[inline]
929
    pub fn update_bind_group(&self, buffer_index: u32) -> Option<&BindGroup> {
×
930
        self.init_bind_group(buffer_index)
×
931
    }
932

933
    pub fn create_sim_bind_group(
×
934
        &mut self,
935
        buffer_index: u32,
936
        render_device: &RenderDevice,
937
        particle_group_buffer: &Buffer,
938
        particle_group_offset: u64,
939
        particle_group_size: NonZeroU64,
940
    ) -> Result<(), ()> {
941
        // Create the bind group
942
        let effect_buffer: &mut Option<EffectBuffer> =
×
943
            self.buffers.get_mut(buffer_index as usize).ok_or(())?;
×
944
        let effect_buffer = effect_buffer.as_mut().ok_or(())?;
×
945
        effect_buffer.create_sim_bind_group(
×
946
            buffer_index,
×
947
            render_device,
×
948
            particle_group_buffer,
×
949
            particle_group_offset,
×
950
            particle_group_size,
×
951
        );
952
        Ok(())
×
953
    }
954

955
    /// Remove an effect from the cache. If this was the last effect, drop the
956
    /// underlying buffer and return the index of the dropped buffer.
957
    pub fn remove(&mut self, cached_effect: &CachedEffect) -> Result<BufferState, ()> {
1✔
958
        // Resolve the buffer by index
959
        let Some(maybe_buffer) = self.buffers.get_mut(cached_effect.buffer_index as usize) else {
2✔
960
            return Err(());
×
961
        };
962
        let Some(buffer) = maybe_buffer.as_mut() else {
1✔
963
            return Err(());
×
964
        };
965

966
        // Reconstruct the original slice
967
        let slice = SliceRef {
968
            range: cached_effect.slices.ranges[0]..*cached_effect.slices.ranges.last().unwrap(),
969
            // FIXME: clone() needed to return CachedEffectIndices, but really we don't care about
970
            // returning the ParticleLayout, so should split...
971
            particle_layout: cached_effect.slices.particle_layout.clone(),
972
        };
973

974
        // Free the slice inside the resolved buffer
975
        if buffer.free_slice(slice) == BufferState::Free {
976
            *maybe_buffer = None;
1✔
977
            return Ok(BufferState::Free);
1✔
978
        }
979

980
        Ok(BufferState::Used)
×
981
    }
982
}
983

984
#[cfg(all(test, feature = "gpu_tests"))]
985
mod gpu_tests {
986
    use std::borrow::Cow;
987

988
    use bevy::math::Vec4;
989

990
    use super::*;
991
    use crate::{
992
        graph::{Value, VectorValue},
993
        test_utils::MockRenderer,
994
        Attribute, AttributeInner,
995
    };
996

997
    #[test]
998
    fn effect_slice_ord() {
999
        let particle_layout = ParticleLayout::new().append(Attribute::POSITION).build();
1000
        let slice1 = EffectSlices {
1001
            slices: vec![0, 32],
1002
            buffer_index: 1,
1003
            particle_layout: particle_layout.clone(),
1004
        };
1005
        let slice2 = EffectSlices {
1006
            slices: vec![32, 64],
1007
            buffer_index: 1,
1008
            particle_layout: particle_layout.clone(),
1009
        };
1010
        assert!(slice1 < slice2);
1011
        assert!(slice1 <= slice2);
1012
        assert!(slice2 > slice1);
1013
        assert!(slice2 >= slice1);
1014

1015
        let slice3 = EffectSlices {
1016
            slices: vec![0, 32],
1017
            buffer_index: 0,
1018
            particle_layout,
1019
        };
1020
        assert!(slice3 < slice1);
1021
        assert!(slice3 < slice2);
1022
        assert!(slice1 > slice3);
1023
        assert!(slice2 > slice3);
1024
    }
1025

1026
    const F4A_INNER: &AttributeInner = &AttributeInner::new(
1027
        Cow::Borrowed("F4A"),
1028
        Value::Vector(VectorValue::new_vec4(Vec4::ONE)),
1029
    );
1030
    const F4B_INNER: &AttributeInner = &AttributeInner::new(
1031
        Cow::Borrowed("F4B"),
1032
        Value::Vector(VectorValue::new_vec4(Vec4::ONE)),
1033
    );
1034
    const F4C_INNER: &AttributeInner = &AttributeInner::new(
1035
        Cow::Borrowed("F4C"),
1036
        Value::Vector(VectorValue::new_vec4(Vec4::ONE)),
1037
    );
1038
    const F4D_INNER: &AttributeInner = &AttributeInner::new(
1039
        Cow::Borrowed("F4D"),
1040
        Value::Vector(VectorValue::new_vec4(Vec4::ONE)),
1041
    );
1042

1043
    const F4A: Attribute = Attribute(F4A_INNER);
1044
    const F4B: Attribute = Attribute(F4B_INNER);
1045
    const F4C: Attribute = Attribute(F4C_INNER);
1046
    const F4D: Attribute = Attribute(F4D_INNER);
1047

1048
    #[test]
1049
    fn slice_ref() {
1050
        let l16 = ParticleLayout::new().append(F4A).build();
1051
        assert_eq!(16, l16.size());
1052
        let l32 = ParticleLayout::new().append(F4A).append(F4B).build();
1053
        assert_eq!(32, l32.size());
1054
        let l48 = ParticleLayout::new()
1055
            .append(F4A)
1056
            .append(F4B)
1057
            .append(F4C)
1058
            .build();
1059
        assert_eq!(48, l48.size());
1060
        for (range, particle_layout, len, byte_size) in [
1061
            (0..0, &l16, 0, 0),
1062
            (0..16, &l16, 16, 16 * 16),
1063
            (0..16, &l32, 16, 16 * 32),
1064
            (240..256, &l48, 16, 16 * 48),
1065
        ] {
1066
            let sr = SliceRef {
1067
                range,
1068
                particle_layout: particle_layout.clone(),
1069
            };
1070
            assert_eq!(sr.len(), len);
1071
            assert_eq!(sr.byte_size(), byte_size);
1072
        }
1073
    }
1074

1075
    #[test]
1076
    fn effect_buffer() {
1077
        let renderer = MockRenderer::new();
1078
        let render_device = renderer.device();
1079

1080
        let l64 = ParticleLayout::new()
1081
            .append(F4A)
1082
            .append(F4B)
1083
            .append(F4C)
1084
            .append(F4D)
1085
            .build();
1086
        assert_eq!(64, l64.size());
1087

1088
        let asset = Handle::<EffectAsset>::default();
1089
        let capacity = 4096;
1090
        let mut buffer = EffectBuffer::new(
1091
            asset,
1092
            capacity,
1093
            l64.clone(),
1094
            LayoutFlags::NONE,
1095
            &render_device,
1096
            Some("my_buffer"),
1097
        );
1098

1099
        assert_eq!(buffer.capacity, capacity.max(EffectBuffer::MIN_CAPACITY));
1100
        assert_eq!(64, buffer.particle_layout.size());
1101
        assert_eq!(64, buffer.particle_layout.min_binding_size().get());
1102
        assert_eq!(0, buffer.used_size);
1103
        assert!(buffer.free_slices.is_empty());
1104

1105
        assert_eq!(None, buffer.allocate_slice(buffer.capacity + 1, &l64));
1106

1107
        let mut offset = 0;
1108
        let mut slices = vec![];
1109
        for size in [32, 128, 55, 148, 1, 2048, 42] {
1110
            let slice = buffer.allocate_slice(size, &l64);
1111
            assert!(slice.is_some());
1112
            let slice = slice.unwrap();
1113
            assert_eq!(64, slice.particle_layout.size());
1114
            assert_eq!(64, buffer.particle_layout.min_binding_size().get());
1115
            assert_eq!(offset..offset + size, slice.range);
1116
            slices.push(slice);
1117
            offset += size;
1118
        }
1119
        assert_eq!(offset, buffer.used_size);
1120

1121
        assert_eq!(BufferState::Used, buffer.free_slice(slices[2].clone()));
1122
        assert_eq!(1, buffer.free_slices.len());
1123
        let free_slice = &buffer.free_slices[0];
1124
        assert_eq!(160..215, *free_slice);
1125
        assert_eq!(offset, buffer.used_size); // didn't move
1126

1127
        assert_eq!(BufferState::Used, buffer.free_slice(slices[3].clone()));
1128
        assert_eq!(BufferState::Used, buffer.free_slice(slices[4].clone()));
1129
        assert_eq!(BufferState::Used, buffer.free_slice(slices[5].clone()));
1130
        assert_eq!(4, buffer.free_slices.len());
1131
        assert_eq!(offset, buffer.used_size); // didn't move
1132

1133
        // this will collapse all the way to slices[1], the highest allocated
1134
        assert_eq!(BufferState::Used, buffer.free_slice(slices[6].clone()));
1135
        assert_eq!(0, buffer.free_slices.len()); // collapsed
1136
        assert_eq!(160, buffer.used_size); // collapsed
1137

1138
        assert_eq!(BufferState::Used, buffer.free_slice(slices[0].clone()));
1139
        assert_eq!(1, buffer.free_slices.len());
1140
        assert_eq!(160, buffer.used_size); // didn't move
1141

1142
        // collapse all, and free buffer
1143
        assert_eq!(BufferState::Free, buffer.free_slice(slices[1].clone()));
1144
        assert_eq!(0, buffer.free_slices.len());
1145
        assert_eq!(0, buffer.used_size); // collapsed and empty
1146
    }
1147

1148
    #[test]
1149
    fn pop_free_slice() {
1150
        let renderer = MockRenderer::new();
1151
        let render_device = renderer.device();
1152

1153
        let l64 = ParticleLayout::new()
1154
            .append(F4A)
1155
            .append(F4B)
1156
            .append(F4C)
1157
            .append(F4D)
1158
            .build();
1159
        assert_eq!(64, l64.size());
1160

1161
        let asset = Handle::<EffectAsset>::default();
1162
        let capacity = 2048; // EffectBuffer::MIN_CAPACITY;
1163
        assert!(capacity >= 2048); // otherwise the logic below breaks
1164
        let mut buffer = EffectBuffer::new(
1165
            asset,
1166
            capacity,
1167
            l64.clone(),
1168
            LayoutFlags::NONE,
1169
            &render_device,
1170
            Some("my_buffer"),
1171
        );
1172

1173
        let slice0 = buffer.allocate_slice(32, &l64);
1174
        assert!(slice0.is_some());
1175
        let slice0 = slice0.unwrap();
1176
        assert_eq!(slice0.range, 0..32);
1177
        assert!(buffer.free_slices.is_empty());
1178

1179
        let slice1 = buffer.allocate_slice(1024, &l64);
1180
        assert!(slice1.is_some());
1181
        let slice1 = slice1.unwrap();
1182
        assert_eq!(slice1.range, 32..1056);
1183
        assert!(buffer.free_slices.is_empty());
1184

1185
        let state = buffer.free_slice(slice0);
1186
        assert_eq!(state, BufferState::Used);
1187
        assert_eq!(buffer.free_slices.len(), 1);
1188
        assert_eq!(buffer.free_slices[0], 0..32);
1189

1190
        // Try to allocate a slice larger than slice0, such that slice0 cannot be
1191
        // recycled, and instead the new slice has to be appended after all
1192
        // existing ones.
1193
        let slice2 = buffer.allocate_slice(64, &l64);
1194
        assert!(slice2.is_some());
1195
        let slice2 = slice2.unwrap();
1196
        assert_eq!(slice2.range.start, slice1.range.end); // after slice1
1197
        assert_eq!(slice2.range, 1056..1120);
1198
        assert_eq!(buffer.free_slices.len(), 1);
1199

1200
        // Now allocate a small slice that fits, to recycle (part of) slice0.
1201
        let slice3 = buffer.allocate_slice(16, &l64);
1202
        assert!(slice3.is_some());
1203
        let slice3 = slice3.unwrap();
1204
        assert_eq!(slice3.range, 0..16);
1205
        assert_eq!(buffer.free_slices.len(), 1); // split
1206
        assert_eq!(buffer.free_slices[0], 16..32);
1207

1208
        // Allocate a second small slice that fits exactly the left space, completely
1209
        // recycling
1210
        let slice4 = buffer.allocate_slice(16, &l64);
1211
        assert!(slice4.is_some());
1212
        let slice4 = slice4.unwrap();
1213
        assert_eq!(slice4.range, 16..32);
1214
        assert!(buffer.free_slices.is_empty()); // recycled
1215
    }
1216

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

1222
        let l32 = ParticleLayout::new().append(F4A).append(F4B).build();
1223
        assert_eq!(32, l32.size());
1224

1225
        let mut effect_cache = EffectCache::new(render_device);
1226
        assert_eq!(effect_cache.buffers().len(), 0);
1227

1228
        let asset = Handle::<EffectAsset>::default();
1229
        let capacity = EffectBuffer::MIN_CAPACITY;
1230
        let capacities = vec![capacity];
1231
        let group_order = vec![0];
1232
        let item_size = l32.size();
1233

1234
        // Insert an effect
1235
        let effect1 = effect_cache.insert(
1236
            asset.clone(),
1237
            capacities.clone(),
1238
            &l32,
1239
            LayoutFlags::NONE,
1240
            group_order.clone(),
1241
        );
1242
        //assert!(effect1.is_valid());
1243
        let slice1 = &effect1.slices;
1244
        assert_eq!(slice1.group_count(), 1);
1245
        assert_eq!(slice1.group_capacity(0), capacity);
1246
        assert_eq!(slice1.total_capacity(), capacity);
1247
        assert_eq!(
1248
            slice1.particle_layout.min_binding_size().get() as u32,
1249
            item_size
1250
        );
1251
        assert_eq!(slice1.ranges, vec![0, capacity]);
1252
        assert_eq!(effect_cache.buffers().len(), 1);
1253

1254
        // Insert a second copy of the same effect
1255
        let effect2 = effect_cache.insert(
1256
            asset.clone(),
1257
            capacities.clone(),
1258
            &l32,
1259
            LayoutFlags::NONE,
1260
            group_order.clone(),
1261
        );
1262
        //assert!(effect2.is_valid());
1263
        let slice2 = &effect2.slices;
1264
        assert_eq!(slice2.group_count(), 1);
1265
        assert_eq!(slice2.group_capacity(0), capacity);
1266
        assert_eq!(slice2.total_capacity(), capacity);
1267
        assert_eq!(
1268
            slice2.particle_layout.min_binding_size().get() as u32,
1269
            item_size
1270
        );
1271
        assert_eq!(slice2.ranges, vec![0, capacity]);
1272
        assert_eq!(effect_cache.buffers().len(), 2);
1273

1274
        // Remove the first effect instance
1275
        let buffer_state = effect_cache.remove(&effect1).unwrap();
1276
        // Note: currently batching is disabled, so each instance has its own buffer,
1277
        // which becomes unused once the instance is destroyed.
1278
        assert_eq!(buffer_state, BufferState::Free);
1279
        assert_eq!(effect_cache.buffers().len(), 2);
1280
        {
1281
            let buffers = effect_cache.buffers();
1282
            assert!(buffers[0].is_none());
1283
            assert!(buffers[1].is_some()); // id2
1284
        }
1285

1286
        // Regression #60
1287
        let effect3 = effect_cache.insert(asset, capacities, &l32, LayoutFlags::NONE, group_order);
1288
        //assert!(effect3.is_valid());
1289
        let slice3 = &effect3.slices;
1290
        assert_eq!(slice3.group_count(), 1);
1291
        assert_eq!(slice3.group_capacity(0), capacity);
1292
        assert_eq!(slice3.total_capacity(), capacity);
1293
        assert_eq!(
1294
            slice3.particle_layout.min_binding_size().get() as u32,
1295
            item_size
1296
        );
1297
        assert_eq!(slice3.ranges, vec![0, capacity]);
1298
        // Note: currently batching is disabled, so each instance has its own buffer.
1299
        assert_eq!(effect_cache.buffers().len(), 2);
1300
        {
1301
            let buffers = effect_cache.buffers();
1302
            assert!(buffers[0].is_some()); // id3
1303
            assert!(buffers[1].is_some()); // id2
1304
        }
1305
    }
1306
}
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