Skip to main content

pebble/graphics/pipeline/
material.rs

1use crate::{
2    assets::{
3        handle::Handle,
4        storage::Assets,
5        upload::{Asset, AssetSource},
6    },
7    ecs::resources::Read,
8    graphics::{
9        pipeline::{
10            binding::{BindGroupLayout, BindGroupLayoutBuilder, BindGroupTarget, BindingEntry},
11            layout::{
12                GlobalLayoutPool, GroupEntry, PipelineKind, assemble_group_layouts,
13                find_own_entries,
14            },
15        },
16        render::Backend,
17        types::{
18            Face, PolygonMode,
19            flags::ShaderStages,
20            pipeline_state::{ColorTargetState, DepthStencilState, VertexBufferLayout},
21        },
22    },
23};
24
25/// A compiled GPU render pipeline, wrapping `wgpu::RenderPipeline`.
26pub struct RenderPipeline(wgpu::RenderPipeline);
27
28impl RenderPipeline {
29    pub(crate) fn raw(&self) -> &wgpu::RenderPipeline {
30        &self.0
31    }
32}
33
34/// A render pipeline asset — WGSL shader source plus the fixed-function
35/// state (vertex layouts, cull mode, depth, targets) needed to compile it.
36/// Bind group layout is inferred from [`with_entries`](Self::with_entries).
37pub struct Material {
38    label: Option<&'static str>,
39    shader_source: &'static str,
40    vertex_entry: Option<&'static str>,
41    fragment_entry: Option<&'static str>,
42    vertex_layouts: Vec<VertexBufferLayout>,
43    groups: Vec<GroupEntry>,
44    cull_mode: Option<Face>,
45    depth: Option<DepthStencilState>,
46    targets: Vec<ColorTargetState>,
47    polygon_mode: PolygonMode,
48    sample_count: u32,
49}
50
51impl Default for Material {
52    fn default() -> Self {
53        Self {
54            label: None,
55            shader_source: "",
56            vertex_entry: Some("vs_main"),
57            fragment_entry: Some("fs_main"),
58            vertex_layouts: Vec::new(),
59            groups: Vec::new(),
60            cull_mode: Some(Face::Back),
61            depth: None,
62            targets: Vec::new(),
63            polygon_mode: PolygonMode::Fill,
64            sample_count: 1,
65        }
66    }
67}
68
69impl Material {
70    pub fn new(shader_source: &'static str) -> Self {
71        Self {
72            shader_source,
73            ..Self::default()
74        }
75    }
76
77    pub fn with_label(mut self, label: &'static str) -> Self {
78        self.label = Some(label);
79        self
80    }
81
82    pub fn with_vertex_entry(mut self, entry: &'static str) -> Self {
83        self.vertex_entry = Some(entry);
84        self
85    }
86
87    pub fn without_vertex_entry(mut self) -> Self {
88        self.vertex_entry = None;
89        self
90    }
91
92    pub fn with_fragment_entry(mut self, entry: &'static str) -> Self {
93        self.fragment_entry = Some(entry);
94        self
95    }
96
97    pub fn without_fragment_entry(mut self) -> Self {
98        self.fragment_entry = None;
99        self
100    }
101
102    pub fn with_vertex_layouts(mut self, layouts: Vec<VertexBufferLayout>) -> Self {
103        self.vertex_layouts = layouts;
104        self
105    }
106
107    /// The bind group layout this material's shader expects — entries are
108    /// pooled and deduplicated with other materials/computes via [`GlobalLayoutPool`].
109    pub fn with_entries(mut self, groups: Vec<GroupEntry>) -> Self {
110        self.groups = groups;
111        self
112    }
113
114    pub fn with_cull_mode(mut self, mode: Face) -> Self {
115        self.cull_mode = Some(mode);
116        self
117    }
118
119    pub fn without_cull_mode(mut self) -> Self {
120        self.cull_mode = None;
121        self
122    }
123
124    pub fn with_depth(mut self, depth: DepthStencilState) -> Self {
125        self.depth = Some(depth);
126        self
127    }
128
129    pub fn with_targets(mut self, targets: Vec<ColorTargetState>) -> Self {
130        self.targets = targets;
131        self
132    }
133
134    pub fn with_polygon_mode(mut self, mode: PolygonMode) -> Self {
135        self.polygon_mode = mode;
136        self
137    }
138
139    pub fn with_sample_count(mut self, count: u32) -> Self {
140        self.sample_count = count;
141        self
142    }
143
144    fn validate(&self) {
145        if self.targets.is_empty() {
146            tracing::warn!(
147                "Material{}: no color targets set — a render pipeline normally writes to \
148                 at least one; consider calling .with_targets(...) (unless this is intentionally a \
149                 depth-only pass)",
150                self.label.map(|l| format!(" '{l}'")).unwrap_or_default(),
151            );
152        }
153    }
154
155    pub fn build_asset(self, name: &str, assets: &mut Assets<Material>) -> Handle<Material> {
156        self.validate();
157        assets.insert(name, self)
158    }
159}
160
161fn check_material_limits(device: &wgpu::Device, desc: &Material) {
162    let limits = device.limits();
163    let labeled = || desc.label.map(|l| format!(" '{l}'")).unwrap_or_default();
164
165    let buffer_count = desc.vertex_layouts.len() as u32;
166    if buffer_count > limits.max_vertex_buffers {
167        panic!(
168            "material{}: {buffer_count} vertex buffer layouts exceeds this device's \
169             max_vertex_buffers ({})",
170            labeled(),
171            limits.max_vertex_buffers
172        );
173    }
174
175    let attribute_count: u32 = desc
176        .vertex_layouts
177        .iter()
178        .map(|l| l.attributes.len() as u32)
179        .sum();
180    if attribute_count > limits.max_vertex_attributes {
181        panic!(
182            "material{}: {attribute_count} vertex attributes (summed across every vertex \
183             layout) exceeds this device's max_vertex_attributes ({})",
184            labeled(),
185            limits.max_vertex_attributes
186        );
187    }
188
189    let target_count = desc.targets.len() as u32;
190    if target_count > limits.max_color_attachments {
191        panic!(
192            "material{}: {target_count} color targets exceeds this device's max_color_attachments ({})",
193            labeled(),
194            limits.max_color_attachments
195        );
196    }
197}
198
199/// Compiles a [`Material`] into a raw pipeline + bind group layout. Used
200/// internally by the asset upload path; exposed for callers building their
201/// own asset wiring around a `Material` outside the usual [`Assets`] flow.
202pub fn build_material(
203    backend: &Backend,
204    desc: &Material,
205    pool: &GlobalLayoutPool,
206) -> Option<(RenderPipeline, BindGroupLayout)> {
207    check_material_limits(&backend.device, desc);
208
209    let own_entries = find_own_entries(desc.label, PipelineKind::Material, &desc.groups);
210    for entry in own_entries {
211        if entry.kind.visibility().intersects(ShaderStages::COMPUTE) {
212            panic!(
213                "material{}: entry '{}' is visible to the compute stage — material bind \
214                 group entries must not be COMPUTE-visible",
215                desc.label.map(|l| format!(" '{l}'")).unwrap_or_default(),
216                entry.name,
217            );
218        }
219    }
220
221    let layout = BindGroupLayoutBuilder::new()
222        .with_label(desc.label)
223        .with_entries(own_entries.iter().cloned())
224        .build(backend);
225
226    let device = &backend.device;
227    let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
228        label: desc.label,
229        source: wgpu::ShaderSource::Wgsl(desc.shader_source.into()),
230    });
231
232    let bind_group_layouts = assemble_group_layouts(
233        desc.label,
234        &desc.groups,
235        &layout,
236        pool,
237        device.limits().max_bind_groups,
238    )?;
239
240    let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
241        label: desc.label,
242        bind_group_layouts: &bind_group_layouts,
243        immediate_size: 0,
244    });
245
246    let attribute_sets: Vec<Vec<wgpu::VertexAttribute>> = desc
247        .vertex_layouts
248        .iter()
249        .map(|l| l.attributes.iter().map(|a| (*a).into()).collect())
250        .collect();
251    let vertex_buffers: Vec<Option<wgpu::VertexBufferLayout>> = desc
252        .vertex_layouts
253        .iter()
254        .zip(attribute_sets.iter())
255        .map(|(l, attrs)| {
256            Some(wgpu::VertexBufferLayout {
257                array_stride: l.array_stride,
258                step_mode: l.step_mode.into(),
259                attributes: attrs,
260            })
261        })
262        .collect();
263
264    let targets: Vec<Option<wgpu::ColorTargetState>> = desc
265        .targets
266        .iter()
267        .cloned()
268        .map(|t| Some(t.into()))
269        .collect();
270
271    let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
272        label: desc.label,
273        layout: Some(&pipeline_layout),
274        vertex: wgpu::VertexState {
275            module: &module,
276            entry_point: desc.vertex_entry,
277            compilation_options: Default::default(),
278            buffers: &vertex_buffers,
279        },
280        primitive: wgpu::PrimitiveState {
281            topology: wgpu::PrimitiveTopology::TriangleList,
282            strip_index_format: None,
283            front_face: wgpu::FrontFace::Ccw,
284            cull_mode: desc.cull_mode.map(Into::into),
285            unclipped_depth: false,
286            polygon_mode: desc.polygon_mode.into(),
287            conservative: false,
288        },
289        depth_stencil: desc.depth.clone().map(Into::into),
290        multisample: wgpu::MultisampleState {
291            count: desc.sample_count,
292            mask: !0,
293            alpha_to_coverage_enabled: false,
294        },
295        fragment: Some(wgpu::FragmentState {
296            module: &module,
297            entry_point: desc.fragment_entry,
298            compilation_options: Default::default(),
299            targets: &targets,
300        }),
301        multiview_mask: None,
302        cache: None,
303    });
304
305    Some((RenderPipeline(pipeline), layout))
306}
307
308/// The GPU-resident pipeline an uploaded [`Material`] produces.
309pub struct GPUMaterial {
310    pub pipeline: RenderPipeline,
311    layout: BindGroupLayout,
312    entries: Vec<BindingEntry>,
313}
314
315impl BindGroupTarget for GPUMaterial {
316    fn bind_group_layout(&self) -> &BindGroupLayout {
317        &self.layout
318    }
319    fn binding_entries(&self) -> &[BindingEntry] {
320        &self.entries
321    }
322}
323
324impl AssetSource for Material {
325    type Processed = GPUMaterial;
326}
327
328impl Asset<Backend> for Material {
329    type Deps<'a> = Read<'a, GlobalLayoutPool>;
330
331    fn upload<'a>(
332        &self,
333        backend: &Backend,
334        pool: &Read<'a, GlobalLayoutPool>,
335    ) -> Option<GPUMaterial> {
336        let (pipeline, layout) = build_material(backend, self, pool)?;
337        let entries = find_own_entries(self.label, PipelineKind::Material, &self.groups).to_vec();
338
339        Some(GPUMaterial {
340            pipeline,
341            layout,
342            entries,
343        })
344    }
345}