pebble/graphics/pipeline/
material.rs1use 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
27pub struct RenderPipeline(wgpu::RenderPipeline);
29
30impl RenderPipeline {
31 pub(crate) fn raw(&self) -> &wgpu::RenderPipeline {
32 &self.0
33 }
34}
35
36pub 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 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 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
221pub 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
330pub 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}