
Currently, our specialization API works through a series of wrapper structs and traits, which make things confusing to follow and difficult to generalize. This pr takes a different approach, where "specializers" (types that implement `Specialize`) are composable, but "flat" rather than composed of a series of wrappers. The key is that specializers don't *produce* pipeline descriptors, but instead *modify* existing ones: ```rs pub trait Specialize<T: Specializable> { type Key: SpecializeKey; fn specialize( &self, key: Self::Key, descriptor: &mut T::Descriptor ) -> Result<Canonical<Self::Key>, BevyError>; } ``` This lets us use some derive magic to stick multiple specializers together: ```rs pub struct A; pub struct B; impl Specialize<RenderPipeline> for A { ... } impl Specialize<RenderPipeline> for A { ... } #[derive(Specialize)] #[specialize(RenderPipeline)] struct C { // specialization is applied in struct field order applied_first: A, applied_second: B, } type C::Key = (A::Key, B::Key); ``` This approach is much easier to understand, IMO, and also lets us separate concerns better. Specializers can be placed in fully separate crates/modules, and key computation can be shared as well. The only real breaking change here is that since specializers only modify descriptors, we need a "base" descriptor to work off of. This can either be manually supplied when constructing a `Specializer` (the new collection replacing `Specialized[Render/Compute]Pipelines`), or supplied by implementing `HasBaseDescriptor` on a specializer. See `examples/shader/custom_phase_item.rs` for an example implementation. ## Testing - Did some simple manual testing of the derive macro, it seems robust. --- ## Showcase ```rs #[derive(Specialize, HasBaseDescriptor)] #[specialize(RenderPipeline)] pub struct SpecializeMeshMaterial<M: Material> { // set mesh bind group layout and shader defs mesh: SpecializeMesh, // set view bind group layout and shader defs view: SpecializeView, // since type SpecializeMaterial::Key = (), // we can hide it from the wrapper's external API #[key(default)] // defer to the GetBaseDescriptor impl of SpecializeMaterial, // since it carries the vertex and fragment handles #[base_descriptor] // set material bind group layout, etc material: SpecializeMaterial<M>, } // implementation generated by the derive macro impl <M: Material> Specialize<RenderPipeline> for SpecializeMeshMaterial<M> { type Key = (MeshKey, ViewKey); fn specialize( &self, key: Self::Key, descriptor: &mut RenderPipelineDescriptor ) -> Result<Canonical<Self::Key>, BevyError> { let mesh_key = self.mesh.specialize(key.0, descriptor)?; let view_key = self.view.specialize(key.1, descriptor)?; let _ = self.material.specialize((), descriptor)?; Ok((mesh_key, view_key)); } } impl <M: Material> HasBaseDescriptor<RenderPipeline> for SpecializeMeshMaterial<M> { fn base_descriptor(&self) -> RenderPipelineDescriptor { self.material.base_descriptor() } } ``` --------- Co-authored-by: Tim Overbeek <158390905+Bleachfuel@users.noreply.github.com>
4.9 KiB
title | pull_requests | |
---|---|---|
Composable Specialization |
|
The existing pipeline specialization APIs (SpecializedRenderPipeline
etc.) have
been replaced with a single Specializer
trait and SpecializedCache
collection:
pub trait Specializer<T: Specializable>: Send + Sync + 'static {
type Key: SpecializerKey;
fn specialize(
&self,
key: Self::Key,
descriptor: &mut T::Descriptor,
) -> Result<Canonical<Self::Key>, BevyError>;
}
pub struct SpecializedCache<T: Specializable, S: Specializer<T>>{ ... };
The main difference is the change from producing a pipeline descriptor to
mutating one based on a key. The "base descriptor" that the SpecializedCache
passes to the Specializer
can either be specified manually with Specializer::new
or by implementing GetBaseDescriptor
. There's also a new trait for specialization
keys, SpecializeKey
, that can be derived with the included macro in most cases.
Composing multiple different specializers together with the derive(Specializer)
macro can be a lot more powerful (see the Specialize
docs), but migrating
individual specializers is fairly simple. All static parts of the pipeline
should be specified in the base descriptor, while the Specializer
impl
should mutate the key as little as necessary to match the key.
pub struct MySpecializer {
layout: BindGroupLayout,
layout_msaa: BindGroupLayout,
vertex: Handle<Shader>,
fragment: Handle<Shader>,
}
// before
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
// after
#[derive(Clone, Copy, PartialEq, Eq, Hash, SpecializerKey)]
pub struct MyKey {
blend_state: BlendState,
msaa: Msaa,
}
impl FromWorld for MySpecializer {
fn from_world(&mut World) -> Self {
...
}
}
// before
impl SpecializedRenderPipeline for MySpecializer {
type Key = MyKey;
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
RenderPipelineDescriptor {
label: Some("my_pipeline".into()),
layout: vec![
if key.msaa.samples() > 0 {
self.layout_msaa.clone()
} else {
self.layout.clone()
}
],
push_constant_ranges: vec![],
vertex: VertexState {
shader: self.vertex.clone(),
shader_defs: vec![],
entry_point: "vertex".into(),
buffers: vec![],
},
primitive: Default::default(),
depth_stencil: None,
multisample: MultisampleState {
count: key.msaa.samples(),
..Default::default()
},
fragment: Some(FragmentState {
shader: self.fragment.clone(),
shader_defs: vec![],
entry_point: "fragment".into(),
targets: vec![Some(ColorTargetState {
format: TextureFormat::Rgba8Unorm,
blend: Some(key.blend_state),
write_mask: ColorWrites::all(),
})],
}),
zero_initialize_workgroup_memory: false,
},
}
}
app.init_resource::<SpecializedRenderPipelines<MySpecializer>>();
// after
impl Specializer<RenderPipeline> for MySpecializer {
type Key = MyKey;
fn specialize(
&self,
key: Self::Key,
descriptor: &mut RenderPipeline,
) -> Result<Canonical<Self::Key>, BevyError> {
descriptor.multisample.count = key.msaa.samples();
descriptor.layout[0] = if key.msaa.samples() > 0 {
self.layout_msaa.clone()
} else {
self.layout.clone()
};
descriptor.fragment.targets[0].as_mut().unwrap().blend_mode = key.blend_state;
Ok(key)
}
}
impl GetBaseDescriptor for MySpecializer {
fn get_base_descriptor(&self) -> RenderPipelineDescriptor {
RenderPipelineDescriptor {
label: Some("my_pipeline".into()),
layout: vec![self.layout.clone()],
push_constant_ranges: vec![],
vertex: VertexState {
shader: self.vertex.clone(),
shader_defs: vec![],
entry_point: "vertex".into(),
buffers: vec![],
},
primitive: Default::default(),
depth_stencil: None,
multisample: MultiSampleState::default(),
fragment: Some(FragmentState {
shader: self.fragment.clone(),
shader_defs: vec![],
entry_point: "fragment".into(),
targets: vec![Some(ColorTargetState {
format: TextureFormat::Rgba8Unorm,
blend: None,
write_mask: ColorWrites::all(),
})],
}),
zero_initialize_workgroup_memory: false,
},
}
}
app.init_resource::<SpecializedCache<RenderPipeline, MySpecializer>>();