use crate::{
assets::upload::Asset,
wgpu::{backend::WGPUBackend, binding::BindingEntry},
};
pub struct MaterialDescriptor<'a> {
pub label: Option<&'a str>,
pub shader_source: &'a str,
pub vertex_entry: Option<&'a str>,
pub fragment_entry: Option<&'a str>,
pub vertex_layouts: Vec<wgpu::VertexBufferLayout<'static>>,
pub entries: Vec<BindingEntry>,
pub cull_mode: Option<wgpu::Face>,
pub depth: Option<wgpu::DepthStencilState>,
pub targets: Vec<wgpu::ColorTargetState>,
pub polygon_mode: wgpu::PolygonMode,
pub own_group: Option<u32>,
pub extra_layouts: Vec<super::layout::OwnedGroupLayout>,
}
pub const DEFAULT_TARGET: [wgpu::ColorTargetState; 1] = [wgpu::ColorTargetState {
format: wgpu::TextureFormat::Rgba8Unorm,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
}];
impl<'a> Default for MaterialDescriptor<'a> {
fn default() -> Self {
Self {
label: None,
shader_source: "",
vertex_entry: Some("vs_main"),
fragment_entry: Some("fs_main"),
vertex_layouts: Vec::new(),
entries: Vec::new(),
cull_mode: Some(wgpu::Face::Back),
depth: None,
targets: Vec::new(),
own_group: Some(0),
extra_layouts: Vec::new(),
polygon_mode: wgpu::PolygonMode::Fill,
}
}
}
pub fn build_material(
device: &wgpu::Device,
desc: &MaterialDescriptor,
) -> (wgpu::RenderPipeline, wgpu::BindGroupLayout) {
for entry in &desc.entries {
if entry.kind.visibility().intersects(wgpu::ShaderStages::COMPUTE) {
panic!(
"material{}: entry '{}' is visible to the compute stage ({:?}) — material bind \
group entries must not be COMPUTE-visible",
desc.label.map(|l| format!(" '{l}'")).unwrap_or_default(),
entry.name,
entry.kind.visibility()
);
}
}
let layout = super::binding::build_bind_group_layout(device, desc.label, &desc.entries);
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: desc.label,
source: wgpu::ShaderSource::Wgsl(desc.shader_source.into()),
});
let mut slots: Vec<super::layout::GroupLayout> = desc
.extra_layouts
.iter()
.map(|g| super::layout::GroupLayout { group: g.group, layout: &g.layout })
.collect();
if let Some(own_group) = desc.own_group {
slots.push(super::layout::GroupLayout { group: own_group, layout: &layout });
}
let bind_group_layouts = super::layout::assemble_bind_group_layouts(desc.label, slots);
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: desc.label,
bind_group_layouts: &bind_group_layouts,
immediate_size: 0,
});
let targets: Vec<Option<wgpu::ColorTargetState>> =
desc.targets.iter().cloned().map(Some).collect();
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: desc.label,
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &module,
entry_point: desc.vertex_entry,
compilation_options: Default::default(),
buffers: &desc.vertex_layouts,
},
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: desc.cull_mode,
unclipped_depth: false,
polygon_mode: desc.polygon_mode,
conservative: false,
},
depth_stencil: desc.depth.clone(),
multisample: wgpu::MultisampleState::default(),
fragment: Some(wgpu::FragmentState {
module: &module,
entry_point: desc.fragment_entry,
compilation_options: Default::default(),
targets: &targets,
}),
multiview_mask: None,
cache: None,
});
(pipeline, layout)
}
pub struct GPUMaterial {
pub pipeline: wgpu::RenderPipeline,
pub layout: wgpu::BindGroupLayout,
pub entries: Vec<BindingEntry>,
}
impl super::binding::BindGroupTarget for GPUMaterial {
fn bind_group_layout(&self) -> &wgpu::BindGroupLayout {
&self.layout
}
fn binding_entries(&self) -> &[BindingEntry] {
&self.entries
}
}
impl Asset<WGPUBackend> for GPUMaterial {
type Source = MaterialDescriptor<'static>;
type Deps<'a> = ();
fn upload<'a>(source: &MaterialDescriptor, backend: &WGPUBackend, _deps: &()) -> Option<Self> {
let (pipeline, layout) = build_material(&backend.device, source);
Some(Self {
pipeline,
layout,
entries: source.entries.to_vec(),
})
}
}
crate::wgpu::plugin_macros::asset_plugin! {
MaterialPlugin, GPUMaterial
}
#[cfg(test)]
mod tests {
use super::*;
use crate::wgpu::binding::{BindingEntry, BindingKind};
use crate::wgpu::test_util::with_device;
const MINIMAL_SHADER: &str = r#"
@vertex
fn vs_main() -> @builtin(position) vec4<f32> {
return vec4<f32>(0.0, 0.0, 0.0, 1.0);
}
@fragment
fn fs_main() -> @location(0) vec4<f32> {
return vec4<f32>(1.0, 1.0, 1.0, 1.0);
}
"#;
#[test]
fn a_compute_visible_entry_panics_before_touching_the_device() {
with_device!(device, _queue, {
let desc = MaterialDescriptor {
shader_source: MINIMAL_SHADER,
entries: vec![BindingEntry {
name: "bad",
binding: 0,
kind: BindingKind::storage_buffer_read_write(wgpu::ShaderStages::COMPUTE),
}],
targets: DEFAULT_TARGET.to_vec(),
..Default::default()
};
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
build_material(&device, &desc);
}));
assert!(result.is_err(), "expected a panic for a COMPUTE-visible material entry");
});
}
#[test]
fn a_fragment_visible_entry_builds_without_panicking() {
with_device!(device, _queue, {
let desc = MaterialDescriptor {
shader_source: MINIMAL_SHADER,
entries: vec![],
own_group: None,
targets: DEFAULT_TARGET.to_vec(),
..Default::default()
};
build_material(&device, &desc);
});
}
}