pebble/graphics/pipeline/
material.rs1use 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
18pub struct RenderPipeline(wgpu::RenderPipeline);
20
21impl RenderPipeline {
22 pub(crate) fn raw(&self) -> &wgpu::RenderPipeline {
23 &self.0
24 }
25}
26
27pub 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 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
185pub 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
284pub 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}