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

djeedai / bevy_hanabi / 15813086919

22 Jun 2025 09:34PM UTC coverage: 66.748% (+0.2%) from 66.537%
15813086919

Pull #480

github

web-flow
Merge 2cdfbbc21 into aa073c5e6
Pull Request #480: Allow multiple effects to be packed into a single buffer again.

133 of 152 new or added lines in 4 files covered. (87.5%)

298 existing lines in 7 files now uncovered.

5191 of 7777 relevant lines covered (66.75%)

357.88 hits per line

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

44.09
/src/render/sort.rs
1
use std::num::{NonZeroU32, NonZeroU64};
2

3
use bevy::{
4
    asset::Handle,
5
    ecs::{resource::Resource, world::World},
6
    platform::collections::{hash_map::Entry, HashMap},
7
    render::{
8
        render_resource::{
9
            BindGroup, BindGroupLayout, Buffer, BufferId, CachedComputePipelineId,
10
            ComputePipelineDescriptor, PipelineCache, Shader,
11
        },
12
        renderer::RenderDevice,
13
    },
14
    utils::default,
15
};
16
use wgpu::{
17
    BindGroupEntry, BindGroupLayoutEntry, BindingResource, BindingType, BufferBinding,
18
    BufferBindingType, BufferDescriptor, BufferUsages, CommandEncoder, ShaderStages,
19
};
20

21
use super::{gpu_buffer::GpuBuffer, GpuDispatchIndirect, GpuEffectMetadata, StorageType};
22
use crate::{Attribute, ParticleLayout};
23

24
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
25
struct SortFillBindGroupLayoutKey {
26
    particle_min_binding_size: NonZeroU32,
27
    particle_ribbon_id_offset: u32,
28
    particle_age_offset: u32,
29
}
30

31
impl SortFillBindGroupLayoutKey {
32
    pub fn from_particle_layout(particle_layout: &ParticleLayout) -> Result<Self, ()> {
×
33
        let particle_ribbon_id_offset = particle_layout.offset(Attribute::RIBBON_ID).ok_or(())?;
×
34
        let particle_age_offset = particle_layout.offset(Attribute::AGE).ok_or(())?;
×
35
        let key = SortFillBindGroupLayoutKey {
36
            particle_min_binding_size: particle_layout.min_binding_size32(),
37
            particle_ribbon_id_offset,
38
            particle_age_offset,
39
        };
40
        Ok(key)
41
    }
42
}
43

44
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
45
struct SortFillBindGroupKey {
46
    particle: BufferId,
47
    indirect_index: BufferId,
48
    effect_metadata: BufferId,
49
}
50

51
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
52
struct SortCopyBindGroupKey {
53
    indirect_index: BufferId,
54
    sort: BufferId,
55
    effect_metadata: BufferId,
56
}
57

58
#[derive(Resource)]
59
pub struct SortBindGroups {
60
    /// Render device.
61
    render_device: RenderDevice,
62
    /// Sort-fill pass compute shader.
63
    sort_fill_shader: Handle<Shader>,
64
    /// GPU buffer of key-value pairs to sort.
65
    sort_buffer: Buffer,
66
    /// GPU buffer containing the [`GpuDispatchIndirect`] structs for the
67
    /// sort-fill and sort passes.
68
    indirect_buffer: GpuBuffer<GpuDispatchIndirect>,
69
    /// Bind group layouts for group #0 of the sort-fill compute pass.
70
    sort_fill_bind_group_layouts:
71
        HashMap<SortFillBindGroupLayoutKey, (BindGroupLayout, CachedComputePipelineId)>,
72
    /// Bind groups for group #0 of the sort-fill compute pass.
73
    sort_fill_bind_groups: HashMap<SortFillBindGroupKey, BindGroup>,
74
    /// Bind group for group #0 of the sort compute pass.
75
    sort_bind_group: BindGroup,
76
    sort_copy_bind_group_layout: BindGroupLayout,
77
    /// Pipeline for sort pass.
78
    sort_pipeline_id: CachedComputePipelineId,
79
    /// Pipeline for sort-copy pass.
80
    sort_copy_pipeline_id: CachedComputePipelineId,
81
    /// Bind groups for group #0 of the sort-copy compute pass.
82
    sort_copy_bind_groups: HashMap<SortCopyBindGroupKey, BindGroup>,
83
}
84

85
impl SortBindGroups {
86
    pub fn new(
3✔
87
        world: &mut World,
88
        sort_fill_shader: Handle<Shader>,
89
        sort_shader: Handle<Shader>,
90
        sort_copy_shader: Handle<Shader>,
91
    ) -> Self {
92
        let render_device = world.resource::<RenderDevice>();
3✔
93
        let pipeline_cache = world.resource::<PipelineCache>();
3✔
94

95
        let sort_buffer = render_device.create_buffer(&BufferDescriptor {
3✔
96
            label: Some("hanabi:buffer:sort:pairs"),
3✔
97
            size: 3 * 1024 * 1024,
3✔
98
            usage: BufferUsages::COPY_DST | BufferUsages::STORAGE,
3✔
99
            mapped_at_creation: false,
3✔
100
        });
101

102
        let indirect_buffer_size = 3 * 1024;
3✔
103
        let indirect_buffer = render_device.create_buffer(&BufferDescriptor {
3✔
104
            label: Some("hanabi:buffer:sort:indirect"),
3✔
105
            size: indirect_buffer_size,
3✔
106
            usage: BufferUsages::COPY_SRC
3✔
107
                | BufferUsages::COPY_DST
3✔
108
                | BufferUsages::STORAGE
3✔
109
                | BufferUsages::INDIRECT,
3✔
110
            mapped_at_creation: false,
3✔
111
        });
112
        let indirect_buffer = GpuBuffer::new_allocated(
113
            indirect_buffer,
3✔
114
            indirect_buffer_size as u32,
3✔
115
            Some("hanabi:buffer:sort:indirect".to_string()),
3✔
116
        );
117

118
        let sort_bind_group_layout = render_device.create_bind_group_layout(
3✔
119
            "hanabi:bind_group_layout:sort",
120
            &[BindGroupLayoutEntry {
3✔
121
                binding: 0,
3✔
122
                visibility: ShaderStages::COMPUTE,
3✔
123
                ty: BindingType::Buffer {
3✔
124
                    ty: BufferBindingType::Storage { read_only: false },
3✔
125
                    has_dynamic_offset: false,
3✔
126
                    min_binding_size: Some(NonZeroU64::new(16).unwrap()), // count + dual kv pair
3✔
127
                },
128
                count: None,
3✔
129
            }],
130
        );
131

132
        let sort_bind_group = render_device.create_bind_group(
3✔
133
            "hanabi:bind_group:sort",
134
            &sort_bind_group_layout,
3✔
135
            &[
3✔
136
                // @group(0) @binding(0) var<storage, read_write> pairs : array<KeyValuePair>;
137
                BindGroupEntry {
3✔
138
                    binding: 0,
3✔
139
                    resource: BindingResource::Buffer(BufferBinding {
3✔
140
                        buffer: &sort_buffer,
3✔
141
                        offset: 0,
3✔
142
                        size: None,
3✔
143
                    }),
144
                },
145
            ],
146
        );
147

148
        let sort_pipeline_id = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor {
3✔
149
            label: Some("hanabi:pipeline:sort".into()),
3✔
150
            layout: vec![sort_bind_group_layout],
3✔
151
            shader: sort_shader,
3✔
152
            shader_defs: vec!["HAS_DUAL_KEY".into()],
3✔
153
            entry_point: "main".into(),
3✔
154
            push_constant_ranges: vec![],
3✔
155
            zero_initialize_workgroup_memory: false,
3✔
156
        });
157

158
        let effect_metadata_min_binding_size = GpuEffectMetadata::aligned_size(
159
            render_device.limits().min_storage_buffer_offset_alignment,
3✔
160
        );
161
        let sort_copy_bind_group_layout = render_device.create_bind_group_layout(
3✔
162
            "hanabi:bind_group_layout:sort_copy",
163
            &[
3✔
164
                // @group(0) @binding(0) var<storage, read_write> indirect_index_buffer :
165
                // IndirectIndexBuffer;
166
                BindGroupLayoutEntry {
3✔
167
                    binding: 0,
3✔
168
                    visibility: ShaderStages::COMPUTE,
3✔
169
                    ty: BindingType::Buffer {
3✔
170
                        ty: BufferBindingType::Storage { read_only: false },
3✔
171
                        has_dynamic_offset: false,
3✔
172
                        min_binding_size: Some(NonZeroU64::new(12).unwrap()), // ping/pong+dead
3✔
173
                    },
174
                    count: None,
3✔
175
                },
176
                // @group(0) @binding(1) var<storage, read> sort_buffer : SortBuffer;
177
                BindGroupLayoutEntry {
3✔
178
                    binding: 1,
3✔
179
                    visibility: ShaderStages::COMPUTE,
3✔
180
                    ty: BindingType::Buffer {
3✔
181
                        ty: BufferBindingType::Storage { read_only: true },
3✔
182
                        has_dynamic_offset: false,
3✔
183
                        min_binding_size: Some(NonZeroU64::new(16).unwrap()), /* count + dual kv
3✔
184
                                                                               * pair */
3✔
185
                    },
186
                    count: None,
3✔
187
                },
188
                // @group(0) @binding(2) var<storage, read_write> effect_metadata : EffectMetadata;
189
                BindGroupLayoutEntry {
3✔
190
                    binding: 2,
3✔
191
                    visibility: ShaderStages::COMPUTE,
3✔
192
                    ty: BindingType::Buffer {
3✔
193
                        ty: BufferBindingType::Storage { read_only: false },
3✔
194
                        has_dynamic_offset: true,
3✔
195
                        min_binding_size: Some(effect_metadata_min_binding_size),
3✔
196
                    },
197
                    count: None,
3✔
198
                },
199
            ],
200
        );
201

202
        let sort_copy_pipeline_id =
3✔
203
            pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor {
3✔
204
                label: Some("hanabi:pipeline:sort_copy".into()),
3✔
205
                layout: vec![sort_copy_bind_group_layout.clone()],
3✔
206
                shader: sort_copy_shader,
3✔
207
                shader_defs: vec![],
3✔
208
                entry_point: "main".into(),
3✔
209
                push_constant_ranges: vec![],
3✔
210
                zero_initialize_workgroup_memory: false,
3✔
211
            });
212

213
        Self {
214
            render_device: render_device.clone(),
3✔
215
            sort_fill_shader,
216
            sort_buffer,
217
            indirect_buffer,
218
            sort_fill_bind_group_layouts: default(),
3✔
219
            sort_fill_bind_groups: default(),
3✔
220
            sort_bind_group,
221
            sort_copy_bind_group_layout,
222
            sort_pipeline_id,
223
            sort_copy_pipeline_id,
224
            sort_copy_bind_groups: default(),
3✔
225
        }
226
    }
227

228
    #[inline]
229
    pub fn clear_indirect_dispatch_buffer(&mut self) {
1,030✔
230
        self.indirect_buffer.clear();
1,030✔
231
    }
232

233
    #[inline]
234
    pub fn allocate_indirect_dispatch(&mut self) -> u32 {
×
235
        self.indirect_buffer.allocate()
×
236
    }
237

238
    #[inline]
239
    pub fn get_indirect_dispatch_byte_offset(&self, index: u32) -> u32 {
×
240
        self.indirect_buffer.item_size() as u32 * index
×
241
    }
242

243
    #[inline]
244
    #[allow(dead_code)]
245
    pub fn sort_buffer(&self) -> &Buffer {
×
246
        &self.sort_buffer
×
247
    }
248

249
    #[inline]
250
    pub fn indirect_buffer(&self) -> Option<&Buffer> {
1,014✔
251
        self.indirect_buffer.buffer()
1,014✔
252
    }
253

254
    #[inline]
255
    pub fn sort_bind_group(&self) -> &BindGroup {
×
256
        &self.sort_bind_group
×
257
    }
258

259
    #[inline]
260
    pub fn sort_pipeline_id(&self) -> CachedComputePipelineId {
×
261
        self.sort_pipeline_id
×
262
    }
263

264
    #[inline]
265
    pub fn prepare_buffers(&mut self, render_device: &RenderDevice) {
1,030✔
266
        self.indirect_buffer.prepare_buffers(render_device);
1,030✔
267
    }
268

269
    #[inline]
270
    pub fn write_buffers(&self, command_encoder: &mut CommandEncoder) {
1,030✔
271
        self.indirect_buffer.write_buffers(command_encoder);
1,030✔
272
    }
273

274
    #[inline]
275
    pub fn clear_previous_frame_resizes(&mut self) {
1,030✔
276
        self.indirect_buffer.clear_previous_frame_resizes();
1,030✔
277
    }
278

279
    pub fn ensure_sort_fill_bind_group_layout(
×
280
        &mut self,
281
        pipeline_cache: &PipelineCache,
282
        particle_layout: &ParticleLayout,
283
    ) -> Result<&BindGroupLayout, ()> {
284
        let key = SortFillBindGroupLayoutKey::from_particle_layout(particle_layout)?;
×
285
        let (layout, _) = self
286
            .sort_fill_bind_group_layouts
287
            .entry(key)
288
            .or_insert_with(|| {
×
289
                let alignment = self
×
290
                    .render_device
×
291
                    .limits()
×
292
                    .min_storage_buffer_offset_alignment;
×
293
                let bind_group_layout = self.render_device.create_bind_group_layout(
×
294
                    "hanabi:bind_group_layout:sort_fill",
×
295
                    &[
×
296
                        // @group(0) @binding(0) var<storage, read_write> pairs: array<KeyValuePair>;
297
                        BindGroupLayoutEntry {
×
298
                            binding: 0,
×
299
                            visibility: ShaderStages::COMPUTE,
×
300
                            ty: BindingType::Buffer {
×
301
                                ty: BufferBindingType::Storage { read_only: false },
×
302
                                has_dynamic_offset: false,
×
303
                                min_binding_size: Some(NonZeroU64::new(16).unwrap()), // count + dual kv pair
×
304
                            },
305
                            count: None,
×
306
                        },
307
                        // @group(0) @binding(1) var<storage, read> particle_buffer: ParticleBuffer;
308
                        BindGroupLayoutEntry {
×
309
                            binding: 1,
×
310
                            visibility: ShaderStages::COMPUTE,
×
311
                            ty: BindingType::Buffer {
×
312
                                ty: BufferBindingType::Storage { read_only: true },
×
NEW
313
                                has_dynamic_offset: false,
×
314
                                min_binding_size: Some(key.particle_min_binding_size.into()),
×
315
                            },
316
                            count: None,
×
317
                        },
318
                        // @group(0) @binding(2) var<storage, read> indirect_index_buffer : array<u32>;
319
                        BindGroupLayoutEntry {
×
320
                            binding: 2,
×
321
                            visibility: ShaderStages::COMPUTE,
×
322
                            ty: BindingType::Buffer {
×
323
                                ty: BufferBindingType::Storage { read_only: true },
×
NEW
324
                                has_dynamic_offset: false,
×
325
                                min_binding_size: Some(NonZeroU64::new(12).unwrap()), // ping/pong+dead
×
326
                            },
327
                            count: None,
×
328
                        },
329
                        // @group(0) @binding(3) var<storage, read_write> effect_metadata : EffectMetadata;
330
                        BindGroupLayoutEntry {
×
331
                            binding: 3,
×
332
                            visibility: ShaderStages::COMPUTE,
×
333
                            ty: BindingType::Buffer {
×
334
                                ty: BufferBindingType::Storage { read_only: false },
×
335
                                has_dynamic_offset: true,
×
336
                                min_binding_size: Some(GpuEffectMetadata::aligned_size(alignment)),
×
337
                            },
338
                            count: None,
×
339
                        },
340
                    ],
341
                );
342
                let pipeline_id =
×
343
                    pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor {
×
344
                        label: Some("hanabi:pipeline:sort_fill".into()),
×
345
                        layout: vec![bind_group_layout.clone()],
×
346
                        shader: self.sort_fill_shader.clone(),
×
347
                        shader_defs: vec!["HAS_DUAL_KEY".into()],
×
348
                        entry_point: "main".into(),
×
349
                        push_constant_ranges: vec![],
×
350
                        zero_initialize_workgroup_memory: false,
×
351
                    });
352
                (bind_group_layout, pipeline_id)
×
353
            });
354
        Ok(layout)
355
    }
356

357
    // We currently only use the bind group layout internally in
358
    // ensure_sort_fill_bind_group()
359
    #[allow(dead_code)]
360
    pub fn get_sort_fill_bind_group_layout(
×
361
        &self,
362
        particle_layout: &ParticleLayout,
363
    ) -> Option<&BindGroupLayout> {
364
        let key = SortFillBindGroupLayoutKey::from_particle_layout(particle_layout).ok()?;
×
365
        self.sort_fill_bind_group_layouts
366
            .get(&key)
367
            .map(|(layout, _)| layout)
×
368
    }
369

370
    pub fn get_sort_fill_pipeline_id(
×
371
        &self,
372
        particle_layout: &ParticleLayout,
373
    ) -> Option<CachedComputePipelineId> {
374
        let key = SortFillBindGroupLayoutKey::from_particle_layout(particle_layout).ok()?;
×
375
        self.sort_fill_bind_group_layouts
376
            .get(&key)
377
            .map(|(_, pipeline_id)| *pipeline_id)
×
378
    }
379

380
    pub fn get_sort_copy_pipeline_id(&self) -> CachedComputePipelineId {
×
381
        self.sort_copy_pipeline_id
×
382
    }
383

384
    pub fn ensure_sort_fill_bind_group(
×
385
        &mut self,
386
        particle_layout: &ParticleLayout,
387
        particle: &Buffer,
388
        indirect_index: &Buffer,
389
        effect_metadata: &Buffer,
390
    ) -> Result<&BindGroup, ()> {
391
        let key = SortFillBindGroupKey {
392
            particle: particle.id(),
×
393
            indirect_index: indirect_index.id(),
×
394
            effect_metadata: effect_metadata.id(),
×
395
        };
396
        let entry = self.sort_fill_bind_groups.entry(key);
×
397
        let bind_group = match entry {
×
398
            Entry::Occupied(entry) => entry.into_mut(),
×
399
            Entry::Vacant(entry) => {
×
400
                // Note: can't use get_bind_group_layout() because the function call mixes the
401
                // lifetimes of the two hash maps and complains the bind group one is already
402
                // borrowed. Doing a manual access to the layout one instead makes the compiler
403
                // happy.
404
                let key = SortFillBindGroupLayoutKey::from_particle_layout(particle_layout)?;
×
405
                let layout = &self.sort_fill_bind_group_layouts.get(&key).ok_or(())?.0;
×
406
                entry.insert(
407
                    self.render_device.create_bind_group(
408
                        "hanabi:bind_group:sort_fill",
409
                        layout,
410
                        &[
411
                            // @group(0) @binding(0) var<storage, read_write> pairs:
412
                            // array<KeyValuePair>;
413
                            BindGroupEntry {
414
                                binding: 0,
415
                                resource: BindingResource::Buffer(BufferBinding {
416
                                    buffer: &self.sort_buffer,
417
                                    offset: 0,
418
                                    size: None,
419
                                }),
420
                            },
421
                            // @group(0) @binding(1) var<storage, read> particle_buffer:
422
                            // ParticleBuffer;
423
                            BindGroupEntry {
424
                                binding: 1,
425
                                resource: BindingResource::Buffer(BufferBinding {
426
                                    buffer: particle,
427
                                    offset: 0,
428
                                    size: None,
429
                                }),
430
                            },
431
                            // @group(0) @binding(2) var<storage, read> indirect_index_buffer :
432
                            // array<u32>;
433
                            BindGroupEntry {
434
                                binding: 2,
435
                                resource: BindingResource::Buffer(BufferBinding {
436
                                    buffer: indirect_index,
437
                                    offset: 0,
438
                                    size: None,
439
                                }),
440
                            },
441
                            // @group(0) @binding(3) var<storage, read> effect_metadata :
442
                            // EffectMetadata;
443
                            BindGroupEntry {
444
                                binding: 3,
445
                                resource: BindingResource::Buffer(BufferBinding {
446
                                    buffer: effect_metadata,
447
                                    offset: 0,
448
                                    size: Some(GpuEffectMetadata::aligned_size(
449
                                        self.render_device
450
                                            .limits()
451
                                            .min_storage_buffer_offset_alignment,
452
                                    )),
453
                                }),
454
                            },
455
                        ],
456
                    ),
457
                )
458
            }
459
        };
460
        Ok(bind_group)
461
    }
462

463
    pub fn sort_fill_bind_group(
×
464
        &self,
465
        particle: BufferId,
466
        indirect_index: BufferId,
467
        effect_metadata: BufferId,
468
    ) -> Option<&BindGroup> {
469
        let key = SortFillBindGroupKey {
470
            particle,
471
            indirect_index,
472
            effect_metadata,
473
        };
474
        self.sort_fill_bind_groups.get(&key)
×
475
    }
476

477
    pub fn ensure_sort_copy_bind_group(
×
478
        &mut self,
479
        indirect_index_buffer: &Buffer,
480
        effect_metadata_buffer: &Buffer,
481
    ) -> Result<&BindGroup, ()> {
482
        let key = SortCopyBindGroupKey {
483
            indirect_index: indirect_index_buffer.id(),
×
484
            sort: self.sort_buffer.id(),
×
485
            effect_metadata: effect_metadata_buffer.id(),
×
486
        };
487
        let entry = self.sort_copy_bind_groups.entry(key);
×
488
        let bind_group = match entry {
×
489
            Entry::Occupied(entry) => entry.into_mut(),
×
490
            Entry::Vacant(entry) => {
×
491
                entry.insert(
×
492
                    self.render_device.create_bind_group(
×
493
                        "hanabi:bind_group:sort_copy",
×
494
                        &self.sort_copy_bind_group_layout,
×
495
                        &[
×
496
                            // @group(0) @binding(0) var<storage, read_write> indirect_index_buffer
497
                            // : IndirectIndexBuffer;
498
                            BindGroupEntry {
×
499
                                binding: 0,
×
500
                                resource: BindingResource::Buffer(BufferBinding {
×
501
                                    buffer: indirect_index_buffer,
×
502
                                    offset: 0,
×
503
                                    size: None,
×
504
                                }),
505
                            },
506
                            // @group(0) @binding(1) var<storage, read> sort_buffer : SortBuffer;
507
                            BindGroupEntry {
×
508
                                binding: 1,
×
509
                                resource: BindingResource::Buffer(BufferBinding {
×
510
                                    buffer: &self.sort_buffer,
×
511
                                    offset: 0,
×
512
                                    size: None,
×
513
                                }),
514
                            },
515
                            // @group(0) @binding(2) var<storage, read> effect_metadata :
516
                            // EffectMetadata;
517
                            BindGroupEntry {
×
518
                                binding: 2,
×
519
                                resource: BindingResource::Buffer(BufferBinding {
×
520
                                    buffer: effect_metadata_buffer,
×
521
                                    offset: 0,
×
522
                                    size: Some(GpuEffectMetadata::aligned_size(
×
523
                                        self.render_device
×
524
                                            .limits()
×
525
                                            .min_storage_buffer_offset_alignment,
×
526
                                    )),
527
                                }),
528
                            },
529
                        ],
530
                    ),
531
                )
532
            }
533
        };
534
        Ok(bind_group)
×
535
    }
536

537
    pub fn sort_copy_bind_group(
×
538
        &self,
539
        indirect_index: BufferId,
540
        effect_metadata: BufferId,
541
    ) -> Option<&BindGroup> {
542
        let key = SortCopyBindGroupKey {
543
            indirect_index,
544
            sort: self.sort_buffer.id(),
×
545
            effect_metadata,
546
        };
547
        self.sort_copy_bind_groups.get(&key)
×
548
    }
549
}
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