Skip to main content

pebble/graphics/pipeline/
material.rs

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