Skip to main content

pebble/wgpu/
layout.rs

1/// A bind group layout tagged with the `@group(N)` it occupies in the pipeline.
2pub struct GroupLayout<'a> {
3    pub group: u32,
4    pub layout: &'a wgpu::BindGroupLayout,
5}
6
7/// An owned bind group layout tagged with the `@group(N)` it occupies, for descriptors that
8/// hold layouts by value.
9pub struct OwnedGroupLayout {
10    pub group: u32,
11    pub layout: wgpu::BindGroupLayout,
12}
13
14/// Assembles bind group layouts for a pipeline from explicit, group-tagged slots, rather
15/// than an implicit position-based order. Panics if any group index in `0..=max_index` is
16/// missing a layout, or if two slots claim the same index — both are almost always a
17/// mistake that would otherwise show up later as an opaque wgpu shader validation error.
18///
19/// `slots` empty means zero bind groups, full stop — a material/compute
20/// pass with `own_group: None` and no `extra_layouts` legitimately has no
21/// bind group at all (e.g. a shader with no `@group` of its own), and that
22/// must not be confused with "group 0 is missing," which is what
23/// `max_group`'s `unwrap_or(0)` would otherwise imply once the loop below
24/// runs against a one-slot-of-`None` array.
25pub fn assemble_bind_group_layouts<'a>(
26    label: Option<&str>,
27    slots: Vec<GroupLayout<'a>>,
28) -> Vec<Option<&'a wgpu::BindGroupLayout>> {
29    if slots.is_empty() {
30        return Vec::new();
31    }
32
33    let max_group = slots.iter().map(|s| s.group).max().unwrap();
34    let mut assembled: Vec<Option<&wgpu::BindGroupLayout>> = vec![None; (max_group + 1) as usize];
35
36    for GroupLayout { group, layout } in slots {
37        let slot = &mut assembled[group as usize];
38        if slot.is_some() {
39            panic!(
40                "bind group {group} assigned more than once building pipeline layout{}",
41                label.map(|l| format!(" '{l}'")).unwrap_or_default()
42            );
43        }
44        *slot = Some(layout);
45    }
46
47    for (i, slot) in assembled.iter().enumerate() {
48        if slot.is_none() {
49            panic!(
50                "bind group {i} has no layout assigned building pipeline layout{} (groups must be contiguous from 0)",
51                label.map(|l| format!(" '{l}'")).unwrap_or_default()
52            );
53        }
54    }
55
56    assembled
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62    use crate::wgpu::test_util::with_device;
63
64    fn empty_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
65        device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
66            label: None,
67            entries: &[],
68        })
69    }
70
71    #[test]
72    fn no_slots_at_all_means_zero_bind_groups_not_a_missing_group_zero() {
73        // Regression test: `slots` empty (a material/compute with
74        // `own_group: None` and no `extra_layouts` — a shader with no
75        // `@group` of its own) used to panic here, because
76        // `max_group.unwrap_or(0)` created a single required-but-unfilled
77        // slot for "group 0" out of nothing.
78        let assembled = assemble_bind_group_layouts(None, vec![]);
79        assert!(assembled.is_empty());
80    }
81
82    #[test]
83    fn slots_out_of_order_still_assemble_into_group_order() {
84        with_device!(device, _queue, {
85            let a = empty_layout(&device);
86            let b = empty_layout(&device);
87            let c = empty_layout(&device);
88
89            // Deliberately out of order — group 2 declared before group 0.
90            let assembled = assemble_bind_group_layouts(
91                None,
92                vec![
93                    GroupLayout { group: 2, layout: &c },
94                    GroupLayout { group: 0, layout: &a },
95                    GroupLayout { group: 1, layout: &b },
96                ],
97            );
98
99            assert_eq!(assembled.len(), 3);
100            assert!(std::ptr::eq(assembled[0].unwrap(), &a));
101            assert!(std::ptr::eq(assembled[1].unwrap(), &b));
102            assert!(std::ptr::eq(assembled[2].unwrap(), &c));
103        });
104    }
105
106    // Not `#[should_panic]`: these need a real device, which `with_device!`
107    // skips gracefully (no panic at all) when none is available — a
108    // `#[should_panic]` test would wrongly fail on exactly the machines
109    // this is meant to tolerate. `catch_unwind` lets "skipped" and
110    // "panicked as expected" both read as a passing test.
111    #[test]
112    fn two_slots_claiming_the_same_group_panics() {
113        with_device!(device, _queue, {
114            let a = empty_layout(&device);
115            let b = empty_layout(&device);
116            let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
117                assemble_bind_group_layouts(
118                    None,
119                    vec![GroupLayout { group: 0, layout: &a }, GroupLayout { group: 0, layout: &b }],
120                );
121            }));
122            assert!(result.is_err(), "expected a panic for a duplicate group index");
123        });
124    }
125
126    #[test]
127    fn a_gap_in_group_indices_panics() {
128        with_device!(device, _queue, {
129            let a = empty_layout(&device);
130            let c = empty_layout(&device);
131            // Group 1 is missing — groups must be contiguous from 0.
132            let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
133                assemble_bind_group_layouts(
134                    None,
135                    vec![GroupLayout { group: 0, layout: &a }, GroupLayout { group: 2, layout: &c }],
136                );
137            }));
138            assert!(result.is_err(), "expected a panic for a gap in group indices");
139        });
140    }
141}