diff --git a/release-content/migration-guides/composable_specialization.md b/release-content/migration-guides/composable_specialization.md index f87beef8cb..f3fc936ed7 100644 --- a/release-content/migration-guides/composable_specialization.md +++ b/release-content/migration-guides/composable_specialization.md @@ -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: Send + Sync + 'static { type Key: SpecializerKey; fn specialize( @@ -19,20 +19,55 @@ pub trait Specializer: Send + Sync + 'static { pub struct SpecializedCache>{ ... }; ``` -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 for MsaaSpecializer {...} + +pub struct MeshLayoutSpecializer {...} +impl Specialize 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, @@ -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::(); + let asset_server = world.resource::(); + + 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::>(); +render_app + .init_resource::(); + .init_resource::>(); +``` + +After: + +```rust +#[derive(Resource)] +pub struct MyPipeline { + layout: BindGroupLayout, + layout_msaa: BindGroupLayout, + vertex: Handle, + fragment: Handle, + specialized_cache: SpecializedCache, +} + +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::(); + let asset_server = world.resource::(); + + 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 for MySpecializer { type Key = MyKey; @@ -109,45 +213,19 @@ impl Specializer for MySpecializer { descriptor: &mut RenderPipeline, ) -> Result, 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::(); -app.init_resource::>(); ```