Skip to main content

bevy_shader_plugin/
auto_array.rs

1use std::{
2    marker::PhantomData,
3    num::NonZeroU64,
4    sync::{Mutex, PoisonError},
5};
6
7use bevy::{
8    prelude::*,
9    render::{
10        Extract, ExtractSchedule, Render, RenderApp, RenderStartup,
11        render_resource::{
12            Buffer, BufferInitDescriptor, BufferUsages,
13            encase::{self, ShaderSize, internal::WriteInto},
14        },
15        renderer::{RenderDevice, RenderQueue},
16    },
17};
18
19use crate::{
20    ShaderSystems,
21    bindings::{AutoBufferKind, BindingTable, register_binding},
22};
23
24/// Main-world resource for queuing index-value changes to a fixed-size array buffer.
25///
26/// `Tag` is a zero-sized marker type that uniquely identifies this buffer, allowing
27/// multiple buffers of the same element type and length to coexist. The element type
28/// and length are specified at registration and do not appear in this type.
29///
30/// Changes are serialized to bytes immediately in [`set`](Self::set), then batched
31/// into contiguous `write_buffer` runs each frame. Indices need not be sorted or
32/// unique — duplicates use last-write-wins.
33///
34/// # Example
35///
36/// ```rust,ignore
37/// struct Colors;
38/// // registered via: app.register_array_buffer::<Colors, Vec4, 64>(0, 0, false)
39///
40/// fn update(mut changes: ResMut<ArrayBufferChanges<Colors>>) {
41///     changes.set(0, Vec4::new(1.0, 0.0, 0.0, 1.0)); // red at index 0
42///     changes.set_many([(1, Vec4::Y), (2, Vec4::Z)]); // green, blue
43/// }
44/// ```
45#[derive(Resource)]
46pub struct ArrayBufferChanges<Tag> {
47    /// Behind a mutex only so extraction can take the queue through a shared reference.
48    /// The setters have `&mut self` and use `get_mut`, which doesn't lock.
49    queue: Mutex<ChangeQueue>,
50    len: usize,
51    stride: usize,
52    _marker: PhantomData<Tag>,
53}
54
55impl<Tag> Default for ArrayBufferChanges<Tag> {
56    fn default() -> Self {
57        Self {
58            queue: Mutex::default(),
59            len: 0,
60            stride: 0,
61            _marker: PhantomData,
62        }
63    }
64}
65
66/// Changes queued for one array buffer, with every element serialized to `stride` bytes.
67#[derive(Default)]
68struct ChangeQueue {
69    /// Set by `set_all`: every element becomes `fill` before `indices` are applied.
70    has_fill: bool,
71    fill: Vec<u8>,
72    /// Element indices in the order they were set. Element `i` of this list has its bytes
73    /// at `bytes[i * stride..(i + 1) * stride]`.
74    indices: Vec<usize>,
75    bytes: Vec<u8>,
76}
77
78impl ChangeQueue {
79    fn is_empty(&self) -> bool {
80        !self.has_fill && self.indices.is_empty()
81    }
82
83    /// Empties the queue, keeping its allocations.
84    fn clear(&mut self) {
85        self.has_fill = false;
86        self.fill.clear();
87        self.indices.clear();
88        self.bytes.clear();
89    }
90
91    /// Serializes `value` into a new `stride`-byte slot at the end of `bytes`.
92    fn push<T: ShaderSize + WriteInto>(&mut self, index: usize, value: &T, stride: usize) {
93        let start = self.bytes.len();
94        self.bytes.resize(start + stride, 0);
95        write_element(value, &mut self.bytes[start..]);
96        self.indices.push(index);
97    }
98
99    /// Moves everything queued in `newer` onto the end of `self`, leaving `newer` empty.
100    fn append(&mut self, newer: &mut ChangeQueue) {
101        if self.is_empty() || newer.has_fill {
102            // Nothing to keep: a fill replaces every earlier change.
103            std::mem::swap(self, newer);
104        } else {
105            self.indices.append(&mut newer.indices);
106            self.bytes.append(&mut newer.bytes);
107        }
108        newer.clear();
109    }
110}
111
112/// Serializes `value` into the start of `out`, which must be at least `T::SHADER_SIZE` bytes.
113fn write_element<T: ShaderSize + WriteInto>(value: &T, out: &mut [u8]) {
114    encase::StorageBuffer::new(out).write(value).unwrap();
115}
116
117/// WGSL array element stride of `T` in bytes (element size plus alignment padding).
118pub(crate) fn array_stride<T: ShaderSize>() -> usize {
119    // [T; 1]::SHADER_SIZE equals the WGSL array element stride for any length.
120    <[T; 1] as ShaderSize>::SHADER_SIZE.get() as usize
121}
122
123/// Bytes of a WGSL `array<T, len>` with every element set to `T::default()`.
124pub(crate) fn default_filled_bytes<T: ShaderSize + WriteInto + Default>(len: usize) -> Vec<u8> {
125    let stride = array_stride::<T>();
126    let mut element = vec![0u8; stride];
127    write_element(&T::default(), &mut element);
128    element.repeat(len)
129}
130
131/// Register a fixed-size array buffer. See
132/// [`ShaderAppExt::register_array_buffer`](crate::ShaderAppExt::register_array_buffer).
133pub(crate) fn register<Tag, T, const N: usize>(
134    app: &mut App,
135    group_index: u32,
136    binding_index: u32,
137    read_write: bool,
138) where
139    Tag: Send + Sync + 'static,
140    T: ShaderSize + WriteInto + Default + Send + Sync + 'static,
141{
142    let stride = array_stride::<T>();
143    app.insert_resource(ArrayBufferChanges::<Tag> {
144        queue: Mutex::default(),
145        len: N,
146        stride,
147        _marker: PhantomData,
148    });
149
150    let render_app = app.sub_app_mut(RenderApp);
151    register_binding(
152        render_app,
153        group_index,
154        binding_index,
155        AutoBufferKind::Storage {
156            read_only: !read_write,
157        },
158        NonZeroU64::new((stride * N) as u64),
159    );
160
161    // RenderStartup: create the persistent GPU buffer pre-filled with T::default().
162    render_app.add_systems(
163        RenderStartup,
164        move |mut commands: Commands, render_device: Res<RenderDevice>| {
165            let buffer = render_device.create_buffer_with_data(&BufferInitDescriptor {
166                label: Some("array_buffer"),
167                contents: &default_filled_bytes::<T>(N),
168                // COPY_SRC lets a future GPU→CPU readback copy out of this buffer.
169                usage: BufferUsages::STORAGE | BufferUsages::COPY_DST | BufferUsages::COPY_SRC,
170            });
171
172            commands.insert_resource(ArrayBufferState::<Tag> {
173                buffer,
174                stride,
175                len: N,
176                _marker: PhantomData,
177            });
178        },
179    );
180
181    render_app.init_resource::<PendingArrayChanges<Tag>>();
182    render_app.add_systems(ExtractSchedule, extract_array_changes::<Tag>);
183
184    render_app.add_systems(
185        Render,
186        (move |state: Option<Res<ArrayBufferState<Tag>>>,
187               mut pending: ResMut<PendingArrayChanges<Tag>>,
188               render_queue: Res<RenderQueue>,
189               mut table: ResMut<BindingTable>| {
190            let Some(state) = state else {
191                return;
192            };
193            let PendingArrayChanges { queue, scratch, .. } = &mut *pending;
194            plan_writes(queue, state.stride, state.len, scratch, |offset, bytes| {
195                render_queue.write_buffer(&state.buffer, offset, bytes);
196            });
197            queue.clear();
198            table.set_buffer(group_index, binding_index, &state.buffer);
199        })
200        .in_set(ShaderSystems::PrepareBindings),
201    );
202}
203
204impl<Tag> ArrayBufferChanges<Tag> {
205    /// Queue a change: element at `index` will be updated to `value` this frame.
206    /// The value is serialized to bytes immediately.
207    pub fn set<T: ShaderSize + WriteInto>(&mut self, index: usize, value: T) {
208        self.check_index(index);
209        let stride = self.stride;
210        self.queue_mut().push(index, &value, stride);
211    }
212
213    /// Queue multiple changes at once from any iterable of `(index, value)` pairs.
214    ///
215    /// Equivalent to calling [`set`](Self::set) for each pair. Accepts arrays, vecs,
216    /// iterators, or anything that implements `IntoIterator<Item = (usize, T)>`:
217    ///
218    /// ```rust,ignore
219    /// changes.set_many([(8, color_a), (9, color_a), (24, color_b), (25, color_b)]);
220    /// changes.set_many((0..4).map(|i| (i, Vec4::ONE)));
221    /// ```
222    pub fn set_many<T, I>(&mut self, pairs: I)
223    where
224        T: ShaderSize + WriteInto,
225        I: IntoIterator<Item = (usize, T)>,
226    {
227        for (index, value) in pairs {
228            self.set(index, value);
229        }
230    }
231
232    /// Queue a change to set every element in the buffer to `value`.
233    /// The value is serialized once and the bytes are reused for each element.
234    pub fn set_all<T: ShaderSize + WriteInto>(&mut self, value: T) {
235        let stride = self.stride;
236        let queue = self.queue_mut();
237        // Every change queued so far is overwritten.
238        queue.clear();
239        queue.has_fill = true;
240        queue.fill.resize(stride, 0);
241        write_element(&value, &mut queue.fill);
242    }
243
244    fn check_index(&self, index: usize) {
245        if index >= self.len {
246            panic!(
247                "ArrayBufferChanges::set index out of bounds: {} >= {}",
248                index, self.len
249            );
250        }
251    }
252
253    fn queue_mut(&mut self) -> &mut ChangeQueue {
254        self.queue.get_mut().unwrap_or_else(PoisonError::into_inner)
255    }
256}
257
258/// Render-world resource holding the persistent GPU buffer and element stride.
259#[derive(Resource)]
260pub struct ArrayBufferState<Tag> {
261    pub(crate) buffer: Buffer,
262    /// WGSL array element stride in bytes (includes any alignment padding).
263    pub(crate) stride: usize,
264    /// Number of elements.
265    pub(crate) len: usize,
266    pub(crate) _marker: PhantomData<Tag>,
267}
268
269/// Render-world changes waiting to be uploaded, plus reusable working memory.
270#[derive(Resource)]
271struct PendingArrayChanges<Tag> {
272    queue: ChangeQueue,
273    scratch: WriteScratch,
274    _marker: PhantomData<Tag>,
275}
276
277impl<Tag> Default for PendingArrayChanges<Tag> {
278    fn default() -> Self {
279        Self {
280            queue: ChangeQueue::default(),
281            scratch: WriteScratch::default(),
282            _marker: PhantomData,
283        }
284    }
285}
286
287/// `ExtractSchedule` system: moves queued changes from the main world to the render world.
288///
289/// Both sides keep their allocations: when nothing is pending (the usual case) the two
290/// queues are swapped, so the main world gets back the render world's emptied one.
291/// Taking the queue here, rather than clearing it in a main-world schedule, means
292/// changes set during `Startup` survive to the first extraction.
293fn extract_array_changes<Tag: Send + Sync + 'static>(
294    changes: Extract<Option<Res<ArrayBufferChanges<Tag>>>>,
295    mut pending: ResMut<PendingArrayChanges<Tag>>,
296) {
297    let Some(changes) = changes.as_ref() else {
298        return;
299    };
300    let mut queue = changes.queue.lock().unwrap_or_else(PoisonError::into_inner);
301    if !queue.is_empty() {
302        pending.queue.append(&mut queue);
303    }
304}
305
306/// Reusable memory for [`plan_writes`].
307#[derive(Default)]
308struct WriteScratch {
309    /// Positions in the queue, sorted by element index.
310    order: Vec<usize>,
311    /// Bytes of the write being assembled.
312    bytes: Vec<u8>,
313}
314
315/// Turns `queue` into as few writes as possible and passes each to `write` as
316/// `(byte offset, bytes)`. Later changes to an index win over earlier ones.
317fn plan_writes(
318    queue: &ChangeQueue,
319    stride: usize,
320    len: usize,
321    scratch: &mut WriteScratch,
322    mut write: impl FnMut(u64, &[u8]),
323) {
324    let element = |position: usize| &queue.bytes[position * stride..(position + 1) * stride];
325    let WriteScratch { order, bytes } = scratch;
326
327    if queue.has_fill {
328        // One write covering the whole buffer.
329        bytes.clear();
330        for _ in 0..len {
331            bytes.extend_from_slice(&queue.fill);
332        }
333        for (position, &index) in queue.indices.iter().enumerate() {
334            bytes[index * stride..(index + 1) * stride].copy_from_slice(element(position));
335        }
336        write(0, bytes);
337        return;
338    }
339
340    // Sort queue positions by index. The sort is stable, so for a repeated index the
341    // last position in each group is the newest change.
342    order.clear();
343    order.extend(0..queue.indices.len());
344    order.sort_by_key(|&position| queue.indices[position]);
345
346    let mut run_start = 0;
347    bytes.clear();
348    let mut iter = order.iter().copied().peekable();
349    while let Some(position) = iter.next() {
350        let index = queue.indices[position];
351        if iter
352            .peek()
353            .is_some_and(|&next| queue.indices[next] == index)
354        {
355            continue; // superseded by a newer change to the same index
356        }
357        if !bytes.is_empty() && index != run_start + bytes.len() / stride {
358            write((run_start * stride) as u64, bytes);
359            bytes.clear();
360        }
361        if bytes.is_empty() {
362            run_start = index;
363        }
364        bytes.extend_from_slice(element(position));
365    }
366    if !bytes.is_empty() {
367        write((run_start * stride) as u64, bytes);
368    }
369}
370
371#[cfg(test)]
372mod tests {
373    use super::*;
374
375    const STRIDE: usize = 16;
376    const LEN: usize = 32;
377
378    /// One queued operation, applied the obvious way by `naive`.
379    #[derive(Clone, Copy, Debug)]
380    enum Op {
381        Set(usize, u32),
382        SetAll(u32),
383    }
384
385    fn element_bytes(value: u32) -> Vec<u8> {
386        let mut bytes = vec![0u8; STRIDE];
387        write_element(&UVec4::splat(value), &mut bytes);
388        bytes
389    }
390
391    fn queue_from(ops: &[Op]) -> ChangeQueue {
392        let mut queue = ChangeQueue::default();
393        for &op in ops {
394            match op {
395                Op::Set(index, value) => queue.push(index, &UVec4::splat(value), STRIDE),
396                Op::SetAll(value) => {
397                    queue.clear();
398                    queue.has_fill = true;
399                    queue.fill = element_bytes(value);
400                }
401            }
402        }
403        queue
404    }
405
406    fn naive(initial: &[u8], ops: &[Op]) -> Vec<u8> {
407        let mut buffer = initial.to_vec();
408        for &op in ops {
409            match op {
410                Op::Set(index, value) => buffer[index * STRIDE..(index + 1) * STRIDE]
411                    .copy_from_slice(&element_bytes(value)),
412                Op::SetAll(value) => buffer = element_bytes(value).repeat(LEN),
413            }
414        }
415        buffer
416    }
417
418    /// Applies `queue` to `initial` through `plan_writes`, returning the result and the
419    /// number of writes.
420    fn apply(initial: &[u8], queue: &ChangeQueue) -> (Vec<u8>, usize) {
421        let mut buffer = initial.to_vec();
422        let mut writes = 0;
423        plan_writes(
424            queue,
425            STRIDE,
426            LEN,
427            &mut WriteScratch::default(),
428            |offset, bytes| {
429                let offset = offset as usize;
430                buffer[offset..offset + bytes.len()].copy_from_slice(bytes);
431                writes += 1;
432            },
433        );
434        (buffer, writes)
435    }
436
437    fn initial() -> Vec<u8> {
438        (0..LEN as u32)
439            .flat_map(|i| element_bytes(1000 + i))
440            .collect()
441    }
442
443    #[test]
444    fn empty_queue_writes_nothing() {
445        assert_eq!(apply(&initial(), &ChangeQueue::default()), (initial(), 0));
446    }
447
448    #[test]
449    fn contiguous_indices_become_one_write() {
450        let ops = [Op::Set(5, 1), Op::Set(3, 2), Op::Set(4, 3)];
451        let (buffer, writes) = apply(&initial(), &queue_from(&ops));
452        assert_eq!(buffer, naive(&initial(), &ops));
453        assert_eq!(writes, 1);
454    }
455
456    #[test]
457    fn gaps_split_writes() {
458        let ops = [Op::Set(0, 1), Op::Set(1, 2), Op::Set(9, 3), Op::Set(31, 4)];
459        let (buffer, writes) = apply(&initial(), &queue_from(&ops));
460        assert_eq!(buffer, naive(&initial(), &ops));
461        assert_eq!(writes, 3);
462    }
463
464    #[test]
465    fn last_write_wins() {
466        let ops = [Op::Set(7, 1), Op::Set(8, 2), Op::Set(7, 3), Op::Set(7, 4)];
467        let (buffer, writes) = apply(&initial(), &queue_from(&ops));
468        assert_eq!(buffer, naive(&initial(), &ops));
469        assert_eq!(writes, 1);
470    }
471
472    #[test]
473    fn set_all_then_set() {
474        let ops = [Op::Set(2, 9), Op::SetAll(5), Op::Set(3, 6)];
475        let (buffer, writes) = apply(&initial(), &queue_from(&ops));
476        assert_eq!(buffer, naive(&initial(), &ops));
477        assert_eq!(writes, 1);
478    }
479
480    #[test]
481    fn matches_naive_on_random_sequences() {
482        // Small deterministic LCG, so the test needs no extra dependencies.
483        let mut seed = 0x2545_f491_u64;
484        let mut next = |bound: u64| {
485            seed = seed
486                .wrapping_mul(6364136223846793005)
487                .wrapping_add(1442695040888963407);
488            ((seed >> 33) % bound) as usize
489        };
490        for _ in 0..500 {
491            let ops: Vec<Op> = (0..next(40))
492                .map(|_| match next(20) {
493                    0 => Op::SetAll(next(100) as u32),
494                    _ => Op::Set(next(LEN as u64), next(100) as u32),
495                })
496                .collect();
497            assert_eq!(
498                apply(&initial(), &queue_from(&ops)).0,
499                naive(&initial(), &ops),
500                "{ops:?}"
501            );
502        }
503    }
504
505    #[test]
506    fn append_keeps_order_and_fill_semantics() {
507        // Render world still has unapplied changes when new ones arrive.
508        let older = [Op::Set(1, 1), Op::Set(2, 2)];
509        let newer = [Op::Set(1, 3), Op::Set(4, 4)];
510        let mut pending = queue_from(&older);
511        pending.append(&mut queue_from(&newer));
512        let all: Vec<Op> = older.iter().chain(&newer).copied().collect();
513        assert_eq!(apply(&initial(), &pending).0, naive(&initial(), &all));
514
515        // A fill in the newer batch replaces everything older.
516        let newer = [Op::SetAll(8), Op::Set(0, 9)];
517        let mut pending = queue_from(&older);
518        let mut incoming = queue_from(&newer);
519        pending.append(&mut incoming);
520        assert!(incoming.is_empty());
521        let all: Vec<Op> = older.iter().chain(&newer).copied().collect();
522        assert_eq!(apply(&initial(), &pending).0, naive(&initial(), &all));
523    }
524}