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

djeedai / bevy_hanabi / 29046178118

09 Jul 2026 07:57PM UTC coverage: 59.296% (+0.7%) from 58.625%
29046178118

Pull #547

github

web-flow
Merge 6dce63c0e into cf35d53d5
Pull Request #547: Batch compatible effect instances in init/update passes

291 of 500 new or added lines in 7 files covered. (58.2%)

11 existing lines in 3 files now uncovered.

5422 of 9144 relevant lines covered (59.3%)

685.51 hits per line

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

52.58
/src/render/gpu_buffer.rs
1
use std::marker::PhantomData;
2

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

16
struct BufferAndSize {
17
    /// Allocate GPU buffer.
18
    pub buffer: Buffer,
19
    /// Size of the buffer, in number of elements.
20
    pub size: u32,
21
}
22

23
/// GPU-only buffer without CPU-side storage.
24
///
25
/// This is a rather specialized helper to allocate an array on the GPU and
26
/// manage its buffer, depending on the device constraints and the WGSL rules
27
/// for data alignment, and allowing to resize the buffer without losing its
28
/// content (so, scheduling a buffer-to-buffer copy on GPU after reallocatin).
29
///
30
/// The element type `T` needs to implement the following traits:
31
/// - [`Pod`] to prevent user error. This is not strictly necessary, as there's
32
///   no copy from or to CPU, but if the placeholder type is not POD this might
33
///   indicate some user error.
34
/// - [`ShaderSize`] to ensure a fixed footprint, to allow packing multiple
35
///   instances inside a single buffer. This therefore excludes any
36
///   runtime-sized array (T being the element type here; it will itself be part
37
///   of an array).
38
pub struct GpuBuffer<T: Pod + ShaderSize> {
39
    /// GPU buffer if already allocated, or `None` otherwise.
40
    buffer: Option<BufferAndSize>,
41
    /// Previous GPU buffer, pending copy.
42
    old_buffer: Option<BufferAndSize>,
43
    /// GPU buffer usages.
44
    buffer_usage: BufferUsages,
45
    /// Optional GPU buffer name, for debugging.
46
    label: Option<String>,
47
    /// Used size, in element count. Elements past this are all free. Elements
48
    /// with a lower index are either allocated or in the free list.
49
    used_size: u32,
50
    /// Free list.
51
    free_list: Vec<u32>,
52
    _phantom: PhantomData<T>,
53
}
54

55
impl<T: Pod + ShaderType + ShaderSize> Default for GpuBuffer<T> {
56
    fn default() -> Self {
12✔
57
        Self {
58
            buffer: None,
59
            old_buffer: None,
60
            buffer_usage: BufferUsages::all(),
24✔
61
            label: None,
62
            used_size: 0,
63
            free_list: vec![],
12✔
64
            _phantom: PhantomData,
65
        }
66
    }
67
}
68

69
impl<T: Pod + ShaderType + ShaderSize> GpuBuffer<T> {
70
    /// Create a new collection.
71
    ///
72
    /// The buffer usage is always augmented by [`BufferUsages::COPY_SRC`] and
73
    /// [`BufferUsages::COPY_DST`] in order to allow buffer-to-buffer copy when
74
    /// reallocating, to preserve old content.
75
    ///
76
    /// # Panics
77
    ///
78
    /// Panics if `buffer_usage` contains [`BufferUsages::UNIFORM`] and the
79
    /// layout of the element type `T` does not meet the requirements of the
80
    /// uniform address space, as tested by
81
    /// [`ShaderType::assert_uniform_compat()`].
82
    ///
83
    /// [`BufferUsages::UNIFORM`]: bevy::render::render_resource::BufferUsages::UNIFORM
84
    #[allow(dead_code)]
85
    pub fn new(buffer_usage: BufferUsages, label: Option<String>) -> Self {
8✔
86
        // GPU-aligned item size, compatible with WGSL rules
87
        let item_size = <T as ShaderSize>::SHADER_SIZE.get() as usize;
16✔
88
        trace!("GpuBuffer: item_size={}", item_size);
8✔
89
        if buffer_usage.contains(BufferUsages::UNIFORM) {
16✔
90
            <T as ShaderType>::assert_uniform_compat();
×
91
        }
92
        Self {
93
            // We need both COPY_SRC and COPY_DST for copy_buffer_to_buffer() on realloc
94
            buffer_usage: buffer_usage | BufferUsages::COPY_SRC | BufferUsages::COPY_DST,
16✔
95
            label,
96
            ..Default::default()
97
        }
98
    }
99

100
    /// Create a new collection from an allocated buffer.
101
    ///
102
    /// The buffer usage must contain [`BufferUsages::COPY_SRC`] and
103
    /// [`BufferUsages::COPY_DST`] in order to allow buffer-to-buffer copy when
104
    /// reallocating, to preserve old content.
105
    ///
106
    /// # Panics
107
    ///
108
    /// Panics if `buffer_usage` doesn't contain [`BufferUsages::COPY_SRC`] or
109
    /// [`BufferUsages::COPY_DST`].
110
    ///
111
    /// Panics if `buffer_usage` contains [`BufferUsages::UNIFORM`] and the
112
    /// layout of the element type `T` does not meet the requirements of the
113
    /// uniform address space, as tested by
114
    /// [`ShaderType::assert_uniform_compat()`].
115
    ///
116
    /// [`BufferUsages::UNIFORM`]: bevy::render::render_resource::BufferUsages::UNIFORM
117
    pub fn new_allocated(buffer: Buffer, label: Option<String>) -> Self {
4✔
118
        // GPU-aligned item size, compatible with WGSL rules.
119
        let item_size = <T as ShaderSize>::SHADER_SIZE.get();
8✔
120
        let buffer_usage = buffer.usage();
8✔
121
        assert!(
4✔
122
            buffer_usage.contains(BufferUsages::COPY_SRC | BufferUsages::COPY_DST),
12✔
123
            "GpuBuffer requires COPY_SRC and COPY_DST buffer usages to allow copy on reallocation."
×
124
        );
125
        if buffer_usage.contains(BufferUsages::UNIFORM) {
8✔
126
            <T as ShaderType>::assert_uniform_compat();
×
127
        }
128
        // Capacity is derived from the physical buffer so the two can't disagree.
129
        debug_assert_eq!(
4✔
130
            buffer.size() % item_size,
4✔
131
            0,
×
132
            "GpuBuffer physical size ({} bytes) is not a multiple of the element size ({} bytes)",
×
133
            buffer.size(),
×
134
            item_size,
×
135
        );
136
        let size = (buffer.size() / item_size) as u32;
8✔
137
        trace!("GpuBuffer: item_size={item_size} capacity={size}");
4✔
138
        Self {
139
            buffer: Some(BufferAndSize { buffer, size }),
8✔
140
            buffer_usage,
141
            label,
142
            ..Default::default()
143
        }
144
    }
145

146
    /// Clear the buffer.
147
    ///
148
    /// This doesn't de-allocate any GPU buffer.
149
    pub fn clear(&mut self) {
1,140✔
150
        self.free_list.clear();
2,280✔
151
        self.used_size = 0;
1,140✔
152
    }
153

154
    /// Allocate a new entry in the buffer.
155
    ///
156
    /// If the GPU buffer has not enough storage, or is not allocated yet, this
157
    /// schedules a (re-)allocation, which must be applied by calling
158
    /// [`allocate_gpu()`] once a frame after all [`allocate()`] calls were made
159
    /// for that frame.
160
    ///
161
    /// # Returns
162
    ///
163
    /// The index of the allocated entry.
164
    ///
165
    /// [`allocate_gpu()`]: Self::allocate_gpu
166
    /// [`allocate()`]: Self::allocate
UNCOV
167
    pub fn allocate(&mut self) -> u32 {
×
UNCOV
168
        if let Some(index) = self.free_list.pop() {
×
169
            index
×
170
        } else {
171
            // Note: we may return an index past the buffer capacity. This will instruct
172
            // allocate_gpu() to re-allocate the buffer.
UNCOV
173
            let index = self.used_size;
×
UNCOV
174
            self.used_size += 1;
×
UNCOV
175
            index
×
176
        }
177
    }
178

179
    /// Allocate a contiguous slice of new entries in the buffer.
180
    ///
181
    /// If the GPU buffer has not enough storage, or is not allocated yet, this
182
    /// schedules a (re-)allocation, which must be applied by calling
183
    /// [`allocate_gpu()`] once a frame after all [`allocate()`] calls were made
184
    /// for that frame.
185
    ///
186
    /// # Returns
187
    ///
188
    /// The index of the first allocated entry.
189
    ///
190
    /// [`allocate_gpu()`]: Self::allocate_gpu
191
    /// [`allocate()`]: Self::allocate
192
    pub fn allocate_slice(&mut self, count: u32) -> u32 {
551✔
193
        // FIXME - we bypass the free list to be sure to have a contiguous slice
194
        // Note: we may return an index past the buffer capacity. This will instruct
195
        // allocate_gpu() to re-allocate the buffer.
196
        let base_index = self.used_size;
1,102✔
197
        self.used_size += count;
551✔
198
        base_index
551✔
199
    }
200

201
    /// Free an existing entry.
202
    ///
203
    /// # Panics
204
    ///
205
    /// In debug only, panics if the entry is not allocated (double-free). In
206
    /// non-debug, the behavior is undefined and will generally lead to bugs.
207
    // Currently we use GpuBuffer in sorting, and re-allocate everything each frame.
208
    #[allow(dead_code)]
209
    pub fn free(&mut self, index: u32) {
×
210
        if index < self.used_size {
×
211
            debug_assert!(
×
212
                !self.free_list.contains(&index),
×
213
                "Double-free in GpuBuffer at index #{}",
×
214
                index
×
215
            );
216
            self.free_list.push(index);
×
217
        }
218
    }
219

220
    /// Get the current GPU buffer, if allocated.
221
    #[inline]
222
    pub fn buffer(&self) -> Option<&Buffer> {
1,102✔
223
        self.buffer.as_ref().map(|b| &b.buffer)
3,306✔
224
    }
225

226
    /// Get a binding for the entire GPU buffer, if allocated.
227
    #[inline]
228
    #[allow(dead_code)]
229
    pub fn as_entire_binding(&self) -> Option<BindingResource<'_>> {
×
230
        let buffer = self.buffer()?;
×
231
        Some(buffer.as_entire_binding())
×
232
    }
233

234
    /// Get the current buffer capacity, in element count.
235
    ///
236
    /// This is the CPU view of allocations, which counts the number of
237
    /// [`allocate()`] and [`free()`] calls.
238
    ///
239
    /// [`allocate()`]: Self::allocate
240
    /// [`free()`]: Self::allocate_gpu
241
    #[inline]
242
    #[allow(dead_code)]
243
    pub fn capacity(&self) -> u32 {
×
244
        debug_assert!(self.used_size >= self.free_list.len() as u32);
×
245
        self.used_size - self.free_list.len() as u32
×
246
    }
247

248
    /// Get the current GPU buffer capacity, in element count.
249
    ///
250
    /// Note that it is possible for [`allocate()`] to return an index greater
251
    /// than or equal to the value returned by [`capacity()`], at least
252
    /// temporarily until [`allocate_gpu()`] is called.
253
    ///
254
    /// [`allocate()`]: Self::allocate
255
    /// [`gpu_capacity()`]: Self::gpu_capacity
256
    /// [`allocate_gpu()`]: Self::allocate_gpu
257
    #[inline]
258
    pub fn gpu_capacity(&self) -> u32 {
1,710✔
259
        self.buffer.as_ref().map(|b| b.size).unwrap_or(0)
6,840✔
260
    }
261

262
    /// Size in bytes of a single item in the buffer.
263
    ///
264
    /// This is equal to [`ShaderSize::SHADER_SIZE`] for the buffer element `T`.
265
    #[inline]
266
    pub fn item_size(&self) -> usize {
3✔
267
        <T as ShaderSize>::SHADER_SIZE.get() as usize
3✔
268
    }
269

270
    /// Check if the buffer is empty.
271
    ///
272
    /// The check is based on the CPU representation of the buffer, that is the
273
    /// number of calls to [`allocate()`]. The buffer is considered empty if no
274
    /// [`allocate()`] call was made, or they all have been followed by a
275
    /// corresponding [`free()`] call. This makes no assumption about the GPU
276
    /// buffer.
277
    ///
278
    /// [`allocate()`]: Self::allocate
279
    /// [`free()`]: Self::free
280
    #[inline]
281
    #[allow(dead_code)]
282
    pub fn is_empty(&self) -> bool {
×
283
        self.used_size == 0
×
284
    }
285

286
    /// Allocate or reallocate the GPU buffer if needed.
287
    ///
288
    /// This allocates or reallocates a GPU buffer to ensure storage for all
289
    /// previous calls to [`allocate()`]. This is a no-op if a GPU buffer is
290
    /// already allocated and has sufficient storage.
291
    ///
292
    /// This should be called once a frame after any new [`allocate()`] in that
293
    /// frame. After this call, [`buffer()`] is guaranteed to return `Some(..)`.
294
    ///
295
    /// # Returns
296
    ///
297
    /// `true` if the buffer was (re)allocated, or `false` if an existing buffer
298
    /// was reused which already had enough capacity.
299
    ///
300
    /// [`reserve()`]: Self::reserve
301
    /// [`allocate()`]: Self::allocate
302
    /// [`buffer()`]: Self::buffer
303
    pub fn prepare_buffers(&mut self, render_device: &RenderDevice) -> bool {
1,710✔
304
        // Don't do anything if we still have some storage.
305
        let old_capacity = self.gpu_capacity();
5,130✔
306
        if self.used_size <= old_capacity {
1,710✔
307
            return false;
1,707✔
308
        }
309

310
        // Ensure we allocate at least 256 more entries than what we need this frame,
311
        // and round that to make it nicer for the GPU.
312
        let new_capacity = (self.used_size + 256).next_multiple_of(1024);
×
313
        if new_capacity <= old_capacity {
×
314
            return false;
×
315
        }
316

317
        // Save the old buffer, we will need to copy it to the new one later.
318
        assert!(self.old_buffer.is_none(), "Multiple calls to GpuTable::prepare_buffers() before write_buffers() was called to copy old content.");
×
319
        self.old_buffer = self.buffer.take();
9✔
320

321
        // Allocate a new buffer of the appropriate size.
322
        let byte_size = self.item_size() * new_capacity as usize;
9✔
323
        trace!(
3✔
324
            "prepare_buffers(): increase capacity from {} to {} elements, new size {} bytes",
×
325
            old_capacity,
×
326
            new_capacity,
×
327
            byte_size
×
328
        );
329
        let buffer = render_device.create_buffer(&BufferDescriptor {
9✔
330
            label: self.label.as_ref().map(|s| &s[..]),
12✔
331
            size: byte_size as BufferAddress,
3✔
332
            usage: BufferUsages::COPY_DST | self.buffer_usage,
3✔
333
            mapped_at_creation: false,
×
334
        });
335
        self.buffer = Some(BufferAndSize {
6✔
336
            buffer,
3✔
337
            size: new_capacity,
3✔
338
        });
339

340
        true
3✔
341
    }
342

343
    /// Schedule any pending buffer copy.
344
    ///
345
    /// If a new buffer was (re-)allocated this frame, this schedules a
346
    /// buffer-to-buffer copy from the old buffer to the new one, then releases
347
    /// the old buffer.
348
    ///
349
    /// This should be called once a frame after [`prepare_buffers()`]. This is
350
    /// a no-op if there's no need for a buffer copy.
351
    ///
352
    /// [`prepare_buffers()`]: Self::prepare_buffers
353
    pub fn write_buffers(&self, command_encoder: &mut CommandEncoder) {
1,710✔
354
        if let Some(old_buffer) = self.old_buffer.as_ref() {
1,710✔
355
            let new_buffer = self.buffer.as_ref().unwrap();
×
356
            assert!(
×
357
                new_buffer.size >= old_buffer.size,
×
358
                "Old buffer is smaller than the new one. This is unexpected."
×
359
            );
360
            command_encoder.copy_buffer_to_buffer(
×
361
                &old_buffer.buffer,
×
362
                0,
363
                &new_buffer.buffer,
×
364
                0,
365
                old_buffer.size as u64,
×
366
            );
367
        }
368
    }
369

370
    /// Clear any stale buffer used for resize in the previous frame during
371
    /// rendering while the data structure was immutable.
372
    ///
373
    /// This must be called before any new [`allocate()`].
374
    ///
375
    /// [`allocate()`]: Self::allocate
376
    pub fn clear_previous_frame_resizes(&mut self) {
1,710✔
377
        if let Some(old_buffer) = self.old_buffer.take() {
1,710✔
378
            old_buffer.buffer.destroy();
×
379
        }
380
    }
381
}
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