
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>
154 lines
4.9 KiB
Markdown
154 lines
4.9 KiB
Markdown
---
|
|
title: Composable Specialization
|
|
pull_requests: [17373]
|
|
---
|
|
|
|
The existing pipeline specialization APIs (`SpecializedRenderPipeline` etc.) have
|
|
been replaced with a single `Specializer` trait and `SpecializedCache` collection:
|
|
|
|
```rs
|
|
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.
|
|
|
|
```rs
|
|
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>>();
|
|
```
|