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

djeedai / bevy_hanabi / 14010391093

22 Mar 2025 04:31PM UTC coverage: 40.154% (+0.01%) from 40.144%
14010391093

push

github

web-flow
Fix sort binding offset not aligned to device limit (#439)

Some sort compute pass were using a dynamic binding offset not a multiple of
the device limit `min_storage_buffer_offset_alignment`. Instead of fixing the
alignment, since the compute shader already adds a custom `u32` offset, use
that offset to index precisely the dispatch indirect struct we want (12 bytes),
and always bind the entire buffer with a zero binding offset.

Fixes #438

0 of 8 new or added lines in 1 file covered. (0.0%)

2 existing lines in 2 files now uncovered.

3232 of 8049 relevant lines covered (40.15%)

18.68 hits per line

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

230
    #[inline]
231
    pub fn clear_indirect_dispatch_buffer(&mut self) {
×
232
        self.indirect_buffer.clear();
×
233
    }
234

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

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

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

251
    #[inline]
252
    pub fn indirect_buffer(&self) -> Option<&Buffer> {
×
253
        self.indirect_buffer.buffer()
×
254
    }
255

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

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

266
    #[inline]
267
    pub fn prepare_buffers(&mut self, render_device: &RenderDevice) {
×
268
        self.indirect_buffer.prepare_buffers(render_device);
×
269
    }
270

271
    #[inline]
272
    pub fn write_buffers(&self, command_encoder: &mut CommandEncoder) {
×
273
        self.indirect_buffer.write_buffers(command_encoder);
×
274
    }
275

276
    #[inline]
277
    pub fn clear_previous_frame_resizes(&mut self) {
×
278
        self.indirect_buffer.clear_previous_frame_resizes();
×
279
    }
280

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

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

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

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

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

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

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

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