Skip to main content

loam_render/
material.rs

1use wgpu::{
2    BindGroupLayout, BindGroupLayoutDescriptor, BindGroupLayoutEntry, BlendState, ColorTargetState,
3    ColorWrites, CompareFunction, DepthStencilState, FragmentState, MultisampleState,
4    PipelineLayoutDescriptor, PrimitiveState, PrimitiveTopology, RenderPipeline,
5    RenderPipelineDescriptor, ShaderModuleDescriptor, ShaderSource, StencilState, TextureFormat,
6    VertexBufferLayout, VertexState,
7};
8
9use crate::device::{FeatureRequest, GpuContext, MissingGpuCapability};
10use crate::{DepthConvention, DepthMode};
11
12pub struct MaterialSpec<'a> {
13    pub label: &'a str,
14    pub shader: &'a str,
15    pub vertex_entry: &'a str,
16    pub fragment_entry: &'a str,
17    pub vertex_layouts: &'a [VertexBufferLayout<'a>],
18    /// One bind group layout per entry, in group order.
19    pub resources: &'a [&'a [BindGroupLayoutEntry]],
20    pub features: FeatureRequest,
21    pub topology: PrimitiveTopology,
22    pub blend: Option<BlendState>,
23    pub depth: DepthMode,
24    pub convention: DepthConvention,
25    pub standard_z_compare: CompareFunction,
26}
27
28pub struct MaterialPipeline {
29    pub pipeline: RenderPipeline,
30    pub bind_group_layouts: Vec<BindGroupLayout>,
31    convention: DepthConvention,
32}
33
34impl MaterialPipeline {
35    pub fn convention(&self) -> DepthConvention {
36        self.convention
37    }
38
39    pub fn group_layout(&self, group: usize) -> Option<&BindGroupLayout> {
40        self.bind_group_layouts.get(group)
41    }
42}
43
44impl MaterialSpec<'_> {
45    /// Resolves `features` before creating any GPU object, so a refusal leaves the device untouched.
46    pub fn build(
47        &self,
48        gpu: &GpuContext,
49        target: TextureFormat,
50    ) -> Result<MaterialPipeline, MissingGpuCapability> {
51        let device = &gpu.device;
52        self.features.resolve(device.features(), &device.limits())?;
53
54        let module = device.create_shader_module(ShaderModuleDescriptor {
55            label: Some(self.label),
56            source: ShaderSource::Wgsl(self.shader.into()),
57        });
58        let bind_group_layouts: Vec<BindGroupLayout> = self
59            .resources
60            .iter()
61            .map(|entries| {
62                device.create_bind_group_layout(&BindGroupLayoutDescriptor {
63                    label: Some(self.label),
64                    entries,
65                })
66            })
67            .collect();
68        let borrowed: Vec<&BindGroupLayout> = bind_group_layouts.iter().collect();
69        let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
70            label: Some(self.label),
71            bind_group_layouts: &borrowed,
72            push_constant_ranges: &[],
73        });
74        let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
75            label: Some(self.label),
76            layout: Some(&pipeline_layout),
77            vertex: VertexState {
78                module: &module,
79                entry_point: Some(self.vertex_entry),
80                buffers: self.vertex_layouts,
81                compilation_options: Default::default(),
82            },
83            fragment: Some(FragmentState {
84                module: &module,
85                entry_point: Some(self.fragment_entry),
86                targets: &[Some(ColorTargetState {
87                    format: target,
88                    blend: self.blend,
89                    write_mask: ColorWrites::ALL,
90                })],
91                compilation_options: Default::default(),
92            }),
93            primitive: PrimitiveState {
94                topology: self.topology,
95                ..Default::default()
96            },
97            depth_stencil: self.depth.format().map(|format| DepthStencilState {
98                format,
99                depth_write_enabled: self.depth.writes(),
100                depth_compare: self.convention.compare(self.standard_z_compare),
101                stencil: StencilState::default(),
102                bias: wgpu::DepthBiasState::default(),
103            }),
104            multisample: MultisampleState {
105                count: 1,
106                ..Default::default()
107            },
108            multiview: None,
109            cache: None,
110        });
111        Ok(MaterialPipeline {
112            pipeline,
113            bind_group_layouts,
114            convention: self.convention,
115        })
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use wgpu::{BindingType, BufferBindingType, Features, Limits, ShaderStages};
122
123    use super::*;
124    use crate::device::noop_context;
125
126    const UNIFORM_AT_ZERO: [BindGroupLayoutEntry; 1] = [BindGroupLayoutEntry {
127        binding: 0,
128        visibility: ShaderStages::VERTEX_FRAGMENT,
129        ty: BindingType::Buffer {
130            ty: BufferBindingType::Uniform,
131            has_dynamic_offset: false,
132            min_binding_size: None,
133        },
134        count: None,
135    }];
136
137    const PROBE_WGSL: &str = r#"
138@vertex
139fn vs_probe(@builtin(vertex_index) vid: u32) -> @builtin(position) vec4<f32> {
140    return vec4<f32>(f32(vid), 0.0, 0.0, 1.0);
141}
142
143@fragment
144fn fs_probe() -> @location(0) vec4<f32> {
145    return vec4<f32>(1.0);
146}
147"#;
148
149    fn spec(features: FeatureRequest) -> MaterialSpec<'static> {
150        MaterialSpec {
151            label: "material probe",
152            shader: PROBE_WGSL,
153            vertex_entry: "vs_probe",
154            fragment_entry: "fs_probe",
155            vertex_layouts: &[],
156            resources: &[&UNIFORM_AT_ZERO],
157            features,
158            topology: PrimitiveTopology::TriangleList,
159            blend: None,
160            depth: DepthMode::Off,
161            convention: DepthConvention::StandardZ,
162            standard_z_compare: CompareFunction::Less,
163        }
164    }
165
166    #[test]
167    fn a_material_the_device_cannot_serve_is_refused_by_the_feature_it_names() {
168        let gpu = noop_context();
169        let wanted = Features::SHADER_F16 - gpu.device.features();
170        assert!(!wanted.is_empty(), "pick a feature this device lacks");
171        let refused = spec(FeatureRequest {
172            required_features: wanted,
173            optional_features: Features::empty(),
174            required_limits: Limits::downlevel_webgl2_defaults(),
175        })
176        .build(&gpu, TextureFormat::Rgba8Unorm);
177        assert_eq!(refused.err(), Some(MissingGpuCapability::Feature(wanted)));
178    }
179}