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
25pub struct RenderPipeline(wgpu::RenderPipeline);
27
28impl RenderPipeline {
29 pub(crate) fn raw(&self) -> &wgpu::RenderPipeline {
30 &self.0
31 }
32}
33
34pub 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 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
199pub 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
308pub 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}