From 0d9f1c81ce6db5e71425e7ce9415dc60f53fa1e5 Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Wed, 9 Jul 2025 21:09:06 -0700 Subject: [PATCH 01/19] nuke most of stuff TODO: migration guide --- crates/bevy_render/macros/src/specializer.rs | 116 ++---------------- .../src/render_resource/specializer.rs | 101 +-------------- examples/shader/custom_phase_item.rs | 81 ++++++------ 3 files changed, 57 insertions(+), 241 deletions(-) diff --git a/crates/bevy_render/macros/src/specializer.rs b/crates/bevy_render/macros/src/specializer.rs index d755c73736863..d3677229932c5 100644 --- a/crates/bevy_render/macros/src/specializer.rs +++ b/crates/bevy_render/macros/src/specializer.rs @@ -87,7 +87,6 @@ struct FieldInfo { ty: Type, member: Member, key: Key, - use_base_descriptor: bool, } impl FieldInfo { @@ -117,15 +116,6 @@ impl FieldInfo { parse_quote!(#ty: #specialize_path::Specializer<#target_path>) } } - - fn get_base_descriptor_predicate( - &self, - specialize_path: &Path, - target_path: &Path, - ) -> WherePredicate { - let ty = &self.ty; - parse_quote!(#ty: #specialize_path::GetBaseDescriptor<#target_path>) - } } fn get_field_info( @@ -190,7 +180,6 @@ fn get_field_info( ty: field_ty, member: field_member, key, - use_base_descriptor, }); } @@ -261,41 +250,18 @@ pub fn impl_specializer(input: TokenStream) -> TokenStream { }) .collect(); - let base_descriptor_fields = field_info - .iter() - .filter(|field| field.use_base_descriptor) - .collect::>(); - - if base_descriptor_fields.len() > 1 { - return syn::Error::new( - Span::call_site(), - "Too many #[base_descriptor] attributes found. It must be present on exactly one field", - ) - .into_compile_error() - .into(); - } - - let base_descriptor_field = base_descriptor_fields.first().copied(); - match targets { - SpecializeImplTargets::All => { - let specialize_impl = impl_specialize_all( - &specialize_path, - &ecs_path, - &ast, - &field_info, - &key_patterns, - &key_tuple_idents, - ); - let get_base_descriptor_impl = base_descriptor_field - .map(|field_info| impl_get_base_descriptor_all(&specialize_path, &ast, field_info)) - .unwrap_or_default(); - [specialize_impl, get_base_descriptor_impl] - .into_iter() - .collect() - } - SpecializeImplTargets::Specific(targets) => { - let specialize_impls = targets.iter().map(|target| { + SpecializeImplTargets::All => impl_specialize_all( + &specialize_path, + &ecs_path, + &ast, + &field_info, + &key_patterns, + &key_tuple_idents, + ), + SpecializeImplTargets::Specific(targets) => targets + .iter() + .map(|target| { impl_specialize_specific( &specialize_path, &ecs_path, @@ -305,14 +271,8 @@ pub fn impl_specializer(input: TokenStream) -> TokenStream { &key_patterns, &key_tuple_idents, ) - }); - let get_base_descriptor_impls = targets.iter().filter_map(|target| { - base_descriptor_field.map(|field_info| { - impl_get_base_descriptor_specific(&specialize_path, &ast, field_info, target) - }) - }); - specialize_impls.chain(get_base_descriptor_impls).collect() - } + }) + .collect(), } } @@ -406,56 +366,6 @@ fn impl_specialize_specific( }) } -fn impl_get_base_descriptor_specific( - specialize_path: &Path, - ast: &DeriveInput, - base_descriptor_field_info: &FieldInfo, - target_path: &Path, -) -> TokenStream { - let struct_name = &ast.ident; - let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl(); - let field_ty = &base_descriptor_field_info.ty; - let field_member = &base_descriptor_field_info.member; - TokenStream::from(quote!( - impl #impl_generics #specialize_path::GetBaseDescriptor<#target_path> for #struct_name #type_generics #where_clause { - fn get_base_descriptor(&self) -> <#target_path as #specialize_path::Specializable>::Descriptor { - <#field_ty as #specialize_path::GetBaseDescriptor<#target_path>>::get_base_descriptor(&self.#field_member) - } - } - )) -} - -fn impl_get_base_descriptor_all( - specialize_path: &Path, - ast: &DeriveInput, - base_descriptor_field_info: &FieldInfo, -) -> TokenStream { - let target_path = Path::from(format_ident!("T")); - let struct_name = &ast.ident; - let mut generics = ast.generics.clone(); - generics.params.insert( - 0, - parse_quote!(#target_path: #specialize_path::Specializable), - ); - - let where_clause = generics.make_where_clause(); - where_clause.predicates.push( - base_descriptor_field_info.get_base_descriptor_predicate(specialize_path, &target_path), - ); - - let (_, type_generics, _) = ast.generics.split_for_impl(); - let (impl_generics, _, where_clause) = &generics.split_for_impl(); - let field_ty = &base_descriptor_field_info.ty; - let field_member = &base_descriptor_field_info.member; - TokenStream::from(quote! { - impl #impl_generics #specialize_path::GetBaseDescriptor<#target_path> for #struct_name #type_generics #where_clause { - fn get_base_descriptor(&self) -> <#target_path as #specialize_path::Specializable>::Descriptor { - <#field_ty as #specialize_path::GetBaseDescriptor<#target_path>>::get_base_descriptor(&self.#field_member) - } - } - }) -} - pub fn impl_specializer_key(input: TokenStream) -> TokenStream { let bevy_render_path: Path = crate::bevy_render_path(); let specialize_path = { diff --git a/crates/bevy_render/src/render_resource/specializer.rs b/crates/bevy_render/src/render_resource/specializer.rs index d7a2f3aca1a03..762458145aff9 100644 --- a/crates/bevy_render/src/render_resource/specializer.rs +++ b/crates/bevy_render/src/render_resource/specializer.rs @@ -2,11 +2,7 @@ use super::{ CachedComputePipelineId, CachedRenderPipelineId, ComputePipeline, ComputePipelineDescriptor, PipelineCache, RenderPipeline, RenderPipelineDescriptor, }; -use bevy_ecs::{ - error::BevyError, - resource::Resource, - world::{FromWorld, World}, -}; +use bevy_ecs::error::BevyError; use bevy_platform::{ collections::{ hash_map::{Entry, VacantEntry}, @@ -260,91 +256,12 @@ macro_rules! impl_specialization_key_tuple { // TODO: How to we fake_variadics this? all_tuples!(impl_specialization_key_tuple, 0, 12, T); -/// Defines a specializer that can also provide a "base descriptor". -/// -/// In order to be composable, [`Specializer`] implementers don't create full -/// descriptors, only transform them. However, [`SpecializedCache`]s need a -/// "base descriptor" at creation time in order to have something for the -/// [`Specializer`] to work off of. This trait allows [`SpecializedCache`] -/// to impl [`FromWorld`] for [`Specializer`]s that also satisfy [`FromWorld`] -/// and [`GetBaseDescriptor`]. -/// -/// This trait can be also derived with `#[derive(Specializer)]`, by marking -/// a field with `#[base_descriptor]` to use its [`GetBaseDescriptor`] implementation. -/// -/// Example: -/// ```rust -/// # use bevy_ecs::error::BevyError; -/// # use bevy_render::render_resource::Specializer; -/// # use bevy_render::render_resource::GetBaseDescriptor; -/// # use bevy_render::render_resource::SpecializerKey; -/// # use bevy_render::render_resource::RenderPipeline; -/// # use bevy_render::render_resource::RenderPipelineDescriptor; -/// struct A; -/// struct B; -/// -/// impl Specializer for A { -/// # type Key = (); -/// # -/// # fn specialize( -/// # &self, -/// # key: (), -/// # _descriptor: &mut RenderPipelineDescriptor -/// # ) -> Result<(), BevyError> { -/// # Ok(key) -/// # } -/// // ... -/// } -/// -/// impl Specializer for B { -/// # type Key = (); -/// # -/// # fn specialize( -/// # &self, -/// # key: (), -/// # _descriptor: &mut RenderPipelineDescriptor -/// # ) -> Result<(), BevyError> { -/// # Ok(key) -/// # } -/// // ... -/// } -/// -/// impl GetBaseDescriptor for B { -/// fn get_base_descriptor(&self) -> RenderPipelineDescriptor { -/// # todo!() -/// // ... -/// } -/// } -/// -/// -/// #[derive(Specializer)] -/// #[specialize(RenderPipeline)] -/// struct C { -/// a: A, -/// #[base_descriptor] -/// b: B, -/// } -/// -/// /* -/// The generated implementation: -/// impl GetBaseDescriptor for C { -/// fn get_base_descriptor(&self) -> RenderPipelineDescriptor { -/// self.b.base_descriptor() -/// } -/// } -/// */ -/// ``` -pub trait GetBaseDescriptor: Specializer { - fn get_base_descriptor(&self) -> T::Descriptor; -} - pub type SpecializerFn = fn(>::Key, &mut ::Descriptor) -> Result<(), BevyError>; /// A cache for specializable resources. For a given key type the resulting /// resource will only be created if it is missing, retrieving it from the /// cache otherwise. -#[derive(Resource)] pub struct SpecializedCache> { specializer: S, user_specializer: Option>, @@ -447,19 +364,3 @@ impl> SpecializedCache { Ok(id) } } - -/// [`SpecializedCache`] implements [`FromWorld`] for [`Specializer`]s -/// that also satisfy [`FromWorld`] and [`GetBaseDescriptor`]. This will -/// create a [`SpecializedCache`] with no user specializer, and the base -/// descriptor take from the specializer's [`GetBaseDescriptor`] implementation. -impl FromWorld for SpecializedCache -where - T: Specializable, - S: FromWorld + Specializer + GetBaseDescriptor, -{ - fn from_world(world: &mut World) -> Self { - let specializer = S::from_world(world); - let base_descriptor = specializer.get_base_descriptor(); - Self::new(specializer, None, base_descriptor) - } -} diff --git a/examples/shader/custom_phase_item.rs b/examples/shader/custom_phase_item.rs index fd8dc063d02d2..5e4ab594acd88 100644 --- a/examples/shader/custom_phase_item.rs +++ b/examples/shader/custom_phase_item.rs @@ -25,8 +25,8 @@ use bevy::{ }, render_resource::{ BufferUsages, Canonical, ColorTargetState, ColorWrites, CompareFunction, - DepthStencilState, FragmentState, GetBaseDescriptor, IndexFormat, PipelineCache, - RawBufferVec, RenderPipeline, RenderPipelineDescriptor, SpecializedCache, Specializer, + DepthStencilState, FragmentState, IndexFormat, PipelineCache, RawBufferVec, + RenderPipeline, RenderPipelineDescriptor, SpecializedCache, Specializer, SpecializerKey, TextureFormat, VertexAttribute, VertexBufferLayout, VertexFormat, VertexState, VertexStepMode, }, @@ -165,9 +165,8 @@ fn main() { .add_systems(Startup, setup); // We make sure to add these to the render app, not the main app. - app.get_sub_app_mut(RenderApp) - .unwrap() - .init_resource::>() + app.sub_app_mut(RenderApp) + .init_resource::() .add_render_command::() .add_systems( Render, @@ -212,9 +211,9 @@ fn prepare_custom_phase_item_buffers(mut commands: Commands) { /// the opaque render phases of each view. fn queue_custom_phase_item( pipeline_cache: Res, + mut pipeline: ResMut, mut opaque_render_phases: ResMut>, opaque_draw_functions: Res>, - mut specializer: ResMut>, views: Query<(&ExtractedView, &RenderVisibleEntities, &Msaa)>, mut next_tick: Local, ) { @@ -237,7 +236,9 @@ fn queue_custom_phase_item( // some per-view settings, such as whether the view is HDR, but for // simplicity's sake we simply hard-code the view's characteristics, // with the exception of number of MSAA samples. - let Ok(pipeline_id) = specializer.specialize(&pipeline_cache, CustomPhaseKey(*msaa)) + let Ok(pipeline_id) = pipeline + .specialized_cache + .specialize(&pipeline_cache, CustomPhaseKey(*msaa)) else { continue; }; @@ -275,44 +276,24 @@ fn queue_custom_phase_item( } } -/// Holds a reference to our shader. -/// -/// This is loaded at app creation time. -struct CustomPhaseSpecializer { +struct CustomPhaseSpecializer; + +#[derive(Resource)] +struct CustomPhasePipeline { + /// Holds a reference to our shader. shader: Handle, + specialized_cache: SpecializedCache, } -impl FromWorld for CustomPhaseSpecializer { +impl FromWorld for CustomPhasePipeline { fn from_world(world: &mut World) -> Self { let asset_server = world.resource::(); - Self { - shader: asset_server.load("shaders/custom_phase_item.wgsl"), - } - } -} - -#[derive(Copy, Clone, PartialEq, Eq, Hash, SpecializerKey)] -struct CustomPhaseKey(Msaa); - -impl Specializer for CustomPhaseSpecializer { - type Key = CustomPhaseKey; + let shader = asset_server.load("shaders/custom_phase_item.wgsl"); - fn specialize( - &self, - key: Self::Key, - descriptor: &mut RenderPipelineDescriptor, - ) -> Result, BevyError> { - descriptor.multisample.count = key.0.samples(); - Ok(key) - } -} - -impl GetBaseDescriptor for CustomPhaseSpecializer { - fn get_base_descriptor(&self) -> RenderPipelineDescriptor { - RenderPipelineDescriptor { + let base_descriptor = RenderPipelineDescriptor { label: Some("custom render pipeline".into()), vertex: VertexState { - shader: self.shader.clone(), + shader: shader.clone(), buffers: vec![VertexBufferLayout { array_stride: size_of::() as u64, step_mode: VertexStepMode::Vertex, @@ -333,7 +314,7 @@ impl GetBaseDescriptor for CustomPhaseSpecializer { ..default() }, fragment: Some(FragmentState { - shader: self.shader.clone(), + shader: shader.clone(), targets: vec![Some(ColorTargetState { // Ordinarily, you'd want to check whether the view has the // HDR format and substitute the appropriate texture format @@ -354,10 +335,34 @@ impl GetBaseDescriptor for CustomPhaseSpecializer { bias: default(), }), ..default() + }; + + let specialized_cache = + SpecializedCache::new(CustomPhaseSpecializer, None, base_descriptor); + + Self { + shader, + specialized_cache, } } } +#[derive(Copy, Clone, PartialEq, Eq, Hash, SpecializerKey)] +struct CustomPhaseKey(Msaa); + +impl Specializer for CustomPhaseSpecializer { + type Key = CustomPhaseKey; + + fn specialize( + &self, + key: Self::Key, + descriptor: &mut RenderPipelineDescriptor, + ) -> Result, BevyError> { + descriptor.multisample.count = key.0.samples(); + Ok(key) + } +} + impl FromWorld for CustomPhaseItemBuffers { fn from_world(world: &mut World) -> Self { let render_device = world.resource::(); From 2db1ef9b69c6aadd42356073cc83df5e3cf00c72 Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Sun, 13 Jul 2025 11:11:35 -0700 Subject: [PATCH 02/19] edit migration guide with new patterns --- .../composable_specialization.md | 216 ++++++++++++------ 1 file changed, 147 insertions(+), 69 deletions(-) diff --git a/release-content/migration-guides/composable_specialization.md b/release-content/migration-guides/composable_specialization.md index f87beef8cbbdb..f3fc936ed7b74 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::>(); ``` From c1c4da854b4bd7311d9d6b10f569499d427203b2 Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Sun, 13 Jul 2025 11:17:44 -0700 Subject: [PATCH 03/19] remove unused code --- crates/bevy_render/macros/src/specializer.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/crates/bevy_render/macros/src/specializer.rs b/crates/bevy_render/macros/src/specializer.rs index d3677229932c5..b5dd56eeb5c3a 100644 --- a/crates/bevy_render/macros/src/specializer.rs +++ b/crates/bevy_render/macros/src/specializer.rs @@ -20,8 +20,6 @@ const SPECIALIZE_ALL_IDENT: &str = "all"; const KEY_ATTR_IDENT: &str = "key"; const KEY_DEFAULT_IDENT: &str = "default"; -const BASE_DESCRIPTOR_ATTR_IDENT: &str = "base_descriptor"; - enum SpecializeImplTargets { All, Specific(Vec), @@ -141,12 +139,8 @@ fn get_field_info( let mut use_key_field = true; let mut key = Key::Index(key_index); - let mut use_base_descriptor = false; for attr in &field.attrs { match &attr.meta { - Meta::Path(path) if path.is_ident(&BASE_DESCRIPTOR_ATTR_IDENT) => { - use_base_descriptor = true; - } Meta::List(MetaList { path, tokens, .. }) if path.is_ident(&KEY_ATTR_IDENT) => { let owned_tokens = tokens.clone().into(); let Ok(parsed_key) = syn::parse::(owned_tokens) else { From e0f7036e2d22717b0e0d317b2e530fbdd7076a54 Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Sun, 13 Jul 2025 11:31:10 -0700 Subject: [PATCH 04/19] fix ci --- examples/shader/custom_phase_item.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/shader/custom_phase_item.rs b/examples/shader/custom_phase_item.rs index 5e4ab594acd88..4f76d413502d9 100644 --- a/examples/shader/custom_phase_item.rs +++ b/examples/shader/custom_phase_item.rs @@ -281,7 +281,7 @@ struct CustomPhaseSpecializer; #[derive(Resource)] struct CustomPhasePipeline { /// Holds a reference to our shader. - shader: Handle, + _shader: Handle, specialized_cache: SpecializedCache, } @@ -341,7 +341,7 @@ impl FromWorld for CustomPhasePipeline { SpecializedCache::new(CustomPhaseSpecializer, None, base_descriptor); Self { - shader, + _shader: shader, specialized_cache, } } From f893fd5ba2c48113dc50003cb5525d6764f9684a Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Sun, 13 Jul 2025 12:07:12 -0700 Subject: [PATCH 05/19] clarification --- examples/shader/custom_phase_item.rs | 5 +++-- .../migration-guides/composable_specialization.md | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/examples/shader/custom_phase_item.rs b/examples/shader/custom_phase_item.rs index 4f76d413502d9..eb9f91e5417f8 100644 --- a/examples/shader/custom_phase_item.rs +++ b/examples/shader/custom_phase_item.rs @@ -280,9 +280,10 @@ struct CustomPhaseSpecializer; #[derive(Resource)] struct CustomPhasePipeline { - /// Holds a reference to our shader. - _shader: Handle, specialized_cache: SpecializedCache, + /// the base_descriptor holds onto the shader handle, this field is + /// unused but here for demonstration purposes. + _shader: Handle, } impl FromWorld for CustomPhasePipeline { diff --git a/release-content/migration-guides/composable_specialization.md b/release-content/migration-guides/composable_specialization.md index f3fc936ed7b74..5187cdff24f85 100644 --- a/release-content/migration-guides/composable_specialization.md +++ b/release-content/migration-guides/composable_specialization.md @@ -145,11 +145,13 @@ After: ```rust #[derive(Resource)] pub struct MyPipeline { + specialized_cache: SpecializedCache, + + // these fields are unused, they're just here for demonstration purposes layout: BindGroupLayout, layout_msaa: BindGroupLayout, vertex: Handle, fragment: Handle, - specialized_cache: SpecializedCache, } pub struct MySpecializer { From eb53ccc9b11405badb214530710254ed6eae141e Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Sun, 13 Jul 2025 12:13:35 -0700 Subject: [PATCH 06/19] more clarification --- examples/shader/custom_phase_item.rs | 4 +--- .../migration-guides/composable_specialization.md | 11 +++++------ 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/examples/shader/custom_phase_item.rs b/examples/shader/custom_phase_item.rs index eb9f91e5417f8..45bca5652b7fe 100644 --- a/examples/shader/custom_phase_item.rs +++ b/examples/shader/custom_phase_item.rs @@ -280,10 +280,8 @@ struct CustomPhaseSpecializer; #[derive(Resource)] struct CustomPhasePipeline { + /// the specialized_cache holds onto the shader handle through the base descriptor specialized_cache: SpecializedCache, - /// the base_descriptor holds onto the shader handle, this field is - /// unused but here for demonstration purposes. - _shader: Handle, } impl FromWorld for CustomPhasePipeline { diff --git a/release-content/migration-guides/composable_specialization.md b/release-content/migration-guides/composable_specialization.md index 5187cdff24f85..c33e642b3a559 100644 --- a/release-content/migration-guides/composable_specialization.md +++ b/release-content/migration-guides/composable_specialization.md @@ -145,13 +145,12 @@ After: ```rust #[derive(Resource)] pub struct MyPipeline { + // the base_descriptor and specializer each hold onto the static + // wgpu resources (layout, shader handles), so we don't need + // explicit fields for them here. However, real-world cases + // may still need to duplicate them as fields to create bind + // groups from, etc. specialized_cache: SpecializedCache, - - // these fields are unused, they're just here for demonstration purposes - layout: BindGroupLayout, - layout_msaa: BindGroupLayout, - vertex: Handle, - fragment: Handle, } pub struct MySpecializer { From 913f53be93f44e6ef3b4c380c10926eb0ec57182 Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Sun, 13 Jul 2025 12:31:18 -0700 Subject: [PATCH 07/19] fix example --- examples/shader/custom_phase_item.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/examples/shader/custom_phase_item.rs b/examples/shader/custom_phase_item.rs index 45bca5652b7fe..e8d7f36d29550 100644 --- a/examples/shader/custom_phase_item.rs +++ b/examples/shader/custom_phase_item.rs @@ -339,10 +339,7 @@ impl FromWorld for CustomPhasePipeline { let specialized_cache = SpecializedCache::new(CustomPhaseSpecializer, None, base_descriptor); - Self { - _shader: shader, - specialized_cache, - } + Self { specialized_cache } } } From 74d91e0f171eeb8eafca5aed4c19d1571763a340 Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Sun, 13 Jul 2025 12:38:24 -0700 Subject: [PATCH 08/19] fix ci --- examples/shader/custom_phase_item.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/shader/custom_phase_item.rs b/examples/shader/custom_phase_item.rs index e8d7f36d29550..16a775ac4a35a 100644 --- a/examples/shader/custom_phase_item.rs +++ b/examples/shader/custom_phase_item.rs @@ -280,7 +280,7 @@ struct CustomPhaseSpecializer; #[derive(Resource)] struct CustomPhasePipeline { - /// the specialized_cache holds onto the shader handle through the base descriptor + /// the `specialized_cache` holds onto the shader handle through the base descriptor specialized_cache: SpecializedCache, } From 33fcee8393b6bdface049f6a701167f4549c45de Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Sun, 13 Jul 2025 12:39:28 -0700 Subject: [PATCH 09/19] phrasing --- release-content/migration-guides/composable_specialization.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/release-content/migration-guides/composable_specialization.md b/release-content/migration-guides/composable_specialization.md index c33e642b3a559..a40a6203c2643 100644 --- a/release-content/migration-guides/composable_specialization.md +++ b/release-content/migration-guides/composable_specialization.md @@ -148,8 +148,8 @@ pub struct MyPipeline { // the base_descriptor and specializer each hold onto the static // wgpu resources (layout, shader handles), so we don't need // explicit fields for them here. However, real-world cases - // may still need to duplicate them as fields to create bind - // groups from, etc. + // may still need to expose them as fields to create bind groups + // from, for example. specialized_cache: SpecializedCache, } From f1ccd013814995d609d121d48c0e65fc7e4d23d8 Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Sun, 13 Jul 2025 21:05:39 -0700 Subject: [PATCH 10/19] nuke user specializers --- .../src/render_resource/specializer.rs | 22 ++----------------- examples/shader/custom_phase_item.rs | 3 +-- 2 files changed, 3 insertions(+), 22 deletions(-) diff --git a/crates/bevy_render/src/render_resource/specializer.rs b/crates/bevy_render/src/render_resource/specializer.rs index 762458145aff9..da63dc3323eac 100644 --- a/crates/bevy_render/src/render_resource/specializer.rs +++ b/crates/bevy_render/src/render_resource/specializer.rs @@ -256,34 +256,22 @@ macro_rules! impl_specialization_key_tuple { // TODO: How to we fake_variadics this? all_tuples!(impl_specialization_key_tuple, 0, 12, T); -pub type SpecializerFn = - fn(>::Key, &mut ::Descriptor) -> Result<(), BevyError>; - /// A cache for specializable resources. For a given key type the resulting /// resource will only be created if it is missing, retrieving it from the /// cache otherwise. pub struct SpecializedCache> { specializer: S, - user_specializer: Option>, base_descriptor: T::Descriptor, primary_cache: HashMap, secondary_cache: HashMap, T::CachedId>, } impl> SpecializedCache { - /// Creates a new [`SpecializedCache`] from a [`Specializer`], - /// an optional "user specializer", and a base descriptor. The - /// user specializer is applied after the [`Specializer`], with - /// the same key. + /// Creates a new [`SpecializedCache`] from a [`Specializer`] and a base descriptor. #[inline] - pub fn new( - specializer: S, - user_specializer: Option>, - base_descriptor: T::Descriptor, - ) -> Self { + pub fn new(specializer: S, base_descriptor: T::Descriptor) -> Self { Self { specializer, - user_specializer, base_descriptor, primary_cache: Default::default(), secondary_cache: Default::default(), @@ -302,7 +290,6 @@ impl> SpecializedCache { Entry::Occupied(entry) => Ok(entry.get().clone()), Entry::Vacant(entry) => Self::specialize_slow( &self.specializer, - self.user_specializer, self.base_descriptor.clone(), pipeline_cache, key, @@ -315,7 +302,6 @@ impl> SpecializedCache { #[cold] fn specialize_slow( specializer: &S, - user_specializer: Option>, base_descriptor: T::Descriptor, pipeline_cache: &PipelineCache, key: S::Key, @@ -325,10 +311,6 @@ impl> SpecializedCache { let mut descriptor = base_descriptor.clone(); let canonical_key = specializer.specialize(key.clone(), &mut descriptor)?; - if let Some(user_specializer) = user_specializer { - (user_specializer)(key, &mut descriptor)?; - } - // if the whole key is canonical, the secondary cache isn't needed. if ::IS_CANONICAL { return Ok(primary_entry diff --git a/examples/shader/custom_phase_item.rs b/examples/shader/custom_phase_item.rs index 16a775ac4a35a..3140915acfd13 100644 --- a/examples/shader/custom_phase_item.rs +++ b/examples/shader/custom_phase_item.rs @@ -336,8 +336,7 @@ impl FromWorld for CustomPhasePipeline { ..default() }; - let specialized_cache = - SpecializedCache::new(CustomPhaseSpecializer, None, base_descriptor); + let specialized_cache = SpecializedCache::new(CustomPhaseSpecializer, base_descriptor); Self { specialized_cache } } From d787a0fc1f86aa7dc00d3d481565aee28428ee7d Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Mon, 14 Jul 2025 12:08:26 -0700 Subject: [PATCH 11/19] add empty bind group layout --- crates/bevy_render/src/lib.rs | 3 +++ .../src/render_resource/bind_group_layout.rs | 20 ++++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/crates/bevy_render/src/lib.rs b/crates/bevy_render/src/lib.rs index 7a2ad060878b0..da965ec5f2ae5 100644 --- a/crates/bevy_render/src/lib.rs +++ b/crates/bevy_render/src/lib.rs @@ -96,6 +96,7 @@ use render_asset::{ extract_render_asset_bytes_per_frame, reset_render_asset_bytes_per_frame, RenderAssetBytesPerFrame, RenderAssetBytesPerFrameLimiter, }; +use render_resource::init_empty_bind_group_layout; use renderer::{RenderAdapter, RenderDevice, RenderQueue}; use settings::RenderResources; use sync_world::{ @@ -465,6 +466,8 @@ impl Plugin for RenderPlugin { Render, reset_render_asset_bytes_per_frame.in_set(RenderSystems::Cleanup), ); + + render_app.add_systems(RenderStartup, init_empty_bind_group_layout); } app.register_type::() diff --git a/crates/bevy_render/src/render_resource/bind_group_layout.rs b/crates/bevy_render/src/render_resource/bind_group_layout.rs index 2d674f46d1ff8..b0a8a4b5759ce 100644 --- a/crates/bevy_render/src/render_resource/bind_group_layout.rs +++ b/crates/bevy_render/src/render_resource/bind_group_layout.rs @@ -1,4 +1,6 @@ -use crate::define_atomic_id; +use crate::{define_atomic_id, renderer::RenderDevice}; +use bevy_ecs::system::Res; +use bevy_platform::sync::OnceLock; use bevy_utils::WgpuWrapper; use core::ops::Deref; @@ -62,3 +64,19 @@ impl Deref for BindGroupLayout { &self.value } } + +static EMPTY_BIND_GROUP_LAYOUT: OnceLock = OnceLock::new(); + +pub(crate) fn init_empty_bind_group_layout(render_device: Res) { + let layout = render_device.create_bind_group_layout(Some("empty_bind_group_layout"), &[]); + EMPTY_BIND_GROUP_LAYOUT + .set(layout) + .expect("init_empty_bind_group_layout was called more than once"); +} + +pub fn empty_bind_group_layout() -> BindGroupLayout { + EMPTY_BIND_GROUP_LAYOUT + .get() + .expect("init_empty_bind_group_layout was not called") + .clone() +} From f995079348db875e69809aa9818490317c6a96cf Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Mon, 14 Jul 2025 12:11:47 -0700 Subject: [PATCH 12/19] add set_layout --- .../bevy_render/src/render_resource/pipeline.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/crates/bevy_render/src/render_resource/pipeline.rs b/crates/bevy_render/src/render_resource/pipeline.rs index e94cf27cd32c8..c09546aaf1bfb 100644 --- a/crates/bevy_render/src/render_resource/pipeline.rs +++ b/crates/bevy_render/src/render_resource/pipeline.rs @@ -1,4 +1,4 @@ -use super::ShaderDefVal; +use super::{empty_bind_group_layout, ShaderDefVal}; use crate::mesh::VertexBufferLayout; use crate::{ define_atomic_id, @@ -112,6 +112,20 @@ pub struct RenderPipelineDescriptor { pub zero_initialize_workgroup_memory: bool, } +#[derive(Copy, Clone, Debug, Error)] +#[error("RenderPipelineDescriptor has no FragmentState configured")] +pub struct NoFragmentStateError; + +impl RenderPipelineDescriptor { + pub fn fragment_mut(&mut self) -> Result<&mut FragmentState, NoFragmentStateError> { + self.fragment.as_mut().ok_or(NoFragmentStateError) + } + + pub fn set_layout(&mut self, index: usize, layout: BindGroupLayout) { + filling_set_at(&mut self.layout, index, empty_bind_group_layout(), layout); + } +} + #[derive(Clone, Debug, Eq, PartialEq, Default)] pub struct VertexState { /// The compiled shader module for this stage. From 95f98e40bc86b51bda45a4c83957b143abd13944 Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Tue, 15 Jul 2025 11:54:37 -0700 Subject: [PATCH 13/19] oops --- crates/bevy_render/src/render_resource/pipeline.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/bevy_render/src/render_resource/pipeline.rs b/crates/bevy_render/src/render_resource/pipeline.rs index c09546aaf1bfb..66f7aa29d55b1 100644 --- a/crates/bevy_render/src/render_resource/pipeline.rs +++ b/crates/bevy_render/src/render_resource/pipeline.rs @@ -7,7 +7,9 @@ use crate::{ use alloc::borrow::Cow; use bevy_asset::Handle; use bevy_utils::WgpuWrapper; +use core::iter; use core::ops::Deref; +use thiserror::Error; use wgpu::{ ColorTargetState, DepthStencilState, MultisampleState, PrimitiveState, PushConstantRange, }; @@ -167,3 +169,11 @@ pub struct ComputePipelineDescriptor { /// If this is false, reading from workgroup variables before writing to them will result in garbage values. pub zero_initialize_workgroup_memory: bool, } + +// utility function to set a value at the specified index, extending with +// a filler value if the index is out of bounds. +fn filling_set_at(vec: &mut Vec, index: usize, filler: T, value: T) { + let num_to_fill = (index + 1).saturating_sub(vec.len()); + vec.extend(iter::repeat_n(filler, num_to_fill)); + vec[index] = value; +} From 879b36280b32f15f5caf9dd2039dbad0c107933e Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Tue, 15 Jul 2025 14:31:08 -0700 Subject: [PATCH 14/19] re-add set_target --- crates/bevy_render/src/render_resource/pipeline.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/bevy_render/src/render_resource/pipeline.rs b/crates/bevy_render/src/render_resource/pipeline.rs index 66f7aa29d55b1..68b85671a26e8 100644 --- a/crates/bevy_render/src/render_resource/pipeline.rs +++ b/crates/bevy_render/src/render_resource/pipeline.rs @@ -153,6 +153,12 @@ pub struct FragmentState { pub targets: Vec>, } +impl FragmentState { + pub fn set_target(&mut self, index: usize, target: ColorTargetState) { + filling_set_at(&mut self.targets, index, None, Some(target)); + } +} + /// Describes a compute pipeline. #[derive(Clone, Debug, PartialEq, Eq, Default)] pub struct ComputePipelineDescriptor { From 76ed8fd72e0f1e98507056117076405284d90369 Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Tue, 15 Jul 2025 18:36:38 -0700 Subject: [PATCH 15/19] fix migration guide --- .../migration-guides/composable_specialization.md | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/release-content/migration-guides/composable_specialization.md b/release-content/migration-guides/composable_specialization.md index a40a6203c2643..32138cd478626 100644 --- a/release-content/migration-guides/composable_specialization.md +++ b/release-content/migration-guides/composable_specialization.md @@ -150,7 +150,7 @@ pub struct MyPipeline { // explicit fields for them here. However, real-world cases // may still need to expose them as fields to create bind groups // from, for example. - specialized_cache: SpecializedCache, + variants: SpecializedCache, } pub struct MySpecializer { @@ -187,20 +187,16 @@ impl FromWorld for MyPipeline { ..default() }, - let specialized_cache = SpecializedCache::new( + let variants = SpecializedCache::new( MySpecializer { layout: layout.clone(), layout_msaa: layout_msaa.clone(), }, - None, base_descriptor, ); Self { - layout, - layout_msaa, - vertex, - fragment, + variants } } } From 3d80314f70168275c55350299d88f80868b286f1 Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Fri, 18 Jul 2025 10:32:37 -0700 Subject: [PATCH 16/19] add dynamic specializer wrapper --- .../src/render_resource/specializer.rs | 130 +++++++++++++++++- 1 file changed, 127 insertions(+), 3 deletions(-) diff --git a/crates/bevy_render/src/render_resource/specializer.rs b/crates/bevy_render/src/render_resource/specializer.rs index da63dc3323eac..ee8bf6653aa1d 100644 --- a/crates/bevy_render/src/render_resource/specializer.rs +++ b/crates/bevy_render/src/render_resource/specializer.rs @@ -21,7 +21,7 @@ pub use bevy_render_macros::{Specializer, SpecializerKey}; /// likely will not have much utility for other types. /// /// See docs on [`Specializer`] for more info. -pub trait Specializable { +pub trait Specializable: 'static { type Descriptor: PartialEq + Clone + Send + Sync; type CachedId: Clone + Send + Sync; fn queue(pipeline_cache: &PipelineCache, descriptor: Self::Descriptor) -> Self::CachedId; @@ -209,13 +209,13 @@ pub trait Specializer: Send + Sync + 'static { /// differently, they should nearly always produce different descriptors. /// /// [canonical]: https://en.wikipedia.org/wiki/Canonicalization -pub trait SpecializerKey: Clone + Hash + Eq { +pub trait SpecializerKey: Send + Sync + Clone + Hash + Eq + 'static { /// Denotes whether this key is canonical or not. This should only be `true` /// if and only if `Canonical = Self`. const IS_CANONICAL: bool; /// The canonical key type to convert this into during specialization. - type Canonical: Hash + Eq; + type Canonical: SpecializerKey; } pub type Canonical = ::Canonical; @@ -346,3 +346,127 @@ impl> SpecializedCache { Ok(id) } } + +pub use dyn_specializer::{DynSpecializer, DynSpecializerKey}; + +mod dyn_specializer { + use core::{ + any::Any, + hash::{Hash, Hasher}, + }; + + use bevy_ecs::{ + error::BevyError, + label::{DynEq, DynHash}, + }; + use thiserror::Error; + + use super::{Canonical, Specializable, Specializer, SpecializerKey}; + + pub trait DynSpecializerTrait: Any + Send + Sync + 'static { + fn dyn_specialize( + &self, + key: DynSpecializerKey, + descriptor: &mut T::Descriptor, + ) -> Result; + } + + #[derive(Error, Debug)] + #[error("Incorrect key type passed to a DynSpecializer")] + struct IncorrectKeyTypeError; + + impl> DynSpecializerTrait for S { + fn dyn_specialize( + &self, + key: DynSpecializerKey, + descriptor: &mut T::Descriptor, + ) -> Result { + let real_key = (&key.0 as &dyn Any) + .downcast_ref::() + .ok_or(IncorrectKeyTypeError)? + .clone(); + let canonical_key = self.specialize(real_key, descriptor)?; + Ok(DynSpecializerKey::new(canonical_key)) + } + } + + pub struct DynSpecializer(Box>); + + impl DynSpecializer { + pub fn new(specializer: impl Specializer) -> Self { + Self(Box::new(specializer)) + } + } + + impl Specializer for DynSpecializer { + type Key = DynSpecializerKey; + + #[inline] + fn specialize( + &self, + key: Self::Key, + descriptor: &mut T::Descriptor, + ) -> Result, BevyError> { + self.0.dyn_specialize(key, descriptor) + } + } + + pub trait DynSpecializerKeyTrait: Send + Sync + DynEq + DynHash { + fn dyn_clone(&self) -> Box; + } + + impl Clone for Box { + fn clone(&self) -> Self { + self.dyn_clone() + } + } + + impl PartialEq for dyn DynSpecializerKeyTrait { + fn eq(&self, other: &Self) -> bool { + self.dyn_eq(other) + } + } + + impl Eq for dyn DynSpecializerKeyTrait {} + + impl Hash for dyn DynSpecializerKeyTrait { + fn hash(&self, state: &mut H) { + self.dyn_hash(state); + } + } + + impl DynSpecializerKeyTrait for T { + fn dyn_clone(&self) -> Box { + Box::new(self.clone()) + } + } + + #[derive(Clone)] + pub struct DynSpecializerKey(Box); + + impl Eq for DynSpecializerKey {} + + impl Hash for DynSpecializerKey { + fn hash(&self, state: &mut H) { + self.0.hash(state); + } + } + + impl PartialEq for DynSpecializerKey { + fn eq(&self, other: &Self) -> bool { + self.0.dyn_eq(&other.0) + } + } + + impl DynSpecializerKey { + pub fn new(key: impl SpecializerKey) -> Self { + Self(Box::new(key)) + } + } + + impl SpecializerKey for DynSpecializerKey { + const IS_CANONICAL: bool = false; + + type Canonical = DynSpecializerKey; + } +} From ec35f7bae06b9db70b5ba900bc97598560ab0308 Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Fri, 18 Jul 2025 10:44:29 -0700 Subject: [PATCH 17/19] rename traits and types --- .../src/render_resource/specializer.rs | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/crates/bevy_render/src/render_resource/specializer.rs b/crates/bevy_render/src/render_resource/specializer.rs index ee8bf6653aa1d..cb8c503da8264 100644 --- a/crates/bevy_render/src/render_resource/specializer.rs +++ b/crates/bevy_render/src/render_resource/specializer.rs @@ -347,7 +347,7 @@ impl> SpecializedCache { } } -pub use dyn_specializer::{DynSpecializer, DynSpecializerKey}; +pub use dyn_specializer::{ErasedSpecializer, ErasedSpecializerKey}; mod dyn_specializer { use core::{ @@ -363,43 +363,43 @@ mod dyn_specializer { use super::{Canonical, Specializable, Specializer, SpecializerKey}; - pub trait DynSpecializerTrait: Any + Send + Sync + 'static { + pub trait DynSpecializer: Any + Send + Sync + 'static { fn dyn_specialize( &self, - key: DynSpecializerKey, + key: ErasedSpecializerKey, descriptor: &mut T::Descriptor, - ) -> Result; + ) -> Result; } #[derive(Error, Debug)] #[error("Incorrect key type passed to a DynSpecializer")] struct IncorrectKeyTypeError; - impl> DynSpecializerTrait for S { + impl> DynSpecializer for S { fn dyn_specialize( &self, - key: DynSpecializerKey, + key: ErasedSpecializerKey, descriptor: &mut T::Descriptor, - ) -> Result { + ) -> Result { let real_key = (&key.0 as &dyn Any) .downcast_ref::() .ok_or(IncorrectKeyTypeError)? .clone(); let canonical_key = self.specialize(real_key, descriptor)?; - Ok(DynSpecializerKey::new(canonical_key)) + Ok(ErasedSpecializerKey::new(canonical_key)) } } - pub struct DynSpecializer(Box>); + pub struct ErasedSpecializer(Box>); - impl DynSpecializer { + impl ErasedSpecializer { pub fn new(specializer: impl Specializer) -> Self { Self(Box::new(specializer)) } } - impl Specializer for DynSpecializer { - type Key = DynSpecializerKey; + impl Specializer for ErasedSpecializer { + type Key = ErasedSpecializerKey; #[inline] fn specialize( @@ -411,62 +411,62 @@ mod dyn_specializer { } } - pub trait DynSpecializerKeyTrait: Send + Sync + DynEq + DynHash { - fn dyn_clone(&self) -> Box; + pub trait DynSpecializerKey: Send + Sync + DynEq + DynHash { + fn dyn_clone(&self) -> Box; } - impl Clone for Box { + impl Clone for Box { fn clone(&self) -> Self { self.dyn_clone() } } - impl PartialEq for dyn DynSpecializerKeyTrait { + impl PartialEq for dyn DynSpecializerKey { fn eq(&self, other: &Self) -> bool { self.dyn_eq(other) } } - impl Eq for dyn DynSpecializerKeyTrait {} + impl Eq for dyn DynSpecializerKey {} - impl Hash for dyn DynSpecializerKeyTrait { + impl Hash for dyn DynSpecializerKey { fn hash(&self, state: &mut H) { self.dyn_hash(state); } } - impl DynSpecializerKeyTrait for T { - fn dyn_clone(&self) -> Box { + impl DynSpecializerKey for T { + fn dyn_clone(&self) -> Box { Box::new(self.clone()) } } #[derive(Clone)] - pub struct DynSpecializerKey(Box); + pub struct ErasedSpecializerKey(Box); - impl Eq for DynSpecializerKey {} + impl Eq for ErasedSpecializerKey {} - impl Hash for DynSpecializerKey { + impl Hash for ErasedSpecializerKey { fn hash(&self, state: &mut H) { self.0.hash(state); } } - impl PartialEq for DynSpecializerKey { + impl PartialEq for ErasedSpecializerKey { fn eq(&self, other: &Self) -> bool { self.0.dyn_eq(&other.0) } } - impl DynSpecializerKey { + impl ErasedSpecializerKey { pub fn new(key: impl SpecializerKey) -> Self { Self(Box::new(key)) } } - impl SpecializerKey for DynSpecializerKey { + impl SpecializerKey for ErasedSpecializerKey { const IS_CANONICAL: bool = false; - type Canonical = DynSpecializerKey; + type Canonical = ErasedSpecializerKey; } } From b3566b10b2c66dfce0f1ae51e224be2ef7cdb1b4 Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Fri, 18 Jul 2025 20:58:13 -0700 Subject: [PATCH 18/19] cleanup trait impls --- .../bevy_render/src/render_resource/specializer.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/crates/bevy_render/src/render_resource/specializer.rs b/crates/bevy_render/src/render_resource/specializer.rs index cb8c503da8264..e1adb2fecabb6 100644 --- a/crates/bevy_render/src/render_resource/specializer.rs +++ b/crates/bevy_render/src/render_resource/specializer.rs @@ -415,12 +415,6 @@ mod dyn_specializer { fn dyn_clone(&self) -> Box; } - impl Clone for Box { - fn clone(&self) -> Self { - self.dyn_clone() - } - } - impl PartialEq for dyn DynSpecializerKey { fn eq(&self, other: &Self) -> bool { self.dyn_eq(other) @@ -441,9 +435,14 @@ mod dyn_specializer { } } - #[derive(Clone)] pub struct ErasedSpecializerKey(Box); + impl Clone for ErasedSpecializerKey { + fn clone(&self) -> Self { + Self(self.0.dyn_clone()) + } + } + impl Eq for ErasedSpecializerKey {} impl Hash for ErasedSpecializerKey { From ea11726d399ae9784e91da8a41faba7ab91815dc Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Fri, 18 Jul 2025 21:08:38 -0700 Subject: [PATCH 19/19] comments --- crates/bevy_render/src/render_resource/specializer.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/bevy_render/src/render_resource/specializer.rs b/crates/bevy_render/src/render_resource/specializer.rs index e1adb2fecabb6..48a2ad52d94c4 100644 --- a/crates/bevy_render/src/render_resource/specializer.rs +++ b/crates/bevy_render/src/render_resource/specializer.rs @@ -253,7 +253,7 @@ macro_rules! impl_specialization_key_tuple { }; } -// TODO: How to we fake_variadics this? +// TODO: How do we fake_variadics this? all_tuples!(impl_specialization_key_tuple, 0, 12, T); /// A cache for specializable resources. For a given key type the resulting @@ -363,6 +363,7 @@ mod dyn_specializer { use super::{Canonical, Specializable, Specializer, SpecializerKey}; + /// A type-erased wrapper around a `Specializer` pub trait DynSpecializer: Any + Send + Sync + 'static { fn dyn_specialize( &self, @@ -424,7 +425,7 @@ mod dyn_specializer { impl Eq for dyn DynSpecializerKey {} impl Hash for dyn DynSpecializerKey { - fn hash(&self, state: &mut H) { + fn hash(&self, state: &mut H) { self.dyn_hash(state); } } @@ -435,6 +436,7 @@ mod dyn_specializer { } } + /// A type-erased wrapper around a `SpecializerKey` pub struct ErasedSpecializerKey(Box); impl Clone for ErasedSpecializerKey {