Files
yawn/renderer/src/renderer/pipeline_library.rs
T

545 lines
20 KiB
Rust

use std::{collections::HashMap, num::NonZeroU32};
use crate::render_data::PipelineKey;
use super::DEPTH_FORMAT;
/// Identity of a set of bind-group layouts registered with this library.
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub struct PipelineLayoutKey(u64);
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct OwnedVertexBufferLayout {
pub array_stride: u64,
pub step_mode: wgpu::VertexStepMode,
pub attributes: Vec<wgpu::VertexAttribute>,
}
#[derive(Clone, Debug)]
pub struct OwnedProgrammableStage {
pub shader_source: String,
pub entry_point: String,
pub constants: Vec<(String, f64)>,
pub zero_initialize_workgroup_memory: bool,
}
#[derive(Clone, Debug)]
pub struct RenderPipelineSpec {
pub layout: Option<PipelineLayoutKey>,
pub vertex: OwnedProgrammableStage,
pub vertex_layouts: Vec<OwnedVertexBufferLayout>,
pub fragment: Option<OwnedProgrammableStage>,
pub primitive: wgpu::PrimitiveState,
pub depth_stencil: Option<wgpu::DepthStencilState>,
pub multisample: wgpu::MultisampleState,
pub targets: Vec<Option<wgpu::ColorTargetState>>,
pub multiview: Option<NonZeroU32>,
}
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
struct StageKey {
shader_source: String,
entry_point: String,
constants: Vec<(String, u64)>,
zero_initialize_workgroup_memory: bool,
}
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
struct RenderPipelineKey {
layout: Option<PipelineLayoutKey>,
vertex: StageKey,
vertex_layouts: Vec<OwnedVertexBufferLayout>,
fragment: Option<StageKey>,
primitive: wgpu::PrimitiveState,
depth_stencil: Option<wgpu::DepthStencilState>,
multisample: wgpu::MultisampleState,
targets: Vec<Option<wgpu::ColorTargetState>>,
multiview: Option<NonZeroU32>,
}
impl StageKey {
fn from_stage(stage: &OwnedProgrammableStage) -> Self {
let mut constants: Vec<_> = stage
.constants
.iter()
.map(|(name, value)| (name.clone(), value.to_bits()))
.collect();
constants.sort_by(|a, b| a.0.cmp(&b.0));
Self {
shader_source: stage.shader_source.clone(),
entry_point: stage.entry_point.clone(),
constants,
zero_initialize_workgroup_memory: stage.zero_initialize_workgroup_memory,
}
}
}
impl RenderPipelineSpec {
fn key(&self) -> RenderPipelineKey {
RenderPipelineKey {
layout: self.layout,
vertex: StageKey::from_stage(&self.vertex),
vertex_layouts: self.vertex_layouts.clone(),
fragment: self.fragment.as_ref().map(StageKey::from_stage),
primitive: self.primitive,
depth_stencil: self.depth_stencil.clone(),
multisample: self.multisample,
targets: self.targets.clone(),
multiview: self.multiview,
}
}
}
fn target_variant_spec(
mut spec: RenderPipelineSpec,
color_format: wgpu::TextureFormat,
depth_format: Option<wgpu::TextureFormat>,
depth_compare: wgpu::CompareFunction,
depth_write: bool,
) -> RenderPipelineSpec {
spec.targets = vec![Some(wgpu::ColorTargetState {
format: color_format,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})];
spec.depth_stencil = depth_format.map(|format| wgpu::DepthStencilState {
format,
depth_write_enabled: depth_write,
depth_compare,
stencil: Default::default(),
bias: Default::default(),
});
spec
}
pub struct PipelineLibrary {
pipelines: Vec<wgpu::RenderPipeline>,
specs: Vec<RenderPipelineSpec>,
layout_bindings: HashMap<PipelineLayoutKey, Vec<wgpu::BindGroupLayout>>,
pipeline_layouts: HashMap<PipelineLayoutKey, wgpu::PipelineLayout>,
default_layout: Option<PipelineLayoutKey>,
material_layout: Option<PipelineLayoutKey>,
next_layout: u64,
named_bases: HashMap<String, (PipelineKey, RenderPipelineKey)>,
descriptor_cache: HashMap<RenderPipelineKey, PipelineKey>,
}
impl PipelineLibrary {
pub fn new() -> Self {
Self {
pipelines: Vec::new(),
specs: Vec::new(),
layout_bindings: HashMap::new(),
pipeline_layouts: HashMap::new(),
default_layout: None,
material_layout: None,
next_layout: 0,
named_bases: HashMap::new(),
descriptor_cache: HashMap::new(),
}
}
/// Registers a new default layout identity. Existing pipelines retain their layout.
pub fn set_bind_group_layouts(
&mut self,
layouts: &[wgpu::BindGroupLayout; 2],
) -> PipelineLayoutKey {
let key = PipelineLayoutKey(self.next_layout);
self.next_layout = self
.next_layout
.checked_add(1)
.expect("pipeline layout key overflow");
self.layout_bindings.insert(key, layouts.to_vec());
self.default_layout = Some(key);
key
}
/// Registers the glTF-only layout, preserving scene groups at 0 and 1.
pub fn set_material_bind_group_layout(&mut self, layout: &wgpu::BindGroupLayout) {
let base = self
.default_layout
.expect("scene layouts must be registered first");
let mut layouts = self.layout_bindings[&base].clone();
layouts.push(layout.clone());
let key = PipelineLayoutKey(self.next_layout);
self.next_layout += 1;
self.layout_bindings.insert(key, layouts);
self.material_layout = Some(key);
}
fn compatibility_spec(
&self,
name: &str,
layouts: &[wgpu::VertexBufferLayout],
shader: &str,
format: wgpu::TextureFormat,
) -> RenderPipelineSpec {
let (vertex_entry, fragment_entry) = if name == "triangle_colored" {
("v_main", "f_main")
} else {
("vs_main", "fs_main")
};
let stage = |entry: &str| OwnedProgrammableStage {
shader_source: shader.to_owned(),
entry_point: entry.to_owned(),
constants: Vec::new(),
zero_initialize_workgroup_memory: true,
};
RenderPipelineSpec {
layout: if name.starts_with("gltf_") {
self.material_layout.or(self.default_layout)
} else {
self.default_layout
},
vertex: stage(vertex_entry),
vertex_layouts: layouts
.iter()
.map(|layout| OwnedVertexBufferLayout {
array_stride: layout.array_stride,
step_mode: layout.step_mode,
attributes: layout.attributes.to_vec(),
})
.collect(),
fragment: Some(stage(fragment_entry)),
primitive: wgpu::PrimitiveState {
cull_mode: (name != "gltf_standard_double_sided").then_some(wgpu::Face::Back),
..Default::default()
},
depth_stencil: Some(wgpu::DepthStencilState {
format: DEPTH_FORMAT,
depth_write_enabled: true,
depth_compare: wgpu::CompareFunction::LessEqual,
stencil: Default::default(),
bias: Default::default(),
}),
multisample: Default::default(),
targets: vec![Some(wgpu::ColorTargetState {
format,
blend: Some(wgpu::BlendState::REPLACE),
write_mask: wgpu::ColorWrites::ALL,
})],
multiview: None,
}
}
/// Creates or reuses a pipeline solely by its owned descriptor identity.
pub fn get_or_create_from_spec(
&mut self,
device: &wgpu::Device,
spec: &RenderPipelineSpec,
label: Option<&str>,
) -> PipelineKey {
let key = spec.key();
if let Some(pipeline) = self.descriptor_cache.get(&key) {
return *pipeline;
}
let vertex_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label,
source: wgpu::ShaderSource::Wgsl(spec.vertex.shader_source.as_str().into()),
});
let fragment_shader = spec.fragment.as_ref().map(|stage| {
device.create_shader_module(wgpu::ShaderModuleDescriptor {
label,
source: wgpu::ShaderSource::Wgsl(stage.shader_source.as_str().into()),
})
});
let vertex_constants: Vec<_> = spec
.vertex
.constants
.iter()
.map(|(name, value)| (name.as_str(), *value))
.collect();
let fragment_constants: Option<Vec<_>> = spec.fragment.as_ref().map(|stage| {
stage
.constants
.iter()
.map(|(name, value)| (name.as_str(), *value))
.collect()
});
let vertex_layouts: Vec<_> = spec
.vertex_layouts
.iter()
.map(|layout| wgpu::VertexBufferLayout {
array_stride: layout.array_stride,
step_mode: layout.step_mode,
attributes: &layout.attributes,
})
.collect();
let layout = spec.layout.map(|layout_key| {
if !self.pipeline_layouts.contains_key(&layout_key) {
let bindings = self
.layout_bindings
.get(&layout_key)
.unwrap_or_else(|| panic!("unregistered pipeline layout key {layout_key:?}"));
let refs: Vec<_> = bindings.iter().collect();
self.pipeline_layouts.insert(
layout_key,
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label,
bind_group_layouts: &refs,
push_constant_ranges: &[],
}),
);
}
self.pipeline_layouts.get(&layout_key).unwrap()
});
let fragment = spec.fragment.as_ref().map(|stage| wgpu::FragmentState {
module: fragment_shader.as_ref().unwrap(),
entry_point: Some(&stage.entry_point),
compilation_options: wgpu::PipelineCompilationOptions {
constants: fragment_constants.as_ref().unwrap(),
zero_initialize_workgroup_memory: stage.zero_initialize_workgroup_memory,
},
targets: &spec.targets,
});
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label,
layout,
vertex: wgpu::VertexState {
module: &vertex_shader,
entry_point: Some(&spec.vertex.entry_point),
compilation_options: wgpu::PipelineCompilationOptions {
constants: &vertex_constants,
zero_initialize_workgroup_memory: spec.vertex.zero_initialize_workgroup_memory,
},
buffers: &vertex_layouts,
},
primitive: spec.primitive,
depth_stencil: spec.depth_stencil.clone(),
multisample: spec.multisample,
fragment,
multiview: spec.multiview,
cache: None,
});
let pipeline_key = PipelineKey::new(self.pipelines.len() as u32);
self.pipelines.push(pipeline);
self.specs.push(spec.clone());
self.descriptor_cache.insert(key, pipeline_key);
pipeline_key
}
pub fn create_pipeline(
&mut self,
device: &wgpu::Device,
name: &str,
layouts: &[wgpu::VertexBufferLayout],
shader: &str,
format: wgpu::TextureFormat,
) -> Result<PipelineKey, String> {
let spec = self.compatibility_spec(name, layouts, shader, format);
let descriptor = spec.key();
if let Some((_, existing)) = self.named_bases.get(name) {
return Err(if existing == &descriptor {
format!("Pipeline '{name}' already exists")
} else {
format!("Pipeline '{name}' already exists with a different descriptor")
});
}
let key = self.get_or_create_from_spec(device, &spec, Some(name));
self.named_bases.insert(name.to_owned(), (key, descriptor));
Ok(key)
}
pub fn find_pipeline(&self, name: &str) -> Option<PipelineKey> {
self.named_bases.get(name).map(|v| v.0)
}
pub fn get_or_create_pipeline(
&mut self,
device: &wgpu::Device,
name: &str,
layouts: &[wgpu::VertexBufferLayout],
shader: &str,
format: wgpu::TextureFormat,
) -> PipelineKey {
let wanted = self.compatibility_spec(name, layouts, shader, format).key();
if let Some((key, existing)) = self.named_bases.get(name) {
assert_eq!(
existing, &wanted,
"Pipeline '{name}' requested with a different descriptor"
);
return *key;
}
self.create_pipeline(device, name, layouts, shader, format)
.unwrap_or_else(|e| panic!("Failed to create pipeline '{name}': {e}"))
}
pub fn get_pipeline(&self, key: PipelineKey) -> &wgpu::RenderPipeline {
&self.pipelines[key.get() as usize]
}
pub fn requires_material(&self, key: PipelineKey) -> bool {
self.material_layout.is_some()
&& self.specs[key.get() as usize].layout == self.material_layout
}
pub fn create_target_variant(
&self,
device: &wgpu::Device,
base: PipelineKey,
color_format: wgpu::TextureFormat,
depth_format: Option<wgpu::TextureFormat>,
depth_compare: wgpu::CompareFunction,
depth_write: bool,
) -> Result<wgpu::RenderPipeline, String> {
let spec = self
.specs
.get(base.get() as usize)
.cloned()
.ok_or_else(|| "unknown base pipeline".to_owned())?;
let spec =
target_variant_spec(spec, color_format, depth_format, depth_compare, depth_write);
let vertex_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some(" target variant"),
source: wgpu::ShaderSource::Wgsl(spec.vertex.shader_source.as_str().into()),
});
let fragment_shader = spec.fragment.as_ref().map(|stage| {
device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some(" target variant"),
source: wgpu::ShaderSource::Wgsl(stage.shader_source.as_str().into()),
})
});
let vertex_constants: Vec<_> = spec
.vertex
.constants
.iter()
.map(|(name, value)| (name.as_str(), *value))
.collect();
let fragment_constants: Option<Vec<_>> = spec.fragment.as_ref().map(|stage| {
stage
.constants
.iter()
.map(|(name, value)| (name.as_str(), *value))
.collect()
});
let vertex_layouts: Vec<_> = spec
.vertex_layouts
.iter()
.map(|layout| wgpu::VertexBufferLayout {
array_stride: layout.array_stride,
step_mode: layout.step_mode,
attributes: &layout.attributes,
})
.collect();
let layout = spec
.layout
.map(|key| {
self.pipeline_layouts
.get(&key)
.ok_or("pipeline layout missing")
})
.transpose()?;
let fragment = spec.fragment.as_ref().map(|stage| wgpu::FragmentState {
module: fragment_shader.as_ref().unwrap(),
entry_point: Some(&stage.entry_point),
compilation_options: wgpu::PipelineCompilationOptions {
constants: fragment_constants.as_ref().unwrap(),
zero_initialize_workgroup_memory: stage.zero_initialize_workgroup_memory,
},
targets: &spec.targets,
});
Ok(
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some(" target variant"),
layout,
vertex: wgpu::VertexState {
module: &vertex_shader,
entry_point: Some(&spec.vertex.entry_point),
compilation_options: wgpu::PipelineCompilationOptions {
constants: &vertex_constants,
zero_initialize_workgroup_memory: spec
.vertex
.zero_initialize_workgroup_memory,
},
buffers: &vertex_layouts,
},
primitive: spec.primitive,
depth_stencil: spec.depth_stencil,
multisample: spec.multisample,
fragment,
multiview: spec.multiview,
cache: None,
}),
)
}
}
impl Default for PipelineLibrary {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn spec() -> RenderPipelineSpec {
PipelineLibrary::new().compatibility_spec(
"x",
&[],
"shader",
wgpu::TextureFormat::Rgba8Unorm,
)
}
#[test]
fn target_spec_disables_blending_without_mutating_base() {
let mut base = spec();
base.primitive.cull_mode = Some(wgpu::Face::Front);
base.multisample.count = 4;
base.targets[0].as_mut().unwrap().write_mask = wgpu::ColorWrites::RED;
let preserved_vertex = StageKey::from_stage(&base.vertex);
let preserved_fragment = base.fragment.as_ref().map(StageKey::from_stage);
assert!(base.targets[0].as_ref().unwrap().blend.is_some());
let variant = target_variant_spec(
base.clone(),
wgpu::TextureFormat::Rgba16Float,
None,
wgpu::CompareFunction::Always,
false,
);
assert_eq!(variant.targets[0].as_ref().unwrap().blend, None);
assert_eq!(
variant.targets[0].as_ref().unwrap().format,
wgpu::TextureFormat::Rgba16Float
);
assert!(base.targets[0].as_ref().unwrap().blend.is_some());
assert_eq!(variant.primitive, base.primitive);
assert_eq!(variant.multisample, base.multisample);
assert_eq!(StageKey::from_stage(&variant.vertex), preserved_vertex);
assert_eq!(
variant.fragment.as_ref().map(StageKey::from_stage),
preserved_fragment
);
assert_eq!(
variant.targets[0].as_ref().unwrap().write_mask,
wgpu::ColorWrites::ALL
);
}
#[test]
fn descriptor_identity_covers_optional_and_exact_state() {
let base = spec().key();
let mut changed = spec();
changed.layout = Some(PipelineLayoutKey(1));
assert_ne!(base, changed.key());
let first_layout = changed.key();
changed.layout = Some(PipelineLayoutKey(2));
assert_ne!(first_layout, changed.key());
let mut changed = spec();
changed.fragment = None;
assert_ne!(base, changed.key());
let mut changed = spec();
changed.multiview = NonZeroU32::new(2);
assert_ne!(base, changed.key());
let mut changed = spec();
changed.vertex.constants.push(("x".into(), -0.0));
assert_ne!(base, changed.key());
let mut changed2 = changed.clone();
changed2.vertex.constants[0].1 = 0.0;
assert_ne!(changed.key(), changed2.key());
let mut changed = spec();
changed.vertex.zero_initialize_workgroup_memory = false;
assert_ne!(base, changed.key());
}
}