Skip to main content

pebble/graphics/pipeline/
layout.rs

1use std::cell::RefCell;
2use std::collections::HashMap;
3
4use crate::graphics::{
5    pipeline::{
6        binding::{BindGroupLayout, BindingEntry, BindingKind},
7        compute::ComputePipeline,
8        material::RenderPipeline,
9    },
10    types::{
11        Face, PolygonMode,
12        pipeline_state::{ColorTargetState, DepthStencilState, VertexBufferLayout},
13    },
14};
15
16/// One bind group beyond a [`Material`](super::material::Material)/[`Compute`](super::compute::Compute)'s
17/// own (group 0, built automatically from its `.texture(...)`/`.sampler(...)`/etc.
18/// calls) — passed to `.with_extra_group(...)` for group 1 and up: a
19/// pre-built [`BindGroupLayout`], or a name looked up in the
20/// [`GlobalLayoutPool`] (for layouts shared across pipelines). `Own` also
21/// exists for the rare case of assembling one by hand instead.
22#[derive(Clone)]
23pub enum GroupEntry {
24    Own(Vec<BindingEntry>),
25    Layout(BindGroupLayout),
26    Global(&'static str),
27}
28
29/// Builds the [`GroupEntry::Own`] list for a pipeline's own bind group —
30/// auto-increments binding indices unless you use
31/// [`with_entry_at`](Self::with_entry_at) to pin one explicitly.
32#[derive(Default)]
33pub struct OwnEntriesBuilder {
34    entries: Vec<BindingEntry>,
35    next_binding: u32,
36}
37
38impl OwnEntriesBuilder {
39    pub fn new() -> Self {
40        Self::default()
41    }
42
43    pub fn with_entry(self, name: &'static str, kind: BindingKind) -> Self {
44        let binding = self.next_binding;
45        self.with_entry_at(name, binding, kind)
46    }
47
48    pub fn with_entry_at(mut self, name: &'static str, binding: u32, kind: BindingKind) -> Self {
49        self.entries.push(BindingEntry { name, binding, kind });
50        self.next_binding = self.next_binding.max(binding + 1);
51        self
52    }
53
54    pub fn build(self) -> GroupEntry {
55        GroupEntry::Own(self.entries)
56    }
57
58    /// Peeks at the entries accumulated so far, without consuming the
59    /// builder — lets `Material`/`Compute` read their own accumulated
60    /// entries repeatedly (upload runs on `&self`) instead of only once via
61    /// [`build`](Self::build).
62    pub(crate) fn entries(&self) -> &[BindingEntry] {
63        &self.entries
64    }
65}
66
67/// A registry of named bind group layouts, inserted as a resource by
68/// [`BuiltinAssetsPlugin`](crate::graphics::BuiltinAssetsPlugin) — lets
69/// unrelated materials/computes share one layout via [`GroupEntry::Global`]
70/// instead of each declaring their own.
71#[derive(Default)]
72pub struct GlobalLayoutPool {
73    entries: std::collections::HashMap<&'static str, BindGroupLayout>,
74}
75
76impl GlobalLayoutPool {
77    pub fn new() -> Self {
78        Self::default()
79    }
80
81    /// Registers a layout under `name`. Panics if `name` is already registered.
82    pub fn register(&mut self, name: &'static str, layout: BindGroupLayout) {
83        if self.entries.insert(name, layout).is_some() {
84            panic!("global layout pool: '{name}' is already registered");
85        }
86    }
87
88    pub fn get(&self, name: &str) -> Option<BindGroupLayout> {
89        self.entries.get(name).cloned()
90    }
91
92    pub(crate) fn get_ref(&self, name: &str) -> Option<&BindGroupLayout> {
93        self.entries.get(name)
94    }
95}
96
97#[derive(Clone, Copy)]
98pub(crate) enum PipelineKind {
99    Material,
100    Compute,
101}
102
103impl std::fmt::Display for PipelineKind {
104    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105        f.write_str(match self {
106            PipelineKind::Material => "material",
107            PipelineKind::Compute => "compute pass",
108        })
109    }
110}
111
112pub(crate) fn find_own_entries<'a>(
113    label: Option<&str>,
114    kind: PipelineKind,
115    groups: &'a [GroupEntry],
116) -> &'a [BindingEntry] {
117    let mut found: Option<&[BindingEntry]> = None;
118    for g in groups {
119        if let GroupEntry::Own(entries) = g {
120            if found.is_some() {
121                panic!(
122                    "{kind}{}: more than one GroupEntry::Own(...) in .entries(...) — a {kind} \
123                     can only have one group of its own bind group entries",
124                    label.map(|l| format!(" '{l}'")).unwrap_or_default()
125                );
126            }
127            found = Some(entries);
128        }
129    }
130    found.unwrap_or(&[])
131}
132
133pub(crate) fn assemble_group_layouts<'a>(
134    label: Option<&str>,
135    groups: &'a [GroupEntry],
136    own_layout: &'a BindGroupLayout,
137    pool: &'a GlobalLayoutPool,
138    max_bind_groups: u32,
139) -> Option<Vec<Option<&'a wgpu::BindGroupLayout>>> {
140    if groups.len() as u32 > max_bind_groups {
141        panic!(
142            "pipeline layout{} needs {} bind groups, but this device only supports \
143             {max_bind_groups} — trim .entries(...) to only the groups actually used",
144            label.map(|l| format!(" '{l}'")).unwrap_or_default(),
145            groups.len(),
146        );
147    }
148
149    groups
150        .iter()
151        .map(|g| {
152            let layout = match g {
153                GroupEntry::Own(_) => own_layout,
154                GroupEntry::Layout(l) => l,
155                GroupEntry::Global(name) => pool.get_ref(name)?,
156            };
157            Some(Some(layout.raw()))
158        })
159        .collect()
160}
161
162/// Structural key for one [`GroupEntry`] in a pipeline-cache key. No variant
163/// for `GroupEntry::Layout(_)` — see [`group_keys`].
164#[derive(PartialEq, Eq, Hash)]
165enum GroupKey {
166    Own(Vec<BindingEntry>),
167    Global(&'static str),
168}
169
170/// Builds the cacheable key for `groups`, or `None` if any entry is a
171/// `GroupEntry::Layout(_)` (an inline pre-built layout has no meaningful
172/// structural equality to key on) — a `Material`/`Compute` using one always
173/// compiles its own pipeline, same as every pipeline did before caching
174/// existed. The common case (`Own`/`Global` only, which is what sharing a
175/// shader across many uniform-value combinations actually uses) is fully
176/// cacheable.
177fn group_keys(groups: &[GroupEntry]) -> Option<Vec<GroupKey>> {
178    groups
179        .iter()
180        .map(|g| match g {
181            GroupEntry::Own(entries) => Some(GroupKey::Own(entries.clone())),
182            GroupEntry::Global(name) => Some(GroupKey::Global(name)),
183            GroupEntry::Layout(_) => None,
184        })
185        .collect()
186}
187
188/// Every field of a [`Material`](super::material::Material) that actually
189/// affects the compiled `wgpu::RenderPipeline`/pipeline layout — `label` is
190/// deliberately excluded (it's a debug name, not pipeline-affecting).
191/// `targets` must already be resolved (`TargetsSpec::resolve`) before
192/// building this, so `Material::standard`'s surface-format marker keys on
193/// the real resolved format, not the marker itself.
194#[derive(PartialEq, Eq, Hash)]
195pub(crate) struct MaterialPipelineKey {
196    pub shader_source: &'static str,
197    pub vertex_entry: Option<&'static str>,
198    pub fragment_entry: Option<&'static str>,
199    pub vertex_layouts: Vec<VertexBufferLayout>,
200    pub cull_mode: Option<Face>,
201    pub depth: Option<DepthStencilState>,
202    pub targets: Vec<ColorTargetState>,
203    pub polygon_mode: PolygonMode,
204    pub sample_count: u32,
205    groups: Vec<GroupKey>,
206}
207
208impl MaterialPipelineKey {
209    /// `None` if `groups` contains a `GroupEntry::Layout(_)` — see [`group_keys`].
210    #[allow(clippy::too_many_arguments)]
211    pub fn new(
212        shader_source: &'static str,
213        vertex_entry: Option<&'static str>,
214        fragment_entry: Option<&'static str>,
215        vertex_layouts: Vec<VertexBufferLayout>,
216        cull_mode: Option<Face>,
217        depth: Option<DepthStencilState>,
218        targets: Vec<ColorTargetState>,
219        polygon_mode: PolygonMode,
220        sample_count: u32,
221        groups: &[GroupEntry],
222    ) -> Option<Self> {
223        Some(Self {
224            shader_source,
225            vertex_entry,
226            fragment_entry,
227            vertex_layouts,
228            cull_mode,
229            depth,
230            targets,
231            polygon_mode,
232            sample_count,
233            groups: group_keys(groups)?,
234        })
235    }
236}
237
238/// Deduplicates compiled [`Material`](super::material::Material) pipelines —
239/// many `Material`s sharing the same shader/fixed-function state (e.g.
240/// several uniform-value combinations against one shader) compile once and
241/// share the result, instead of each getting its own `wgpu::RenderPipeline`.
242/// Registered as a resource by
243/// [`BuiltinAssetsPlugin`](crate::graphics::BuiltinAssetsPlugin).
244/// `RefCell`-backed so it can be mutated from inside `Material::upload`,
245/// which only ever gets a `Read<'_, _>` borrow of its dependencies (see
246/// [`Dependencies`](crate::assets::deps::Dependencies) — there's no
247/// `Write<T>` dependency support).
248#[derive(Default)]
249pub struct MaterialPipelineCache {
250    entries: RefCell<HashMap<MaterialPipelineKey, (RenderPipeline, BindGroupLayout)>>,
251}
252
253impl MaterialPipelineCache {
254    pub fn new() -> Self {
255        Self::default()
256    }
257
258    /// The cached `(pipeline, layout)` for `key`, compiling and caching it
259    /// via `compile` on a miss. `key` of `None` (a `Material` using
260    /// `GroupEntry::Layout(_)`) always compiles, never caches.
261    pub(crate) fn get_or_compile(
262        &self,
263        key: Option<MaterialPipelineKey>,
264        compile: impl FnOnce() -> Option<(RenderPipeline, BindGroupLayout)>,
265    ) -> Option<(RenderPipeline, BindGroupLayout)> {
266        let Some(key) = key else { return compile() };
267        if let Some(cached) = self.entries.borrow().get(&key) {
268            return Some(cached.clone());
269        }
270        let built = compile()?;
271        self.entries.borrow_mut().insert(key, built.clone());
272        Some(built)
273    }
274}
275
276/// Same as [`MaterialPipelineKey`], for [`Compute`](super::compute::Compute)
277/// — no vertex/fragment/target/depth state, just what a compute pipeline
278/// actually has.
279#[derive(PartialEq, Eq, Hash)]
280pub(crate) struct ComputePipelineKey {
281    pub shader_source: &'static str,
282    pub entry_point: Option<&'static str>,
283    groups: Vec<GroupKey>,
284}
285
286impl ComputePipelineKey {
287    /// `None` if `groups` contains a `GroupEntry::Layout(_)` — see [`group_keys`].
288    pub fn new(shader_source: &'static str, entry_point: Option<&'static str>, groups: &[GroupEntry]) -> Option<Self> {
289        Some(Self { shader_source, entry_point, groups: group_keys(groups)? })
290    }
291}
292
293/// Same as [`MaterialPipelineCache`], for [`Compute`](super::compute::Compute).
294#[derive(Default)]
295pub struct ComputePipelineCache {
296    entries: RefCell<HashMap<ComputePipelineKey, (ComputePipeline, BindGroupLayout)>>,
297}
298
299impl ComputePipelineCache {
300    pub fn new() -> Self {
301        Self::default()
302    }
303
304    pub(crate) fn get_or_compile(
305        &self,
306        key: Option<ComputePipelineKey>,
307        compile: impl FnOnce() -> Option<(ComputePipeline, BindGroupLayout)>,
308    ) -> Option<(ComputePipeline, BindGroupLayout)> {
309        let Some(key) = key else { return compile() };
310        if let Some(cached) = self.entries.borrow().get(&key) {
311            return Some(cached.clone());
312        }
313        let built = compile()?;
314        self.entries.borrow_mut().insert(key, built.clone());
315        Some(built)
316    }
317}
318
319#[cfg(test)]
320mod tests {
321    use super::*;
322
323    #[test]
324    fn find_own_entries_returns_empty_slice_when_there_is_no_own_group() {
325        let entries = find_own_entries(None, PipelineKind::Material, &[]);
326        assert!(entries.is_empty());
327    }
328}