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>>();
|
|
```
|