bevy/crates/bevy_pbr/src/render/mesh_view_bindings.rs

504 lines
18 KiB
Rust
Raw Normal View History

Variable `MeshPipeline` View Bind Group Layout (#10156) # Objective This PR aims to make it so that we don't accidentally go over `MAX_TEXTURE_IMAGE_UNITS` (in WebGL) or `maxSampledTexturesPerShaderStage` (in WebGPU), giving us some extra leeway to add more view bind group textures. (This PR is extracted from—and unblocks—#8015) ## Solution - We replace the existing `view_layout` and `view_layout_multisampled` pair with an array of 32 bind group layouts, generated ahead of time; - For now, these layouts cover all the possible combinations of: `multisampled`, `depth_prepass`, `normal_prepass`, `motion_vector_prepass` and `deferred_prepass`: - In the future, as @JMS55 pointed out, we can likely take out `motion_vector_prepass` and `deferred_prepass`, as these are not really needed for the mesh pipeline and can use separate pipelines. This would bring the possible combinations down to 8; - We can also add more "optional" textures as they become needed, allowing the engine to scale to a wider variety of use cases in lower end/web environments (e.g. some apps might just want normal and depth prepasses, others might only want light probes), while still keeping a high ceiling for high end native environments where more textures are supported. - While preallocating bind group layouts is relatively cheap, the number of combinations grows exponentially, so we should likely limit ourselves to something like at most 256–1024 total layouts until we find a better solution (like generating them lazily) - To make this mechanism a little bit more explicit/discoverable, so that compatibility with WebGPU/WebGL is not broken by accident, we add a `MESH_PIPELINE_VIEW_LAYOUT_SAFE_MAX_TEXTURES` const and warn whenever the number of textures in the layout crosses it. - The warning is gated by `#[cfg(debug_assertions)]` and not issued in release builds; - We're counting the actual textures in the bind group layout instead of using some roundabout metric so it should be accurate; - Right now `MESH_PIPELINE_VIEW_LAYOUT_SAFE_MAX_TEXTURES` is set to 10 in order to leave 6 textures free for other groups; - Currently there's no combination that would cause us to go over the limit, but that will change once #8015 lands. --- ## Changelog - `MeshPipeline` view bind group layouts now vary based on the current multisampling and prepass states, saving a couple of texture binding entries when prepasses are not in use. ## Migration Guide - `MeshPipeline::view_layout` and `MeshPipeline::view_layout_multisampled` have been replaced with a private array to accomodate for variable view bind group layouts. To obtain a view bind group layout for the current pipeline state, use the new `MeshPipeline::get_view_layout()` or `MeshPipeline::get_view_layout_from_key()` methods.
2023-10-21 11:19:44 +00:00
use std::array;
use bevy_core_pipeline::{
prepass::ViewPrepassTextures,
tonemapping::{
get_lut_bind_group_layout_entries, get_lut_bindings, Tonemapping, TonemappingLuts,
},
};
use bevy_ecs::{
component::Component,
entity::Entity,
system::{Commands, Query, Res},
};
use bevy_render::{
globals::{GlobalsBuffer, GlobalsUniform},
render_asset::RenderAssets,
render_resource::{
BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, BindGroupLayoutDescriptor,
BindGroupLayoutEntry, BindingResource, BindingType, BufferBindingType, SamplerBindingType,
ShaderStages, ShaderType, TextureFormat, TextureSampleType, TextureViewDimension,
},
renderer::RenderDevice,
texture::{BevyDefault, FallbackImageCubemap, FallbackImageMsaa, Image},
view::{Msaa, ViewUniform, ViewUniforms},
};
use crate::{
environment_map, prepass, EnvironmentMapLight, FogMeta, GlobalLightMeta, GpuFog, GpuLights,
GpuPointLights, LightMeta, MeshPipeline, MeshPipelineKey, ScreenSpaceAmbientOcclusionTextures,
ShadowSamplers, ViewClusterBindings, ViewShadowBindings,
};
#[derive(Clone)]
pub struct MeshPipelineViewLayout {
pub bind_group_layout: BindGroupLayout,
#[cfg(debug_assertions)]
pub texture_count: usize,
}
bitflags::bitflags! {
/// A key that uniquely identifies a [`MeshPipelineViewLayout`].
///
/// Used to generate all possible layouts for the mesh pipeline in [`generate_view_layouts`],
/// so special care must be taken to not add too many flags, as the number of possible layouts
/// will grow exponentially.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[repr(transparent)]
pub struct MeshPipelineViewLayoutKey: u32 {
const MULTISAMPLED = (1 << 0);
const DEPTH_PREPASS = (1 << 1);
const NORMAL_PREPASS = (1 << 2);
const MOTION_VECTOR_PREPASS = (1 << 3);
const DEFERRED_PREPASS = (1 << 4);
}
}
impl MeshPipelineViewLayoutKey {
// The number of possible layouts
pub const COUNT: usize = Self::all().bits() as usize + 1;
/// Builds a unique label for each layout based on the flags
pub fn label(&self) -> String {
use MeshPipelineViewLayoutKey as Key;
format!(
"mesh_view_layout{}{}{}{}{}",
self.contains(Key::MULTISAMPLED)
.then_some("_multisampled")
.unwrap_or_default(),
self.contains(Key::DEPTH_PREPASS)
.then_some("_depth")
.unwrap_or_default(),
self.contains(Key::NORMAL_PREPASS)
.then_some("_normal")
.unwrap_or_default(),
self.contains(Key::MOTION_VECTOR_PREPASS)
.then_some("_motion")
.unwrap_or_default(),
self.contains(Key::DEFERRED_PREPASS)
.then_some("_deferred")
.unwrap_or_default(),
)
}
}
impl From<MeshPipelineKey> for MeshPipelineViewLayoutKey {
fn from(value: MeshPipelineKey) -> Self {
let mut result = MeshPipelineViewLayoutKey::empty();
if value.msaa_samples() > 1 {
result |= MeshPipelineViewLayoutKey::MULTISAMPLED;
}
if value.contains(MeshPipelineKey::DEPTH_PREPASS) {
result |= MeshPipelineViewLayoutKey::DEPTH_PREPASS;
}
if value.contains(MeshPipelineKey::NORMAL_PREPASS) {
result |= MeshPipelineViewLayoutKey::NORMAL_PREPASS;
}
if value.contains(MeshPipelineKey::MOTION_VECTOR_PREPASS) {
result |= MeshPipelineViewLayoutKey::MOTION_VECTOR_PREPASS;
}
if value.contains(MeshPipelineKey::DEFERRED_PREPASS) {
result |= MeshPipelineViewLayoutKey::DEFERRED_PREPASS;
}
result
}
}
impl From<Msaa> for MeshPipelineViewLayoutKey {
fn from(value: Msaa) -> Self {
let mut result = MeshPipelineViewLayoutKey::empty();
if value.samples() > 1 {
result |= MeshPipelineViewLayoutKey::MULTISAMPLED;
}
result
}
}
impl From<Option<&ViewPrepassTextures>> for MeshPipelineViewLayoutKey {
fn from(value: Option<&ViewPrepassTextures>) -> Self {
let mut result = MeshPipelineViewLayoutKey::empty();
if let Some(prepass_textures) = value {
if prepass_textures.depth.is_some() {
result |= MeshPipelineViewLayoutKey::DEPTH_PREPASS;
}
if prepass_textures.normal.is_some() {
result |= MeshPipelineViewLayoutKey::NORMAL_PREPASS;
}
if prepass_textures.motion_vectors.is_some() {
result |= MeshPipelineViewLayoutKey::MOTION_VECTOR_PREPASS;
}
if prepass_textures.deferred.is_some() {
result |= MeshPipelineViewLayoutKey::DEFERRED_PREPASS;
}
}
result
}
}
/// Returns the appropriate bind group layout vec based on the parameters
fn layout_entries(
clustered_forward_buffer_binding_type: BufferBindingType,
layout_key: MeshPipelineViewLayoutKey,
) -> Vec<BindGroupLayoutEntry> {
let mut entries = vec![
// View
BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
ty: BindingType::Buffer {
ty: BufferBindingType::Uniform,
has_dynamic_offset: true,
min_binding_size: Some(ViewUniform::min_size()),
},
count: None,
},
// Lights
BindGroupLayoutEntry {
binding: 1,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Buffer {
ty: BufferBindingType::Uniform,
has_dynamic_offset: true,
min_binding_size: Some(GpuLights::min_size()),
},
count: None,
},
// Point Shadow Texture Cube Array
BindGroupLayoutEntry {
binding: 2,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Texture {
multisampled: false,
sample_type: TextureSampleType::Depth,
#[cfg(any(not(feature = "webgl"), not(target_arch = "wasm32")))]
view_dimension: TextureViewDimension::CubeArray,
#[cfg(all(feature = "webgl", target_arch = "wasm32"))]
view_dimension: TextureViewDimension::Cube,
},
count: None,
},
// Point Shadow Texture Array Sampler
BindGroupLayoutEntry {
binding: 3,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Sampler(SamplerBindingType::Comparison),
count: None,
},
// Directional Shadow Texture Array
BindGroupLayoutEntry {
binding: 4,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Texture {
multisampled: false,
sample_type: TextureSampleType::Depth,
#[cfg(any(not(feature = "webgl"), not(target_arch = "wasm32")))]
view_dimension: TextureViewDimension::D2Array,
#[cfg(all(feature = "webgl", target_arch = "wasm32"))]
view_dimension: TextureViewDimension::D2,
},
count: None,
},
// Directional Shadow Texture Array Sampler
BindGroupLayoutEntry {
binding: 5,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Sampler(SamplerBindingType::Comparison),
count: None,
},
// PointLights
BindGroupLayoutEntry {
binding: 6,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Buffer {
ty: clustered_forward_buffer_binding_type,
has_dynamic_offset: false,
min_binding_size: Some(GpuPointLights::min_size(
clustered_forward_buffer_binding_type,
)),
},
count: None,
},
// ClusteredLightIndexLists
BindGroupLayoutEntry {
binding: 7,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Buffer {
ty: clustered_forward_buffer_binding_type,
has_dynamic_offset: false,
min_binding_size: Some(ViewClusterBindings::min_size_cluster_light_index_lists(
clustered_forward_buffer_binding_type,
)),
},
count: None,
},
// ClusterOffsetsAndCounts
BindGroupLayoutEntry {
binding: 8,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Buffer {
ty: clustered_forward_buffer_binding_type,
has_dynamic_offset: false,
min_binding_size: Some(ViewClusterBindings::min_size_cluster_offsets_and_counts(
clustered_forward_buffer_binding_type,
)),
},
count: None,
},
// Globals
BindGroupLayoutEntry {
binding: 9,
visibility: ShaderStages::VERTEX_FRAGMENT,
ty: BindingType::Buffer {
ty: BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: Some(GlobalsUniform::min_size()),
},
count: None,
},
// Fog
BindGroupLayoutEntry {
binding: 10,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Buffer {
ty: BufferBindingType::Uniform,
has_dynamic_offset: true,
min_binding_size: Some(GpuFog::min_size()),
},
count: None,
},
// Screen space ambient occlusion texture
BindGroupLayoutEntry {
binding: 11,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Texture {
multisampled: false,
sample_type: TextureSampleType::Float { filterable: false },
view_dimension: TextureViewDimension::D2,
},
count: None,
},
];
// EnvironmentMapLight
let environment_map_entries = environment_map::get_bind_group_layout_entries([12, 13, 14]);
entries.extend_from_slice(&environment_map_entries);
// Tonemapping
let tonemapping_lut_entries = get_lut_bind_group_layout_entries([15, 16]);
entries.extend_from_slice(&tonemapping_lut_entries);
if cfg!(any(not(feature = "webgl"), not(target_arch = "wasm32")))
|| (cfg!(all(feature = "webgl", target_arch = "wasm32"))
&& !layout_key.contains(MeshPipelineViewLayoutKey::MULTISAMPLED))
{
entries.extend_from_slice(&prepass::get_bind_group_layout_entries(
[17, 18, 19, 20],
layout_key,
));
}
entries
}
/// Generates all possible view layouts for the mesh pipeline, based on all combinations of
/// [`MeshPipelineViewLayoutKey`] flags.
pub fn generate_view_layouts(
render_device: &RenderDevice,
clustered_forward_buffer_binding_type: BufferBindingType,
) -> [MeshPipelineViewLayout; MeshPipelineViewLayoutKey::COUNT] {
array::from_fn(|i| {
let key = MeshPipelineViewLayoutKey::from_bits_truncate(i as u32);
let entries = layout_entries(clustered_forward_buffer_binding_type, key);
#[cfg(debug_assertions)]
let texture_count: usize = entries
.iter()
.filter(|entry| matches!(entry.ty, BindingType::Texture { .. }))
.count();
MeshPipelineViewLayout {
bind_group_layout: render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
label: Some(key.label().as_str()),
entries: &entries,
}),
#[cfg(debug_assertions)]
texture_count,
}
})
}
#[derive(Component)]
pub struct MeshViewBindGroup {
pub value: BindGroup,
}
#[allow(clippy::too_many_arguments)]
pub fn prepare_mesh_view_bind_groups(
mut commands: Commands,
render_device: Res<RenderDevice>,
mesh_pipeline: Res<MeshPipeline>,
shadow_samplers: Res<ShadowSamplers>,
light_meta: Res<LightMeta>,
global_light_meta: Res<GlobalLightMeta>,
fog_meta: Res<FogMeta>,
view_uniforms: Res<ViewUniforms>,
views: Query<(
Entity,
&ViewShadowBindings,
&ViewClusterBindings,
Option<&ScreenSpaceAmbientOcclusionTextures>,
Option<&ViewPrepassTextures>,
Option<&EnvironmentMapLight>,
&Tonemapping,
)>,
(images, mut fallback_images, fallback_cubemap): (
Res<RenderAssets<Image>>,
FallbackImageMsaa,
Res<FallbackImageCubemap>,
),
msaa: Res<Msaa>,
globals_buffer: Res<GlobalsBuffer>,
tonemapping_luts: Res<TonemappingLuts>,
) {
if let (
Some(view_binding),
Some(light_binding),
Some(point_light_binding),
Some(globals),
Some(fog_binding),
) = (
view_uniforms.uniforms.binding(),
light_meta.view_gpu_lights.binding(),
global_light_meta.gpu_point_lights.binding(),
globals_buffer.buffer.binding(),
fog_meta.gpu_fogs.binding(),
) {
for (
entity,
view_shadow_bindings,
view_cluster_bindings,
ssao_textures,
prepass_textures,
environment_map,
tonemapping,
) in &views
{
let fallback_ssao = fallback_images
.image_for_samplecount(1, TextureFormat::bevy_default())
.texture_view
.clone();
let layout = &mesh_pipeline.get_view_layout(
MeshPipelineViewLayoutKey::from(*msaa)
| MeshPipelineViewLayoutKey::from(prepass_textures),
);
let mut entries = vec![
BindGroupEntry {
binding: 0,
resource: view_binding.clone(),
},
BindGroupEntry {
binding: 1,
resource: light_binding.clone(),
},
BindGroupEntry {
binding: 2,
resource: BindingResource::TextureView(
&view_shadow_bindings.point_light_depth_texture_view,
),
},
BindGroupEntry {
binding: 3,
resource: BindingResource::Sampler(&shadow_samplers.point_light_sampler),
},
BindGroupEntry {
binding: 4,
resource: BindingResource::TextureView(
&view_shadow_bindings.directional_light_depth_texture_view,
),
},
BindGroupEntry {
binding: 5,
resource: BindingResource::Sampler(&shadow_samplers.directional_light_sampler),
},
BindGroupEntry {
binding: 6,
resource: point_light_binding.clone(),
},
BindGroupEntry {
binding: 7,
resource: view_cluster_bindings.light_index_lists_binding().unwrap(),
},
BindGroupEntry {
binding: 8,
resource: view_cluster_bindings.offsets_and_counts_binding().unwrap(),
},
BindGroupEntry {
binding: 9,
resource: globals.clone(),
},
BindGroupEntry {
binding: 10,
resource: fog_binding.clone(),
},
BindGroupEntry {
binding: 11,
resource: BindingResource::TextureView(
ssao_textures
.map(|t| &t.screen_space_ambient_occlusion_texture.default_view)
.unwrap_or(&fallback_ssao),
),
},
];
let env_map = environment_map::get_bindings(
environment_map,
&images,
&fallback_cubemap,
[12, 13, 14],
);
entries.extend_from_slice(&env_map);
let tonemapping_luts =
get_lut_bindings(&images, &tonemapping_luts, tonemapping, [15, 16]);
entries.extend_from_slice(&tonemapping_luts);
let label = Some("mesh_view_bind_group");
// When using WebGL, we can't have a depth texture with multisampling
let prepass_bindings = if cfg!(any(not(feature = "webgl"), not(target_arch = "wasm32")))
|| (cfg!(all(feature = "webgl", target_arch = "wasm32")) && msaa.samples() == 1)
{
Some(prepass::get_bindings(prepass_textures))
} else {
None
};
// This if statement is here to make the borrow checker happy.
// Ideally we could just have `entries.extend_from_slice(&prepass_bindings.get_entries([17, 18, 19, 20]));`
// in the existing if statement above, but that either doesn't allow `prepass_bindings` to live long enough,
// as its used when creating the bind group at the end of the function, or causes a `cannot move out of` error.
if let Some(prepass_bindings) = &prepass_bindings {
entries.extend_from_slice(&prepass_bindings.get_entries([17, 18, 19, 20]));
}
commands.entity(entity).insert(MeshViewBindGroup {
value: render_device.create_bind_group(&BindGroupDescriptor {
entries: &entries,
label,
layout,
}),
});
}
}
}