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},
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
36enum TargetsSpec {
42 Explicit(Vec<ColorTargetState>),
43 SurfaceDefault,
44}
45
46impl TargetsSpec {
47 fn len(&self) -> usize {
48 match self {
49 Self::Explicit(targets) => targets.len(),
50 Self::SurfaceDefault => 1,
51 }
52 }
53
54 fn is_empty(&self) -> bool {
55 matches!(self, Self::Explicit(targets) if targets.is_empty())
56 }
57
58 fn resolve(&self, backend: &Backend) -> Vec<ColorTargetState> {
59 match self {
60 Self::Explicit(targets) => targets.clone(),
61 Self::SurfaceDefault => {
62 vec![ColorTargetState { format: backend.surface_format(), ..Default::default() }]
63 }
64 }
65 }
66}
67
68pub struct Material {
72 label: Option<&'static str>,
73 shader_source: &'static str,
74 vertex_entry: Option<&'static str>,
75 fragment_entry: Option<&'static str>,
76 vertex_layouts: Vec<VertexBufferLayout>,
77 groups: Vec<GroupEntry>,
78 cull_mode: Option<Face>,
79 depth: Option<DepthStencilState>,
80 targets: TargetsSpec,
81 polygon_mode: PolygonMode,
82 sample_count: u32,
83}
84
85impl Default for Material {
86 fn default() -> Self {
87 Self {
88 label: None,
89 shader_source: "",
90 vertex_entry: Some("vs_main"),
91 fragment_entry: Some("fs_main"),
92 vertex_layouts: Vec::new(),
93 groups: Vec::new(),
94 cull_mode: Some(Face::default()),
95 depth: None,
96 targets: TargetsSpec::Explicit(Vec::new()),
97 polygon_mode: PolygonMode::default(),
98 sample_count: 1,
99 }
100 }
101}
102
103impl Material {
104 pub fn new(shader_source: &'static str) -> Self {
105 Self {
106 shader_source,
107 ..Self::default()
108 }
109 }
110
111 pub fn standard(shader_source: &'static str) -> Self {
127 let mut material = Self::new(shader_source)
128 .with_vertex_layouts(vec![Vertex::layout()])
129 .with_depth(DepthStencilState::DEFAULT);
130 material.targets = TargetsSpec::SurfaceDefault;
131 material
132 }
133
134 pub fn with_label(mut self, label: &'static str) -> Self {
135 self.label = Some(label);
136 self
137 }
138
139 pub fn with_vertex_entry(mut self, entry: &'static str) -> Self {
140 self.vertex_entry = Some(entry);
141 self
142 }
143
144 pub fn without_vertex_entry(mut self) -> Self {
145 self.vertex_entry = None;
146 self
147 }
148
149 pub fn with_fragment_entry(mut self, entry: &'static str) -> Self {
150 self.fragment_entry = Some(entry);
151 self
152 }
153
154 pub fn without_fragment_entry(mut self) -> Self {
155 self.fragment_entry = None;
156 self
157 }
158
159 pub fn with_vertex_layouts(mut self, layouts: Vec<VertexBufferLayout>) -> Self {
160 self.vertex_layouts = layouts;
161 self
162 }
163
164 pub fn with_entries(mut self, groups: Vec<GroupEntry>) -> Self {
167 self.groups = groups;
168 self
169 }
170
171 pub fn with_cull_mode(mut self, mode: Face) -> Self {
172 self.cull_mode = Some(mode);
173 self
174 }
175
176 pub fn without_cull_mode(mut self) -> Self {
177 self.cull_mode = None;
178 self
179 }
180
181 pub fn with_depth(mut self, depth: DepthStencilState) -> Self {
182 self.depth = Some(depth);
183 self
184 }
185
186 pub fn without_depth(mut self) -> Self {
187 self.depth = None;
188 self
189 }
190
191 pub fn with_targets(mut self, targets: Vec<ColorTargetState>) -> Self {
192 self.targets = TargetsSpec::Explicit(targets);
193 self
194 }
195
196 pub fn with_polygon_mode(mut self, mode: PolygonMode) -> Self {
197 self.polygon_mode = mode;
198 self
199 }
200
201 pub fn with_sample_count(mut self, count: u32) -> Self {
202 self.sample_count = count;
203 self
204 }
205
206 fn validate(&self) {
207 if self.targets.is_empty() {
208 tracing::warn!(
209 "Material{}: no color targets set — a render pipeline normally writes to \
210 at least one; consider calling .with_targets(...) (unless this is intentionally a \
211 depth-only pass)",
212 self.label.map(|l| format!(" '{l}'")).unwrap_or_default(),
213 );
214 }
215 }
216
217 pub fn build_asset(self, name: &str, assets: &mut Assets<Material>) -> Handle<Material> {
218 self.validate();
219 assets.insert(name, self)
220 }
221}
222
223fn check_material_limits(device: &wgpu::Device, desc: &Material) {
224 let limits = device.limits();
225 let labeled = || desc.label.map(|l| format!(" '{l}'")).unwrap_or_default();
226
227 let buffer_count = desc.vertex_layouts.len() as u32;
228 if buffer_count > limits.max_vertex_buffers {
229 panic!(
230 "material{}: {buffer_count} vertex buffer layouts exceeds this device's \
231 max_vertex_buffers ({})",
232 labeled(),
233 limits.max_vertex_buffers
234 );
235 }
236
237 let attribute_count: u32 = desc
238 .vertex_layouts
239 .iter()
240 .map(|l| l.attributes.len() as u32)
241 .sum();
242 if attribute_count > limits.max_vertex_attributes {
243 panic!(
244 "material{}: {attribute_count} vertex attributes (summed across every vertex \
245 layout) exceeds this device's max_vertex_attributes ({})",
246 labeled(),
247 limits.max_vertex_attributes
248 );
249 }
250
251 let target_count = desc.targets.len() as u32;
252 if target_count > limits.max_color_attachments {
253 panic!(
254 "material{}: {target_count} color targets exceeds this device's max_color_attachments ({})",
255 labeled(),
256 limits.max_color_attachments
257 );
258 }
259}
260
261pub fn build_material(
265 backend: &Backend,
266 desc: &Material,
267 pool: &GlobalLayoutPool,
268) -> Option<(RenderPipeline, BindGroupLayout)> {
269 check_material_limits(&backend.device, desc);
270
271 let own_entries = find_own_entries(desc.label, PipelineKind::Material, &desc.groups);
272 for entry in own_entries {
273 if entry.kind.visibility().intersects(ShaderStages::COMPUTE) {
274 panic!(
275 "material{}: entry '{}' is visible to the compute stage — material bind \
276 group entries must not be COMPUTE-visible",
277 desc.label.map(|l| format!(" '{l}'")).unwrap_or_default(),
278 entry.name,
279 );
280 }
281 }
282
283 let layout = BindGroupLayoutBuilder::new()
284 .with_label(desc.label)
285 .with_entries(own_entries.iter().cloned())
286 .build(backend);
287
288 let device = &backend.device;
289 let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
290 label: desc.label,
291 source: wgpu::ShaderSource::Wgsl(desc.shader_source.into()),
292 });
293
294 let bind_group_layouts = assemble_group_layouts(
295 desc.label,
296 &desc.groups,
297 &layout,
298 pool,
299 device.limits().max_bind_groups,
300 )?;
301
302 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
303 label: desc.label,
304 bind_group_layouts: &bind_group_layouts,
305 immediate_size: 0,
306 });
307
308 let attribute_sets: Vec<Vec<wgpu::VertexAttribute>> = desc
309 .vertex_layouts
310 .iter()
311 .map(|l| l.attributes.iter().map(|a| (*a).into()).collect())
312 .collect();
313 let vertex_buffers: Vec<Option<wgpu::VertexBufferLayout>> = desc
314 .vertex_layouts
315 .iter()
316 .zip(attribute_sets.iter())
317 .map(|(l, attrs)| {
318 Some(wgpu::VertexBufferLayout {
319 array_stride: l.array_stride,
320 step_mode: l.step_mode.into(),
321 attributes: attrs,
322 })
323 })
324 .collect();
325
326 let targets: Vec<Option<wgpu::ColorTargetState>> =
327 desc.targets.resolve(backend).into_iter().map(|t| Some(t.into())).collect();
328
329 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
330 label: desc.label,
331 layout: Some(&pipeline_layout),
332 vertex: wgpu::VertexState {
333 module: &module,
334 entry_point: desc.vertex_entry,
335 compilation_options: Default::default(),
336 buffers: &vertex_buffers,
337 },
338 primitive: wgpu::PrimitiveState {
339 topology: wgpu::PrimitiveTopology::TriangleList,
340 strip_index_format: None,
341 front_face: wgpu::FrontFace::Ccw,
342 cull_mode: desc.cull_mode.map(Into::into),
343 unclipped_depth: false,
344 polygon_mode: desc.polygon_mode.into(),
345 conservative: false,
346 },
347 depth_stencil: desc.depth.clone().map(Into::into),
348 multisample: wgpu::MultisampleState {
349 count: desc.sample_count,
350 mask: !0,
351 alpha_to_coverage_enabled: false,
352 },
353 fragment: Some(wgpu::FragmentState {
354 module: &module,
355 entry_point: desc.fragment_entry,
356 compilation_options: Default::default(),
357 targets: &targets,
358 }),
359 multiview_mask: None,
360 cache: None,
361 });
362
363 Some((RenderPipeline(pipeline), layout))
364}
365
366pub struct GPUMaterial {
368 pub pipeline: RenderPipeline,
369 layout: BindGroupLayout,
370 entries: Vec<BindingEntry>,
371}
372
373impl BindGroupTarget for GPUMaterial {
374 fn bind_group_layout(&self) -> &BindGroupLayout {
375 &self.layout
376 }
377 fn binding_entries(&self) -> &[BindingEntry] {
378 &self.entries
379 }
380}
381
382impl AssetSource for Material {
383 type Processed = GPUMaterial;
384}
385
386impl Asset<Backend> for Material {
387 type Deps<'a> = Read<'a, GlobalLayoutPool>;
388
389 fn upload<'a>(
390 &self,
391 backend: &Backend,
392 pool: &Read<'a, GlobalLayoutPool>,
393 ) -> Option<GPUMaterial> {
394 let (pipeline, layout) = build_material(backend, self, pool)?;
395 let entries = find_own_entries(self.label, PipelineKind::Material, &self.groups).to_vec();
396
397 Some(GPUMaterial {
398 pipeline,
399 layout,
400 entries,
401 })
402 }
403}