1use crate::{
2 assets::{handle::Handle, storage::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 Material {
391 label: Option<&'static str>,
394 shader_source: &'static str,
396 vertex_entry: Option<&'static str>,
398 fragment_entry: Option<&'static str>,
400 vertex_layouts: Vec<VertexBufferLayout>,
403 groups: Vec<super::layout::GroupEntry>,
406 cull_mode: Option<Face>,
408 depth: Option<DepthStencilState>,
410 targets: Vec<ColorTargetState>,
413 polygon_mode: PolygonMode,
415 sample_count: u32,
424}
425
426pub struct MaterialBuilder {
430 label: Option<&'static str>,
431 shader_source: &'static str,
432 vertex_entry: Option<&'static str>,
433 fragment_entry: Option<&'static str>,
434 vertex_layouts: Vec<VertexBufferLayout>,
435 groups: Vec<super::layout::GroupEntry>,
436 cull_mode: Option<Face>,
437 depth: Option<DepthStencilState>,
438 targets: Vec<ColorTargetState>,
439 polygon_mode: PolygonMode,
440 sample_count: u32,
441}
442
443impl Default for MaterialBuilder {
444 fn default() -> Self {
445 Self {
446 label: None,
447 shader_source: "",
448 vertex_entry: Some("vs_main"),
449 fragment_entry: Some("fs_main"),
450 vertex_layouts: Vec::new(),
451 groups: Vec::new(),
452 cull_mode: Some(Face::Back),
453 depth: None,
454 targets: Vec::new(),
455 polygon_mode: PolygonMode::Fill,
456 sample_count: 1,
457 }
458 }
459}
460
461impl MaterialBuilder {
462 pub fn new(shader_source: &'static str) -> Self {
465 Self { shader_source, ..Self::default() }
466 }
467
468 pub fn label(mut self, label: &'static str) -> Self {
469 self.label = Some(label);
470 self
471 }
472
473 pub fn vertex_entry(mut self, entry: &'static str) -> Self {
474 self.vertex_entry = Some(entry);
475 self
476 }
477
478 pub fn no_vertex_entry(mut self) -> Self {
483 self.vertex_entry = None;
484 self
485 }
486
487 pub fn fragment_entry(mut self, entry: &'static str) -> Self {
488 self.fragment_entry = Some(entry);
489 self
490 }
491
492 pub fn no_fragment_entry(mut self) -> Self {
495 self.fragment_entry = None;
496 self
497 }
498
499 pub fn vertex_layouts(mut self, layouts: Vec<VertexBufferLayout>) -> Self {
500 self.vertex_layouts = layouts;
501 self
502 }
503
504 pub fn entries(mut self, groups: Vec<super::layout::GroupEntry>) -> Self {
521 self.groups = groups;
522 self
523 }
524
525 pub fn cull_mode(mut self, mode: Face) -> Self {
527 self.cull_mode = Some(mode);
528 self
529 }
530
531 pub fn no_cull_mode(mut self) -> Self {
535 self.cull_mode = None;
536 self
537 }
538
539 pub fn depth(mut self, depth: DepthStencilState) -> Self {
540 self.depth = Some(depth);
541 self
542 }
543
544 pub fn targets(mut self, targets: Vec<ColorTargetState>) -> Self {
545 self.targets = targets;
546 self
547 }
548
549 pub fn polygon_mode(mut self, mode: PolygonMode) -> Self {
550 self.polygon_mode = mode;
551 self
552 }
553
554 pub fn sample_count(mut self, count: u32) -> Self {
555 self.sample_count = count;
556 self
557 }
558
559 fn validate(&self) {
564 if self.targets.is_empty() {
565 tracing::warn!(
566 "MaterialBuilder{}: no color targets set — a render pipeline normally writes to \
567 at least one; consider calling .targets(...) (unless this is intentionally a \
568 depth-only pass)",
569 self.label.map(|l| format!(" '{l}'")).unwrap_or_default(),
570 );
571 }
572 }
573
574 pub fn build(self) -> Material {
576 self.validate();
577 Material {
578 label: self.label,
579 shader_source: self.shader_source,
580 vertex_entry: self.vertex_entry,
581 fragment_entry: self.fragment_entry,
582 vertex_layouts: self.vertex_layouts,
583 groups: self.groups,
584 cull_mode: self.cull_mode,
585 depth: self.depth,
586 targets: self.targets,
587 polygon_mode: self.polygon_mode,
588 sample_count: self.sample_count,
589 }
590 }
591
592 pub fn build_asset(self, name: &str, assets: &mut Assets<Material>) -> Handle<Material> {
595 let material = self.build();
596 assets.insert(name, material)
597 }
598}
599
600pub fn build_material(
618 backend: &WGPUBackend,
619 desc: &Material,
620 pool: &super::layout::GlobalLayoutPool,
621) -> Option<(RenderPipeline, BindGroupLayout)> {
622 build_material_raw(&backend.device, desc, pool)
623}
624
625fn check_material_limits(device: &wgpu::Device, desc: &Material) {
631 let limits = device.limits();
632 let labeled = || desc.label.map(|l| format!(" '{l}'")).unwrap_or_default();
633
634 let buffer_count = desc.vertex_layouts.len() as u32;
635 if buffer_count > limits.max_vertex_buffers {
636 panic!(
637 "material{}: {buffer_count} vertex buffer layouts exceeds this device's \
638 max_vertex_buffers ({})",
639 labeled(),
640 limits.max_vertex_buffers
641 );
642 }
643
644 let attribute_count: u32 = desc.vertex_layouts.iter().map(|l| l.attributes.len() as u32).sum();
645 if attribute_count > limits.max_vertex_attributes {
646 panic!(
647 "material{}: {attribute_count} vertex attributes (summed across every vertex \
648 layout) exceeds this device's max_vertex_attributes ({})",
649 labeled(),
650 limits.max_vertex_attributes
651 );
652 }
653
654 let target_count = desc.targets.len() as u32;
655 if target_count > limits.max_color_attachments {
656 panic!(
657 "material{}: {target_count} color targets exceeds this device's max_color_attachments ({})",
658 labeled(),
659 limits.max_color_attachments
660 );
661 }
662}
663
664pub(crate) fn build_material_raw(
667 device: &wgpu::Device,
668 desc: &Material,
669 pool: &super::layout::GlobalLayoutPool,
670) -> Option<(RenderPipeline, BindGroupLayout)> {
671 check_material_limits(device, desc);
672
673 let own_entries =
674 super::layout::find_own_entries(desc.label, super::layout::PipelineKind::Material, &desc.groups);
675 for entry in own_entries {
676 if entry.kind.visibility().intersects(ShaderStages::COMPUTE) {
677 panic!(
678 "material{}: entry '{}' is visible to the compute stage — material bind \
679 group entries must not be COMPUTE-visible",
680 desc.label.map(|l| format!(" '{l}'")).unwrap_or_default(),
681 entry.name,
682 );
683 }
684 }
685
686 let layout = BindGroupLayoutBuilder::new()
687 .label(desc.label)
688 .entries(own_entries.iter().cloned())
689 .build_raw(device);
690
691 let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
692 label: desc.label,
693 source: wgpu::ShaderSource::Wgsl(desc.shader_source.into()),
694 });
695
696 let bind_group_layouts = super::layout::assemble_group_layouts(
697 desc.label,
698 &desc.groups,
699 &layout,
700 pool,
701 device.limits().max_bind_groups,
702 )?;
703
704 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
705 label: desc.label,
706 bind_group_layouts: &bind_group_layouts,
707 immediate_size: 0,
708 });
709
710 let attribute_sets: Vec<Vec<wgpu::VertexAttribute>> = desc
711 .vertex_layouts
712 .iter()
713 .map(|l| l.attributes.iter().map(|a| (*a).into()).collect())
714 .collect();
715 let vertex_buffers: Vec<wgpu::VertexBufferLayout> = desc
716 .vertex_layouts
717 .iter()
718 .zip(attribute_sets.iter())
719 .map(|(l, attrs)| wgpu::VertexBufferLayout {
720 array_stride: l.array_stride,
721 step_mode: l.step_mode.into(),
722 attributes: attrs,
723 })
724 .collect();
725
726 let targets: Vec<Option<wgpu::ColorTargetState>> =
727 desc.targets.iter().cloned().map(|t| Some(t.into())).collect();
728
729 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
730 label: desc.label,
731 layout: Some(&pipeline_layout),
732 vertex: wgpu::VertexState {
733 module: &module,
734 entry_point: desc.vertex_entry,
735 compilation_options: Default::default(),
736 buffers: &vertex_buffers,
737 },
738 primitive: wgpu::PrimitiveState {
739 topology: wgpu::PrimitiveTopology::TriangleList,
740 strip_index_format: None,
741 front_face: wgpu::FrontFace::Ccw,
742 cull_mode: desc.cull_mode.map(Into::into),
743 unclipped_depth: false,
744 polygon_mode: desc.polygon_mode.into(),
745 conservative: false,
746 },
747 depth_stencil: desc.depth.clone().map(Into::into),
748 multisample: wgpu::MultisampleState {
749 count: desc.sample_count,
750 mask: !0,
751 alpha_to_coverage_enabled: false,
752 },
753 fragment: Some(wgpu::FragmentState {
754 module: &module,
755 entry_point: desc.fragment_entry,
756 compilation_options: Default::default(),
757 targets: &targets,
758 }),
759 multiview_mask: None,
760 cache: None,
761 });
762
763 Some((RenderPipeline(pipeline), layout))
764}
765
766pub struct GPUMaterial {
771 pub pipeline: RenderPipeline,
772 layout: BindGroupLayout,
773 entries: Vec<BindingEntry>,
774}
775
776impl super::binding::BindGroupTarget for GPUMaterial {
777 fn bind_group_layout(&self) -> &BindGroupLayout {
778 &self.layout
779 }
780 fn binding_entries(&self) -> &[BindingEntry] {
781 &self.entries
782 }
783}
784
785impl Asset<WGPUBackend> for GPUMaterial {
786 type Source = Material;
787 type Deps<'a> = crate::ecs::system::Res<'a, super::layout::GlobalLayoutPool>;
788
789 fn upload<'a>(
790 source: &Material,
791 backend: &WGPUBackend,
792 pool: &crate::ecs::system::Res<'a, super::layout::GlobalLayoutPool>,
793 ) -> Option<Self> {
794 let (pipeline, layout) = build_material(backend, source, pool)?;
795 let entries =
796 super::layout::find_own_entries(source.label, super::layout::PipelineKind::Material, &source.groups)
797 .to_vec();
798
799 Some(Self { pipeline, layout, entries })
800 }
801}
802
803crate::wgpu::plugin_macros::asset_plugin! {
804 MaterialPlugin, GPUMaterial
809}
810
811#[cfg(test)]
812mod tests {
813 use super::*;
814 use crate::wgpu::binding::{BindingEntry, BindingKind};
815 use crate::wgpu::test_util::with_device;
816 use crate::wgpu::vertex_format::{VertexAttribute, VertexFormat, VertexStepMode};
817
818 const MINIMAL_SHADER: &str = r#"
819 @vertex
820 fn vs_main() -> @builtin(position) vec4<f32> {
821 return vec4<f32>(0.0, 0.0, 0.0, 1.0);
822 }
823 @fragment
824 fn fs_main() -> @location(0) vec4<f32> {
825 return vec4<f32>(1.0, 1.0, 1.0, 1.0);
826 }
827 "#;
828
829 #[test]
830 fn a_compute_visible_own_entry_panics_before_touching_the_device() {
831 with_device!(device, _queue, {
832 let pool = super::super::layout::GlobalLayoutPool::new();
833 let desc = MaterialBuilder::new(MINIMAL_SHADER)
834 .entries(vec![super::super::layout::GroupEntry::Own(vec![BindingEntry {
835 name: "bad",
836 binding: 0,
837 kind: BindingKind::storage_buffer_read_write(ShaderStages::COMPUTE),
838 }])])
839 .targets(DEFAULT_TARGET.to_vec())
840 .build();
841 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
842 build_material_raw(&device, &desc, &pool);
843 }));
844 assert!(result.is_err(), "expected a panic for a COMPUTE-visible material entry");
845 });
846 }
847
848 #[test]
849 fn no_entries_at_all_builds_without_panicking() {
850 with_device!(device, _queue, {
851 let pool = super::super::layout::GlobalLayoutPool::new();
852 let desc = MaterialBuilder::new(MINIMAL_SHADER).targets(DEFAULT_TARGET.to_vec()).build();
853 build_material_raw(&device, &desc, &pool).unwrap();
854 });
855 }
856
857 #[test]
858 fn a_layout_pulled_from_the_global_pool_ends_up_in_the_pipeline_layout() {
859 with_device!(device, _queue, {
860 let mut pool = super::super::layout::GlobalLayoutPool::new();
861 pool.register("camera", crate::wgpu::binding::BindGroupLayoutBuilder::new().build_raw(&device));
862
863 let desc = MaterialBuilder::new(MINIMAL_SHADER)
864 .entries(vec![super::super::layout::GroupEntry::Layout(pool.get("camera").unwrap())])
865 .targets(DEFAULT_TARGET.to_vec())
866 .build();
867
868 build_material_raw(&device, &desc, &pool).unwrap();
869 });
870 }
871
872 #[test]
873 fn a_global_entry_resolves_from_the_pool_at_build_time() {
874 with_device!(device, _queue, {
875 let mut pool = super::super::layout::GlobalLayoutPool::new();
876 pool.register("camera", crate::wgpu::binding::BindGroupLayoutBuilder::new().build_raw(&device));
877
878 let desc = MaterialBuilder::new(MINIMAL_SHADER)
879 .entries(vec![super::super::layout::GroupEntry::Global("camera")])
880 .targets(DEFAULT_TARGET.to_vec())
881 .build();
882
883 build_material_raw(&device, &desc, &pool).unwrap();
884 });
885 }
886
887 #[test]
888 fn a_global_entry_not_yet_registered_returns_none_instead_of_panicking() {
889 with_device!(device, _queue, {
890 let pool = super::super::layout::GlobalLayoutPool::new(); let desc = MaterialBuilder::new(MINIMAL_SHADER)
892 .entries(vec![super::super::layout::GroupEntry::Global("camera")])
893 .targets(DEFAULT_TARGET.to_vec())
894 .build();
895
896 assert!(build_material_raw(&device, &desc, &pool).is_none());
897 });
898 }
899
900 #[test]
901 fn own_and_layout_groups_are_ordered_by_position() {
902 with_device!(device, _queue, {
903 let pool = super::super::layout::GlobalLayoutPool::new();
904 let extra = crate::wgpu::binding::BindGroupLayoutBuilder::new().build_raw(&device);
905 let desc = MaterialBuilder::new(MINIMAL_SHADER)
906 .entries(vec![
907 super::super::layout::GroupEntry::Own(vec![]),
908 super::super::layout::GroupEntry::Layout(extra),
909 ])
910 .targets(DEFAULT_TARGET.to_vec())
911 .build();
912
913 build_material_raw(&device, &desc, &pool).unwrap();
914 });
915 }
916
917 #[test]
918 fn more_than_one_own_group_panics() {
919 with_device!(device, _queue, {
920 let pool = super::super::layout::GlobalLayoutPool::new();
921 let desc = MaterialBuilder::new(MINIMAL_SHADER)
922 .entries(vec![
923 super::super::layout::GroupEntry::Own(vec![]),
924 super::super::layout::GroupEntry::Own(vec![]),
925 ])
926 .targets(DEFAULT_TARGET.to_vec())
927 .build();
928
929 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
930 build_material_raw(&device, &desc, &pool);
931 }));
932 assert!(result.is_err(), "expected a panic for more than one Own group");
933 });
934 }
935
936 #[test]
937 fn exceeding_max_bind_groups_panics() {
938 with_device!(device, _queue, {
939 let pool = super::super::layout::GlobalLayoutPool::new();
940 let groups: Vec<super::super::layout::GroupEntry> = (0..5)
942 .map(|_| {
943 super::super::layout::GroupEntry::Layout(
944 crate::wgpu::binding::BindGroupLayoutBuilder::new().build_raw(&device),
945 )
946 })
947 .collect();
948 let desc =
949 MaterialBuilder::new(MINIMAL_SHADER).entries(groups).targets(DEFAULT_TARGET.to_vec()).build();
950
951 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
952 build_material_raw(&device, &desc, &pool);
953 }));
954 assert!(result.is_err(), "expected a panic for exceeding max_bind_groups");
955 });
956 }
957
958 #[test]
959 fn exceeding_max_vertex_buffers_panics() {
960 with_device!(device, _queue, {
961 let pool = super::super::layout::GlobalLayoutPool::new();
962 let too_many = device.limits().max_vertex_buffers + 1;
963 let layouts: Vec<VertexBufferLayout> = (0..too_many)
964 .map(|_| VertexBufferLayout { array_stride: 4, step_mode: VertexStepMode::Vertex, attributes: vec![] })
965 .collect();
966 let desc = MaterialBuilder::new(MINIMAL_SHADER)
967 .vertex_layouts(layouts)
968 .targets(DEFAULT_TARGET.to_vec())
969 .build();
970
971 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
972 build_material_raw(&device, &desc, &pool);
973 }));
974 assert!(result.is_err(), "expected a panic for exceeding max_vertex_buffers");
975 });
976 }
977
978 #[test]
979 fn exceeding_max_vertex_attributes_panics() {
980 with_device!(device, _queue, {
981 let pool = super::super::layout::GlobalLayoutPool::new();
982 let too_many = device.limits().max_vertex_attributes + 1;
983 let attributes: Vec<VertexAttribute> = (0..too_many)
984 .map(|i| VertexAttribute { format: VertexFormat::Float32, offset: 0, shader_location: i })
985 .collect();
986 let desc = MaterialBuilder::new(MINIMAL_SHADER)
987 .vertex_layouts(vec![VertexBufferLayout {
988 array_stride: 4,
989 step_mode: VertexStepMode::Vertex,
990 attributes,
991 }])
992 .targets(DEFAULT_TARGET.to_vec())
993 .build();
994
995 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
996 build_material_raw(&device, &desc, &pool);
997 }));
998 assert!(result.is_err(), "expected a panic for exceeding max_vertex_attributes");
999 });
1000 }
1001
1002 #[test]
1003 fn exceeding_max_color_attachments_panics() {
1004 with_device!(device, _queue, {
1005 let pool = super::super::layout::GlobalLayoutPool::new();
1006 let too_many = device.limits().max_color_attachments + 1;
1007 let targets: Vec<ColorTargetState> = (0..too_many).map(|_| DEFAULT_TARGET[0].clone()).collect();
1008 let desc = MaterialBuilder::new(MINIMAL_SHADER).targets(targets).build();
1009
1010 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1011 build_material_raw(&device, &desc, &pool);
1012 }));
1013 assert!(result.is_err(), "expected a panic for exceeding max_color_attachments");
1014 });
1015 }
1016}