1use crate::{
2 assets::upload::Asset,
3 wgpu::{
4 backend::WGPUBackend,
5 binding::{BindGroupLayout, BindGroupLayoutBuilder, BindingEntry},
6 flags::ShaderStages,
7 texture_format::TextureFormat,
8 vertex_format::VertexBufferLayout,
9 },
10};
11
12pub struct RenderPipeline(wgpu::RenderPipeline);
19
20impl RenderPipeline {
21 pub(crate) fn raw(&self) -> &wgpu::RenderPipeline {
22 &self.0
23 }
24}
25
26#[derive(Copy, Clone, PartialEq, Eq, Hash)]
28pub enum Face {
29 Front,
30 Back,
31}
32
33impl From<Face> for wgpu::Face {
34 fn from(value: Face) -> Self {
35 match value {
36 Face::Front => Self::Front,
37 Face::Back => Self::Back,
38 }
39 }
40}
41
42#[derive(Copy, Clone, PartialEq, Eq, Hash)]
44pub enum PolygonMode {
45 Fill,
46 Line,
47 Point,
48}
49
50impl From<PolygonMode> for wgpu::PolygonMode {
51 fn from(value: PolygonMode) -> Self {
52 match value {
53 PolygonMode::Fill => Self::Fill,
54 PolygonMode::Line => Self::Line,
55 PolygonMode::Point => Self::Point,
56 }
57 }
58}
59
60#[derive(Copy, Clone, PartialEq, Eq, Hash)]
62pub enum BlendFactor {
63 Zero,
64 One,
65 Src,
66 OneMinusSrc,
67 SrcAlpha,
68 OneMinusSrcAlpha,
69 Dst,
70 OneMinusDst,
71 DstAlpha,
72 OneMinusDstAlpha,
73 SrcAlphaSaturated,
74 Constant,
75 OneMinusConstant,
76 Src1,
77 OneMinusSrc1,
78 Src1Alpha,
79 OneMinusSrc1Alpha,
80}
81
82impl From<BlendFactor> for wgpu::BlendFactor {
83 fn from(value: BlendFactor) -> Self {
84 match value {
85 BlendFactor::Zero => Self::Zero,
86 BlendFactor::One => Self::One,
87 BlendFactor::Src => Self::Src,
88 BlendFactor::OneMinusSrc => Self::OneMinusSrc,
89 BlendFactor::SrcAlpha => Self::SrcAlpha,
90 BlendFactor::OneMinusSrcAlpha => Self::OneMinusSrcAlpha,
91 BlendFactor::Dst => Self::Dst,
92 BlendFactor::OneMinusDst => Self::OneMinusDst,
93 BlendFactor::DstAlpha => Self::DstAlpha,
94 BlendFactor::OneMinusDstAlpha => Self::OneMinusDstAlpha,
95 BlendFactor::SrcAlphaSaturated => Self::SrcAlphaSaturated,
96 BlendFactor::Constant => Self::Constant,
97 BlendFactor::OneMinusConstant => Self::OneMinusConstant,
98 BlendFactor::Src1 => Self::Src1,
99 BlendFactor::OneMinusSrc1 => Self::OneMinusSrc1,
100 BlendFactor::Src1Alpha => Self::Src1Alpha,
101 BlendFactor::OneMinusSrc1Alpha => Self::OneMinusSrc1Alpha,
102 }
103 }
104}
105
106#[derive(Copy, Clone, PartialEq, Eq, Hash)]
108pub enum BlendOperation {
109 Add,
110 Subtract,
111 ReverseSubtract,
112 Min,
113 Max,
114}
115
116impl From<BlendOperation> for wgpu::BlendOperation {
117 fn from(value: BlendOperation) -> Self {
118 match value {
119 BlendOperation::Add => Self::Add,
120 BlendOperation::Subtract => Self::Subtract,
121 BlendOperation::ReverseSubtract => Self::ReverseSubtract,
122 BlendOperation::Min => Self::Min,
123 BlendOperation::Max => Self::Max,
124 }
125 }
126}
127
128#[derive(Copy, Clone, PartialEq, Eq, Hash)]
130pub struct BlendComponent {
131 pub src_factor: BlendFactor,
132 pub dst_factor: BlendFactor,
133 pub operation: BlendOperation,
134}
135
136impl BlendComponent {
137 pub const REPLACE: Self = Self {
139 src_factor: BlendFactor::One,
140 dst_factor: BlendFactor::Zero,
141 operation: BlendOperation::Add,
142 };
143
144 pub const OVER: Self = Self {
146 src_factor: BlendFactor::One,
147 dst_factor: BlendFactor::OneMinusSrcAlpha,
148 operation: BlendOperation::Add,
149 };
150}
151
152impl From<BlendComponent> for wgpu::BlendComponent {
153 fn from(value: BlendComponent) -> Self {
154 Self {
155 src_factor: value.src_factor.into(),
156 dst_factor: value.dst_factor.into(),
157 operation: value.operation.into(),
158 }
159 }
160}
161
162#[derive(Copy, Clone, PartialEq, Eq, Hash)]
164pub struct BlendState {
165 pub color: BlendComponent,
166 pub alpha: BlendComponent,
167}
168
169impl BlendState {
170 pub const REPLACE: Self = Self { color: BlendComponent::REPLACE, alpha: BlendComponent::REPLACE };
172
173 pub const ALPHA_BLENDING: Self = Self {
175 color: BlendComponent {
176 src_factor: BlendFactor::SrcAlpha,
177 dst_factor: BlendFactor::OneMinusSrcAlpha,
178 operation: BlendOperation::Add,
179 },
180 alpha: BlendComponent::OVER,
181 };
182
183 pub const PREMULTIPLIED_ALPHA_BLENDING: Self =
185 Self { color: BlendComponent::OVER, alpha: BlendComponent::OVER };
186}
187
188impl From<BlendState> for wgpu::BlendState {
189 fn from(value: BlendState) -> Self {
190 Self { color: value.color.into(), alpha: value.alpha.into() }
191 }
192}
193
194#[derive(Clone, PartialEq, Eq, Hash)]
197pub struct ColorTargetState {
198 pub format: TextureFormat,
200 pub blend: Option<BlendState>,
202 pub write_mask: super::flags::ColorWrites,
204}
205
206impl From<ColorTargetState> for wgpu::ColorTargetState {
207 fn from(value: ColorTargetState) -> Self {
208 Self {
209 format: value.format.into(),
210 blend: value.blend.map(Into::into),
211 write_mask: value.write_mask.into(),
212 }
213 }
214}
215
216pub const DEFAULT_TARGET: [ColorTargetState; 1] = [ColorTargetState {
223 format: TextureFormat::Rgba8Unorm,
224 blend: None,
225 write_mask: super::flags::ColorWrites::ALL,
226}];
227
228#[derive(Copy, Clone, PartialEq, Eq, Hash)]
231pub enum CompareFunction {
232 Never,
233 Less,
234 Equal,
235 LessEqual,
236 Greater,
237 NotEqual,
238 GreaterEqual,
239 Always,
240}
241
242impl From<CompareFunction> for wgpu::CompareFunction {
243 fn from(value: CompareFunction) -> Self {
244 match value {
245 CompareFunction::Never => Self::Never,
246 CompareFunction::Less => Self::Less,
247 CompareFunction::Equal => Self::Equal,
248 CompareFunction::LessEqual => Self::LessEqual,
249 CompareFunction::Greater => Self::Greater,
250 CompareFunction::NotEqual => Self::NotEqual,
251 CompareFunction::GreaterEqual => Self::GreaterEqual,
252 CompareFunction::Always => Self::Always,
253 }
254 }
255}
256
257#[derive(Copy, Clone, PartialEq, Eq, Hash)]
259pub enum StencilOperation {
260 Keep,
261 Zero,
262 Replace,
263 Invert,
264 IncrementClamp,
265 DecrementClamp,
266 IncrementWrap,
267 DecrementWrap,
268}
269
270impl From<StencilOperation> for wgpu::StencilOperation {
271 fn from(value: StencilOperation) -> Self {
272 match value {
273 StencilOperation::Keep => Self::Keep,
274 StencilOperation::Zero => Self::Zero,
275 StencilOperation::Replace => Self::Replace,
276 StencilOperation::Invert => Self::Invert,
277 StencilOperation::IncrementClamp => Self::IncrementClamp,
278 StencilOperation::DecrementClamp => Self::DecrementClamp,
279 StencilOperation::IncrementWrap => Self::IncrementWrap,
280 StencilOperation::DecrementWrap => Self::DecrementWrap,
281 }
282 }
283}
284
285#[derive(Copy, Clone, PartialEq, Eq, Hash)]
289pub struct StencilFaceState {
290 pub compare: CompareFunction,
291 pub fail_op: StencilOperation,
292 pub depth_fail_op: StencilOperation,
293 pub pass_op: StencilOperation,
294}
295
296impl StencilFaceState {
297 pub const IGNORE: Self = Self {
298 compare: CompareFunction::Always,
299 fail_op: StencilOperation::Keep,
300 depth_fail_op: StencilOperation::Keep,
301 pass_op: StencilOperation::Keep,
302 };
303}
304
305impl Default for StencilFaceState {
306 fn default() -> Self {
307 Self::IGNORE
308 }
309}
310
311impl From<StencilFaceState> for wgpu::StencilFaceState {
312 fn from(value: StencilFaceState) -> Self {
313 Self {
314 compare: value.compare.into(),
315 fail_op: value.fail_op.into(),
316 depth_fail_op: value.depth_fail_op.into(),
317 pass_op: value.pass_op.into(),
318 }
319 }
320}
321
322#[derive(Copy, Clone, PartialEq, Eq, Hash, Default)]
325pub struct StencilState {
326 pub front: StencilFaceState,
327 pub back: StencilFaceState,
328 pub read_mask: u32,
329 pub write_mask: u32,
330}
331
332impl From<StencilState> for wgpu::StencilState {
333 fn from(value: StencilState) -> Self {
334 Self {
335 front: value.front.into(),
336 back: value.back.into(),
337 read_mask: value.read_mask,
338 write_mask: value.write_mask,
339 }
340 }
341}
342
343#[derive(Copy, Clone, PartialEq, Default)]
346pub struct DepthBiasState {
347 pub constant: i32,
348 pub slope_scale: f32,
349 pub clamp: f32,
350}
351
352impl From<DepthBiasState> for wgpu::DepthBiasState {
353 fn from(value: DepthBiasState) -> Self {
354 Self { constant: value.constant, slope_scale: value.slope_scale, clamp: value.clamp }
355 }
356}
357
358#[derive(Clone, PartialEq)]
360pub struct DepthStencilState {
361 pub format: TextureFormat,
364 pub depth_write_enabled: Option<bool>,
366 pub depth_compare: Option<CompareFunction>,
368 pub stencil: StencilState,
370 pub bias: DepthBiasState,
372}
373
374impl From<DepthStencilState> for wgpu::DepthStencilState {
375 fn from(value: DepthStencilState) -> Self {
376 Self {
377 format: value.format.into(),
378 depth_write_enabled: value.depth_write_enabled,
379 depth_compare: value.depth_compare.map(Into::into),
380 stencil: value.stencil.into(),
381 bias: value.bias.into(),
382 }
383 }
384}
385
386pub struct MaterialDescriptor<'a> {
391 pub label: Option<&'a str>,
394 pub shader_source: &'a str,
396 pub vertex_entry: Option<&'a str>,
398 pub fragment_entry: Option<&'a str>,
400 pub vertex_layouts: Vec<VertexBufferLayout>,
403 pub entries: Vec<BindingEntry>,
408 pub cull_mode: Option<Face>,
410 pub depth: Option<DepthStencilState>,
412 pub targets: Vec<ColorTargetState>,
415 pub polygon_mode: PolygonMode,
417 pub sample_count: u32,
426 pub own_group: Option<u32>,
429 pub extra_layouts: Vec<super::layout::OwnedGroupLayout>,
434}
435
436impl<'a> Default for MaterialDescriptor<'a> {
437 fn default() -> Self {
438 Self {
439 label: None,
440 shader_source: "",
441 vertex_entry: Some("vs_main"),
442 fragment_entry: Some("fs_main"),
443 vertex_layouts: Vec::new(),
444 entries: Vec::new(),
445 cull_mode: Some(Face::Back),
446 depth: None,
447 targets: Vec::new(),
448 own_group: Some(0),
449 extra_layouts: Vec::new(),
450 polygon_mode: PolygonMode::Fill,
451 sample_count: 1,
452 }
453 }
454}
455
456pub fn build_material(backend: &WGPUBackend, desc: &MaterialDescriptor) -> (RenderPipeline, BindGroupLayout) {
471 build_material_raw(&backend.device, desc)
472}
473
474pub(crate) fn build_material_raw(
477 device: &wgpu::Device,
478 desc: &MaterialDescriptor,
479) -> (RenderPipeline, BindGroupLayout) {
480 for entry in &desc.entries {
481 if entry.kind.visibility().intersects(ShaderStages::COMPUTE) {
482 panic!(
483 "material{}: entry '{}' is visible to the compute stage — material bind \
484 group entries must not be COMPUTE-visible",
485 desc.label.map(|l| format!(" '{l}'")).unwrap_or_default(),
486 entry.name,
487 );
488 }
489 }
490
491 let layout = BindGroupLayoutBuilder::new()
492 .label(desc.label)
493 .entries(desc.entries.iter().cloned())
494 .build_raw(device);
495
496 let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
497 label: desc.label,
498 source: wgpu::ShaderSource::Wgsl(desc.shader_source.into()),
499 });
500
501 let mut slots: Vec<super::layout::GroupLayout> = desc
502 .extra_layouts
503 .iter()
504 .map(|g| super::layout::GroupLayout { group: g.group, layout: &g.layout })
505 .collect();
506 if let Some(own_group) = desc.own_group {
507 slots.push(super::layout::GroupLayout { group: own_group, layout: &layout });
508 }
509 let bind_group_layouts = super::layout::assemble_bind_group_layouts(desc.label, slots);
510
511 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
512 label: desc.label,
513 bind_group_layouts: &bind_group_layouts,
514 immediate_size: 0,
515 });
516
517 let attribute_sets: Vec<Vec<wgpu::VertexAttribute>> = desc
518 .vertex_layouts
519 .iter()
520 .map(|l| l.attributes.iter().map(|a| (*a).into()).collect())
521 .collect();
522 let vertex_buffers: Vec<wgpu::VertexBufferLayout> = desc
523 .vertex_layouts
524 .iter()
525 .zip(attribute_sets.iter())
526 .map(|(l, attrs)| wgpu::VertexBufferLayout {
527 array_stride: l.array_stride,
528 step_mode: l.step_mode.into(),
529 attributes: attrs,
530 })
531 .collect();
532
533 let targets: Vec<Option<wgpu::ColorTargetState>> =
534 desc.targets.iter().cloned().map(|t| Some(t.into())).collect();
535
536 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
537 label: desc.label,
538 layout: Some(&pipeline_layout),
539 vertex: wgpu::VertexState {
540 module: &module,
541 entry_point: desc.vertex_entry,
542 compilation_options: Default::default(),
543 buffers: &vertex_buffers,
544 },
545 primitive: wgpu::PrimitiveState {
546 topology: wgpu::PrimitiveTopology::TriangleList,
547 strip_index_format: None,
548 front_face: wgpu::FrontFace::Ccw,
549 cull_mode: desc.cull_mode.map(Into::into),
550 unclipped_depth: false,
551 polygon_mode: desc.polygon_mode.into(),
552 conservative: false,
553 },
554 depth_stencil: desc.depth.clone().map(Into::into),
555 multisample: wgpu::MultisampleState {
556 count: desc.sample_count,
557 mask: !0,
558 alpha_to_coverage_enabled: false,
559 },
560 fragment: Some(wgpu::FragmentState {
561 module: &module,
562 entry_point: desc.fragment_entry,
563 compilation_options: Default::default(),
564 targets: &targets,
565 }),
566 multiview_mask: None,
567 cache: None,
568 });
569
570 (RenderPipeline(pipeline), layout)
571}
572
573pub struct GPUMaterial {
578 pub pipeline: RenderPipeline,
579 layout: BindGroupLayout,
580 entries: Vec<BindingEntry>,
581}
582
583impl super::binding::BindGroupTarget for GPUMaterial {
584 fn bind_group_layout(&self) -> &BindGroupLayout {
585 &self.layout
586 }
587 fn binding_entries(&self) -> &[BindingEntry] {
588 &self.entries
589 }
590}
591
592impl Asset<WGPUBackend> for GPUMaterial {
593 type Source = MaterialDescriptor<'static>;
594 type Deps<'a> = ();
595
596 fn upload<'a>(source: &MaterialDescriptor, backend: &WGPUBackend, _deps: &()) -> Option<Self> {
597 let (pipeline, layout) = build_material(backend, source);
598
599 Some(Self {
600 pipeline,
601 layout,
602 entries: source.entries.to_vec(),
603 })
604 }
605}
606
607crate::wgpu::plugin_macros::asset_plugin! {
608 MaterialPlugin, GPUMaterial
613}
614
615#[cfg(test)]
616mod tests {
617 use super::*;
618 use crate::wgpu::binding::{BindingEntry, BindingKind};
619 use crate::wgpu::test_util::with_device;
620
621 const MINIMAL_SHADER: &str = r#"
622 @vertex
623 fn vs_main() -> @builtin(position) vec4<f32> {
624 return vec4<f32>(0.0, 0.0, 0.0, 1.0);
625 }
626 @fragment
627 fn fs_main() -> @location(0) vec4<f32> {
628 return vec4<f32>(1.0, 1.0, 1.0, 1.0);
629 }
630 "#;
631
632 #[test]
633 fn a_compute_visible_entry_panics_before_touching_the_device() {
634 with_device!(device, _queue, {
635 let desc = MaterialDescriptor {
636 shader_source: MINIMAL_SHADER,
637 entries: vec![BindingEntry {
638 name: "bad",
639 binding: 0,
640 kind: BindingKind::storage_buffer_read_write(ShaderStages::COMPUTE),
641 }],
642 targets: DEFAULT_TARGET.to_vec(),
643 ..Default::default()
644 };
645 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
646 build_material_raw(&device, &desc);
647 }));
648 assert!(result.is_err(), "expected a panic for a COMPUTE-visible material entry");
649 });
650 }
651
652 #[test]
653 fn a_fragment_visible_entry_builds_without_panicking() {
654 with_device!(device, _queue, {
655 let desc = MaterialDescriptor {
656 shader_source: MINIMAL_SHADER,
657 entries: vec![],
658 own_group: None,
659 targets: DEFAULT_TARGET.to_vec(),
660 ..Default::default()
661 };
662 build_material_raw(&device, &desc);
663 });
664 }
665}