bevy/crates/bevy_render/src/render_resource/pipeline_specializer.rs
Patrick Walton 37522fd0ae
Micro-optimize queue_material_meshes, primarily to remove bit manipulation. (#12791)
This commit makes the following optimizations:

## `MeshPipelineKey`/`BaseMeshPipelineKey` split

`MeshPipelineKey` has been split into `BaseMeshPipelineKey`, which lives
in `bevy_render` and `MeshPipelineKey`, which lives in `bevy_pbr`.
Conceptually, `BaseMeshPipelineKey` is a superclass of
`MeshPipelineKey`. For `BaseMeshPipelineKey`, the bits start at the
highest (most significant) bit and grow downward toward the lowest bit;
for `MeshPipelineKey`, the bits start at the lowest bit and grow upward
toward the highest bit. This prevents them from colliding.

The goal of this is to avoid having to reassemble bits of the pipeline
key for every mesh every frame. Instead, we can just use a bitwise or
operation to combine the pieces that make up a `MeshPipelineKey`.

## `specialize_slow`

Previously, all of `specialize()` was marked as `#[inline]`. This
bloated `queue_material_meshes` unnecessarily, as a large chunk of it
ended up being a slow path that was rarely hit. This commit refactors
the function to move the slow path to `specialize_slow()`.

Together, these two changes shave about 5% off `queue_material_meshes`:

![Screenshot 2024-03-29
130002](https://github.com/bevyengine/bevy/assets/157897/a7e5a994-a807-4328-b314-9003429dcdd2)

## Migration Guide

- The `primitive_topology` field on `GpuMesh` is now an accessor method:
`GpuMesh::primitive_topology()`.
- For performance reasons, `MeshPipelineKey` has been split into
`BaseMeshPipelineKey`, which lives in `bevy_render`, and
`MeshPipelineKey`, which lives in `bevy_pbr`. These two should be
combined with bitwise-or to produce the final `MeshPipelineKey`.
2024-04-01 21:58:53 +00:00

189 lines
6.6 KiB
Rust

use crate::mesh::MeshVertexBufferLayoutRef;
use crate::render_resource::CachedComputePipelineId;
use crate::{
mesh::MissingVertexAttributeError,
render_resource::{
CachedRenderPipelineId, ComputePipelineDescriptor, PipelineCache, RenderPipelineDescriptor,
VertexBufferLayout,
},
};
use bevy_ecs::system::Resource;
use bevy_utils::hashbrown::hash_map::VacantEntry;
use bevy_utils::{default, hashbrown::hash_map::RawEntryMut, tracing::error, Entry, HashMap};
use std::{fmt::Debug, hash::Hash};
use thiserror::Error;
pub trait SpecializedRenderPipeline {
type Key: Clone + Hash + PartialEq + Eq;
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor;
}
#[derive(Resource)]
pub struct SpecializedRenderPipelines<S: SpecializedRenderPipeline> {
cache: HashMap<S::Key, CachedRenderPipelineId>,
}
impl<S: SpecializedRenderPipeline> Default for SpecializedRenderPipelines<S> {
fn default() -> Self {
Self { cache: default() }
}
}
impl<S: SpecializedRenderPipeline> SpecializedRenderPipelines<S> {
pub fn specialize(
&mut self,
cache: &PipelineCache,
specialize_pipeline: &S,
key: S::Key,
) -> CachedRenderPipelineId {
*self.cache.entry(key.clone()).or_insert_with(|| {
let descriptor = specialize_pipeline.specialize(key);
cache.queue_render_pipeline(descriptor)
})
}
}
pub trait SpecializedComputePipeline {
type Key: Clone + Hash + PartialEq + Eq;
fn specialize(&self, key: Self::Key) -> ComputePipelineDescriptor;
}
#[derive(Resource)]
pub struct SpecializedComputePipelines<S: SpecializedComputePipeline> {
cache: HashMap<S::Key, CachedComputePipelineId>,
}
impl<S: SpecializedComputePipeline> Default for SpecializedComputePipelines<S> {
fn default() -> Self {
Self { cache: default() }
}
}
impl<S: SpecializedComputePipeline> SpecializedComputePipelines<S> {
pub fn specialize(
&mut self,
cache: &PipelineCache,
specialize_pipeline: &S,
key: S::Key,
) -> CachedComputePipelineId {
*self.cache.entry(key.clone()).or_insert_with(|| {
let descriptor = specialize_pipeline.specialize(key);
cache.queue_compute_pipeline(descriptor)
})
}
}
pub trait SpecializedMeshPipeline {
type Key: Clone + Hash + PartialEq + Eq;
fn specialize(
&self,
key: Self::Key,
layout: &MeshVertexBufferLayoutRef,
) -> Result<RenderPipelineDescriptor, SpecializedMeshPipelineError>;
}
#[derive(Resource)]
pub struct SpecializedMeshPipelines<S: SpecializedMeshPipeline> {
mesh_layout_cache: HashMap<(MeshVertexBufferLayoutRef, S::Key), CachedRenderPipelineId>,
vertex_layout_cache: VertexLayoutCache<S>,
}
pub type VertexLayoutCache<S> = HashMap<
VertexBufferLayout,
HashMap<<S as SpecializedMeshPipeline>::Key, CachedRenderPipelineId>,
>;
impl<S: SpecializedMeshPipeline> Default for SpecializedMeshPipelines<S> {
fn default() -> Self {
Self {
mesh_layout_cache: Default::default(),
vertex_layout_cache: Default::default(),
}
}
}
impl<S: SpecializedMeshPipeline> SpecializedMeshPipelines<S> {
#[inline]
pub fn specialize(
&mut self,
cache: &PipelineCache,
specialize_pipeline: &S,
key: S::Key,
layout: &MeshVertexBufferLayoutRef,
) -> Result<CachedRenderPipelineId, SpecializedMeshPipelineError> {
return match self.mesh_layout_cache.entry((layout.clone(), key.clone())) {
Entry::Occupied(entry) => Ok(*entry.into_mut()),
Entry::Vacant(entry) => specialize_slow(
&mut self.vertex_layout_cache,
cache,
specialize_pipeline,
key,
layout,
entry,
),
};
#[cold]
fn specialize_slow<S>(
vertex_layout_cache: &mut VertexLayoutCache<S>,
cache: &PipelineCache,
specialize_pipeline: &S,
key: S::Key,
layout: &MeshVertexBufferLayoutRef,
entry: VacantEntry<(MeshVertexBufferLayoutRef, S::Key), CachedRenderPipelineId>,
) -> Result<CachedRenderPipelineId, SpecializedMeshPipelineError>
where
S: SpecializedMeshPipeline,
{
let descriptor = specialize_pipeline
.specialize(key.clone(), layout)
.map_err(|mut err| {
{
let SpecializedMeshPipelineError::MissingVertexAttribute(err) = &mut err;
err.pipeline_type = Some(std::any::type_name::<S>());
}
err
})?;
// Different MeshVertexBufferLayouts can produce the same final VertexBufferLayout
// We want compatible vertex buffer layouts to use the same pipelines, so we must "deduplicate" them
let layout_map = match vertex_layout_cache
.raw_entry_mut()
.from_key(&descriptor.vertex.buffers[0])
{
RawEntryMut::Occupied(entry) => entry.into_mut(),
RawEntryMut::Vacant(entry) => {
entry
.insert(descriptor.vertex.buffers[0].clone(), Default::default())
.1
}
};
Ok(*entry.insert(match layout_map.entry(key) {
Entry::Occupied(entry) => {
if cfg!(debug_assertions) {
let stored_descriptor = cache.get_render_pipeline_descriptor(*entry.get());
if stored_descriptor != &descriptor {
error!(
"The cached pipeline descriptor for {} is not \
equal to the generated descriptor for the given key. \
This means the SpecializePipeline implementation uses \
unused' MeshVertexBufferLayout information to specialize \
the pipeline. This is not allowed because it would invalidate \
the pipeline cache.",
std::any::type_name::<S>()
);
}
}
*entry.into_mut()
}
Entry::Vacant(entry) => *entry.insert(cache.queue_render_pipeline(descriptor)),
}))
}
}
}
#[derive(Error, Debug)]
pub enum SpecializedMeshPipelineError {
#[error(transparent)]
MissingVertexAttribute(#[from] MissingVertexAttributeError),
}