edit migration guide with new patterns

This commit is contained in:
Emerson Coskey 2025-07-13 11:11:35 -07:00
parent 0d9f1c81ce
commit 2db1ef9b69
No known key found for this signature in database

View File

@ -6,7 +6,7 @@ pull_requests: [17373]
The existing pipeline specialization APIs (`SpecializedRenderPipeline` etc.) have
been replaced with a single `Specializer` trait and `SpecializedCache` collection:
```rs
```rust
pub trait Specializer<T: Specializable>: Send + Sync + 'static {
type Key: SpecializerKey;
fn specialize(
@ -19,20 +19,55 @@ pub trait Specializer<T: Specializable>: Send + Sync + 'static {
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.
For more info on specialization, see the docs for `bevy_render::render_resources::Specializer`
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.
## Mutation and Base Descriptors
```rs
The main difference between the old and new trait is that instead of
*producing* a pipeline descriptor, `Specializer`s *mutate* existing descriptors
based on a key. As such, `SpecializedCache::new` takes in a "base descriptor"
to act as the template from which the specializer creates pipeline variants.
When migrating, the "static" parts of the pipeline (that don't depend
on the key) should become part of the base descriptor, while the specializer
itself should only change the parts demanded by the key. In the full example
below, instead of creating the entire pipeline descriptor the specializer
only changes the msaa sample count and the bind group layout.
## Composing Specializers
`Specializer`s can also be *composed* with the included derive macro to combine
their effects! This is a great way to encapsulate and reuse specialization logic,
though the rest of this guide will focus on migrating "standalone" specializers.
```rust
pub struct MsaaSpecializer {...}
impl Specialize<RenderPipeline> for MsaaSpecializer {...}
pub struct MeshLayoutSpecializer {...}
impl Specialize<RenderPipeline> for MeshLayoutSpecializer {...}
#[derive(Specializer)]
#[specialize(RenderPipeline)]
pub struct MySpecializer {
msaa: MsaaSpecializer,
mesh_layout: MeshLayoutSpecializer,
}
```
## Misc Changes
The analogue of `SpecializedRenderPipelines`, `SpecializedCache`, is no longer a
Bevy `Resource`. Instead, the cache should be stored in a user-created `Resource`
(shown below) or even in a `Component` depending on the use case.
## Full Migration Example
Before:
```rust
#[derive(Resource)]
pub struct MyPipeline {
layout: BindGroupLayout,
layout_msaa: BindGroupLayout,
vertex: Handle<Shader>,
@ -41,65 +76,134 @@ pub struct MySpecializer {
// before
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
// after
#[derive(Clone, Copy, PartialEq, Eq, Hash, SpecializerKey)]
pub struct MyKey {
blend_state: BlendState,
pub struct MyPipelineKey {
msaa: Msaa,
}
impl FromWorld for MySpecializer {
fn from_world(&mut World) -> Self {
...
impl FromWorld for MyPipeline {
fn from_world(world: &mut World) -> Self {
let render_device = world.resource::<RenderDevice>();
let asset_server = world.resource::<AssetServer>();
let layout = render_device.create_bind_group_layout(...);
let layout_msaa = render_device.create_bind_group_layout(...);
let vertex = asset_server.load("vertex.wgsl");
let fragment = asset_server.load("fragment.wgsl");
Self {
layout,
layout_msaa,
vertex,
fragment,
}
}
}
// before
impl SpecializedRenderPipeline for MySpecializer {
type Key = MyKey;
impl SpecializedRenderPipeline for MyPipeline {
type Key = MyPipelineKey;
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
RenderPipelineDescriptor {
label: Some("my_pipeline".into()),
layout: vec![
if key.msaa.samples() > 0 {
if key.msaa.samples() > 1 {
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![],
..default()
},
primitive: Default::default(),
depth_stencil: None,
multisample: MultisampleState {
count: key.msaa.samples(),
..Default::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),
blend: None,
write_mask: ColorWrites::all(),
})],
..default()
}),
zero_initialize_workgroup_memory: false,
..default()
},
}
}
app.init_resource::<SpecializedRenderPipelines<MySpecializer>>();
render_app
.init_resource::<MyPipeline>();
.init_resource::<SpecializedRenderPipelines<MySpecializer>>();
```
After:
```rust
#[derive(Resource)]
pub struct MyPipeline {
layout: BindGroupLayout,
layout_msaa: BindGroupLayout,
vertex: Handle<Shader>,
fragment: Handle<Shader>,
specialized_cache: SpecializedCache<RenderPipeline, MySpecializer>,
}
pub struct MySpecializer {
layout: BindGroupLayout,
layout_msaa: BindGroupLayout,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, SpecializerKey)]
pub struct MyPipelineKey {
msaa: Msaa,
}
impl FromWorld for MyPipeline {
fn from_world(world: &mut World) -> Self {
let render_device = world.resource::<RenderDevice>();
let asset_server = world.resource::<AssetServer>();
let layout = render_device.create_bind_group_layout(...);
let layout_msaa = render_device.create_bind_group_layout(...);
let vertex = asset_server.load("vertex.wgsl");
let fragment = asset_server.load("fragment.wgsl");
let base_descriptor = RenderPipelineDescriptor {
label: Some("my_pipeline".into()),
vertex: VertexState {
shader: vertex.clone(),
..default()
},
fragment: Some(FragmentState {
shader: fragment.clone(),
..default()
}),
..default()
},
let specialized_cache = SpecializedCache::new(
MySpecializer {
layout: layout.clone(),
layout_msaa: layout_msaa.clone(),
},
None,
base_descriptor,
);
Self {
layout,
layout_msaa,
vertex,
fragment,
}
}
}
// after
impl Specializer<RenderPipeline> for MySpecializer {
type Key = MyKey;
@ -109,45 +213,19 @@ impl Specializer<RenderPipeline> for MySpecializer {
descriptor: &mut RenderPipeline,
) -> Result<Canonical<Self::Key>, BevyError> {
descriptor.multisample.count = key.msaa.samples();
descriptor.layout[0] = if key.msaa.samples() > 0 {
let layout = if key.msaa.samples() > 1 {
self.layout_msaa.clone()
} else {
self.layout.clone()
};
descriptor.fragment.targets[0].as_mut().unwrap().blend_mode = key.blend_state;
descriptor.set_layout(0, layout);
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,
},
}
}
render_app.init_resource::<MyPipeline>();
app.init_resource::<SpecializedCache<RenderPipeline, MySpecializer>>();
```