Path: blob/main/crates/bevy_render/src/render_resource/specializer.rs
6596 views
use super::{1CachedComputePipelineId, CachedRenderPipelineId, ComputePipeline, ComputePipelineDescriptor,2PipelineCache, RenderPipeline, RenderPipelineDescriptor,3};4use bevy_ecs::error::BevyError;5use bevy_platform::{6collections::{7hash_map::{Entry, VacantEntry},8HashMap,9},10hash::FixedHasher,11};12use core::{hash::Hash, marker::PhantomData};13use tracing::error;14use variadics_please::all_tuples;1516pub use bevy_render_macros::{Specializer, SpecializerKey};1718/// Defines a type that is able to be "specialized" and cached by creating and transforming19/// its descriptor type. This is implemented for [`RenderPipeline`] and [`ComputePipeline`], and20/// likely will not have much utility for other types.21///22/// See docs on [`Specializer`] for more info.23pub trait Specializable {24type Descriptor: PartialEq + Clone + Send + Sync;25type CachedId: Clone + Send + Sync;26fn queue(pipeline_cache: &PipelineCache, descriptor: Self::Descriptor) -> Self::CachedId;27fn get_descriptor(pipeline_cache: &PipelineCache, id: Self::CachedId) -> &Self::Descriptor;28}2930impl Specializable for RenderPipeline {31type Descriptor = RenderPipelineDescriptor;32type CachedId = CachedRenderPipelineId;3334fn queue(pipeline_cache: &PipelineCache, descriptor: Self::Descriptor) -> Self::CachedId {35pipeline_cache.queue_render_pipeline(descriptor)36}3738fn get_descriptor(39pipeline_cache: &PipelineCache,40id: CachedRenderPipelineId,41) -> &Self::Descriptor {42pipeline_cache.get_render_pipeline_descriptor(id)43}44}4546impl Specializable for ComputePipeline {47type Descriptor = ComputePipelineDescriptor;4849type CachedId = CachedComputePipelineId;5051fn queue(pipeline_cache: &PipelineCache, descriptor: Self::Descriptor) -> Self::CachedId {52pipeline_cache.queue_compute_pipeline(descriptor)53}5455fn get_descriptor(56pipeline_cache: &PipelineCache,57id: CachedComputePipelineId,58) -> &Self::Descriptor {59pipeline_cache.get_compute_pipeline_descriptor(id)60}61}6263/// Defines a type capable of "specializing" values of a type T.64///65/// Specialization is the process of generating variants of a type T66/// from small hashable keys, and specializers themselves can be67/// thought of as [pure functions] from the key type to `T`, that68/// [memoize] their results based on the key.69///70/// <div class="warning">71/// Because specialization is designed for use with render and compute72/// pipelines, specializers act on <i>descriptors</i> of <code>T</code> rather73/// than produce <code>T</code> itself, but the above comparison is still valid.74/// </div>75///76/// Since compiling render and compute pipelines can be so slow,77/// specialization allows a Bevy app to detect when it would compile78/// a duplicate pipeline and reuse what's already in the cache. While79/// pipelines could all be memoized hashing each whole descriptor, this80/// would be much slower and could still create duplicates. In contrast,81/// memoizing groups of *related* pipelines based on a small hashable82/// key is much faster. See the docs on [`SpecializerKey`] for more info.83///84/// ## Composing Specializers85///86/// This trait can be derived with `#[derive(Specializer)]` for structs whose87/// fields all implement [`Specializer`]. This allows for composing multiple88/// specializers together, and makes encapsulation and separating concerns89/// between specializers much nicer. One could make individual specializers90/// for common operations and place them in entirely separate modules, then91/// compose them together with a single `#[derive]`92///93/// ```rust94/// # use bevy_ecs::error::BevyError;95/// # use bevy_render::render_resource::Specializer;96/// # use bevy_render::render_resource::SpecializerKey;97/// # use bevy_render::render_resource::RenderPipeline;98/// # use bevy_render::render_resource::RenderPipelineDescriptor;99/// struct A;100/// struct B;101/// #[derive(Copy, Clone, PartialEq, Eq, Hash, SpecializerKey)]102/// struct BKey { contrived_number: u32 };103///104/// impl Specializer<RenderPipeline> for A {105/// type Key = ();106///107/// fn specialize(108/// &self,109/// key: (),110/// descriptor: &mut RenderPipelineDescriptor111/// ) -> Result<(), BevyError> {112/// # let _ = descriptor;113/// // mutate the descriptor here114/// Ok(key)115/// }116/// }117///118/// impl Specializer<RenderPipeline> for B {119/// type Key = BKey;120///121/// fn specialize(122/// &self,123/// key: BKey,124/// descriptor: &mut RenderPipelineDescriptor125/// ) -> Result<BKey, BevyError> {126/// # let _ = descriptor;127/// // mutate the descriptor here128/// Ok(key)129/// }130/// }131///132/// #[derive(Specializer)]133/// #[specialize(RenderPipeline)]134/// struct C {135/// #[key(default)]136/// a: A,137/// b: B,138/// }139///140/// /*141/// The generated implementation:142/// impl Specializer<RenderPipeline> for C {143/// type Key = BKey;144/// fn specialize(145/// &self,146/// key: Self::Key,147/// descriptor: &mut RenderPipelineDescriptor148/// ) -> Result<Canonical<Self::Key>, BevyError> {149/// let _ = self.a.specialize((), descriptor);150/// let key = self.b.specialize(key, descriptor);151/// Ok(key)152/// }153/// }154/// */155/// ```156///157/// The key type for a composed specializer will be a tuple of the keys158/// of each field, and their specialization logic will be applied in field159/// order. Since derive macros can't have generic parameters, the derive macro160/// requires an additional `#[specialize(..targets)]` attribute to specify a161/// list of types to target for the implementation. `#[specialize(all)]` is162/// also allowed, and will generate a fully generic implementation at the cost163/// of slightly worse error messages.164///165/// Additionally, each field can optionally take a `#[key]` attribute to166/// specify a "key override". This will hide that field's key from being167/// exposed by the wrapper, and always use the value given by the attribute.168/// Values for this attribute may either be `default` which will use the key's169/// [`Default`] implementation, or a valid rust expression of the key type.170///171/// [pure functions]: https://en.wikipedia.org/wiki/Pure_function172/// [memoize]: https://en.wikipedia.org/wiki/Memoization173pub trait Specializer<T: Specializable>: Send + Sync + 'static {174type Key: SpecializerKey;175fn specialize(176&self,177key: Self::Key,178descriptor: &mut T::Descriptor,179) -> Result<Canonical<Self::Key>, BevyError>;180}181182// TODO: update docs for `SpecializerKey` with a more concrete example183// once we've migrated mesh layout specialization184185/// Defines a type that is able to be used as a key for [`Specializer`]s186///187/// <div class = "warning">188/// <strong>Most types should implement this trait with the included derive macro.</strong> <br/>189/// This generates a "canonical" key type, with <code>IS_CANONICAL = true</code>, and <code>Canonical = Self</code>190/// </div>191///192/// ## What's a "canonical" key?193///194/// The specialization API memoizes pipelines based on the hash of each key, but this195/// can still produce duplicates. For example, if one used a list of vertex attributes196/// as a key, even if all the same attributes were present they could be in any order.197/// In each case, though the keys would be "different" they would produce the same198/// pipeline.199///200/// To address this, during specialization keys are processed into a [canonical]201/// (or "standard") form that represents the actual descriptor that was produced.202/// In the previous example, that would be the final `VertexBufferLayout` contained203/// by the pipeline descriptor. This new key is used by [`Variants`] to204/// perform additional checks for duplicates, but only if required. If a key is205/// canonical from the start, then there's no need.206///207/// For implementors: the main property of a canonical key is that if two keys hash208/// differently, they should nearly always produce different descriptors.209///210/// [canonical]: https://en.wikipedia.org/wiki/Canonicalization211pub trait SpecializerKey: Clone + Hash + Eq {212/// Denotes whether this key is canonical or not. This should only be `true`213/// if and only if `Canonical = Self`.214const IS_CANONICAL: bool;215216/// The canonical key type to convert this into during specialization.217type Canonical: Hash + Eq;218}219220pub type Canonical<T> = <T as SpecializerKey>::Canonical;221222impl<T: Specializable> Specializer<T> for () {223type Key = ();224225fn specialize(226&self,227_key: Self::Key,228_descriptor: &mut T::Descriptor,229) -> Result<(), BevyError> {230Ok(())231}232}233234impl<T: Specializable, V: Send + Sync + 'static> Specializer<T> for PhantomData<V> {235type Key = ();236237fn specialize(238&self,239_key: Self::Key,240_descriptor: &mut T::Descriptor,241) -> Result<(), BevyError> {242Ok(())243}244}245246macro_rules! impl_specialization_key_tuple {247($(#[$meta:meta])* $($T:ident),*) => {248$(#[$meta])*249impl <$($T: SpecializerKey),*> SpecializerKey for ($($T,)*) {250const IS_CANONICAL: bool = true $(&& <$T as SpecializerKey>::IS_CANONICAL)*;251type Canonical = ($(Canonical<$T>,)*);252}253};254}255256all_tuples!(257#[doc(fake_variadic)]258impl_specialization_key_tuple,2590,26012,261T262);263264/// A cache for variants of a resource type created by a specializer.265/// At most one resource will be created for each key.266pub struct Variants<T: Specializable, S: Specializer<T>> {267specializer: S,268base_descriptor: T::Descriptor,269primary_cache: HashMap<S::Key, T::CachedId>,270secondary_cache: HashMap<Canonical<S::Key>, T::CachedId>,271}272273impl<T: Specializable, S: Specializer<T>> Variants<T, S> {274/// Creates a new [`Variants`] from a [`Specializer`] and a base descriptor.275#[inline]276pub fn new(specializer: S, base_descriptor: T::Descriptor) -> Self {277Self {278specializer,279base_descriptor,280primary_cache: Default::default(),281secondary_cache: Default::default(),282}283}284285/// Specializes a resource given the [`Specializer`]'s key type.286#[inline]287pub fn specialize(288&mut self,289pipeline_cache: &PipelineCache,290key: S::Key,291) -> Result<T::CachedId, BevyError> {292let entry = self.primary_cache.entry(key.clone());293match entry {294Entry::Occupied(entry) => Ok(entry.get().clone()),295Entry::Vacant(entry) => Self::specialize_slow(296&self.specializer,297self.base_descriptor.clone(),298pipeline_cache,299key,300entry,301&mut self.secondary_cache,302),303}304}305306#[cold]307fn specialize_slow(308specializer: &S,309base_descriptor: T::Descriptor,310pipeline_cache: &PipelineCache,311key: S::Key,312primary_entry: VacantEntry<S::Key, T::CachedId, FixedHasher>,313secondary_cache: &mut HashMap<Canonical<S::Key>, T::CachedId>,314) -> Result<T::CachedId, BevyError> {315let mut descriptor = base_descriptor.clone();316let canonical_key = specializer.specialize(key.clone(), &mut descriptor)?;317318// if the whole key is canonical, the secondary cache isn't needed.319if <S::Key as SpecializerKey>::IS_CANONICAL {320return Ok(primary_entry321.insert(<T as Specializable>::queue(pipeline_cache, descriptor))322.clone());323}324325let id = match secondary_cache.entry(canonical_key) {326Entry::Occupied(entry) => {327if cfg!(debug_assertions) {328let stored_descriptor =329<T as Specializable>::get_descriptor(pipeline_cache, entry.get().clone());330if &descriptor != stored_descriptor {331error!(332"Invalid Specializer<{}> impl for {}: the cached descriptor \333is not equal to the generated descriptor for the given key. \334This means the Specializer implementation uses unused information \335from the key to specialize the pipeline. This is not allowed \336because it would invalidate the cache.",337core::any::type_name::<T>(),338core::any::type_name::<S>()339);340}341}342entry.into_mut().clone()343}344Entry::Vacant(entry) => entry345.insert(<T as Specializable>::queue(pipeline_cache, descriptor))346.clone(),347};348349primary_entry.insert(id.clone());350Ok(id)351}352}353354355