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

djeedai / bevy_hanabi / 18243240671

04 Oct 2025 10:36AM UTC coverage: 66.58% (+0.1%) from 66.455%
18243240671

push

github

web-flow
Split extraction and render into unit systems (#499)

Reorganize most of the extraction and render systems into smaller,
unit-like systems with limited (ideally, a single) responsibility. Split
most of the data into separate, smaller components too. This not only
enable better multithreading, but also greatly simplify maintenance by
clarifying the logic and responsibility of each system and component.

As part of this change, add a "ready state" to the effect, which is read
back from the render world and informs the main world about whether an
effect is ready for simulation and rendering. This includes:

- All GPU resources being allocated, and in particular the PSOs
  (pipelines) which in Bevy are compiled asynchronously and can be very
  slow (many frames of delay).
- The ready state of all descendant effects, recursively. This ensures a
  child is ready to _e.g._ receive GPU spawn events before its parent,
  which emits those events, starts simulating.

This new ready state is accessed via
`CompiledParticleEffect::is_ready()`. Note that the state is updated
during the extract phase with the information collected from the
previous render frame, so by the time `is_ready()` returns `true`,
already one frame of simulation and rendering generally occurred.

Remove the outdated `copyless` dependency.

594 of 896 new or added lines in 12 files covered. (66.29%)

21 existing lines in 3 files now uncovered.

5116 of 7684 relevant lines covered (66.58%)

416.91 hits per line

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

41.99
/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
            CachedPipelineState, 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, GpuDispatchIndirectArgs, 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, ()> {
×
NEW
33
        let particle_ribbon_id_offset = particle_layout
×
NEW
34
            .byte_offset(Attribute::RIBBON_ID)
×
NEW
35
            .ok_or(())?;
×
NEW
36
        let particle_age_offset = particle_layout.byte_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<GpuDispatchIndirectArgs>,
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(
3✔
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>();
9✔
95
        let pipeline_cache = world.resource::<PipelineCache>();
9✔
96

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

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

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

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

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

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

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

215
        Self {
216
            render_device: render_device.clone(),
9✔
217
            sort_fill_shader,
218
            sort_buffer,
219
            indirect_buffer,
220
            sort_fill_bind_group_layouts: default(),
6✔
221
            sort_fill_bind_groups: default(),
6✔
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(),
3✔
227
        }
228
    }
229

230
    #[inline]
231
    pub fn clear_indirect_dispatch_buffer(&mut self) {
1,030✔
232
        self.indirect_buffer.clear();
2,060✔
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> {
1,012✔
253
        self.indirect_buffer.buffer()
2,024✔
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
    /// Check if the sort pipeline is ready to run for the given effect
267
    /// instance.
268
    ///
269
    /// This ensures all compute pipelines are compiled and ready to be used
270
    /// this frame.
NEW
271
    pub fn is_pipeline_ready(
×
272
        &self,
273
        particle_layout: &ParticleLayout,
274
        pipeline_cache: &PipelineCache,
275
    ) -> bool {
276
        // Validate the sort-fill pipeline. It was created and queued for compile by
277
        // ensure_sort_fill_bind_group_layout(), which normally is called just before
278
        // is_pipeline_ready().
NEW
279
        let Some(pipeline_id) = self.get_sort_fill_pipeline_id(particle_layout) else {
×
NEW
280
            return false;
×
281
        };
NEW
282
        if !matches!(
×
283
            pipeline_cache.get_compute_pipeline_state(pipeline_id),
284
            CachedPipelineState::Ok(_)
285
        ) {
NEW
286
            return false;
×
287
        }
288

289
        // The 2 pipelines below are created and queued for compile in new(), so are
290
        // almost always ready.
291
        // FIXME - they could be checked once a frame only, not once per effect...
292

293
        // Validate the sort pipeline
NEW
294
        if !matches!(
×
NEW
295
            pipeline_cache.get_compute_pipeline_state(self.sort_pipeline_id()),
×
296
            CachedPipelineState::Ok(_)
297
        ) {
NEW
298
            return false;
×
299
        }
300

301
        // Validate the sort-copy pipeline
NEW
302
        if !matches!(
×
NEW
303
            pipeline_cache.get_compute_pipeline_state(self.get_sort_copy_pipeline_id()),
×
304
            CachedPipelineState::Ok(_)
305
        ) {
NEW
306
            return false;
×
307
        }
308

NEW
309
        true
×
310
    }
311

312
    #[inline]
313
    pub fn prepare_buffers(&mut self, render_device: &RenderDevice) {
1,030✔
314
        self.indirect_buffer.prepare_buffers(render_device);
3,090✔
315
    }
316

317
    #[inline]
318
    pub fn write_buffers(&self, command_encoder: &mut CommandEncoder) {
1,030✔
319
        self.indirect_buffer.write_buffers(command_encoder);
3,090✔
320
    }
321

322
    #[inline]
323
    pub fn clear_previous_frame_resizes(&mut self) {
1,030✔
324
        self.indirect_buffer.clear_previous_frame_resizes();
2,060✔
325
    }
326

327
    pub fn ensure_sort_fill_bind_group_layout(
×
328
        &mut self,
329
        pipeline_cache: &PipelineCache,
330
        particle_layout: &ParticleLayout,
331
    ) -> Result<&BindGroupLayout, ()> {
332
        let key = SortFillBindGroupLayoutKey::from_particle_layout(particle_layout)?;
×
333
        let (layout, _) = self
334
            .sort_fill_bind_group_layouts
335
            .entry(key)
336
            .or_insert_with(|| {
×
337
                let alignment = self
×
338
                    .render_device
×
339
                    .limits()
×
340
                    .min_storage_buffer_offset_alignment;
×
341
                let bind_group_layout = self.render_device.create_bind_group_layout(
×
342
                    "hanabi:bind_group_layout:sort_fill",
343
                    &[
×
344
                        // @group(0) @binding(0) var<storage, read_write> pairs: array<KeyValuePair>;
345
                        BindGroupLayoutEntry {
×
346
                            binding: 0,
×
347
                            visibility: ShaderStages::COMPUTE,
×
348
                            ty: BindingType::Buffer {
×
349
                                ty: BufferBindingType::Storage { read_only: false },
×
350
                                has_dynamic_offset: false,
×
351
                                min_binding_size: Some(NonZeroU64::new(16).unwrap()), // count + dual kv pair
×
352
                            },
353
                            count: None,
×
354
                        },
355
                        // @group(0) @binding(1) var<storage, read> particle_buffer: ParticleBuffer;
356
                        BindGroupLayoutEntry {
×
357
                            binding: 1,
×
358
                            visibility: ShaderStages::COMPUTE,
×
359
                            ty: BindingType::Buffer {
×
360
                                ty: BufferBindingType::Storage { read_only: true },
×
361
                                has_dynamic_offset: true,
×
362
                                min_binding_size: Some(key.particle_min_binding_size.into()),
×
363
                            },
364
                            count: None,
×
365
                        },
366
                        // @group(0) @binding(2) var<storage, read> indirect_index_buffer : array<u32>;
367
                        BindGroupLayoutEntry {
×
368
                            binding: 2,
×
369
                            visibility: ShaderStages::COMPUTE,
×
370
                            ty: BindingType::Buffer {
×
371
                                ty: BufferBindingType::Storage { read_only: true },
×
372
                                has_dynamic_offset: true,
×
373
                                min_binding_size: Some(NonZeroU64::new(12).unwrap()), // ping/pong+dead
×
374
                            },
375
                            count: None,
×
376
                        },
377
                        // @group(0) @binding(3) var<storage, read_write> effect_metadata : EffectMetadata;
378
                        BindGroupLayoutEntry {
×
379
                            binding: 3,
×
380
                            visibility: ShaderStages::COMPUTE,
×
381
                            ty: BindingType::Buffer {
×
382
                                ty: BufferBindingType::Storage { read_only: false },
×
383
                                has_dynamic_offset: true,
×
384
                                min_binding_size: Some(GpuEffectMetadata::aligned_size(alignment)),
×
385
                            },
386
                            count: None,
×
387
                        },
388
                    ],
389
                );
390
                let pipeline_id =
×
391
                    pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor {
×
392
                        label: Some("hanabi:pipeline:sort_fill".into()),
×
393
                        layout: vec![bind_group_layout.clone()],
×
394
                        shader: self.sort_fill_shader.clone(),
×
395
                        shader_defs: vec!["HAS_DUAL_KEY".into()],
×
396
                        entry_point: "main".into(),
×
397
                        push_constant_ranges: vec![],
×
398
                        zero_initialize_workgroup_memory: false,
×
399
                    });
400
                (bind_group_layout, pipeline_id)
×
401
            });
402
        Ok(layout)
403
    }
404

405
    // We currently only use the bind group layout internally in
406
    // ensure_sort_fill_bind_group()
407
    #[allow(dead_code)]
408
    pub fn get_sort_fill_bind_group_layout(
×
409
        &self,
410
        particle_layout: &ParticleLayout,
411
    ) -> Option<&BindGroupLayout> {
412
        let key = SortFillBindGroupLayoutKey::from_particle_layout(particle_layout).ok()?;
×
413
        self.sort_fill_bind_group_layouts
414
            .get(&key)
415
            .map(|(layout, _)| layout)
416
    }
417

418
    pub fn get_sort_fill_pipeline_id(
×
419
        &self,
420
        particle_layout: &ParticleLayout,
421
    ) -> Option<CachedComputePipelineId> {
422
        let key = SortFillBindGroupLayoutKey::from_particle_layout(particle_layout).ok()?;
×
423
        self.sort_fill_bind_group_layouts
424
            .get(&key)
425
            .map(|(_, pipeline_id)| *pipeline_id)
426
    }
427

428
    pub fn get_sort_copy_pipeline_id(&self) -> CachedComputePipelineId {
×
429
        self.sort_copy_pipeline_id
×
430
    }
431

432
    pub fn ensure_sort_fill_bind_group(
×
433
        &mut self,
434
        particle_layout: &ParticleLayout,
435
        particle: &Buffer,
436
        indirect_index: &Buffer,
437
        effect_metadata: &Buffer,
438
    ) -> Result<&BindGroup, ()> {
439
        let key = SortFillBindGroupKey {
440
            particle: particle.id(),
×
441
            indirect_index: indirect_index.id(),
×
442
            effect_metadata: effect_metadata.id(),
×
443
        };
444
        let entry = self.sort_fill_bind_groups.entry(key);
×
445
        let bind_group = match entry {
×
446
            Entry::Occupied(entry) => entry.into_mut(),
×
447
            Entry::Vacant(entry) => {
×
448
                // Note: can't use get_bind_group_layout() because the function call mixes the
449
                // lifetimes of the two hash maps and complains the bind group one is already
450
                // borrowed. Doing a manual access to the layout one instead makes the compiler
451
                // happy.
452
                let key = SortFillBindGroupLayoutKey::from_particle_layout(particle_layout)?;
×
453
                let layout = &self.sort_fill_bind_group_layouts.get(&key).ok_or(())?.0;
×
454
                entry.insert(
455
                    self.render_device.create_bind_group(
456
                        "hanabi:bind_group:sort_fill",
457
                        layout,
458
                        &[
459
                            // @group(0) @binding(0) var<storage, read_write> pairs:
460
                            // array<KeyValuePair>;
461
                            BindGroupEntry {
462
                                binding: 0,
463
                                resource: BindingResource::Buffer(BufferBinding {
464
                                    buffer: &self.sort_buffer,
465
                                    offset: 0,
466
                                    size: None,
467
                                }),
468
                            },
469
                            // @group(0) @binding(1) var<storage, read> particle_buffer:
470
                            // ParticleBuffer;
471
                            BindGroupEntry {
472
                                binding: 1,
473
                                resource: BindingResource::Buffer(BufferBinding {
474
                                    buffer: particle,
475
                                    offset: 0,
476
                                    size: None,
477
                                }),
478
                            },
479
                            // @group(0) @binding(2) var<storage, read> indirect_index_buffer :
480
                            // array<u32>;
481
                            BindGroupEntry {
482
                                binding: 2,
483
                                resource: BindingResource::Buffer(BufferBinding {
484
                                    buffer: indirect_index,
485
                                    offset: 0,
486
                                    size: None,
487
                                }),
488
                            },
489
                            // @group(0) @binding(3) var<storage, read> effect_metadata :
490
                            // EffectMetadata;
491
                            BindGroupEntry {
492
                                binding: 3,
493
                                resource: BindingResource::Buffer(BufferBinding {
494
                                    buffer: effect_metadata,
495
                                    offset: 0,
496
                                    size: Some(GpuEffectMetadata::aligned_size(
497
                                        self.render_device
498
                                            .limits()
499
                                            .min_storage_buffer_offset_alignment,
500
                                    )),
501
                                }),
502
                            },
503
                        ],
504
                    ),
505
                )
506
            }
507
        };
508
        Ok(bind_group)
509
    }
510

511
    pub fn sort_fill_bind_group(
×
512
        &self,
513
        particle: BufferId,
514
        indirect_index: BufferId,
515
        effect_metadata: BufferId,
516
    ) -> Option<&BindGroup> {
517
        let key = SortFillBindGroupKey {
518
            particle,
519
            indirect_index,
520
            effect_metadata,
521
        };
522
        self.sort_fill_bind_groups.get(&key)
×
523
    }
524

525
    pub fn ensure_sort_copy_bind_group(
×
526
        &mut self,
527
        indirect_index_buffer: &Buffer,
528
        effect_metadata_buffer: &Buffer,
529
    ) -> Result<&BindGroup, ()> {
530
        let key = SortCopyBindGroupKey {
531
            indirect_index: indirect_index_buffer.id(),
×
532
            sort: self.sort_buffer.id(),
×
533
            effect_metadata: effect_metadata_buffer.id(),
×
534
        };
535
        let entry = self.sort_copy_bind_groups.entry(key);
×
536
        let bind_group = match entry {
×
537
            Entry::Occupied(entry) => entry.into_mut(),
×
538
            Entry::Vacant(entry) => {
×
539
                entry.insert(
×
540
                    self.render_device.create_bind_group(
×
541
                        "hanabi:bind_group:sort_copy",
×
542
                        &self.sort_copy_bind_group_layout,
×
543
                        &[
×
544
                            // @group(0) @binding(0) var<storage, read_write> indirect_index_buffer
545
                            // : IndirectIndexBuffer;
546
                            BindGroupEntry {
×
547
                                binding: 0,
×
548
                                resource: BindingResource::Buffer(BufferBinding {
×
549
                                    buffer: indirect_index_buffer,
×
550
                                    offset: 0,
×
551
                                    size: None,
×
552
                                }),
553
                            },
554
                            // @group(0) @binding(1) var<storage, read> sort_buffer : SortBuffer;
555
                            BindGroupEntry {
×
556
                                binding: 1,
×
557
                                resource: BindingResource::Buffer(BufferBinding {
×
558
                                    buffer: &self.sort_buffer,
×
559
                                    offset: 0,
×
560
                                    size: None,
×
561
                                }),
562
                            },
563
                            // @group(0) @binding(2) var<storage, read> effect_metadata :
564
                            // EffectMetadata;
565
                            BindGroupEntry {
×
566
                                binding: 2,
×
567
                                resource: BindingResource::Buffer(BufferBinding {
×
568
                                    buffer: effect_metadata_buffer,
×
569
                                    offset: 0,
×
570
                                    size: Some(GpuEffectMetadata::aligned_size(
×
571
                                        self.render_device
×
572
                                            .limits()
×
573
                                            .min_storage_buffer_offset_alignment,
×
574
                                    )),
575
                                }),
576
                            },
577
                        ],
578
                    ),
579
                )
580
            }
581
        };
582
        Ok(bind_group)
×
583
    }
584

585
    pub fn sort_copy_bind_group(
×
586
        &self,
587
        indirect_index: BufferId,
588
        effect_metadata: BufferId,
589
    ) -> Option<&BindGroup> {
590
        let key = SortCopyBindGroupKey {
591
            indirect_index,
592
            sort: self.sort_buffer.id(),
×
593
            effect_metadata,
594
        };
595
        self.sort_copy_bind_groups.get(&key)
×
596
    }
597
}
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