Skip to main content

pebble/wgpu/
layout.rs

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