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