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

djeedai / bevy_hanabi / 12128238298

02 Dec 2024 09:24PM UTC coverage: 48.661% (-7.6%) from 56.217%
12128238298

Pull #401

github

web-flow
Merge 30c486d1a into 19aee8dbc
Pull Request #401: Upgrade to Bevy v0.15.0

39 of 284 new or added lines in 11 files covered. (13.73%)

435 existing lines in 8 files now uncovered.

3106 of 6383 relevant lines covered (48.66%)

21.61 hits per line

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

61.25
/src/render/aligned_buffer_vec.rs
1
use std::num::NonZeroU64;
2

3
use bevy::{
4
    log::trace,
5
    render::{
6
        render_resource::{
7
            Buffer, BufferAddress, BufferDescriptor, BufferUsages, ShaderSize, ShaderType,
8
        },
9
        renderer::{RenderDevice, RenderQueue},
10
    },
11
};
12
use bytemuck::{cast_slice, Pod};
13
use copyless::VecHelper;
14

15
/// Like Bevy's [`BufferVec`], but with extra per-item alignment.
16
///
17
/// This helper ensures the individual array elements are properly aligned,
18
/// depending on the device constraints and the WGSL rules. In general using
19
/// [`BufferVec`] is enough to ensure alignment; however when some array items
20
/// also need to be bound individually, then each item (not only the array
21
/// itself) needs to be aligned to the device requirements. This is admittedly a
22
/// very specific case, because the device alignment might be very large (256
23
/// bytes) and this causes a lot of wasted space (padding per-element, instead
24
/// of padding for the entire array).
25
///
26
/// For this buffer to work correctly and items be bindable individually, the
27
/// alignment must come from one of the [`WgpuLimits`]. For example for a
28
/// storage buffer, to be able to bind the entire buffer but also any subset of
29
/// it (including individual elements), the extra alignment must
30
/// be [`WgpuLimits::min_storage_buffer_offset_alignment`].
31
///
32
/// The element type `T` needs to implement the following traits:
33
/// - [`Pod`] to allow copy.
34
/// - [`ShaderType`] because it needs to be mapped for a shader.
35
/// - [`ShaderSize`] to ensure a fixed footprint, to allow packing multiple
36
///   instances inside a single buffer. This therefore excludes any
37
///   runtime-sized array.
38
///
39
/// [`BufferVec`]: bevy::render::render_resource::BufferVec
40
/// [`WgpuLimits`]: bevy::render::settings::WgpuLimits
41
pub struct AlignedBufferVec<T: Pod + ShaderSize> {
42
    /// Pending values accumulated on CPU and not yet written to GPU.
43
    values: Vec<T>,
44
    /// GPU buffer if already allocated, or `None` otherwise.
45
    buffer: Option<Buffer>,
46
    /// Capacity of the buffer, in number of elements.
47
    capacity: usize,
48
    /// Size of a single buffer element, in bytes, in CPU memory (Rust layout).
49
    item_size: usize,
50
    /// Size of a single buffer element, in bytes, aligned to GPU memory
51
    /// constraints.
52
    aligned_size: usize,
53
    /// GPU buffer usages.
54
    buffer_usage: BufferUsages,
55
    /// Optional GPU buffer name, for debugging.
56
    label: Option<String>,
57
}
58

59
impl<T: Pod + ShaderSize> Default for AlignedBufferVec<T> {
60
    fn default() -> Self {
22✔
61
        let item_size = std::mem::size_of::<T>();
22✔
62
        let aligned_size = <T as ShaderSize>::SHADER_SIZE.get() as usize;
22✔
63
        assert!(aligned_size >= item_size);
22✔
64
        Self {
65
            values: Vec::new(),
22✔
66
            buffer: None,
67
            capacity: 0,
68
            buffer_usage: BufferUsages::all(),
22✔
69
            item_size,
70
            aligned_size,
71
            label: None,
72
        }
73
    }
74
}
75

76
impl<T: Pod + ShaderSize> AlignedBufferVec<T> {
77
    /// Create a new collection.
78
    ///
79
    /// `item_align` is an optional additional alignment for items in the
80
    /// collection. If greater than the natural alignment dictated by WGSL
81
    /// rules, this extra alignment is enforced. Otherwise it's ignored (so you
82
    /// can pass `0` to ignore).
83
    ///
84
    /// # Panics
85
    ///
86
    /// Panics if `buffer_usage` contains [`BufferUsages::UNIFORM`] and the
87
    /// layout of the element type `T` does not meet the requirements of the
88
    /// uniform address space, as tested by
89
    /// [`ShaderType::assert_uniform_compat()`].
90
    ///
91
    /// [`BufferUsages::UNIFORM`]: bevy::render::render_resource::BufferUsages::UNIFORM
92
    pub fn new(
22✔
93
        buffer_usage: BufferUsages,
94
        item_align: Option<NonZeroU64>,
95
        label: Option<String>,
96
    ) -> Self {
97
        // GPU-aligned item size, compatible with WGSL rules
98
        let item_size = <T as ShaderSize>::SHADER_SIZE.get() as usize;
22✔
99
        // Extra manual alignment for device constraints
100
        let aligned_size = if let Some(item_align) = item_align {
63✔
101
            let item_align = item_align.get() as usize;
×
NEW
102
            let aligned_size = item_size.next_multiple_of(item_align);
×
103
            assert!(aligned_size >= item_size);
×
104
            assert!(aligned_size % item_align == 0);
19✔
105
            aligned_size
19✔
106
        } else {
107
            item_size
3✔
108
        };
109
        trace!(
×
110
            "AlignedBufferVec: item_size={} aligned_size={}",
×
111
            item_size,
×
112
            aligned_size
×
113
        );
114
        if buffer_usage.contains(BufferUsages::UNIFORM) {
22✔
115
            <T as ShaderType>::assert_uniform_compat();
×
116
        }
117
        Self {
118
            buffer_usage,
119
            aligned_size,
120
            label,
121
            ..Default::default()
122
        }
123
    }
124

125
    #[inline]
126
    pub fn buffer(&self) -> Option<&Buffer> {
1✔
127
        self.buffer.as_ref()
1✔
128
    }
129

130
    #[inline]
131
    #[allow(dead_code)]
132
    pub fn capacity(&self) -> usize {
×
133
        self.capacity
×
134
    }
135

136
    #[inline]
137
    pub fn len(&self) -> usize {
21✔
138
        self.values.len()
21✔
139
    }
140

141
    /// Size in bytes of a single item in the buffer, aligned to the item
142
    /// alignment.
143
    #[inline]
144
    pub fn aligned_size(&self) -> usize {
22✔
145
        self.aligned_size
22✔
146
    }
147

148
    #[inline]
149
    #[allow(dead_code)]
150
    pub fn is_empty(&self) -> bool {
42✔
151
        self.values.is_empty()
42✔
152
    }
153

154
    /// Append a value to the buffer.
155
    pub fn push(&mut self, value: T) -> usize {
24✔
156
        let index = self.values.len();
24✔
157
        self.values.alloc().init(value);
24✔
158
        index
24✔
159
    }
160

161
    /// Reserve some capacity into the buffer.
162
    ///
163
    /// If the buffer is reallocated, the old content (on the GPU) is lost, and
164
    /// needs to be re-uploaded to the newly-created buffer. This is done with
165
    /// [`write_buffer()`].
166
    ///
167
    /// # Returns
168
    ///
169
    /// `true` if the buffer was (re)allocated, or `false` if an existing buffer
170
    /// was reused which already had enough capacity.
171
    ///
172
    /// [`write_buffer()`]: AlignedBufferVec::write_buffer
173
    pub fn reserve(&mut self, capacity: usize, device: &RenderDevice) -> bool {
2✔
174
        if capacity > self.capacity {
2✔
175
            let size = self.aligned_size * capacity;
1✔
176
            trace!(
1✔
177
                "reserve: increase capacity from {} to {} elements, new size {} bytes",
×
178
                self.capacity,
×
179
                capacity,
×
180
                size
×
181
            );
182
            self.capacity = capacity;
1✔
183
            self.buffer = Some(device.create_buffer(&BufferDescriptor {
1✔
184
                label: self.label.as_ref().map(|s| &s[..]),
1✔
185
                size: size as BufferAddress,
×
186
                usage: BufferUsages::COPY_DST | self.buffer_usage,
×
187
                mapped_at_creation: false,
×
188
            }));
189
            // FIXME - this discards the old content if any!!!
190
            true
×
191
        } else {
192
            false
1✔
193
        }
194
    }
195

196
    /// Schedule the buffer write to GPU.
197
    ///
198
    /// # Returns
199
    ///
200
    /// `true` if the buffer was (re)allocated, `false` otherwise.
201
    pub fn write_buffer(&mut self, device: &RenderDevice, queue: &RenderQueue) -> bool {
1✔
202
        if self.values.is_empty() {
1✔
UNCOV
203
            return false;
×
204
        }
205
        trace!(
1✔
206
            "write_buffer: values.len={} item_size={} aligned_size={}",
×
207
            self.values.len(),
×
208
            self.item_size,
×
209
            self.aligned_size
×
210
        );
211
        let buffer_changed = self.reserve(self.values.len(), device);
1✔
212
        if let Some(buffer) = &self.buffer {
1✔
213
            let aligned_size = self.aligned_size * self.values.len();
×
214
            trace!("aligned_buffer: size={}", aligned_size);
×
215
            let mut aligned_buffer: Vec<u8> = vec![0; aligned_size];
1✔
216
            for i in 0..self.values.len() {
3✔
217
                let src: &[u8] = cast_slice(std::slice::from_ref(&self.values[i]));
3✔
218
                let dst_offset = i * self.aligned_size;
3✔
219
                let dst_range = dst_offset..dst_offset + self.item_size;
3✔
220
                trace!("+ copy: src={:?} dst={:?}", src.as_ptr(), dst_range);
3✔
221
                let dst = &mut aligned_buffer[dst_range];
3✔
222
                dst.copy_from_slice(src);
3✔
223
            }
224
            let bytes: &[u8] = cast_slice(&aligned_buffer);
1✔
225
            queue.write_buffer(buffer, 0, bytes);
1✔
226
        }
227
        buffer_changed
1✔
228
    }
229

UNCOV
230
    pub fn clear(&mut self) {
×
UNCOV
231
        self.values.clear();
×
232
    }
233
}
234

235
impl<T: Pod + ShaderSize> std::ops::Index<usize> for AlignedBufferVec<T> {
236
    type Output = T;
237

238
    fn index(&self, index: usize) -> &Self::Output {
×
239
        &self.values[index]
×
240
    }
241
}
242

243
impl<T: Pod + ShaderSize> std::ops::IndexMut<usize> for AlignedBufferVec<T> {
244
    fn index_mut(&mut self, index: usize) -> &mut Self::Output {
×
245
        &mut self.values[index]
×
246
    }
247
}
248

249
#[cfg(test)]
250
mod tests {
251
    use bevy::math::Vec3;
252
    use bytemuck::{Pod, Zeroable};
253

254
    use super::*;
255

256
    #[repr(C)]
257
    #[derive(Debug, Default, Clone, Copy, Pod, Zeroable, ShaderType)]
258
    pub(crate) struct GpuDummy {
259
        pub v: Vec3,
260
    }
261

262
    #[repr(C)]
263
    #[derive(Debug, Default, Clone, Copy, Pod, Zeroable, ShaderType)]
264
    pub(crate) struct GpuDummyComposed {
265
        pub simple: GpuDummy,
266
        pub tag: u32,
267
        // GPU padding to 16 bytes due to GpuDummy forcing align to 16 bytes
268
    }
269

270
    #[repr(C)]
271
    #[derive(Debug, Clone, Copy, Pod, Zeroable, ShaderType)]
272
    pub(crate) struct GpuDummyLarge {
273
        pub simple: GpuDummy,
274
        pub tag: u32,
275
        pub large: [f32; 128],
276
    }
277

278
    #[test]
279
    fn abv_sizes() {
280
        // Rust
281
        assert_eq!(std::mem::size_of::<GpuDummy>(), 12);
282
        assert_eq!(std::mem::align_of::<GpuDummy>(), 4);
283
        assert_eq!(std::mem::size_of::<GpuDummyComposed>(), 16); // tight packing
284
        assert_eq!(std::mem::align_of::<GpuDummyComposed>(), 4);
285
        assert_eq!(std::mem::size_of::<GpuDummyLarge>(), 132 * 4); // tight packing
286
        assert_eq!(std::mem::align_of::<GpuDummyLarge>(), 4);
287

288
        // GPU
289
        assert_eq!(<GpuDummy as ShaderType>::min_size().get(), 16); // Vec3 gets padded to 16 bytes
290
        assert_eq!(<GpuDummy as ShaderSize>::SHADER_SIZE.get(), 16);
291
        assert_eq!(<GpuDummyComposed as ShaderType>::min_size().get(), 32); // align is 16 bytes, forces padding
292
        assert_eq!(<GpuDummyComposed as ShaderSize>::SHADER_SIZE.get(), 32);
293
        assert_eq!(<GpuDummyLarge as ShaderType>::min_size().get(), 544); // align is 16 bytes, forces padding
294
        assert_eq!(<GpuDummyLarge as ShaderSize>::SHADER_SIZE.get(), 544);
295

296
        for (item_align, expected_aligned_size) in [
297
            (0, 16),
298
            (4, 16),
299
            (8, 16),
300
            (16, 16),
301
            (32, 32),
302
            (256, 256),
303
            (512, 512),
304
        ] {
305
            let mut abv = AlignedBufferVec::<GpuDummy>::new(
306
                BufferUsages::STORAGE,
307
                NonZeroU64::new(item_align),
308
                None,
309
            );
310
            assert_eq!(abv.aligned_size(), expected_aligned_size);
311
            assert!(abv.is_empty());
312
            abv.push(GpuDummy::default());
313
            assert!(!abv.is_empty());
314
            assert_eq!(abv.len(), 1);
315
        }
316

317
        for (item_align, expected_aligned_size) in [
318
            (0, 32),
319
            (4, 32),
320
            (8, 32),
321
            (16, 32),
322
            (32, 32),
323
            (256, 256),
324
            (512, 512),
325
        ] {
326
            let mut abv = AlignedBufferVec::<GpuDummyComposed>::new(
327
                BufferUsages::STORAGE,
328
                NonZeroU64::new(item_align),
329
                None,
330
            );
331
            assert_eq!(abv.aligned_size(), expected_aligned_size);
332
            assert!(abv.is_empty());
333
            abv.push(GpuDummyComposed::default());
334
            assert!(!abv.is_empty());
335
            assert_eq!(abv.len(), 1);
336
        }
337

338
        for (item_align, expected_aligned_size) in [
339
            (0, 544),
340
            (4, 544),
341
            (8, 544),
342
            (16, 544),
343
            (32, 544),
344
            (256, 768),
345
            (512, 1024),
346
        ] {
347
            let mut abv = AlignedBufferVec::<GpuDummyLarge>::new(
348
                BufferUsages::STORAGE,
349
                NonZeroU64::new(item_align),
350
                None,
351
            );
352
            assert_eq!(abv.aligned_size(), expected_aligned_size);
353
            assert!(abv.is_empty());
354
            abv.push(GpuDummyLarge {
355
                simple: Default::default(),
356
                tag: 0,
357
                large: [0.; 128],
358
            });
359
            assert!(!abv.is_empty());
360
            assert_eq!(abv.len(), 1);
361
        }
362
    }
363
}
364

365
#[cfg(all(test, feature = "gpu_tests"))]
366
mod gpu_tests {
367
    use tests::*;
368

369
    use super::*;
370
    use crate::test_utils::MockRenderer;
371

372
    #[test]
373
    fn abv_write() {
374
        let renderer = MockRenderer::new();
375
        let device = renderer.device();
376
        let queue = renderer.queue();
377

378
        // Create a dummy CommandBuffer to force the write_buffer() call to have any
379
        // effect.
380
        let encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
381
            label: Some("test"),
382
        });
383
        let command_buffer = encoder.finish();
384

385
        let item_align = device.limits().min_storage_buffer_offset_alignment as u64;
386
        let mut abv = AlignedBufferVec::<GpuDummyComposed>::new(
387
            BufferUsages::STORAGE | BufferUsages::MAP_READ,
388
            NonZeroU64::new(item_align),
389
            None,
390
        );
391
        let final_align = item_align.max(<GpuDummyComposed as ShaderSize>::SHADER_SIZE.get());
392
        assert_eq!(abv.aligned_size(), final_align as usize);
393

394
        const CAPACITY: usize = 42;
395

396
        // Write buffer (CPU -> GPU)
397
        abv.push(GpuDummyComposed {
398
            tag: 1,
399
            ..Default::default()
400
        });
401
        abv.push(GpuDummyComposed {
402
            tag: 2,
403
            ..Default::default()
404
        });
405
        abv.push(GpuDummyComposed {
406
            tag: 3,
407
            ..Default::default()
408
        });
409
        abv.reserve(CAPACITY, &device);
410
        abv.write_buffer(&device, &queue);
411
        // need a submit() for write_buffer() to be processed
412
        queue.submit([command_buffer]);
413
        let (tx, rx) = futures::channel::oneshot::channel();
414
        queue.on_submitted_work_done(move || {
415
            tx.send(()).unwrap();
416
        });
417
        device.poll(wgpu::Maintain::Wait);
418
        let _ = futures::executor::block_on(rx);
419
        println!("Buffer written");
420

421
        // Read back (GPU -> CPU)
422
        let buffer = abv.buffer();
423
        let buffer = buffer.as_ref().expect("Buffer was not allocated");
424
        let buffer = buffer.slice(..);
425
        let (tx, rx) = futures::channel::oneshot::channel();
426
        buffer.map_async(wgpu::MapMode::Read, move |result| {
427
            tx.send(result).unwrap();
428
        });
429
        device.poll(wgpu::Maintain::Wait);
430
        let _result = futures::executor::block_on(rx);
431
        let view = buffer.get_mapped_range();
432

433
        // Validate content
434
        assert_eq!(view.len(), final_align as usize * CAPACITY);
435
        for i in 0..3 {
436
            let offset = i * final_align as usize;
437
            let dummy_composed: &[GpuDummyComposed] =
438
                cast_slice(&view[offset..offset + std::mem::size_of::<GpuDummyComposed>()]);
439
            assert_eq!(dummy_composed[0].tag, (i + 1) as u32);
440
        }
441
    }
442
}
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