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

djeedai / bevy_hanabi / 14602822889

22 Apr 2025 07:17PM UTC coverage: 39.892%. Remained the same
14602822889

push

github

web-flow
Remove unused `GpuEffectMetadata::spawner_index` field. (#459)

1 of 6 new or added lines in 2 files covered. (16.67%)

2 existing lines in 1 file now uncovered.

3041 of 7623 relevant lines covered (39.89%)

17.6 hits per line

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

57.45
/src/render/batch.rs
1
use std::{collections::VecDeque, fmt::Debug, num::NonZeroU32, ops::Range};
2

3
use bevy::{
4
    prelude::*,
5
    render::{
6
        render_resource::{BufferId, CachedComputePipelineId},
7
        sync_world::MainEntity,
8
    },
9
    utils::HashMap,
10
};
11

12
use super::{
13
    effect_cache::{DispatchBufferIndices, EffectSlice},
14
    event::{CachedChildInfo, CachedEffectEvents},
15
    BufferBindingSource, CachedMesh, LayoutFlags, PropertyBindGroupKey,
16
};
17
use crate::{AlphaMode, EffectAsset, EffectShader, ParticleLayout, TextureLayout};
18

19
#[derive(Debug, Clone, Copy)]
20
pub(crate) enum BatchSpawnInfo {
21
    /// Spawn a number of particles uploaded from CPU each frame.
22
    CpuSpawner {
23
        /// Total number of particles to spawn for the batch. This is only used
24
        /// to calculate the number of compute workgroups to dispatch.
25
        total_spawn_count: u32,
26
    },
27

28
    /// Spawn a number of particles calculated on GPU from "spawn events", which
29
    /// generally emitted by another effect.
30
    GpuSpawner {
31
        /// Index into the init indirect dispatch buffer of the
32
        /// [`GpuDispatchIndirect`] instance for this batch.
33
        ///
34
        /// [`GpuDispatchIndirect`]: super::GpuDispatchIndirect
35
        init_indirect_dispatch_index: u32,
36
        /// Index of the [`EventBuffer`] where the GPU spawn events consumed by
37
        /// this batch are stored.
38
        ///
39
        /// [`EventBuffer`]: super::event::EventBuffer
40
        #[allow(dead_code)]
41
        event_buffer_index: u32,
42
    },
43
}
44

45
/// Batch of effects dispatched and rendered together.
46
#[derive(Debug, Clone)]
47
pub(crate) struct EffectBatch {
48
    /// Handle of the underlying effect asset describing the effect.
49
    pub handle: Handle<EffectAsset>,
50
    /// Index of the [`EffectBuffer`].
51
    ///
52
    /// [`EffectBuffer`]: super::effect_cache::EffectBuffer
53
    pub buffer_index: u32,
54
    /// Slice of particles in the GPU effect buffer referenced by
55
    /// [`EffectBatch::buffer_index`].
56
    pub slice: Range<u32>,
57
    /// Spawn info for this batch
58
    pub spawn_info: BatchSpawnInfo,
59
    /// Specialized init and update compute pipelines.
60
    pub init_and_update_pipeline_ids: InitAndUpdatePipelineIds,
61
    /// Configured shader used for the particle rendering of this group.
62
    /// Note that we don't need to keep the init/update shaders alive because
63
    /// their pipeline specialization is doing it via the specialization key.
64
    pub render_shader: Handle<Shader>,
65
    /// Index of the buffer of the parent effect, if any. If a parent exists,
66
    /// its particle buffer is made available (read-only) for a child effect to
67
    /// read its attributes.
68
    pub parent_buffer_index: Option<u32>,
69
    pub parent_min_binding_size: Option<NonZeroU32>,
70
    pub parent_binding_source: Option<BufferBindingSource>,
71
    /// Event buffers of child effects, if any.
72
    pub child_event_buffers: Vec<(Entity, BufferBindingSource)>,
73
    /// Index of the property buffer, if any.
74
    pub property_key: Option<PropertyBindGroupKey>,
75
    /// Offset in bytes into the property buffer where the Property struct is
76
    /// located for this effect.
77
    // FIXME: This is a per-instance value which prevents batching :(
78
    pub property_offset: Option<u32>,
79
    /// Index of the first [`GpuSpawnerParams`] entry of the effects in the
80
    /// batch. Subsequent batched effects have their entries following linearly
81
    /// after that one.
82
    ///
83
    /// [`GpuSpawnerParams`]: super::GpuSpawnerParams
84
    pub spawner_base: u32,
85
    /// The indices within the various indirect dispatch buffers.
86
    pub dispatch_buffer_indices: DispatchBufferIndices,
87
    /// Particle layout shared by all batched effects and groups.
88
    pub particle_layout: ParticleLayout,
89
    /// Flags describing the render layout.
90
    pub layout_flags: LayoutFlags,
91
    /// Asset ID of the effect mesh to draw.
92
    pub mesh: AssetId<Mesh>,
93
    /// GPU buffer storing the [`mesh`] of the effect.
94
    ///
95
    /// [`mesh`]: Self::mesh
96
    pub mesh_buffer_id: BufferId,
97
    /// Slice inside the GPU buffer for the effect mesh.
98
    pub mesh_slice: Range<u32>,
99
    /// Texture layout.
100
    pub texture_layout: TextureLayout,
101
    /// Textures.
102
    pub textures: Vec<Handle<Image>>,
103
    /// Alpha mode.
104
    pub alpha_mode: AlphaMode,
105
    /// Entities holding the source [`ParticleEffect`] instances which were
106
    /// batched into this single batch. Used to determine visibility per view.
107
    ///
108
    /// [`ParticleEffect`]: crate::ParticleEffect
109
    pub entities: Vec<u32>,
110
    pub cached_effect_events: Option<CachedEffectEvents>,
111
    pub sort_fill_indirect_dispatch_index: Option<u32>,
112
}
113

114
#[derive(Debug, Clone, Copy)]
115
pub(crate) struct EffectBatchIndex(pub u32);
116

117
pub(crate) struct SortedEffectBatchesIter<'a> {
118
    batches: &'a [EffectBatch],
119
    sorted_indices: &'a [u32],
120
    next: u32,
121
}
122

123
impl<'a> SortedEffectBatchesIter<'a> {
124
    pub fn new(source: &'a SortedEffectBatches) -> Self {
3✔
125
        assert_eq!(source.batches.len(), source.sorted_indices.len());
3✔
126
        Self {
127
            batches: &source.batches[..],
3✔
128
            sorted_indices: &source.sorted_indices[..],
3✔
129
            next: 0,
130
        }
131
    }
132
}
133

134
impl<'a> Iterator for SortedEffectBatchesIter<'a> {
135
    type Item = &'a EffectBatch;
136

137
    fn next(&mut self) -> Option<Self::Item> {
12✔
138
        if self.next < self.sorted_indices.len() as u32 {
12✔
139
            let index = self.sorted_indices[self.next as usize];
9✔
140
            let batch = &self.batches[index as usize];
9✔
141
            self.next += 1;
9✔
142
            Some(batch)
9✔
143
        } else {
144
            None
3✔
145
        }
146
    }
147
}
148

149
impl ExactSizeIterator for SortedEffectBatchesIter<'_> {
150
    fn len(&self) -> usize {
×
151
        self.sorted_indices.len()
×
152
    }
153
}
154

155
#[derive(Debug, Default, Resource)]
156
pub(crate) struct SortedEffectBatches {
157
    /// Effect batches in the order they were inserted by [`push()`], indexed by
158
    /// the returned [`EffectBatchIndex`].
159
    ///
160
    /// [`push()`]: Self::push
161
    batches: Vec<EffectBatch>,
162
    /// Indices into [`batches`] defining the sorted order batches need to be
163
    /// processed in. Calculated by [`sort()`].
164
    ///
165
    /// [`batches`]: Self::batches
166
    /// [`sort()`]: Self::sort
167
    sorted_indices: Vec<u32>,
168
    /// Index of the dispatch queue used for indirect fill dispatch and
169
    /// submitted to [`GpuBufferOperations`].
170
    pub(super) dispatch_queue_index: Option<u32>,
171
}
172

173
impl SortedEffectBatches {
174
    pub fn clear(&mut self) {
×
175
        self.batches.clear();
×
176
        self.sorted_indices.clear();
×
177
        self.dispatch_queue_index = None;
×
178
    }
179

180
    pub fn push(&mut self, effect_batch: EffectBatch) -> EffectBatchIndex {
4✔
181
        let index = self.batches.len() as u32;
4✔
182
        self.batches.push(effect_batch);
4✔
183
        EffectBatchIndex(index)
4✔
184
    }
185

186
    #[allow(dead_code)]
187
    pub fn len(&self) -> usize {
8✔
188
        self.batches.len()
8✔
189
    }
190

191
    pub fn is_empty(&self) -> bool {
8✔
192
        self.batches.is_empty()
8✔
193
    }
194

195
    /// Get an iterator over the sorted sequence of effect batches.
196
    #[inline]
197
    pub fn iter(&self) -> SortedEffectBatchesIter {
3✔
198
        assert_eq!(
3✔
199
            self.batches.len(),
3✔
200
            self.sorted_indices.len(),
3✔
201
            "Invalid sorted size. Did you call sort() beforehand?"
×
202
        );
203
        SortedEffectBatchesIter::new(self)
3✔
204
    }
205

206
    pub fn get(&self, index: EffectBatchIndex) -> Option<&EffectBatch> {
×
207
        if index.0 < self.batches.len() as u32 {
×
208
            Some(&self.batches[index.0 as usize])
×
209
        } else {
210
            None
×
211
        }
212
    }
213

214
    /// Sort the effect batches.
215
    pub fn sort(&mut self) {
3✔
216
        self.sorted_indices.clear();
3✔
217
        self.sorted_indices.reserve_exact(self.batches.len());
3✔
218

219
        // Kahn’s algorithm for topological sorting.
220

221
        // Note: we sort by particle buffer index. In theory with batching this is
222
        // incorrect, because a parent and child could be batched together in the same
223
        // buffer, in the wrong order. However currently batching is broken and we
224
        // allocate one effect instance per buffer, so this works. Ideally we'd take
225
        // care of sorting earlier during batching.
226

227
        // Build a map from buffer index to batch index.
228
        let batch_index_from_buffer_index = self
3✔
229
            .batches
3✔
230
            .iter()
231
            .enumerate()
232
            .map(|(batch_index, effect_batch)| (effect_batch.buffer_index, batch_index))
15✔
233
            .collect::<HashMap<_, _>>();
234
        // In theory with batching we could have multiple batches referencing the same
235
        // buffer if we failed to batch some effect instances together which
236
        // otherwise share a same particle buffer. In practice this currently doesn't
237
        // happen because batching is disabled, so we always create one buffer
238
        // per effect instance. But this will need to be fixed later.
239
        assert_eq!(
3✔
240
            batch_index_from_buffer_index.len(),
3✔
241
            self.batches.len(),
3✔
242
            "FIXME: Duplicate buffer index in batches. This is not implemented yet."
×
243
        );
244

245
        // Build a map from the batch index of a child to the batch index of its
246
        // parent.
247
        let mut parent_batch_index_from_batch_index = HashMap::with_capacity(self.batches.len());
3✔
248
        for (batch_index, effect_batch) in self.batches.iter().enumerate() {
12✔
249
            if let Some(parent_buffer_index) = effect_batch.parent_buffer_index {
6✔
250
                let parent_batch_index = batch_index_from_buffer_index
251
                    .get(&parent_buffer_index)
252
                    .unwrap();
253
                parent_batch_index_from_batch_index.insert(batch_index as u32, *parent_batch_index);
254
            }
255
        }
256

257
        // Store the number of children per batch; we need to decrement it below
258
        // HACK - during tests we don't want to create Buffers so grab the count another
259
        // (slower) way
260
        #[cfg(test)]
261
        let mut child_count = {
262
            let mut counts = vec![0; self.batches.len()];
263
            for (_, parent_batch_index) in &parent_batch_index_from_batch_index {
264
                counts[*parent_batch_index] += 1;
265
            }
266
            counts
267
        };
268
        #[cfg(not(test))]
269
        let mut child_count = self
3✔
270
            .batches
3✔
271
            .iter()
272
            .map(|effect_batch| effect_batch.child_event_buffers.len() as u32)
3✔
273
            .collect::<Vec<_>>();
274

275
        // Insert in queue all effects without any child
276
        let mut queue = VecDeque::new();
3✔
277
        for (batch_index, count) in child_count.iter().enumerate() {
12✔
278
            if *count == 0 {
5✔
279
                queue.push_back(batch_index as u32);
5✔
280
            }
281
        }
282

283
        // Process queue
284
        while let Some(batch_index) = queue.pop_front() {
21✔
285
            // The batch has no unprocessed child, so it can be inserted in the final result
286
            assert!(child_count[batch_index as usize] == 0);
287
            self.sorted_indices.push(batch_index);
9✔
288

289
            // If it has a parent, that parent has one less child to be processed, so is one
290
            // step closer to being inserted itself in the final result.
291
            let Some(parent_batch_index) = parent_batch_index_from_batch_index.get(&batch_index)
15✔
292
            else {
293
                continue;
3✔
294
            };
295
            assert!(child_count[*parent_batch_index] > 0);
6✔
296
            child_count[*parent_batch_index] -= 1;
6✔
297

298
            // If this was the last child effect of that parent, then the parent is ready
299
            // and can be inserted itself.
300
            if child_count[*parent_batch_index] == 0 {
10✔
301
                queue.push_back(*parent_batch_index as u32);
4✔
302
            }
303
        }
304

305
        assert_eq!(
3✔
306
            self.sorted_indices.len(),
3✔
307
            self.batches.len(),
3✔
308
            "Cycle detected in effects"
×
309
        );
310
    }
311
}
312

313
/// Single effect batch to drive rendering.
314
///
315
/// This component is spawned into the render world during the prepare phase
316
/// ([`prepare_effects()`]), once per effect batch per group. In turns it
317
/// references an [`EffectBatch`] component containing all the shared data for
318
/// all the groups of the effect.
319
#[derive(Debug, Component)]
320
pub(crate) struct EffectDrawBatch {
321
    /// Index of the [`EffectBatch`] in the [`SortedEffectBatches`] this draw
322
    /// batch is part of.
323
    ///
324
    /// Note: currently there's a 1:1 mapping between effect batch and draw
325
    /// batch.
326
    pub effect_batch_index: EffectBatchIndex,
327
    /// Position of the emitter so we can compute distance to camera.
328
    pub translation: Vec3,
329
}
330

331
impl EffectBatch {
332
    /// Create a new batch from a single input.
333
    pub fn from_input(
×
334
        cached_mesh: &CachedMesh,
335
        cached_effect_events: Option<&CachedEffectEvents>,
336
        cached_child_info: Option<&CachedChildInfo>,
337
        input: &mut BatchInput,
338
        dispatch_buffer_indices: DispatchBufferIndices,
339
        property_key: Option<PropertyBindGroupKey>,
340
        property_offset: Option<u32>,
341
    ) -> EffectBatch {
342
        assert_eq!(property_key.is_some(), property_offset.is_some());
×
343
        assert_eq!(
×
344
            input.event_buffer_index.is_some(),
×
345
            input.init_indirect_dispatch_index.is_some()
×
346
        );
347

348
        let spawn_info = if let Some(event_buffer_index) = input.event_buffer_index {
×
349
            BatchSpawnInfo::GpuSpawner {
350
                init_indirect_dispatch_index: input.init_indirect_dispatch_index.unwrap(),
351
                event_buffer_index,
352
            }
353
        } else {
354
            BatchSpawnInfo::CpuSpawner {
355
                total_spawn_count: input.spawn_count,
×
356
            }
357
        };
358

359
        EffectBatch {
360
            handle: input.handle.clone(),
×
361
            buffer_index: input.effect_slice.buffer_index,
×
362
            slice: input.effect_slice.slice.clone(),
×
363
            spawn_info,
364
            init_and_update_pipeline_ids: input.init_and_update_pipeline_ids,
×
365
            render_shader: input.shaders.render.clone(),
×
366
            parent_buffer_index: input.parent_buffer_index,
×
367
            parent_min_binding_size: cached_child_info
×
368
                .map(|cci| cci.parent_particle_layout.min_binding_size32()),
369
            parent_binding_source: cached_child_info
×
370
                .map(|cci| cci.parent_buffer_binding_source.clone()),
371
            child_event_buffers: input.child_effects.clone(),
×
372
            property_key,
373
            property_offset,
NEW
374
            spawner_base: input.spawner_index,
×
375
            particle_layout: input.effect_slice.particle_layout.clone(),
×
376
            dispatch_buffer_indices,
377
            layout_flags: input.layout_flags,
×
378
            mesh: cached_mesh.mesh,
×
379
            mesh_buffer_id: cached_mesh.buffer.id(),
×
380
            mesh_slice: cached_mesh.range.clone(),
×
381
            texture_layout: input.texture_layout.clone(),
×
382
            textures: input.textures.clone(),
×
383
            alpha_mode: input.alpha_mode,
×
384
            entities: vec![input.main_entity.id().index()],
×
385
            cached_effect_events: cached_effect_events.cloned(),
×
386
            sort_fill_indirect_dispatch_index: None, // set later as needed
387
        }
388
    }
389
}
390

391
/// Effect batching input, obtained from extracted effects.
392
#[derive(Debug, Component)]
393
pub(crate) struct BatchInput {
394
    /// Handle of the underlying effect asset describing the effect.
395
    pub handle: Handle<EffectAsset>,
396
    /// Main entity of the [`ParticleEffect`], used for visibility.
397
    pub main_entity: MainEntity,
398
    /// Render entity of the [`CachedEffect`].
399
    #[allow(dead_code)]
400
    pub entity: Entity,
401
    /// Effect slices.
402
    pub effect_slice: EffectSlice,
403
    /// Compute pipeline IDs of the specialized and cached pipelines.
404
    pub init_and_update_pipeline_ids: InitAndUpdatePipelineIds,
405
    /// Index of the buffer of the parent effect, if any.
406
    pub parent_buffer_index: Option<u32>,
407
    /// Index of the event buffer, if this effect consumes GPU spawn events.
408
    pub event_buffer_index: Option<u32>,
409
    /// Child effects, if any.
410
    pub child_effects: Vec<(Entity, BufferBindingSource)>,
411
    /// Various flags related to the effect.
412
    pub layout_flags: LayoutFlags,
413
    /// Texture layout.
414
    pub texture_layout: TextureLayout,
415
    /// Textures.
416
    pub textures: Vec<Handle<Image>>,
417
    /// Alpha mode.
418
    pub alpha_mode: AlphaMode,
419
    #[allow(dead_code)]
420
    pub particle_layout: ParticleLayout,
421
    /// Effect shaders.
422
    pub shaders: EffectShader,
423
    /// Index of the [`GpuSpawnerParams`] in the
424
    /// [`EffectsCache::spawner_buffer`].
425
    pub spawner_index: u32,
426
    /// Number of particles to spawn for this effect.
427
    pub spawn_count: u32,
428
    /// Emitter position.
429
    pub position: Vec3,
430
    /// Index of the init indirect dispatch struct, if any.
431
    // FIXME - Contains a single effect's data; should handle multiple ones.
432
    pub init_indirect_dispatch_index: Option<u32>,
433
}
434

435
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
436
pub(crate) struct InitAndUpdatePipelineIds {
437
    pub init: CachedComputePipelineId,
438
    pub update: CachedComputePipelineId,
439
}
440

441
#[cfg(test)]
442
mod tests {
443
    use super::*;
444

445
    fn make_batch(buffer_index: u32, parent_buffer_index: Option<u32>) -> EffectBatch {
446
        EffectBatch {
447
            handle: default(),
448
            buffer_index,
449
            slice: 0..0,
450
            spawn_info: BatchSpawnInfo::CpuSpawner {
451
                total_spawn_count: 0,
452
            },
453
            init_and_update_pipeline_ids: InitAndUpdatePipelineIds {
454
                init: CachedComputePipelineId::INVALID,
455
                update: CachedComputePipelineId::INVALID,
456
            },
457
            render_shader: default(),
458
            parent_buffer_index,
459
            parent_min_binding_size: default(),
460
            parent_binding_source: default(),
461
            child_event_buffers: default(),
462
            property_key: default(),
463
            property_offset: default(),
464
            spawner_base: default(),
465
            dispatch_buffer_indices: default(),
466
            particle_layout: ParticleLayout::empty(),
467
            layout_flags: LayoutFlags::NONE,
468
            mesh: default(),
469
            mesh_buffer_id: NonZeroU32::new(1).unwrap().into(),
470
            mesh_slice: 0..0,
471
            texture_layout: default(),
472
            textures: default(),
473
            alpha_mode: default(),
474
            entities: default(),
475
            cached_effect_events: default(),
476
            sort_fill_indirect_dispatch_index: default(),
477
        }
478
    }
479

480
    #[test]
481
    fn toposort_batches() {
482
        let mut seb = SortedEffectBatches::default();
483
        assert!(seb.is_empty());
484
        assert_eq!(seb.len(), 0);
485

486
        seb.push(make_batch(42, None));
487
        assert!(!seb.is_empty());
488
        assert_eq!(seb.len(), 1);
489

490
        seb.push(make_batch(5, Some(42)));
491
        assert!(!seb.is_empty());
492
        assert_eq!(seb.len(), 2);
493

494
        seb.sort();
495
        assert!(!seb.is_empty());
496
        assert_eq!(seb.len(), 2);
497
        let sorted_batches = seb.iter().collect::<Vec<_>>();
498
        assert_eq!(sorted_batches.len(), 2);
499
        assert_eq!(sorted_batches[0].buffer_index, 5);
500
        assert_eq!(sorted_batches[1].buffer_index, 42);
501

502
        seb.push(make_batch(6, Some(42)));
503
        assert!(!seb.is_empty());
504
        assert_eq!(seb.len(), 3);
505

506
        seb.sort();
507
        assert!(!seb.is_empty());
508
        assert_eq!(seb.len(), 3);
509
        let sorted_batches = seb.iter().collect::<Vec<_>>();
510
        assert_eq!(sorted_batches.len(), 3);
511
        assert_eq!(sorted_batches[0].buffer_index, 5);
512
        assert_eq!(sorted_batches[1].buffer_index, 6);
513
        assert_eq!(sorted_batches[2].buffer_index, 42);
514

515
        seb.push(make_batch(55, Some(5)));
516
        assert!(!seb.is_empty());
517
        assert_eq!(seb.len(), 4);
518

519
        seb.sort();
520
        assert!(!seb.is_empty());
521
        assert_eq!(seb.len(), 4);
522
        let sorted_batches = seb.iter().collect::<Vec<_>>();
523
        assert_eq!(sorted_batches.len(), 4);
524
        assert_eq!(sorted_batches[0].buffer_index, 6);
525
        assert_eq!(sorted_batches[1].buffer_index, 55);
526
        assert_eq!(sorted_batches[2].buffer_index, 5);
527
        assert_eq!(sorted_batches[3].buffer_index, 42);
528
    }
529
}
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