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

djeedai / bevy_hanabi / 28058611486

23 Jun 2026 09:33PM UTC coverage: 58.625% (-0.2%) from 58.797%
28058611486

Pull #545

github

web-flow
Merge e9ff5ea21 into 173027ae5
Pull Request #545: Upgrade to Bevy 0.19

107 of 365 new or added lines in 6 files covered. (29.32%)

17 existing lines in 2 files now uncovered.

5244 of 8945 relevant lines covered (58.62%)

187.3 hits per line

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

62.98
/src/render/buffer_table.rs
1
use std::{
2
    borrow::Cow,
3
    num::{NonZeroU32, NonZeroU64},
4
    ops::Range,
5
};
6

7
use bevy::{
8
    log::trace,
9
    render::{
10
        render_resource::{
11
            Buffer, BufferAddress, BufferDescriptor, BufferUsages, CommandEncoder, ShaderSize,
12
            ShaderType,
13
        },
14
        renderer::{RenderDevice, RenderQueue},
15
    },
16
};
17
use bytemuck::{cast_slice, Pod};
18

19
/// Round a range start down to a given alignment, and return the new range and
20
/// the start offset inside the new range of the old range.
21
fn round_range_start_down(range: Range<u64>, align: u64) -> (Range<u64>, u64) {
10✔
22
    assert!(align > 0);
20✔
23
    let delta = align - 1;
20✔
24
    if range.start >= delta {
10✔
25
        // Snap range start to previous multiple of align
26
        let old_start = range.start;
6✔
27
        let new_start = (range.start - delta).next_multiple_of(align);
12✔
28
        let offset = old_start - new_start;
6✔
29
        (new_start..range.end, offset)
3✔
30
    } else {
31
        // Snap range start to 0
32
        (0..range.end, range.start)
7✔
33
    }
34
}
35

36
/// Index of a row in a [`BufferTable`].
37
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38
pub struct BufferTableId(pub(crate) u32); // TEMP: pub(crate)
39

40
impl BufferTableId {
41
    /// An invalid value, often used as placeholder.
42
    pub const INVALID: BufferTableId = BufferTableId(u32::MAX);
43

44
    /// Check if the current ID is valid, that is, is different from
45
    /// [`INVALID`].
46
    ///
47
    /// [`INVALID`]: Self::INVALID
48
    #[inline]
49
    pub fn is_valid(&self) -> bool {
1,884✔
50
        *self != Self::INVALID
1,884✔
51
    }
52

53
    /// Compute a new buffer table ID by offseting an existing one by `count`
54
    /// rows.
55
    #[inline]
56
    #[allow(dead_code)]
57
    pub fn offset(&self, count: u32) -> BufferTableId {
×
58
        debug_assert!(self.is_valid());
×
59
        BufferTableId(self.0 + count)
×
60
    }
61
}
62

63
impl Default for BufferTableId {
64
    fn default() -> Self {
2✔
65
        Self::INVALID
2✔
66
    }
67
}
68

69
#[derive(Debug)]
70
struct AllocatedBuffer {
71
    /// Currently allocated buffer, of size equal to `size`.
72
    buffer: Buffer,
73
    /// Size of the currently allocated buffer, in number of rows.
74
    count: u32,
75
    /// Previously allocated buffer if any, cached until the next buffer write
76
    /// so that old data can be copied into the newly-allocated buffer.
77
    old_buffer: Option<Buffer>,
78
    /// Size of the old buffer if any, in number of rows.
79
    old_count: u32,
80
}
81

82
impl AllocatedBuffer {
83
    /// Get the number of rows of the currently allocated GPU buffer.
84
    ///
85
    /// On capacity grow, the count is valid until the next buffer swap.
86
    pub fn allocated_count(&self) -> u32 {
3✔
87
        if self.old_buffer.is_some() {
6✔
88
            self.old_count
×
89
        } else {
90
            self.count
3✔
91
        }
92
    }
93
}
94

95
/// GPU buffer holding a table with concurrent interleaved CPU/GPU access.
96
///
97
/// The buffer table data structure represents a GPU buffer holding a table made
98
/// of individual rows. Each row of the table has the same layout (same size),
99
/// and can be allocated (assigned to an existing index) or free (available for
100
/// future allocation). The data structure manages a free list of rows, and copy
101
/// of rows modified on CPU to the GPU without touching other rows. This ensures
102
/// that existing rows in the GPU buffer can be accessed and modified by the GPU
103
/// without being overwritten by the CPU and without the need for the CPU to
104
/// read the data back from GPU into CPU memory.
105
///
106
/// The element type `T` needs to implement the following traits:
107
/// - [`Pod`] to allow copy.
108
/// - [`ShaderType`] because it needs to be mapped for a shader.
109
/// - [`ShaderSize`] to ensure a fixed footprint, to allow packing multiple
110
///   instances inside a single buffer. This therefore excludes any
111
///   runtime-sized array.
112
///
113
/// This is similar to a [`BufferVec`] or [`AlignedBufferVec`], but unlike those
114
/// data structures a buffer table preserves rows modified by the GPU without
115
/// overwriting. This is useful when the buffer is also modified by GPU shaders,
116
/// so neither the CPU side nor the GPU side have an up-to-date view of the
117
/// entire table, and so the CPU cannot re-upload the entire table on changes.
118
///
119
/// # Usage
120
///
121
/// - During the [`RenderStage::Prepare`] stage, call
122
///   [`clear_previous_frame_resizes()`] to clear any stale buffer from the
123
///   previous frame. Then insert new rows with [`insert()`] and if you made
124
///   changes call [`allocate_gpu()`] at the end to allocate any new buffer
125
///   needed.
126
/// - During the [`RenderStage::Render`] stage, call [`write_buffer()`] from a
127
///   command encoder before using any row, to perform any buffer resize copy
128
///   pending.
129
///
130
/// [`BufferVec`]: bevy::render::render_resource::BufferVec
131
/// [`AlignedBufferVec`]: crate::render::aligned_buffer_vec::AlignedBufferVec
132
#[derive(Debug)]
133
pub struct BufferTable<T: Pod + ShaderSize> {
134
    /// GPU buffer if already allocated, or `None` otherwise.
135
    buffer: Option<AllocatedBuffer>,
136
    /// GPU buffer usages.
137
    buffer_usage: BufferUsages,
138
    /// Optional GPU buffer name, for debugging.
139
    label: Option<String>,
140
    /// Size of a single buffer element, in bytes, in CPU memory (Rust layout).
141
    item_size: usize,
142
    /// Size of a single buffer element, in bytes, aligned to GPU memory
143
    /// constraints.
144
    aligned_size: usize,
145
    /// Capacity of the buffer, in number of rows.
146
    ///
147
    /// This is the expected capacity, as requested by CPU side allocations and
148
    /// deallocations. The GPU buffer might not have been resized yet to handle
149
    /// it, so might be allocated with a different size.
150
    capacity: u32,
151
    /// Size of the "active" portion of the table, which includes allocated rows
152
    /// and any row in the free list. All other rows in the
153
    /// `active_size..capacity` range are implicitly unallocated.
154
    active_count: u32,
155
    /// Free list of rows available in the GPU buffer for a new allocation. This
156
    /// only contains indices in the `0..active_size` range; all row indices in
157
    /// `active_size..capacity` are assumed to be unallocated.
158
    free_indices: Vec<u32>,
159
    /// Pending values accumulated on CPU and not yet written to GPU, and their
160
    /// rows.
161
    pending_values: Vec<(u32, T)>,
162
    /// Extra pending values accumulated on CPU like `pending_values`, but for
163
    /// which there's not enough space in the current GPU buffer. Those values
164
    /// are sorted in index order, occupying the range `buffer.size..`.
165
    extra_pending_values: Vec<T>,
166
}
167

168
impl<T: Pod + ShaderSize> Default for BufferTable<T> {
169
    fn default() -> Self {
29✔
170
        let item_size = std::mem::size_of::<T>();
58✔
171
        let aligned_size = <T as ShaderSize>::SHADER_SIZE.get() as usize;
58✔
172
        assert!(aligned_size >= item_size);
58✔
173
        Self {
174
            buffer: None,
175
            buffer_usage: BufferUsages::all(),
58✔
176
            label: None,
177
            item_size,
178
            aligned_size,
179
            capacity: 0,
180
            active_count: 0,
181
            free_indices: Vec::new(),
58✔
182
            pending_values: Vec::new(),
29✔
183
            extra_pending_values: Vec::new(),
29✔
184
        }
185
    }
186
}
187

188
impl<T: Pod + ShaderSize> BufferTable<T> {
189
    /// Create a new collection.
190
    ///
191
    /// `item_align` is an optional additional alignment for items in the
192
    /// collection. If greater than the natural alignment dictated by WGSL
193
    /// rules, this extra alignment is enforced. Otherwise it's ignored (so you
194
    /// can pass `None` to ignore). This is useful if for example you want to
195
    /// bind individual rows or any subset of the table, to ensure each row is
196
    /// aligned to the device constraints.
197
    ///
198
    /// # Panics
199
    ///
200
    /// Panics if `buffer_usage` contains [`BufferUsages::UNIFORM`] and the
201
    /// layout of the element type `T` does not meet the requirements of the
202
    /// uniform address space, as tested by
203
    /// [`ShaderType::assert_uniform_compat()`].
204
    ///
205
    /// [`BufferUsages::UNIFORM`]: bevy::render::render_resource::BufferUsages::UNIFORM
206
    pub fn new(
29✔
207
        buffer_usage: BufferUsages,
208
        item_align: Option<NonZeroU64>,
209
        label: Option<String>,
210
    ) -> Self {
211
        // GPU-aligned item size, compatible with WGSL rules
212
        let item_size = <T as ShaderSize>::SHADER_SIZE.get() as usize;
58✔
213
        // Extra manual alignment for device constraints
214
        let aligned_size = if let Some(item_align) = item_align {
84✔
215
            let item_align = item_align.get() as usize;
×
216
            let aligned_size = item_size.next_multiple_of(item_align);
×
217
            assert!(aligned_size >= item_size);
×
218
            assert!(aligned_size.is_multiple_of(item_align));
104✔
219
            aligned_size
26✔
220
        } else {
221
            item_size
3✔
222
        };
223
        trace!(
×
224
            "BufferTable[\"{}\"]: item_size={} aligned_size={}",
×
225
            label.as_ref().unwrap_or(&String::new()),
16✔
226
            item_size,
×
227
            aligned_size
×
228
        );
229
        if buffer_usage.contains(BufferUsages::UNIFORM) {
×
230
            <T as ShaderType>::assert_uniform_compat();
×
231
        }
232
        Self {
233
            // Need COPY_SRC and COPY_DST to copy from old to new buffer on resize
234
            buffer_usage: buffer_usage | BufferUsages::COPY_SRC | BufferUsages::COPY_DST,
×
235
            aligned_size,
236
            label,
237
            ..Default::default()
238
        }
239
    }
240

241
    /// Get a safe buffer label for debug display.
242
    ///
243
    /// Falls back to an empty string if no label was specified.
244
    pub fn safe_label(&self) -> Cow<'_, str> {
650✔
245
        self.label
650✔
246
            .as_ref()
247
            .map(|s| Cow::Borrowed(&s[..]))
1,300✔
248
            .unwrap_or(Cow::Borrowed(""))
1,300✔
249
    }
250

251
    /// Get a safe buffer name for debug display.
252
    ///
253
    /// Same as [`safe_label()`] but includes the buffer ID as well.
254
    ///
255
    /// [`safe_label()`]: self::BufferTable::safe_label
256
    pub fn safe_name(&self) -> String {
650✔
257
        let id = self
1,300✔
258
            .buffer
650✔
259
            .as_ref()
260
            .map(|ab| {
1,290✔
261
                let id: NonZeroU32 = ab.buffer.id().into();
3,200✔
262
                id.get()
1,280✔
263
            })
264
            .unwrap_or(0);
265
        format!("#{}:{}", id, self.safe_label())
1,950✔
266
    }
267

268
    /// Reference to the GPU buffer, if already allocated.
269
    ///
270
    /// This reference corresponds to the currently allocated GPU buffer, which
271
    /// may not contain all data since the last [`insert()`] call, and could
272
    /// become invalid if a new larger buffer needs to be allocated to store the
273
    /// pending values inserted with [`insert()`].
274
    ///
275
    /// [`insert()]`: BufferTable::insert
276
    #[inline]
277
    pub fn buffer(&self) -> Option<&Buffer> {
1,562✔
278
        self.buffer.as_ref().map(|ab| &ab.buffer)
4,686✔
279
    }
280

281
    /// Maximum number of rows the table can hold without reallocation.
282
    ///
283
    /// This is the maximum number of rows that can be added to the table
284
    /// without forcing a new GPU buffer to be allocated and a copy from the old
285
    /// to the new buffer.
286
    ///
287
    /// Note that this doesn't imply that no GPU buffer allocation will ever
288
    /// occur; if a GPU buffer was never allocated, and there are pending
289
    /// CPU rows to insert, then a new buffer will be allocated on next
290
    /// update with this capacity.
291
    #[inline]
292
    #[allow(dead_code)]
293
    pub fn capacity(&self) -> u32 {
27✔
294
        self.capacity
27✔
295
    }
296

297
    /// Current number of rows in use in the table.
298
    ///
299
    /// Note that rows in use are not necessarily contiguous. There may be gaps
300
    /// between used rows.
301
    #[inline]
302
    #[allow(dead_code)]
303
    pub fn len(&self) -> u32 {
31✔
304
        self.active_count - self.free_indices.len() as u32
62✔
305
    }
306

307
    /// Size of a single row in the table, in bytes, aligned to GPU constraints.
308
    #[inline]
309
    #[allow(dead_code)]
310
    pub fn aligned_size(&self) -> usize {
22✔
311
        self.aligned_size
22✔
312
    }
313

314
    /// Is the table empty?
315
    #[inline]
316
    #[allow(dead_code)]
317
    pub fn is_empty(&self) -> bool {
52✔
318
        self.active_count == 0
52✔
319
    }
320

321
    /// Clear all rows of the table without deallocating any existing GPU
322
    /// buffer.
323
    ///
324
    /// This operation only updates the CPU cache of the table, without touching
325
    /// any GPU buffer. On next GPU buffer update, the GPU buffer will be
326
    /// deallocated.
327
    #[allow(dead_code)]
328
    pub fn clear(&mut self) {
×
329
        self.pending_values.clear();
×
330
        self.extra_pending_values.clear();
×
331
        self.free_indices.clear();
×
332
        self.active_count = 0;
×
333
    }
334

335
    /// Clear any stale buffer used for resize in the previous frame during
336
    /// rendering while the data structure was immutable.
337
    ///
338
    /// This must be called before any new [`insert()`].
339
    ///
340
    /// [`insert()`]: crate::BufferTable::insert
341
    pub fn clear_previous_frame_resizes(&mut self) {
667✔
342
        if let Some(ab) = self.buffer.as_mut() {
1,308✔
343
            ab.old_buffer = None;
×
344
            ab.old_count = 0;
×
345
        }
346
    }
347

348
    /// Calculate the size in byte of `count` rows.
349
    #[inline]
350
    fn to_byte_size(&self, count: u32) -> usize {
23✔
351
        count as usize * self.aligned_size
23✔
352
    }
353

354
    /// Insert a new row into the table.
355
    ///
356
    /// For performance reasons, this buffers the row content on the CPU until
357
    /// the next GPU update, to minimize the number of CPU to GPU transfers.
358
    pub fn insert(&mut self, value: T) -> BufferTableId {
33✔
359
        trace!(
33✔
360
            "Inserting into table buffer '{}' with {} free indices, capacity: {}, active_size: {}",
×
361
            self.safe_name(),
8✔
362
            self.free_indices.len(),
8✔
363
            self.capacity,
×
364
            self.active_count
×
365
        );
366
        let index = if self.free_indices.is_empty() {
99✔
367
            let index = self.active_count;
64✔
368
            if index == self.capacity {
64✔
369
                self.capacity += 1;
32✔
370
            }
371
            debug_assert!(index < self.capacity);
64✔
372
            self.active_count += 1;
32✔
373
            index
32✔
374
        } else {
375
            self.free_indices.pop().unwrap()
1✔
376
        };
377
        let allocated_count = self
×
378
            .buffer
×
379
            .as_ref()
380
            .map(|ab| ab.allocated_count())
6✔
381
            .unwrap_or(0);
382
        trace!(
×
383
            "Found free index {}, capacity: {}, active_count: {}, allocated_count: {}",
×
384
            index,
×
385
            self.capacity,
×
386
            self.active_count,
×
387
            allocated_count
×
388
        );
389
        if index < allocated_count {
2✔
390
            self.pending_values.push((index, value));
6✔
391
        } else {
392
            let extra_index = index - allocated_count;
31✔
393
            if extra_index < self.extra_pending_values.len() as u32 {
×
394
                self.extra_pending_values[extra_index as usize] = value;
×
395
            } else {
396
                self.extra_pending_values.push(value);
31✔
397
            }
398
        }
399
        BufferTableId(index)
×
400
    }
401

402
    /// Calculate a dynamic byte offset for a bind group from a table entry.
403
    ///
404
    /// This returns the product of `id` by the internal [`aligned_size()`].
405
    ///
406
    /// # Panic
407
    ///
408
    /// Panics if the `index` is too large, producing a byte offset larger than
409
    /// `u32::MAX`.
410
    ///
411
    /// [`aligned_size()`]: Self::aligned_size
412
    #[inline]
413
    pub fn dynamic_offset(&self, id: BufferTableId) -> u32 {
×
414
        let offset = self.aligned_size * id.0 as usize;
×
415
        assert!(offset <= u32::MAX as usize);
×
416
        u32::try_from(offset).expect("BufferTable index out of bounds")
×
417
    }
418

419
    /// Update an existing row in the table.
420
    ///
421
    /// For performance reasons, this buffers the row content on the CPU until
422
    /// the next GPU update, to minimize the number of CPU to GPU transfers.
423
    ///
424
    /// Calling this function multiple times overwrites the previous value. Only
425
    /// the last value recorded each frame will be uploaded to GPU.
426
    ///
427
    /// # Panics
428
    ///
429
    /// Panics if the `id` is invalid.
430
    pub fn update(&mut self, id: BufferTableId, value: T) {
2✔
431
        assert!(id.is_valid());
6✔
432
        trace!(
2✔
433
            "Updating row {} of table buffer '{}'",
×
434
            id.0,
×
435
            self.safe_name(),
4✔
436
        );
437
        let allocated_count = self
4✔
438
            .buffer
2✔
439
            .as_ref()
440
            .map(|ab| ab.allocated_count())
2✔
441
            .unwrap_or(0);
442
        if id.0 < allocated_count {
2✔
443
            if let Some(idx) = self
×
444
                .pending_values
×
445
                .iter()
446
                .position(|&(index, _)| index == id.0)
×
447
            {
448
                // Overwrite a previous update. This ensures we never upload more than one
449
                // update per row, which would waste GPU bandwidth.
450
                self.pending_values[idx] = (id.0, value);
×
451
            } else {
452
                self.pending_values.push((id.0, value));
×
453
            }
454
        } else {
455
            let extra_index = (id.0 - allocated_count) as usize;
2✔
456
            assert!(extra_index < self.extra_pending_values.len());
×
457
            // Overwrite a previous update. This ensures we never upload more than one
458
            // update per row, which would waste GPU bandwidth.
459
            self.extra_pending_values[extra_index] = value;
2✔
460
        }
461
    }
462

463
    /// Insert several new contiguous rows into the table.
464
    ///
465
    /// For performance reasons, this buffers the row content on the CPU until
466
    /// the next GPU update, to minimize the number of CPU to GPU transfers.
467
    ///
468
    /// # Returns
469
    ///
470
    /// Returns the index of the first entry. Other entries follow right after
471
    /// it.
472
    #[allow(dead_code)] // unused but annoying to write, so keep if we need in the future
473
    pub fn insert_contiguous(&mut self, values: impl ExactSizeIterator<Item = T>) -> BufferTableId {
5✔
474
        let count = values.len() as u32;
10✔
475
        trace!(
5✔
476
            "Inserting {} contiguous values into table buffer '{}' with {} free indices, capacity: {}, active_size: {}",
×
477
            count,
×
478
            self.safe_name(),
×
479
            self.free_indices.len(),
×
480
            self.capacity,
×
481
            self.active_count
×
482
        );
483
        let first_index = if self.free_indices.is_empty() {
15✔
484
            let index = self.active_count;
6✔
485
            if index == self.capacity {
6✔
486
                self.capacity += count;
3✔
487
            }
488
            debug_assert!(index < self.capacity);
6✔
489
            self.active_count += count;
3✔
490
            index
3✔
491
        } else {
492
            let mut s = 0;
2✔
493
            let mut n = 1;
×
494
            let mut i = 1;
×
495
            while i < self.free_indices.len() {
4✔
496
                debug_assert!(self.free_indices[i] > self.free_indices[i - 1]); // always sorted
3✔
497
                if self.free_indices[i] == self.free_indices[i - 1] + 1 {
2✔
498
                    // contiguous
499
                    n += 1;
1✔
500
                    if n == count {
1✔
501
                        break;
1✔
502
                    }
503
                } else {
504
                    // non-contiguous; restart a new sequence
505
                    debug_assert!(n < count);
×
506
                    s = i;
×
507
                }
508
                i += 1;
×
509
            }
510
            if n == count {
2✔
511
                // Found a range of 'count' consecutive entries. Consume it.
512
                let index = self.free_indices[s];
2✔
513
                self.free_indices.splice(s..=i, []);
4✔
514
                index
1✔
515
            } else {
516
                // No free range for 'count' consecutive entries. Allocate at end instead.
517
                let index = self.active_count;
1✔
518
                if index == self.capacity {
1✔
519
                    self.capacity += count;
1✔
520
                }
521
                debug_assert!(index < self.capacity);
×
522
                self.active_count += count;
1✔
523
                index
1✔
524
            }
525
        };
526
        let allocated_count = self
×
527
            .buffer
×
528
            .as_ref()
529
            .map(|ab| ab.allocated_count())
×
530
            .unwrap_or(0);
531
        trace!(
×
532
            "Found {} free indices {}..{}, capacity: {}, active_count: {}, allocated_count: {}",
×
533
            count,
×
534
            first_index,
×
535
            first_index + count,
×
536
            self.capacity,
×
537
            self.active_count,
×
538
            allocated_count
×
539
        );
540
        for (i, value) in values.enumerate() {
10✔
541
            let index = first_index + i as u32;
×
542
            if index < allocated_count {
×
543
                self.pending_values.push((index, value));
×
544
            } else {
545
                let extra_index = index - allocated_count;
10✔
546
                if extra_index < self.extra_pending_values.len() as u32 {
6✔
547
                    self.extra_pending_values[extra_index as usize] = value;
6✔
548
                } else {
549
                    self.extra_pending_values.push(value);
4✔
550
                }
551
            }
552
        }
553
        BufferTableId(first_index)
×
554
    }
555

556
    /// Remove a row from the table.
557
    #[allow(dead_code)]
558
    pub fn remove(&mut self, id: BufferTableId) {
6✔
559
        let index = id.0;
12✔
560
        assert!(index < self.active_count);
12✔
561

562
        // If this is the last item in the active zone, just shrink the active zone
563
        // (implicit free list).
564
        if index == self.active_count - 1 {
9✔
565
            self.active_count -= 1;
3✔
566
            self.capacity -= 1;
3✔
567
        } else {
568
            // This is very inefficient but we need to apply the same logic as the
569
            // EffectCache because we rely on indices being in sync.
570
            let pos = self
3✔
571
                .free_indices
×
572
                .binary_search(&index) // will fail
573
                .unwrap_or_else(|e| e); // will get position of insertion
574
            self.free_indices.insert(pos, index);
×
575
        }
576
    }
577

578
    /// Remove a range of rows from the table.
579
    #[allow(dead_code)]
580
    pub fn remove_range(&mut self, first: BufferTableId, count: u32) {
4✔
581
        let index = first.0;
8✔
582
        assert!(index + count <= self.active_count);
8✔
583

584
        // If this is the last item in the active zone, just shrink the active zone
585
        // (implicit free list).
586
        if index == self.active_count - count {
8✔
587
            self.active_count -= count;
2✔
588
            self.capacity -= count;
2✔
589

590
            // Also try to remove free indices
591
            if self.free_indices.len() as u32 == self.active_count {
3✔
592
                // Easy case: everything is free, clear everything
593
                self.free_indices.clear();
3✔
594
                self.active_count = 0;
1✔
595
                self.capacity = 0;
1✔
596
            } else {
597
                // Some rows are still allocated. Dequeue from end while we have a contiguous
598
                // tail of free indices.
599
                let mut num_popped = 0;
1✔
600
                while let Some(idx) = self.free_indices.pop() {
5✔
601
                    if idx < self.active_count - 1 - num_popped {
×
602
                        self.free_indices.push(idx);
×
603
                        break;
×
604
                    }
605
                    num_popped += 1;
×
606
                }
607
                self.active_count -= num_popped;
×
608
                self.capacity -= num_popped;
×
609
            }
610
        } else {
611
            // This is very inefficient but we need to apply the same logic as the
612
            // EffectCache because we rely on indices being in sync.
613
            let pos = self
2✔
614
                .free_indices
×
615
                .binary_search(&index) // will fail
×
616
                .unwrap_or_else(|e| e); // will get position of insertion
×
617
            self.free_indices.splice(pos..pos, index..(index + count));
×
618
        }
619

620
        debug_assert!(
4✔
621
            (self.free_indices.is_empty() && self.active_count == 0)
10✔
622
                || (self.free_indices.len() as u32) < self.active_count
3✔
623
        );
624
    }
625

626
    /// Allocate any GPU buffer if needed, based on the most recent capacity
627
    /// requested.
628
    ///
629
    /// This should be called only once per frame after all allocation requests
630
    /// have been made via [`insert()`] but before the GPU buffer is actually
631
    /// updated. This is an optimization to enable allocating the GPU buffer
632
    /// earlier than it's actually needed. Calling this multiple times is not
633
    /// supported, and might assert. Not calling it is safe, as the next
634
    /// update will call it just-in-time anyway.
635
    ///
636
    /// # Returns
637
    ///
638
    /// Returns `true` if a new buffer was (re-)allocated, to indicate any bind
639
    /// group needs to be re-created.
640
    ///
641
    /// [`insert()]`: crate::render::BufferTable::insert
642
    pub fn allocate_gpu(&mut self, device: &RenderDevice, queue: &RenderQueue) -> bool {
668✔
643
        // The allocated capacity is the capacity of the currently allocated GPU buffer,
644
        // which can be different from the expected capacity (self.capacity) for next
645
        // update.
646
        let allocated_count = self.buffer.as_ref().map(|ab| ab.count).unwrap_or(0);
3,340✔
647
        let reallocated = if self.capacity > allocated_count {
1,336✔
648
            let byte_size = self.to_byte_size(self.capacity);
24✔
649
            trace!(
6✔
650
                "reserve('{}'): increase capacity from {} to {} elements, old size {} bytes, new size {} bytes",
×
651
                self.safe_name(),
8✔
652
                allocated_count,
×
653
                self.capacity,
×
654
                self.to_byte_size(allocated_count),
12✔
655
                byte_size
×
656
            );
657

658
            // Create the new buffer, swapping with the old one if any
659
            let has_init_data = !self.extra_pending_values.is_empty();
12✔
660
            let new_buffer = device.create_buffer(&BufferDescriptor {
18✔
661
                label: self.label.as_ref().map(|s| &s[..]),
22✔
662
                size: byte_size as BufferAddress,
6✔
663
                usage: self.buffer_usage,
6✔
664
                mapped_at_creation: has_init_data,
6✔
665
            });
666

667
            // Use any pending data to initialize the buffer. We only use CPU-available
668
            // data, which was inserted after the buffer was (re-)allocated and
669
            // has not been uploaded to GPU yet.
670
            if has_init_data {
6✔
671
                // Leave some space to copy the old buffer if any
672
                let base_size = self.to_byte_size(allocated_count) as u64;
18✔
673
                let extra_count = self.extra_pending_values.len() as u32;
12✔
674
                let extra_size = self.to_byte_size(extra_count) as u64;
18✔
675

676
                // Scope get_mapped_range_mut() to force a drop before unmap()
677
                {
678
                    // Note: get_mapped_range_mut() requires 8-byte alignment of the start offset.
679
                    let unaligned_range = base_size..(base_size + extra_size);
18✔
680
                    let (range, byte_offset) = round_range_start_down(unaligned_range, 8);
18✔
681

682
                    let mut dst_slice = new_buffer.slice(range).get_mapped_range_mut();
18✔
683

684
                    let base_offset = byte_offset as usize;
12✔
685
                    let byte_size = self.aligned_size; // single row
12✔
686
                    for (index, content) in self.extra_pending_values.drain(..).enumerate() {
32✔
687
                        let byte_offset = base_offset + byte_size * index;
×
688

689
                        // Copy Rust value into a GPU-ready format, including GPU padding.
690
                        let src: &[u8] = cast_slice(std::slice::from_ref(&content));
×
691
                        let dst_range = byte_offset..(byte_offset + self.item_size);
×
692
                        trace!(
×
693
                            "+ init_copy: index={} src={:?} dst={:?} byte_offset={} byte_size={}",
×
694
                            index,
×
695
                            src.as_ptr(),
8✔
696
                            dst_range,
×
697
                            byte_offset,
×
698
                            byte_size
×
699
                        );
NEW
700
                        dst_slice.slice(dst_range).copy_from_slice(src);
×
701
                    }
702
                }
703

704
                new_buffer.unmap();
12✔
705
            }
706

707
            if let Some(ab) = self.buffer.as_mut() {
7✔
708
                // If there's any data currently in the GPU buffer, we need to copy it on next
709
                // update to preserve it.
710
                if self.active_count > 0 {
×
711
                    // Current buffer has value to preserve, save it into old_buffer before
712
                    // replacing it with the newly-allocated one.
713

714
                    // By design we can't have all active entries as free ones; we should have
715
                    // updated active_count=0 and cleared the free list if that was the case.
716
                    debug_assert!(self.free_indices.len() < self.active_count as usize);
3✔
717

718
                    // If we already have an old buffer, that means we already have scheduled a copy
719
                    // to preserve some values. And we can't do that twice per frame.
720
                    assert!(
1✔
721
                        ab.old_buffer.is_none(),
2✔
722
                        "allocate_gpu() called twice before write_buffer() took effect."
×
723
                    );
724

725
                    // Swap old <-> new
726
                    let mut old_buffer = new_buffer;
2✔
727
                    std::mem::swap(&mut old_buffer, &mut ab.buffer);
3✔
728
                    ab.old_buffer = Some(old_buffer);
2✔
729
                    ab.old_count = ab.count;
1✔
730
                } else {
731
                    // Current buffer is unused, so we don't need to preserve anything.
732

733
                    // It could happen we reallocate during the frame then immediately free the rows
734
                    // to preserve, such that we don't need in the end to preserve anything.
735
                    if let Some(old_buffer) = ab.old_buffer.take() {
×
736
                        old_buffer.destroy();
×
737
                    }
738

739
                    ab.buffer.destroy();
×
740
                    ab.buffer = new_buffer;
×
741
                }
742
                ab.count = self.capacity;
1✔
743
            } else {
744
                self.buffer = Some(AllocatedBuffer {
10✔
745
                    buffer: new_buffer,
10✔
746
                    count: self.capacity,
5✔
747
                    old_buffer: None,
5✔
748
                    old_count: 0,
5✔
749
                });
750
            }
751

752
            true
6✔
753
        } else {
754
            false
662✔
755
        };
756

757
        // Immediately schedule a copy of old rows.
758
        // - For old rows, copy into the old buffer because the old-to-new buffer copy
759
        //   will be executed during a command queue while any CPU to GPU upload is
760
        //   prepended before the next command queue. To ensure things do get out of
761
        //   order with the CPU upload overwriting the GPU-to-GPU copy, make sure those
762
        //   two are disjoint.
763
        if let Some(ab) = self.buffer.as_ref() {
647✔
764
            let buffer = ab.old_buffer.as_ref().unwrap_or(&ab.buffer);
×
765
            for (index, content) in self.pending_values.drain(..) {
2✔
766
                let byte_size = self.aligned_size;
×
767
                let byte_offset = byte_size * index as usize;
×
768

769
                // Copy Rust value into a GPU-ready format, including GPU padding.
770
                // TODO - Do that in insert()!
771
                let mut aligned_buffer: Vec<u8> = vec![0; self.aligned_size];
×
772
                let src: &[u8] = cast_slice(std::slice::from_ref(&content));
×
773
                let dst_range = ..self.item_size;
×
774
                trace!(
×
775
                    "+ old_copy: index={} src={:?} dst={:?} byte_offset={} byte_size={}",
×
776
                    index,
×
777
                    src.as_ptr(),
×
778
                    dst_range,
×
779
                    byte_offset,
×
780
                    byte_size
×
781
                );
782
                let dst = &mut aligned_buffer[dst_range];
×
783
                dst.copy_from_slice(src);
×
784

785
                // Upload to GPU
786
                // TODO - Merge contiguous blocks into a single write_buffer()
787
                let bytes: &[u8] = cast_slice(&aligned_buffer);
×
788
                queue.write_buffer(buffer, byte_offset as u64, bytes);
×
789
            }
790
        } else {
791
            debug_assert!(self.pending_values.is_empty());
63✔
792
            debug_assert!(self.extra_pending_values.is_empty());
63✔
793
        }
794

795
        reallocated
668✔
796
    }
797

798
    /// Write CPU data to the GPU buffer, (re)allocating it as needed.
799
    pub fn write_buffer(&self, encoder: &mut CommandEncoder) {
667✔
800
        // Check if there's any work to do: either some pending values to upload or some
801
        // existing buffer to copy into a newly-allocated one.
802
        if self.pending_values.is_empty()
1,334✔
803
            && self
667✔
804
                .buffer
667✔
805
                .as_ref()
667✔
806
                .map(|ab| ab.old_buffer.is_none())
1,959✔
807
                .unwrap_or(true)
667✔
808
        {
809
            trace!("write_buffer({}): nothing to do", self.safe_name());
1,946✔
810
            return;
666✔
811
        }
812

813
        trace!(
×
814
            "write_buffer({}): pending_values.len={} item_size={} aligned_size={} buffer={:?}",
×
815
            self.safe_name(),
×
816
            self.pending_values.len(),
×
817
            self.item_size,
×
818
            self.aligned_size,
×
819
            self.buffer,
×
820
        );
821

822
        // If there's no more GPU buffer, there's nothing to do
823
        let Some(ab) = self.buffer.as_ref() else {
1✔
824
            return;
×
825
        };
826

827
        // Copy any old buffer into the new one, and clear the old buffer. Note that we
828
        // only clear the ref-counted reference to the buffer, not the actual buffer,
829
        // which stays alive until the copy is done (but we don't need to care about
830
        // keeping it alive, wgpu does that for us).
831
        if let Some(old_buffer) = ab.old_buffer.as_ref() {
1✔
832
            let old_size = self.to_byte_size(ab.old_count) as u64;
×
833
            trace!("Copy old buffer id {:?} of size {} bytes into newly-allocated buffer {:?} of size {} bytes.", old_buffer.id(), old_size, ab.buffer.id(), self.to_byte_size(ab.count));
×
834
            encoder.copy_buffer_to_buffer(old_buffer, 0, &ab.buffer, 0, old_size);
×
835
        }
836
    }
837
}
838

839
#[cfg(test)]
840
mod tests {
841
    use bevy::math::Vec3;
842
    use bytemuck::{Pod, Zeroable};
843

844
    use super::*;
845

846
    #[test]
847
    fn test_round_range_start_down() {
848
        // r8(0..7) : no-op
849
        {
850
            let (r, o) = round_range_start_down(0..7, 8);
851
            assert_eq!(r, 0..7);
852
            assert_eq!(o, 0);
853
        }
854

855
        // r8(2..7) = 0..7, +2
856
        {
857
            let (r, o) = round_range_start_down(2..7, 8);
858
            assert_eq!(r, 0..7);
859
            assert_eq!(o, 2);
860
        }
861

862
        // r8(7..32) = 0..32, +7
863
        {
864
            let (r, o) = round_range_start_down(7..32, 8);
865
            assert_eq!(r, 0..32);
866
            assert_eq!(o, 7);
867
        }
868

869
        // r8(8..32) = no-op
870
        {
871
            let (r, o) = round_range_start_down(8..32, 8);
872
            assert_eq!(r, 8..32);
873
            assert_eq!(o, 0);
874
        }
875
    }
876

877
    #[repr(C)]
878
    #[derive(Debug, Default, Clone, Copy, Pod, Zeroable, ShaderType)]
879
    pub(crate) struct GpuDummy {
880
        pub v: Vec3,
881
    }
882

883
    #[repr(C)]
884
    #[derive(Debug, Default, Clone, Copy, Pod, Zeroable, ShaderType)]
885
    pub(crate) struct GpuDummyComposed {
886
        pub simple: GpuDummy,
887
        pub tag: u32,
888
        // GPU padding to 16 bytes due to GpuDummy forcing align to 16 bytes
889
    }
890

891
    #[repr(C)]
892
    #[derive(Debug, Clone, Copy, Pod, Zeroable, ShaderType)]
893
    pub(crate) struct GpuDummyLarge {
894
        pub simple: GpuDummy,
895
        pub tag: u32,
896
        pub large: [f32; 128],
897
    }
898

899
    #[test]
900
    fn buffer_table_sizes() {
901
        // Rust
902
        assert_eq!(std::mem::size_of::<GpuDummy>(), 12);
903
        assert_eq!(std::mem::align_of::<GpuDummy>(), 4);
904
        assert_eq!(std::mem::size_of::<GpuDummyComposed>(), 16); // tight packing
905
        assert_eq!(std::mem::align_of::<GpuDummyComposed>(), 4);
906
        assert_eq!(std::mem::size_of::<GpuDummyLarge>(), 132 * 4); // tight packing
907
        assert_eq!(std::mem::align_of::<GpuDummyLarge>(), 4);
908

909
        // GPU
910
        assert_eq!(<GpuDummy as ShaderType>::min_size().get(), 16); // Vec3 gets padded to 16 bytes
911
        assert_eq!(<GpuDummy as ShaderSize>::SHADER_SIZE.get(), 16);
912
        assert_eq!(<GpuDummyComposed as ShaderType>::min_size().get(), 32); // align is 16 bytes, forces padding
913
        assert_eq!(<GpuDummyComposed as ShaderSize>::SHADER_SIZE.get(), 32);
914
        assert_eq!(<GpuDummyLarge as ShaderType>::min_size().get(), 544); // align is 16 bytes, forces padding
915
        assert_eq!(<GpuDummyLarge as ShaderSize>::SHADER_SIZE.get(), 544);
916

917
        for (item_align, expected_aligned_size) in [
918
            (0, 16),
919
            (4, 16),
920
            (8, 16),
921
            (16, 16),
922
            (32, 32),
923
            (256, 256),
924
            (512, 512),
925
        ] {
926
            let mut table = BufferTable::<GpuDummy>::new(
927
                BufferUsages::STORAGE,
928
                NonZeroU64::new(item_align),
929
                None,
930
            );
931
            assert_eq!(table.aligned_size(), expected_aligned_size);
932
            assert!(table.is_empty());
933
            table.insert(GpuDummy::default());
934
            assert!(!table.is_empty());
935
            assert_eq!(table.len(), 1);
936
        }
937

938
        for (item_align, expected_aligned_size) in [
939
            (0, 32),
940
            (4, 32),
941
            (8, 32),
942
            (16, 32),
943
            (32, 32),
944
            (256, 256),
945
            (512, 512),
946
        ] {
947
            let mut table = BufferTable::<GpuDummyComposed>::new(
948
                BufferUsages::STORAGE,
949
                NonZeroU64::new(item_align),
950
                None,
951
            );
952
            assert_eq!(table.aligned_size(), expected_aligned_size);
953
            assert!(table.is_empty());
954
            table.insert(GpuDummyComposed::default());
955
            assert!(!table.is_empty());
956
            assert_eq!(table.len(), 1);
957
        }
958

959
        for (item_align, expected_aligned_size) in [
960
            (0, 544),
961
            (4, 544),
962
            (8, 544),
963
            (16, 544),
964
            (32, 544),
965
            (256, 768),
966
            (512, 1024),
967
        ] {
968
            let mut table = BufferTable::<GpuDummyLarge>::new(
969
                BufferUsages::STORAGE,
970
                NonZeroU64::new(item_align),
971
                None,
972
            );
973
            assert_eq!(table.aligned_size(), expected_aligned_size);
974
            assert!(table.is_empty());
975
            table.insert(GpuDummyLarge {
976
                simple: Default::default(),
977
                tag: 0,
978
                large: [0.; 128],
979
            });
980
            assert!(!table.is_empty());
981
            assert_eq!(table.len(), 1);
982
        }
983
    }
984

985
    #[test]
986
    fn buffer_table_insert() {
987
        let mut table =
988
            BufferTable::<GpuDummy>::new(BufferUsages::STORAGE, NonZeroU64::new(32), None);
989

990
        // [x]
991
        let id1 = table.insert(GpuDummy::default());
992
        assert_eq!(id1.0, 0);
993
        assert_eq!(table.active_count, 1);
994
        assert!(table.free_indices.is_empty());
995

996
        // [x x]
997
        let id2 = table.insert(GpuDummy::default());
998
        assert_eq!(id2.0, 1);
999
        assert_eq!(table.active_count, 2);
1000
        assert!(table.free_indices.is_empty());
1001

1002
        // [- x]
1003
        table.remove(id1);
1004
        assert_eq!(table.active_count, 2);
1005
        assert_eq!(table.free_indices.len(), 1);
1006
        assert_eq!(table.free_indices[0], 0);
1007

1008
        // [- x x x]
1009
        let id3 = table.insert_contiguous([GpuDummy::default(); 2].into_iter());
1010
        assert_eq!(id3.0, 2); // at the end (doesn't fit in free slot #0)
1011
        assert_eq!(table.active_count, 4);
1012
        assert_eq!(table.free_indices.len(), 1);
1013
        assert_eq!(table.free_indices[0], 0);
1014

1015
        // [- - x x]
1016
        table.remove(id2);
1017
        assert_eq!(table.active_count, 4);
1018
        assert_eq!(table.free_indices.len(), 2);
1019
        assert_eq!(table.free_indices[0], 0);
1020
        assert_eq!(table.free_indices[1], 1);
1021

1022
        // [x x x x]
1023
        let id4 = table.insert_contiguous([GpuDummy::default(); 2].into_iter());
1024
        assert_eq!(id4.0, 0); // this times it fit into slot #0-#1
1025
        assert_eq!(table.active_count, 4);
1026
        assert!(table.free_indices.is_empty());
1027

1028
        // [- - x x]
1029
        table.remove_range(id4, 2);
1030
        assert_eq!(table.active_count, 4);
1031
        assert_eq!(table.free_indices.len(), 2);
1032
        assert_eq!(table.free_indices[0], 0);
1033
        assert_eq!(table.free_indices[1], 1);
1034

1035
        // []
1036
        table.remove_range(id3, 2);
1037
        assert_eq!(table.active_count, 0);
1038
        assert!(table.free_indices.is_empty());
1039

1040
        // [x x]
1041
        let id5 = table.insert_contiguous([GpuDummy::default(); 2].into_iter());
1042
        assert_eq!(id5.0, 0);
1043
        assert_eq!(table.active_count, 2);
1044
        assert!(table.free_indices.is_empty());
1045

1046
        // [x x x x]
1047
        let id6 = table.insert_contiguous([GpuDummy::default(); 2].into_iter());
1048
        assert_eq!(id6.0, 2);
1049
        assert_eq!(table.active_count, 4);
1050
        assert!(table.free_indices.is_empty());
1051

1052
        // [x x x x x x]
1053
        let id7 = table.insert_contiguous([GpuDummy::default(); 2].into_iter());
1054
        assert_eq!(id7.0, 4);
1055
        assert_eq!(table.active_count, 6);
1056
        assert!(table.free_indices.is_empty());
1057

1058
        // [x x - - x x]
1059
        table.remove_range(id6, 2);
1060
        assert_eq!(table.active_count, 6);
1061
        assert_eq!(table.free_indices.len(), 2);
1062
        assert_eq!(table.free_indices[0], 2);
1063
        assert_eq!(table.free_indices[1], 3);
1064

1065
        // [x x]
1066
        table.remove_range(id7, 2);
1067
        assert_eq!(table.active_count, 2);
1068
        assert!(table.free_indices.is_empty());
1069
    }
1070
}
1071

1072
#[cfg(all(test, feature = "gpu_tests"))]
1073
mod gpu_tests {
1074
    use std::fmt::Write;
1075

1076
    use bevy::render::render_resource::BufferSlice;
1077
    use tests::*;
1078
    use wgpu::{BufferView, CommandBuffer};
1079

1080
    use super::*;
1081
    use crate::test_utils::MockRenderer;
1082

1083
    /// Read data from GPU back into CPU memory.
1084
    ///
1085
    /// This call blocks until the data is available on CPU. Used for testing
1086
    /// only.
1087
    fn read_back_gpu(device: &RenderDevice, slice: BufferSlice<'_>) -> BufferView {
6✔
1088
        let (tx, rx) = futures::channel::oneshot::channel();
18✔
1089
        slice.map_async(wgpu::MapMode::Read, move |result| {
24✔
1090
            tx.send(result).unwrap();
24✔
1091
        });
1092
        let _ = device.poll(wgpu::PollType::Wait {
18✔
1093
            submission_index: None,
6✔
1094
            timeout: None,
6✔
1095
        });
1096
        let result = futures::executor::block_on(rx);
18✔
1097
        assert!(result.is_ok());
18✔
1098
        slice.get_mapped_range()
6✔
1099
    }
1100

1101
    /// Submit a command buffer to GPU and wait for completion.
1102
    ///
1103
    /// This call blocks until the GPU executed the command buffer. Used for
1104
    /// testing only.
1105
    fn submit_gpu_and_wait(
7✔
1106
        device: &RenderDevice,
1107
        queue: &RenderQueue,
1108
        command_buffer: CommandBuffer,
1109
    ) {
1110
        // Queue command
1111
        queue.submit([command_buffer]);
14✔
1112

1113
        // Register callback to observe completion
1114
        let (tx, rx) = futures::channel::oneshot::channel();
21✔
1115
        queue.on_submitted_work_done(move || {
21✔
1116
            tx.send(()).unwrap();
28✔
1117
        });
1118

1119
        // Poll device, checking for completion and raising callback
1120
        let _ = device.poll(wgpu::PollType::Wait {
21✔
1121
            submission_index: None,
7✔
1122
            timeout: None,
7✔
1123
        });
1124

1125
        // Wait for callback to be raised. This was need in previous versions, however
1126
        // it's a bit unclear if it's still needed or if device.poll() is enough to
1127
        // guarantee that the command was executed.
1128
        let _ = futures::executor::block_on(rx);
7✔
1129
    }
1130

1131
    /// Convert a byte slice to a string of hexadecimal values separated by a
1132
    /// blank space.
1133
    fn to_hex_string(slice: &[u8]) -> String {
19✔
1134
        let len = slice.len();
57✔
1135
        let num_chars = len * 3 - 1;
38✔
1136
        let mut s = String::with_capacity(num_chars);
57✔
1137
        for b in &slice[..len - 1] {
304✔
1138
            write!(&mut s, "{:02x} ", *b).unwrap();
1139
        }
1140
        write!(&mut s, "{:02x}", slice[len - 1]).unwrap();
76✔
1141
        debug_assert_eq!(s.len(), num_chars);
57✔
1142
        s
19✔
1143
    }
1144

1145
    fn write_buffers_and_wait<T: Pod + ShaderSize>(
7✔
1146
        table: &BufferTable<T>,
1147
        device: &RenderDevice,
1148
        queue: &RenderQueue,
1149
    ) {
1150
        let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
28✔
1151
            label: Some("test"),
7✔
1152
        });
1153
        table.write_buffer(&mut encoder);
21✔
1154
        let command_buffer = encoder.finish();
21✔
1155
        submit_gpu_and_wait(device, queue, command_buffer);
28✔
1156
        println!("Buffer written to GPU");
7✔
1157
    }
1158

1159
    #[test]
1160
    fn table_write() {
1161
        let renderer = MockRenderer::new();
1162
        let device = renderer.device();
1163
        let queue = renderer.queue();
1164

1165
        let item_align = device.limits().min_storage_buffer_offset_alignment as u64;
1166
        println!("min_storage_buffer_offset_alignment = {item_align}");
1167
        let mut table = BufferTable::<GpuDummyComposed>::new(
1168
            BufferUsages::STORAGE | BufferUsages::MAP_READ,
1169
            NonZeroU64::new(item_align),
1170
            None,
1171
        );
1172
        let final_align = item_align.max(<GpuDummyComposed as ShaderSize>::SHADER_SIZE.get());
1173
        assert_eq!(table.aligned_size(), final_align as usize);
1174

1175
        // Initial state
1176
        assert!(table.is_empty());
1177
        assert_eq!(table.len(), 0);
1178
        assert_eq!(table.capacity(), 0);
1179
        assert!(table.buffer.is_none());
1180

1181
        // This has no effect while the table is empty
1182
        table.clear_previous_frame_resizes();
1183
        table.allocate_gpu(&device, &queue);
1184
        write_buffers_and_wait(&table, &device, &queue);
1185
        assert!(table.is_empty());
1186
        assert_eq!(table.len(), 0);
1187
        assert_eq!(table.capacity(), 0);
1188
        assert!(table.buffer.is_none());
1189

1190
        // New frame
1191
        table.clear_previous_frame_resizes();
1192

1193
        // Insert some entries
1194
        let len = 3;
1195
        for i in 0..len {
1196
            let row = table.insert(GpuDummyComposed {
1197
                tag: i + 1,
1198
                ..Default::default()
1199
            });
1200
            assert_eq!(row.0, i);
1201
        }
1202
        assert!(!table.is_empty());
1203
        assert_eq!(table.len(), len);
1204
        assert!(table.capacity() >= len); // contract: could over-allocate...
1205
        assert!(table.buffer.is_none()); // not yet allocated on GPU
1206

1207
        // Allocate GPU buffer for current requested state
1208
        table.allocate_gpu(&device, &queue);
1209
        assert!(!table.is_empty());
1210
        assert_eq!(table.len(), len);
1211
        assert!(table.capacity() >= len);
1212
        let ab = table
1213
            .buffer
1214
            .as_ref()
1215
            .expect("GPU buffer should be allocated after allocate_gpu()");
1216
        assert!(ab.old_buffer.is_none()); // no previous copy
1217
        assert_eq!(ab.count, len);
1218
        println!(
1219
            "Allocated buffer #{:?} of {} rows",
1220
            ab.buffer.id(),
1221
            ab.count
1222
        );
1223
        let ab_buffer = ab.buffer.clone();
1224

1225
        // Another allocate_gpu() is a no-op
1226
        table.allocate_gpu(&device, &queue);
1227
        assert!(!table.is_empty());
1228
        assert_eq!(table.len(), len);
1229
        assert!(table.capacity() >= len);
1230
        let ab = table
1231
            .buffer
1232
            .as_ref()
1233
            .expect("GPU buffer should be allocated after allocate_gpu()");
1234
        assert!(ab.old_buffer.is_none()); // no previous copy
1235
        assert_eq!(ab.count, len);
1236
        assert_eq!(ab_buffer.id(), ab.buffer.id()); // same buffer
1237

1238
        // Write buffer (CPU -> GPU)
1239
        write_buffers_and_wait(&table, &device, &queue);
1240

1241
        {
1242
            // Read back (GPU -> CPU)
1243
            let buffer = table.buffer().expect("Buffer was not allocated").clone(); // clone() for lifetime
1244
            {
1245
                let slice = buffer.slice(..);
1246
                let view = read_back_gpu(&device, slice);
1247
                println!(
1248
                    "GPU data read back to CPU for validation: {} bytes",
1249
                    view.len()
1250
                );
1251

1252
                // Validate content
1253
                assert_eq!(view.len(), final_align as usize * table.capacity() as usize);
1254
                for i in 0..len as usize {
1255
                    let offset = i * final_align as usize;
1256
                    let item_size = std::mem::size_of::<GpuDummyComposed>();
1257
                    let src = &view[offset..offset + 16];
1258
                    println!("{}", to_hex_string(src));
1259
                    let dummy_composed: &[GpuDummyComposed] =
1260
                        cast_slice(&view[offset..offset + item_size]);
1261
                    assert_eq!(dummy_composed[0].tag, (i + 1) as u32);
1262
                }
1263
            }
1264
            buffer.unmap();
1265
        }
1266

1267
        // New frame
1268
        table.clear_previous_frame_resizes();
1269

1270
        // Insert more entries
1271
        let old_capacity = table.capacity();
1272
        let mut len = len;
1273
        while table.capacity() == old_capacity {
1274
            let row = table.insert(GpuDummyComposed {
1275
                tag: len + 1,
1276
                ..Default::default()
1277
            });
1278
            assert_eq!(row.0, len);
1279
            len += 1;
1280
        }
1281
        println!(
1282
            "Added {} rows to grow capacity from {} to {}.",
1283
            len - 3,
1284
            old_capacity,
1285
            table.capacity()
1286
        );
1287

1288
        // This re-allocates a new GPU buffer because the capacity changed
1289
        table.allocate_gpu(&device, &queue);
1290
        assert!(!table.is_empty());
1291
        assert_eq!(table.len(), len);
1292
        assert!(table.capacity() >= len);
1293
        let ab = table
1294
            .buffer
1295
            .as_ref()
1296
            .expect("GPU buffer should be allocated after allocate_gpu()");
1297
        assert_eq!(ab.count, len);
1298
        assert!(ab.old_buffer.is_some()); // old buffer to copy
1299
        assert_ne!(ab.old_buffer.as_ref().unwrap().id(), ab.buffer.id());
1300
        println!(
1301
            "Allocated new buffer #{:?} of {} rows",
1302
            ab.buffer.id(),
1303
            ab.count
1304
        );
1305

1306
        // Write buffer (CPU -> GPU)
1307
        write_buffers_and_wait(&table, &device, &queue);
1308

1309
        {
1310
            // Read back (GPU -> CPU)
1311
            let buffer = table.buffer().expect("Buffer was not allocated").clone(); // clone() for lifetime
1312
            {
1313
                let slice = buffer.slice(..);
1314
                let view = read_back_gpu(&device, slice);
1315
                println!(
1316
                    "GPU data read back to CPU for validation: {} bytes",
1317
                    view.len()
1318
                );
1319

1320
                // Validate content
1321
                assert_eq!(view.len(), final_align as usize * table.capacity() as usize);
1322
                for i in 0..len as usize {
1323
                    let offset = i * final_align as usize;
1324
                    let item_size = std::mem::size_of::<GpuDummyComposed>();
1325
                    let src = &view[offset..offset + 16];
1326
                    println!("{}", to_hex_string(src));
1327
                    let dummy_composed: &[GpuDummyComposed] =
1328
                        cast_slice(&view[offset..offset + item_size]);
1329
                    assert_eq!(dummy_composed[0].tag, (i + 1) as u32);
1330
                }
1331
            }
1332
            buffer.unmap();
1333
        }
1334

1335
        // New frame
1336
        table.clear_previous_frame_resizes();
1337

1338
        // Delete the last allocated row
1339
        let old_capacity = table.capacity();
1340
        let len = len - 1;
1341
        table.remove(BufferTableId(len));
1342
        println!(
1343
            "Removed last row to shrink capacity from {} to {}.",
1344
            old_capacity,
1345
            table.capacity()
1346
        );
1347

1348
        // This doesn't do anything since we removed a row only
1349
        table.allocate_gpu(&device, &queue);
1350
        assert!(!table.is_empty());
1351
        assert_eq!(table.len(), len);
1352
        assert!(table.capacity() >= len);
1353
        let ab = table
1354
            .buffer
1355
            .as_ref()
1356
            .expect("GPU buffer should be allocated after allocate_gpu()");
1357
        assert_eq!(ab.count, len + 1); // GPU buffer kept its size
1358
        assert!(ab.old_buffer.is_none());
1359

1360
        // Write buffer (CPU -> GPU)
1361
        write_buffers_and_wait(&table, &device, &queue);
1362

1363
        {
1364
            // Read back (GPU -> CPU)
1365
            let buffer = table.buffer().expect("Buffer was not allocated").clone(); // clone() for lifetime
1366
            {
1367
                let slice = buffer.slice(..);
1368
                let view = read_back_gpu(&device, slice);
1369
                println!(
1370
                    "GPU data read back to CPU for validation: {} bytes",
1371
                    view.len()
1372
                );
1373

1374
                // Validate content
1375
                assert!(view.len() >= final_align as usize * table.capacity() as usize); // note the >=, the buffer is over-allocated
1376
                for i in 0..len as usize {
1377
                    let offset = i * final_align as usize;
1378
                    let item_size = std::mem::size_of::<GpuDummyComposed>();
1379
                    let src = &view[offset..offset + 16];
1380
                    println!("{}", to_hex_string(src));
1381
                    let dummy_composed: &[GpuDummyComposed] =
1382
                        cast_slice(&view[offset..offset + item_size]);
1383
                    assert_eq!(dummy_composed[0].tag, (i + 1) as u32);
1384
                }
1385
            }
1386
            buffer.unmap();
1387
        }
1388

1389
        // New frame
1390
        table.clear_previous_frame_resizes();
1391

1392
        // Delete the first allocated row
1393
        let old_capacity = table.capacity();
1394
        let mut len = len - 1;
1395
        table.remove(BufferTableId(0));
1396
        assert_eq!(old_capacity, table.capacity());
1397
        println!(
1398
            "Removed first row to shrink capacity from {} to {} (no change).",
1399
            old_capacity,
1400
            table.capacity()
1401
        );
1402

1403
        // This doesn't do anything since we only removed a row
1404
        table.allocate_gpu(&device, &queue);
1405
        assert!(!table.is_empty());
1406
        assert_eq!(table.len(), len);
1407
        assert!(table.capacity() >= len);
1408
        let ab = table
1409
            .buffer
1410
            .as_ref()
1411
            .expect("GPU buffer should be allocated after allocate_gpu()");
1412
        assert_eq!(ab.count, len + 2); // GPU buffer kept its size
1413
        assert!(ab.old_buffer.is_none());
1414

1415
        // Write buffer (CPU -> GPU)
1416
        write_buffers_and_wait(&table, &device, &queue);
1417

1418
        {
1419
            // Read back (GPU -> CPU)
1420
            let buffer = table.buffer().expect("Buffer was not allocated").clone(); // clone() for lifetime
1421
            {
1422
                let slice = buffer.slice(..);
1423
                let view = read_back_gpu(&device, slice);
1424
                println!(
1425
                    "GPU data read back to CPU for validation: {} bytes",
1426
                    view.len()
1427
                );
1428

1429
                // Validate content
1430
                assert!(view.len() >= final_align as usize * table.capacity() as usize); // note the >=, the buffer is over-allocated
1431
                for i in 0..len as usize {
1432
                    let offset = i * final_align as usize;
1433
                    let item_size = std::mem::size_of::<GpuDummyComposed>();
1434
                    let src = &view[offset..offset + 16];
1435
                    println!("{}", to_hex_string(src));
1436
                    if i > 0 {
1437
                        let dummy_composed: &[GpuDummyComposed] =
1438
                            cast_slice(&view[offset..offset + item_size]);
1439
                        assert_eq!(dummy_composed[0].tag, (i + 1) as u32);
1440
                    }
1441
                }
1442
            }
1443
            buffer.unmap();
1444
        }
1445

1446
        // New frame
1447
        table.clear_previous_frame_resizes();
1448

1449
        // Insert a row; this should get into row #0 in the buffer
1450
        let row = table.insert(GpuDummyComposed {
1451
            tag: 1,
1452
            ..Default::default()
1453
        });
1454
        assert_eq!(row.0, 0);
1455
        len += 1;
1456
        println!(
1457
            "Added 1 row to grow capacity from {} to {}.",
1458
            old_capacity,
1459
            table.capacity()
1460
        );
1461

1462
        // This doesn't reallocate the GPU buffer since we used a free list entry
1463
        table.allocate_gpu(&device, &queue);
1464
        assert!(!table.is_empty());
1465
        assert_eq!(table.len(), len);
1466
        assert!(table.capacity() >= len);
1467
        let ab = table
1468
            .buffer
1469
            .as_ref()
1470
            .expect("GPU buffer should be allocated after allocate_gpu()");
1471
        assert_eq!(ab.count, 4); // 4 == last time we grew
1472
        assert!(ab.old_buffer.is_none());
1473

1474
        // Write buffer (CPU -> GPU)
1475
        write_buffers_and_wait(&table, &device, &queue);
1476

1477
        {
1478
            // Read back (GPU -> CPU)
1479
            let buffer = table.buffer().expect("Buffer was not allocated").clone(); // clone() for lifetime
1480
            {
1481
                let slice = buffer.slice(..);
1482
                let view = read_back_gpu(&device, slice);
1483
                println!(
1484
                    "GPU data read back to CPU for validation: {} bytes",
1485
                    view.len()
1486
                );
1487

1488
                // Validate content
1489
                assert!(view.len() >= final_align as usize * table.capacity() as usize);
1490
                for i in 0..len as usize {
1491
                    let offset = i * final_align as usize;
1492
                    let item_size = std::mem::size_of::<GpuDummyComposed>();
1493
                    let src = &view[offset..offset + 16];
1494
                    println!("{}", to_hex_string(src));
1495
                    let dummy_composed: &[GpuDummyComposed] =
1496
                        cast_slice(&view[offset..offset + item_size]);
1497
                    assert_eq!(dummy_composed[0].tag, (i + 1) as u32);
1498
                }
1499
            }
1500
            buffer.unmap();
1501
        }
1502

1503
        // New frame
1504
        table.clear_previous_frame_resizes();
1505

1506
        // Insert a row; this should get into row #3 at the end of the allocated buffer
1507
        let row = table.insert(GpuDummyComposed {
1508
            tag: 4,
1509
            ..Default::default()
1510
        });
1511
        assert_eq!(row.0, 3);
1512
        len += 1;
1513
        println!(
1514
            "Added 1 row to grow capacity from {} to {}.",
1515
            old_capacity,
1516
            table.capacity()
1517
        );
1518

1519
        // This doesn't reallocate the GPU buffer since we used an implicit free entry
1520
        table.allocate_gpu(&device, &queue);
1521
        assert!(!table.is_empty());
1522
        assert_eq!(table.len(), len);
1523
        assert!(table.capacity() >= len);
1524
        let ab = table
1525
            .buffer
1526
            .as_ref()
1527
            .expect("GPU buffer should be allocated after allocate_gpu()");
1528
        assert_eq!(ab.count, 4); // 4 == last time we grew
1529
        assert!(ab.old_buffer.is_none());
1530

1531
        // Write buffer (CPU -> GPU)
1532
        write_buffers_and_wait(&table, &device, &queue);
1533

1534
        {
1535
            // Read back (GPU -> CPU)
1536
            let buffer = table.buffer().expect("Buffer was not allocated").clone(); // clone() for lifetime
1537
            {
1538
                let slice = buffer.slice(..);
1539
                let view = read_back_gpu(&device, slice);
1540
                println!(
1541
                    "GPU data read back to CPU for validation: {} bytes",
1542
                    view.len()
1543
                );
1544

1545
                // Validate content
1546
                assert!(view.len() >= final_align as usize * table.capacity() as usize);
1547
                for i in 0..len as usize {
1548
                    let offset = i * final_align as usize;
1549
                    let item_size = std::mem::size_of::<GpuDummyComposed>();
1550
                    let src = &view[offset..offset + 16];
1551
                    println!("{}", to_hex_string(src));
1552
                    let dummy_composed: &[GpuDummyComposed] =
1553
                        cast_slice(&view[offset..offset + item_size]);
1554
                    assert_eq!(dummy_composed[0].tag, (i + 1) as u32);
1555
                }
1556
            }
1557
            buffer.unmap();
1558
        }
1559
    }
1560
}
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