Skip to main content

pebble/wgpu/
layout.rs

1use super::binding::{BindGroupLayout, BindingEntry};
2
3/// One `@group(N)` slot in a material/compute pipeline layout — position in the
4/// [`Material::entries`](super::material::Material::entries)/
5/// [`Compute::entries`](super::compute::Compute::entries) list *is* its `@group(N)` index, so
6/// there's no separate group number to keep in sync with the shader by hand: the first element
7/// occupies `@group(0)`, the second `@group(1)`, and so on.
8pub enum GroupEntry {
9    /// This material/compute's own bind group entries — built into a fresh layout
10    /// internally, and the one group [`GPUMaterial`](super::material::GPUMaterial)/
11    /// [`GPUCompute`](super::compute::GPUCompute) hand to a
12    /// [`GPUBindingInstance`](super::instance::GPUBindingInstance) to bind concrete resources
13    /// against at draw/dispatch time. At most one `Own` entry is allowed in a single
14    /// `.entries(...)` list — `build_material`/`build_compute` panic on a second one, since
15    /// there's only one instance-bindable group per material/compute.
16    Own(Vec<BindingEntry>),
17    /// An already-built layout occupying this position directly — a camera, lights, or any
18    /// other external bind group layout, e.g. pulled from a [`GlobalLayoutPool`] via
19    /// [`GlobalLayoutPool::get`].
20    Layout(BindGroupLayout),
21}
22
23/// A named pool of bind group layouts shared across materials/compute passes — register a
24/// layout once (e.g. a camera's, under `"camera"`) as soon as it exists, then anywhere a
25/// material/compute wants it, pull it with [`get`](Self::get) and wrap it in
26/// [`GroupEntry::Layout`] at whatever position that material/compute's shader declares it.
27///
28/// [`WGPUPlugin`](super::backend::WGPUPlugin) inserts an empty pool as a resource, so it's
29/// always there from the start — grab it with `Res<GlobalLayoutPool>`/
30/// `ResMut<GlobalLayoutPool>` rather than constructing your own; a `LazyResource` that builds a
31/// shared layout (a camera, lights, ...) registers it into that same pool from its own
32/// `construct` (or a follow-up system, once it has `ResMut<GlobalLayoutPool>` alongside it) —
33/// there's no separate "finished pool" step, entries just accumulate as their sources become
34/// ready.
35#[derive(Default)]
36pub struct GlobalLayoutPool {
37    entries: std::collections::HashMap<&'static str, BindGroupLayout>,
38}
39
40impl GlobalLayoutPool {
41    pub fn new() -> Self {
42        Self::default()
43    }
44
45    /// Register `layout` under `name`. Panics if `name` is already registered — almost always
46    /// two sources registering under the same name by mistake, not an intentional overwrite.
47    pub fn register(&mut self, name: &'static str, layout: BindGroupLayout) {
48        if self.entries.insert(name, layout).is_some() {
49            panic!("global layout pool: '{name}' is already registered");
50        }
51    }
52
53    /// The layout registered under `name`, if any — clone it into a [`GroupEntry::Layout`] at
54    /// whatever position your shader declares it. `None` if nothing has registered under that
55    /// name (yet, or ever — a typo'd name and "not built yet" look the same from here, so
56    /// callers with a hard requirement on a given global should treat a miss as "not ready"
57    /// the same way any other `Option`-returning lookup in this engine does).
58    pub fn get(&self, name: &str) -> Option<BindGroupLayout> {
59        self.entries.get(name).cloned()
60    }
61}
62
63/// Which pipeline kind a panic message from [`find_own_entries`] is describing — only used
64/// for wording those messages (`Material`'s bind group entries are validated differently than
65/// `Compute`'s, but both funnel through the same shared function).
66#[derive(Clone, Copy)]
67pub(crate) enum PipelineKind {
68    Material,
69    Compute,
70}
71
72impl std::fmt::Display for PipelineKind {
73    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74        f.write_str(match self {
75            PipelineKind::Material => "material",
76            PipelineKind::Compute => "compute pass",
77        })
78    }
79}
80
81/// Finds the single [`GroupEntry::Own`] in `groups`, if any, returning its entries (or `&[]`
82/// if there isn't one — a shader with no `@group` of its own). Panics if there's more than
83/// one — a material/compute can only expose one concrete bind group for a
84/// [`GPUBindingInstance`](super::instance::GPUBindingInstance) to bind resources against, so
85/// at most one position in `.entries(...)` may be `Own`.
86pub(crate) fn find_own_entries<'a>(
87    label: Option<&str>,
88    kind: PipelineKind,
89    groups: &'a [GroupEntry],
90) -> &'a [BindingEntry] {
91    let mut found: Option<&[BindingEntry]> = None;
92    for g in groups {
93        if let GroupEntry::Own(entries) = g {
94            if found.is_some() {
95                panic!(
96                    "{kind}{}: more than one GroupEntry::Own(...) in .entries(...) — a {kind} \
97                     can only have one group of its own bind group entries",
98                    label.map(|l| format!(" '{l}'")).unwrap_or_default()
99                );
100            }
101            found = Some(entries);
102        }
103    }
104    found.unwrap_or(&[])
105}
106
107/// Assembles the ordered pipeline-layout slots from `groups` — position in `groups` is the
108/// `@group(N)` index, with `own_layout` (built by the caller from
109/// [`find_own_entries`]'s result) filling in wherever [`GroupEntry::Own`] appeared.
110///
111/// Panics if `groups` needs more bind groups than `max_bind_groups` allows — `wgpu` guarantees
112/// only 4 (`@group(0..=3)`) unless a device explicitly requests/supports more, so this is the
113/// difference between a clear message here (the actual limit and how many groups were
114/// requested) and an opaque wgpu validation panic at pipeline-layout creation. This is the
115/// reason to only list the groups a shader actually declares in `.entries(...)` — a
116/// [`GlobalLayoutPool`] registration you don't need is one you shouldn't reach for.
117pub(crate) fn assemble_group_layouts<'a>(
118    label: Option<&str>,
119    groups: &'a [GroupEntry],
120    own_layout: &'a BindGroupLayout,
121    max_bind_groups: u32,
122) -> Vec<Option<&'a wgpu::BindGroupLayout>> {
123    if groups.len() as u32 > max_bind_groups {
124        panic!(
125            "pipeline layout{} needs {} bind groups, but this device only supports \
126             {max_bind_groups} — trim .entries(...) to only the groups actually used",
127            label.map(|l| format!(" '{l}'")).unwrap_or_default(),
128            groups.len(),
129        );
130    }
131
132    groups
133        .iter()
134        .map(|g| {
135            Some(match g {
136                GroupEntry::Own(_) => own_layout.raw(),
137                GroupEntry::Layout(l) => l.raw(),
138            })
139        })
140        .collect()
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146    use crate::wgpu::binding::BindGroupLayoutBuilder;
147    use crate::wgpu::test_util::with_device;
148
149    fn empty_layout(device: &wgpu::Device) -> BindGroupLayout {
150        BindGroupLayoutBuilder::new().build_raw(device)
151    }
152
153    #[test]
154    fn global_layout_pool_get_round_trips_through_register() {
155        with_device!(device, _queue, {
156            let mut pool = GlobalLayoutPool::new();
157            pool.register("camera", empty_layout(&device));
158
159            assert!(pool.get("camera").is_some());
160            assert!(pool.get("missing").is_none());
161        });
162    }
163
164    #[test]
165    fn global_layout_pool_panics_on_duplicate_name() {
166        with_device!(device, _queue, {
167            let mut pool = GlobalLayoutPool::new();
168            pool.register("camera", empty_layout(&device));
169            let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
170                pool.register("camera", empty_layout(&device));
171            }));
172            assert!(result.is_err(), "expected a panic for a duplicate name registered in the pool");
173        });
174    }
175
176    #[test]
177    fn find_own_entries_returns_empty_slice_when_there_is_no_own_group() {
178        let entries = find_own_entries(None, PipelineKind::Material, &[]);
179        assert!(entries.is_empty());
180    }
181
182    #[test]
183    fn find_own_entries_panics_on_more_than_one_own_group() {
184        with_device!(device, _queue, {
185            let groups =
186                vec![GroupEntry::Own(vec![]), GroupEntry::Layout(empty_layout(&device)), GroupEntry::Own(vec![])];
187            let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
188                find_own_entries(None, PipelineKind::Material, &groups);
189            }));
190            assert!(result.is_err(), "expected a panic for more than one GroupEntry::Own");
191        });
192    }
193
194    #[test]
195    fn assemble_group_layouts_orders_slots_by_position() {
196        with_device!(device, _queue, {
197            let own = empty_layout(&device);
198            let a = empty_layout(&device);
199            let b = empty_layout(&device);
200            let groups =
201                vec![GroupEntry::Layout(a), GroupEntry::Own(vec![]), GroupEntry::Layout(b)];
202
203            let assembled = assemble_group_layouts(None, &groups, &own, 4);
204
205            assert_eq!(assembled.len(), 3);
206            let GroupEntry::Layout(a) = &groups[0] else { unreachable!() };
207            let GroupEntry::Layout(b) = &groups[2] else { unreachable!() };
208            assert!(std::ptr::eq(assembled[0].unwrap(), a.raw()));
209            assert!(std::ptr::eq(assembled[1].unwrap(), own.raw()));
210            assert!(std::ptr::eq(assembled[2].unwrap(), b.raw()));
211        });
212    }
213
214    #[test]
215    fn exceeding_max_bind_groups_panics() {
216        with_device!(device, _queue, {
217            let own = empty_layout(&device);
218            let groups = vec![GroupEntry::Layout(empty_layout(&device)), GroupEntry::Layout(empty_layout(&device))];
219            let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
220                assemble_group_layouts(None, &groups, &own, 1);
221            }));
222            assert!(result.is_err(), "expected a panic for exceeding max_bind_groups");
223        });
224    }
225}