From 35826be6f7aaff3b141d90cb4d5e5e460116b063 Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Mon, 16 Dec 2024 15:37:06 -0800 Subject: [PATCH] Implement bindless lightmaps. (#16653) This commit allows Bevy to bind 16 lightmaps at a time, if the current platform supports bindless textures. Naturally, if bindless textures aren't supported, Bevy falls back to binding only a single lightmap at a time. As lightmaps are usually heavily atlased, I doubt many scenes will use more than 16 lightmap textures. This has little performance impact now, but it's desirable for us to reap the benefits of multidraw and bindless textures on scenes that use lightmaps. Otherwise, we might have to break batches in order to switch those lightmaps. Additionally, this PR slightly reduces the cost of binning because it makes the lightmap index in `Opaque3dBinKey` 32 bits instead of an `AssetId`. ## Migration Guide * The `Opaque3dBinKey::lightmap_image` field is now `Opaque3dBinKey::lightmap_slab`, which is a lightweight identifier for an entire binding array of lightmaps. --- assets/shaders/bindless_material.wgsl | 2 +- crates/bevy_core_pipeline/src/core_3d/mod.rs | 10 +- crates/bevy_pbr/src/lightmap/lightmap.wgsl | 18 +- crates/bevy_pbr/src/lightmap/mod.rs | 382 ++++++++++++++++-- crates/bevy_pbr/src/material.rs | 9 +- crates/bevy_pbr/src/material_bind_groups.rs | 10 +- crates/bevy_pbr/src/render/mesh.rs | 74 ++-- crates/bevy_pbr/src/render/mesh_bindings.rs | 109 +++-- .../bevy_pbr/src/render/mesh_preprocess.wgsl | 9 +- crates/bevy_pbr/src/render/mesh_types.wgsl | 5 +- .../bevy_pbr/src/render/parallax_mapping.wgsl | 2 +- crates/bevy_pbr/src/render/pbr_fragment.wgsl | 2 +- .../src/render/pbr_prepass_functions.wgsl | 2 +- .../src/render_resource/bind_group.rs | 3 +- examples/shader/custom_phase_item.rs | 2 +- examples/shader/specialized_mesh_pipeline.rs | 2 +- 16 files changed, 524 insertions(+), 117 deletions(-) diff --git a/assets/shaders/bindless_material.wgsl b/assets/shaders/bindless_material.wgsl index a8d42de19658a..9d9d068d4c069 100644 --- a/assets/shaders/bindless_material.wgsl +++ b/assets/shaders/bindless_material.wgsl @@ -18,7 +18,7 @@ struct Color { @fragment fn fragment(in: VertexOutput) -> @location(0) vec4 { #ifdef BINDLESS - let slot = mesh[in.instance_index].material_bind_group_slot; + let slot = mesh[in.instance_index].material_and_lightmap_bind_group_slot & 0xffffu; let base_color = material_color[slot].base_color; #else // BINDLESS let base_color = material_color.base_color; diff --git a/crates/bevy_core_pipeline/src/core_3d/mod.rs b/crates/bevy_core_pipeline/src/core_3d/mod.rs index 6aee87d2135f5..f70ad1391473f 100644 --- a/crates/bevy_core_pipeline/src/core_3d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_3d/mod.rs @@ -76,10 +76,10 @@ pub use main_opaque_pass_3d_node::*; pub use main_transparent_pass_3d_node::*; use bevy_app::{App, Plugin, PostUpdate}; -use bevy_asset::{AssetId, UntypedAssetId}; +use bevy_asset::UntypedAssetId; use bevy_color::LinearRgba; use bevy_ecs::{entity::EntityHashSet, prelude::*}; -use bevy_image::{BevyDefault, Image}; +use bevy_image::BevyDefault; use bevy_math::FloatOrd; use bevy_render::{ camera::{Camera, ExtractedCamera}, @@ -102,6 +102,7 @@ use bevy_render::{ Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; use bevy_utils::{tracing::warn, HashMap}; +use nonmax::NonMaxU32; use crate::{ core_3d::main_transmissive_pass_3d_node::MainTransmissivePass3dNode, @@ -258,8 +259,9 @@ pub struct Opaque3dBatchSetKey { /// For non-mesh items, you can safely fill this with `None`. pub index_slab: Option, - /// The lightmap, if present. - pub lightmap_image: Option>, + /// Index of the slab that the lightmap resides in, if a lightmap is + /// present. + pub lightmap_slab: Option, } /// Data that must be identical in order to *batch* phase items together. diff --git a/crates/bevy_pbr/src/lightmap/lightmap.wgsl b/crates/bevy_pbr/src/lightmap/lightmap.wgsl index cf3c2275c9a08..da2eaeb2f9dee 100644 --- a/crates/bevy_pbr/src/lightmap/lightmap.wgsl +++ b/crates/bevy_pbr/src/lightmap/lightmap.wgsl @@ -2,8 +2,13 @@ #import bevy_pbr::mesh_bindings::mesh +#ifdef MULTIPLE_LIGHTMAPS_IN_ARRAY +@group(1) @binding(4) var lightmaps_textures: binding_array>; +@group(1) @binding(5) var lightmaps_samplers: binding_array; +#else // MULTIPLE_LIGHTMAPS_IN_ARRAY @group(1) @binding(4) var lightmaps_texture: texture_2d; @group(1) @binding(5) var lightmaps_sampler: sampler; +#endif // MULTIPLE_LIGHTMAPS_IN_ARRAY // Samples the lightmap, if any, and returns indirect illumination from it. fn lightmap(uv: vec2, exposure: f32, instance_index: u32) -> vec3 { @@ -21,9 +26,20 @@ fn lightmap(uv: vec2, exposure: f32, instance_index: u32) -> vec3 { // control flow uniformity problems. // // TODO(pcwalton): Consider bicubic filtering. +#ifdef MULTIPLE_LIGHTMAPS_IN_ARRAY + let lightmap_slot = mesh[instance_index].material_and_lightmap_bind_group_slot >> 16u; + return textureSampleLevel( + lightmaps_textures[lightmap_slot], + lightmaps_samplers[lightmap_slot], + lightmap_uv, + 0.0 + ).rgb * exposure; +#else // MULTIPLE_LIGHTMAPS_IN_ARRAY return textureSampleLevel( lightmaps_texture, lightmaps_sampler, lightmap_uv, - 0.0).rgb * exposure; + 0.0 + ).rgb * exposure; +#endif // MULTIPLE_LIGHTMAPS_IN_ARRAY } diff --git a/crates/bevy_pbr/src/lightmap/mod.rs b/crates/bevy_pbr/src/lightmap/mod.rs index 0e0debc92e3f3..9206b407779a0 100644 --- a/crates/bevy_pbr/src/lightmap/mod.rs +++ b/crates/bevy_pbr/src/lightmap/mod.rs @@ -19,10 +19,11 @@ //! multiple meshes can share the same material, whereas sharing lightmaps is //! nonsensical). //! -//! Note that meshes can't be instanced if they use different lightmap textures. -//! If you want to instance a lightmapped mesh, combine the lightmap textures -//! into a single atlas, and set the `uv_rect` field on [`Lightmap`] -//! appropriately. +//! Note that multiple meshes can't be drawn in a single drawcall if they use +//! different lightmap textures, unless bindless textures are in use. If you +//! want to instance a lightmapped mesh, and your platform doesn't support +//! bindless textures, combine the lightmap textures into a single atlas, and +//! set the `uv_rect` field on [`Lightmap`] appropriately. //! //! [The Lightmapper]: https://github.com/Naxela/The_Lightmapper //! [`Mesh3d`]: bevy_render::mesh::Mesh3d @@ -32,33 +33,46 @@ use bevy_app::{App, Plugin}; use bevy_asset::{load_internal_asset, AssetId, Handle}; +use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ component::Component, entity::Entity, + query::{Changed, Or}, reflect::ReflectComponent, + removal_detection::RemovedComponents, schedule::IntoSystemConfigs, system::{Query, Res, ResMut, Resource}, + world::{FromWorld, World}, }; use bevy_image::Image; use bevy_math::{uvec2, vec4, Rect, UVec2}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; -use bevy_render::sync_world::MainEntityHashMap; use bevy_render::{ - mesh::{Mesh, RenderMesh}, render_asset::RenderAssets, - render_resource::Shader, - texture::GpuImage, + render_resource::{Sampler, Shader, TextureView, WgpuSampler, WgpuTextureView}, + sync_world::MainEntity, + texture::{FallbackImage, GpuImage}, view::ViewVisibility, Extract, ExtractSchedule, RenderApp, }; -use bevy_utils::HashSet; +use bevy_render::{renderer::RenderDevice, sync_world::MainEntityHashMap}; +use bevy_utils::{default, tracing::error, HashSet}; +use fixedbitset::FixedBitSet; +use nonmax::{NonMaxU16, NonMaxU32}; -use crate::{ExtractMeshesSet, RenderMeshInstances}; +use crate::{binding_arrays_are_usable, ExtractMeshesSet}; /// The ID of the lightmap shader. pub const LIGHTMAP_SHADER_HANDLE: Handle = Handle::weak_from_u128(285484768317531991932943596447919767152); +/// The number of lightmaps that we store in a single slab, if bindless textures +/// are in use. +/// +/// If bindless textures aren't in use, then only a single lightmap can be bound +/// at a time. +pub const LIGHTMAPS_PER_SLAB: usize = 16; + /// A plugin that provides an implementation of lightmaps. pub struct LightmapPlugin; @@ -100,13 +114,23 @@ pub(crate) struct RenderLightmap { /// right coordinate is the `max` part of the rect. The rect ranges from (0, /// 0) to (1, 1). pub(crate) uv_rect: Rect, + + /// The index of the slab (i.e. binding array) in which the lightmap is + /// located. + pub(crate) slab_index: LightmapSlabIndex, + + /// The index of the slot (i.e. element within the binding array) in which + /// the lightmap is located. + /// + /// If bindless lightmaps aren't in use, this will be 0. + pub(crate) slot_index: LightmapSlotIndex, } /// Stores data for all lightmaps in the render world. /// /// This is cleared and repopulated each frame during the `extract_lightmaps` /// system. -#[derive(Default, Resource)] +#[derive(Resource)] pub struct RenderLightmaps { /// The mapping from every lightmapped entity to its lightmap info. /// @@ -114,14 +138,43 @@ pub struct RenderLightmaps { /// loaded, won't have entries in this table. pub(crate) render_lightmaps: MainEntityHashMap, - /// All active lightmap images in the scene. - /// - /// Gathering all lightmap images into a set makes mesh bindgroup - /// preparation slightly more efficient, because only one bindgroup needs to - /// be created per lightmap texture. - pub(crate) all_lightmap_images: HashSet>, + /// The slabs (binding arrays) containing the lightmaps. + pub(crate) slabs: Vec, + + free_slabs: FixedBitSet, + + pending_lightmaps: HashSet<(LightmapSlabIndex, LightmapSlotIndex)>, + + /// Whether bindless textures are supported on this platform. + pub(crate) bindless_supported: bool, +} + +/// A binding array that contains lightmaps. +/// +/// This will have a single binding if bindless lightmaps aren't in use. +pub struct LightmapSlab { + /// The GPU images in this slab. + lightmaps: Vec, + free_slots_bitmask: u32, } +struct AllocatedLightmap { + gpu_image: GpuImage, + // This will only be present if the lightmap is allocated but not loaded. + asset_id: Option>, +} + +/// The index of the slab (binding array) in which a lightmap is located. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Deref, DerefMut)] +#[repr(transparent)] +pub struct LightmapSlabIndex(pub(crate) NonMaxU32); + +/// The index of the slot (element within the binding array) in the slab in +/// which a lightmap is located. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Deref, DerefMut)] +#[repr(transparent)] +pub struct LightmapSlotIndex(pub(crate) NonMaxU16); + impl Plugin for LightmapPlugin { fn build(&self, app: &mut App) { load_internal_asset!( @@ -146,48 +199,108 @@ impl Plugin for LightmapPlugin { /// Extracts all lightmaps from the scene and populates the [`RenderLightmaps`] /// resource. fn extract_lightmaps( - mut render_lightmaps: ResMut, - lightmaps: Extract>, - render_mesh_instances: Res, + render_lightmaps: ResMut, + changed_lightmaps_query: Extract< + Query< + (Entity, &ViewVisibility, &Lightmap), + Or<(Changed, Changed)>, + >, + >, + mut removed_lightmaps_query: Extract>, images: Res>, - meshes: Res>, + fallback_images: Res, ) { - // Clear out the old frame's data. - render_lightmaps.render_lightmaps.clear(); - render_lightmaps.all_lightmap_images.clear(); + let render_lightmaps = render_lightmaps.into_inner(); // Loop over each entity. - for (entity, view_visibility, lightmap) in lightmaps.iter() { - // Only process visible entities for which the mesh and lightmap are - // both loaded. - if !view_visibility.get() - || images.get(&lightmap.image).is_none() - || !render_mesh_instances - .mesh_asset_id(entity.into()) - .and_then(|mesh_asset_id| meshes.get(mesh_asset_id)) - .is_some_and(|mesh| mesh.layout.0.contains(Mesh::ATTRIBUTE_UV_1.id)) + for (entity, view_visibility, lightmap) in changed_lightmaps_query.iter() { + if render_lightmaps + .render_lightmaps + .contains_key(&MainEntity::from(entity)) { continue; } - // Store information about the lightmap in the render world. + // Only process visible entities. + if !view_visibility.get() { + continue; + } + + let (slab_index, slot_index) = + render_lightmaps.allocate(&fallback_images, lightmap.image.id()); render_lightmaps.render_lightmaps.insert( entity.into(), - RenderLightmap::new(lightmap.image.id(), lightmap.uv_rect), + RenderLightmap::new( + lightmap.image.id(), + lightmap.uv_rect, + slab_index, + slot_index, + ), ); - // Make a note of the loaded lightmap image so we can efficiently - // process them later during mesh bindgroup creation. render_lightmaps - .all_lightmap_images - .insert(lightmap.image.id()); + .pending_lightmaps + .insert((slab_index, slot_index)); + } + + for entity in removed_lightmaps_query.read() { + if changed_lightmaps_query.contains(entity) { + continue; + } + + let Some(RenderLightmap { + slab_index, + slot_index, + .. + }) = render_lightmaps + .render_lightmaps + .remove(&MainEntity::from(entity)) + else { + continue; + }; + + render_lightmaps.remove(&fallback_images, slab_index, slot_index); + render_lightmaps + .pending_lightmaps + .remove(&(slab_index, slot_index)); } + + render_lightmaps + .pending_lightmaps + .retain(|&(slab_index, slot_index)| { + let Some(asset_id) = render_lightmaps.slabs[usize::from(slab_index)].lightmaps + [usize::from(slot_index)] + .asset_id + else { + error!( + "Allocated lightmap should have been removed from `pending_lightmaps` by now" + ); + return false; + }; + + let Some(gpu_image) = images.get(asset_id) else { + return true; + }; + render_lightmaps.slabs[usize::from(slab_index)].insert(slot_index, gpu_image.clone()); + false + }); } impl RenderLightmap { - /// Creates a new lightmap from a texture and a UV rect. - fn new(image: AssetId, uv_rect: Rect) -> Self { - Self { image, uv_rect } + /// Creates a new lightmap from a texture, a UV rect, and a slab and slot + /// index pair. + fn new( + image: AssetId, + uv_rect: Rect, + slab_index: LightmapSlabIndex, + slot_index: LightmapSlotIndex, + ) -> Self { + Self { + image, + uv_rect, + slab_index, + slot_index, + } } } @@ -215,3 +328,188 @@ impl Default for Lightmap { } } } + +impl FromWorld for RenderLightmaps { + fn from_world(world: &mut World) -> Self { + let render_device = world.resource::(); + let bindless_supported = binding_arrays_are_usable(render_device); + + RenderLightmaps { + render_lightmaps: default(), + slabs: vec![], + free_slabs: FixedBitSet::new(), + pending_lightmaps: default(), + bindless_supported, + } + } +} + +impl RenderLightmaps { + /// Creates a new slab, appends it to the end of the list, and returns its + /// slab index. + fn create_slab(&mut self, fallback_images: &FallbackImage) -> LightmapSlabIndex { + let slab_index = LightmapSlabIndex::from(self.slabs.len()); + self.free_slabs.grow_and_insert(slab_index.into()); + self.slabs + .push(LightmapSlab::new(fallback_images, self.bindless_supported)); + slab_index + } + + fn allocate( + &mut self, + fallback_images: &FallbackImage, + image_id: AssetId, + ) -> (LightmapSlabIndex, LightmapSlotIndex) { + let slab_index = match self.free_slabs.minimum() { + None => self.create_slab(fallback_images), + Some(slab_index) => slab_index.into(), + }; + + let slab = &mut self.slabs[usize::from(slab_index)]; + let slot_index = slab.allocate(image_id); + if slab.is_full() { + self.free_slabs.remove(slab_index.into()); + } + + (slab_index, slot_index) + } + + fn remove( + &mut self, + fallback_images: &FallbackImage, + slab_index: LightmapSlabIndex, + slot_index: LightmapSlotIndex, + ) { + let slab = &mut self.slabs[usize::from(slab_index)]; + slab.remove(fallback_images, slot_index); + + if !slab.is_full() { + self.free_slabs.grow_and_insert(slot_index.into()); + } + } +} + +impl LightmapSlab { + fn new(fallback_images: &FallbackImage, bindless_supported: bool) -> LightmapSlab { + let count = if bindless_supported { + LIGHTMAPS_PER_SLAB + } else { + 1 + }; + + LightmapSlab { + lightmaps: (0..count) + .map(|_| AllocatedLightmap { + gpu_image: fallback_images.d2.clone(), + asset_id: None, + }) + .collect(), + free_slots_bitmask: (1 << count) - 1, + } + } + + fn is_full(&self) -> bool { + self.free_slots_bitmask == 0 + } + + fn allocate(&mut self, image_id: AssetId) -> LightmapSlotIndex { + let index = LightmapSlotIndex::from(self.free_slots_bitmask.trailing_zeros()); + self.free_slots_bitmask &= !(1 << u32::from(index)); + self.lightmaps[usize::from(index)].asset_id = Some(image_id); + index + } + + fn insert(&mut self, index: LightmapSlotIndex, gpu_image: GpuImage) { + self.lightmaps[usize::from(index)] = AllocatedLightmap { + gpu_image, + asset_id: None, + } + } + + fn remove(&mut self, fallback_images: &FallbackImage, index: LightmapSlotIndex) { + self.lightmaps[usize::from(index)] = AllocatedLightmap { + gpu_image: fallback_images.d2.clone(), + asset_id: None, + }; + self.free_slots_bitmask |= 1 << u32::from(index); + } + + /// Returns the texture views and samplers for the lightmaps in this slab, + /// ready to be placed into a bind group. + /// + /// This is used when constructing bind groups in bindless mode. Before + /// returning, this function pads out the arrays with fallback images in + /// order to fulfill requirements of platforms that require full binding + /// arrays (e.g. DX12). + pub(crate) fn build_binding_arrays(&self) -> (Vec<&WgpuTextureView>, Vec<&WgpuSampler>) { + ( + self.lightmaps + .iter() + .map(|allocated_lightmap| &*allocated_lightmap.gpu_image.texture_view) + .collect(), + self.lightmaps + .iter() + .map(|allocated_lightmap| &*allocated_lightmap.gpu_image.sampler) + .collect(), + ) + } + + /// Returns the texture view and sampler corresponding to the first + /// lightmap, which must exist. + /// + /// This is used when constructing bind groups in non-bindless mode. + pub(crate) fn bindings_for_first_lightmap(&self) -> (&TextureView, &Sampler) { + ( + &self.lightmaps[0].gpu_image.texture_view, + &self.lightmaps[0].gpu_image.sampler, + ) + } +} + +impl From for LightmapSlabIndex { + fn from(value: u32) -> Self { + Self(NonMaxU32::new(value).unwrap()) + } +} + +impl From for LightmapSlabIndex { + fn from(value: usize) -> Self { + Self::from(value as u32) + } +} + +impl From for LightmapSlotIndex { + fn from(value: u32) -> Self { + Self(NonMaxU16::new(value as u16).unwrap()) + } +} + +impl From for LightmapSlotIndex { + fn from(value: usize) -> Self { + Self::from(value as u32) + } +} + +impl From for usize { + fn from(value: LightmapSlabIndex) -> Self { + value.0.get() as usize + } +} + +impl From for usize { + fn from(value: LightmapSlotIndex) -> Self { + value.0.get() as usize + } +} + +impl From for u16 { + fn from(value: LightmapSlotIndex) -> Self { + value.0.get() + } +} + +impl From for u32 { + fn from(value: LightmapSlotIndex) -> Self { + value.0.get() as u32 + } +} diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index 53aa8d7bc8ac6..dda50c59e0d84 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -809,11 +809,11 @@ pub fn queue_material_meshes( | MeshPipelineKey::from_bits_retain(mesh.key_bits.bits()) | mesh_pipeline_key_bits; - let lightmap_image = render_lightmaps + let lightmap_slab_index = render_lightmaps .render_lightmaps .get(visible_entity) - .map(|lightmap| lightmap.image); - if lightmap_image.is_some() { + .map(|lightmap| lightmap.slab_index); + if lightmap_slab_index.is_some() { mesh_key |= MeshPipelineKey::LIGHTMAPPED; } @@ -881,7 +881,8 @@ pub fn queue_material_meshes( material_bind_group_index: Some(material.binding.group.0), vertex_slab: vertex_slab.unwrap_or_default(), index_slab, - lightmap_image, + lightmap_slab: lightmap_slab_index + .map(|lightmap_slab_index| *lightmap_slab_index), }, asset_id: mesh_instance.mesh_asset_id.into(), }; diff --git a/crates/bevy_pbr/src/material_bind_groups.rs b/crates/bevy_pbr/src/material_bind_groups.rs index a9bf1e8274f66..718b4fef535e0 100644 --- a/crates/bevy_pbr/src/material_bind_groups.rs +++ b/crates/bevy_pbr/src/material_bind_groups.rs @@ -156,11 +156,17 @@ impl From for MaterialBindGroupIndex { /// non-bindless mode, this slot is always 0. #[derive(Clone, Copy, Debug, Default, Reflect, Deref, DerefMut)] #[reflect(Default)] -pub struct MaterialBindGroupSlot(pub u32); +pub struct MaterialBindGroupSlot(pub u16); impl From for MaterialBindGroupSlot { fn from(value: u32) -> Self { - MaterialBindGroupSlot(value) + MaterialBindGroupSlot(value as u16) + } +} + +impl From for u32 { + fn from(value: MaterialBindGroupSlot) -> Self { + value.0 as u32 } } diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index 35edf83b944f8..0dafca7f96257 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -314,8 +314,11 @@ pub struct MeshUniform { pub current_skin_index: u32, /// The previous skin index, or `u32::MAX` if there's no previous skin. pub previous_skin_index: u32, - /// Index of the material inside the bind group data. - pub material_bind_group_slot: u32, + /// The material and lightmap indices, packed into 32 bits. + /// + /// Low 16 bits: index of the material inside the bind group data. + /// High 16 bits: index of the lightmap in the binding array. + pub material_and_lightmap_bind_group_slot: u32, } /// Information that has to be transferred from CPU to GPU in order to produce @@ -356,8 +359,11 @@ pub struct MeshInputUniform { pub current_skin_index: u32, /// The previous skin index, or `u32::MAX` if there's no previous skin. pub previous_skin_index: u32, - /// Index of the material inside the bind group data. - pub material_bind_group_slot: u32, + /// The material and lightmap indices, packed into 32 bits. + /// + /// Low 16 bits: index of the material inside the bind group data. + /// High 16 bits: index of the lightmap in the binding array. + pub material_and_lightmap_bind_group_slot: u32, } /// Information about each mesh instance needed to cull it on GPU. @@ -388,23 +394,29 @@ impl MeshUniform { mesh_transforms: &MeshTransforms, first_vertex_index: u32, material_bind_group_slot: MaterialBindGroupSlot, - maybe_lightmap_uv_rect: Option, + maybe_lightmap: Option<(LightmapSlotIndex, Rect)>, current_skin_index: Option, previous_skin_index: Option, ) -> Self { let (local_from_world_transpose_a, local_from_world_transpose_b) = mesh_transforms.world_from_local.inverse_transpose_3x3(); + let lightmap_bind_group_slot = match maybe_lightmap { + None => u16::MAX, + Some((slot_index, _)) => slot_index.into(), + }; + Self { world_from_local: mesh_transforms.world_from_local.to_transpose(), previous_world_from_local: mesh_transforms.previous_world_from_local.to_transpose(), - lightmap_uv_rect: pack_lightmap_uv_rect(maybe_lightmap_uv_rect), + lightmap_uv_rect: pack_lightmap_uv_rect(maybe_lightmap.map(|(_, uv_rect)| uv_rect)), local_from_world_transpose_a, local_from_world_transpose_b, flags: mesh_transforms.flags, first_vertex_index, current_skin_index: current_skin_index.unwrap_or(u32::MAX), previous_skin_index: previous_skin_index.unwrap_or(u32::MAX), - material_bind_group_slot: *material_bind_group_slot, + material_and_lightmap_bind_group_slot: u32::from(material_bind_group_slot) + | ((lightmap_bind_group_slot as u32) << 16), } } } @@ -887,6 +899,7 @@ impl RenderMeshInstanceGpuQueue { impl RenderMeshInstanceGpuBuilder { /// Flushes this mesh instance to the [`RenderMeshInstanceGpu`] and /// [`MeshInputUniform`] tables, replacing the existing entry if applicable. + #[allow(clippy::too_many_arguments)] fn update( self, entity: MainEntity, @@ -894,6 +907,7 @@ impl RenderMeshInstanceGpuBuilder { current_input_buffer: &mut InstanceInputUniformBuffer, previous_input_buffer: &mut InstanceInputUniformBuffer, mesh_allocator: &MeshAllocator, + render_lightmaps: &RenderLightmaps, skin_indices: &SkinIndices, ) -> u32 { let first_vertex_index = match mesh_allocator.mesh_vertex_slice(&self.shared.mesh_asset_id) @@ -911,6 +925,11 @@ impl RenderMeshInstanceGpuBuilder { None => u32::MAX, }; + let lightmap_slot = match render_lightmaps.render_lightmaps.get(&entity) { + Some(render_lightmap) => u16::from(*render_lightmap.slot_index), + None => u16::MAX, + }; + // Create the mesh input uniform. let mut mesh_input_uniform = MeshInputUniform { world_from_local: self.world_from_local.to_transpose(), @@ -920,7 +939,9 @@ impl RenderMeshInstanceGpuBuilder { first_vertex_index, current_skin_index, previous_skin_index, - material_bind_group_slot: *self.shared.material_bindings_index.slot, + material_and_lightmap_bind_group_slot: u32::from( + self.shared.material_bindings_index.slot, + ) | ((lightmap_slot as u32) << 16), }; // Did the last frame contain this entity as well? @@ -1345,6 +1366,7 @@ pub fn collect_meshes_for_gpu_building( mut mesh_culling_data_buffer: ResMut, mut render_mesh_instance_queues: ResMut, mesh_allocator: Res, + render_lightmaps: Res, skin_indices: Res, ) { let RenderMeshInstances::GpuBuilding(ref mut render_mesh_instances) = @@ -1381,6 +1403,7 @@ pub fn collect_meshes_for_gpu_building( current_input_buffer, previous_input_buffer, &mesh_allocator, + &render_lightmaps, &skin_indices, ); } @@ -1405,6 +1428,7 @@ pub fn collect_meshes_for_gpu_building( current_input_buffer, previous_input_buffer, &mesh_allocator, + &render_lightmaps, &skin_indices, ); mesh_culling_builder @@ -1588,7 +1612,7 @@ impl GetBatchData for MeshPipeline { &mesh_instance.transforms, first_vertex_index, material_bind_group_index.slot, - maybe_lightmap.map(|lightmap| lightmap.uv_rect), + maybe_lightmap.map(|lightmap| (lightmap.slot_index, lightmap.uv_rect)), current_skin_index, previous_skin_index, ), @@ -1655,7 +1679,7 @@ impl GetFullBatchData for MeshPipeline { &mesh_instance.transforms, first_vertex_index, mesh_instance.material_bindings_index.slot, - maybe_lightmap.map(|lightmap| lightmap.uv_rect), + maybe_lightmap.map(|lightmap| (lightmap.slot_index, lightmap.uv_rect)), current_skin_index, previous_skin_index, )) @@ -2245,6 +2269,7 @@ impl SpecializedMeshPipeline for MeshPipeline { if self.binding_arrays_are_usable { shader_defs.push("MULTIPLE_LIGHT_PROBES_IN_ARRAY".into()); + shader_defs.push("MULTIPLE_LIGHTMAPS_IN_ARRAY".into()); } if IRRADIANCE_VOLUMES_ARE_USABLE { @@ -2328,7 +2353,7 @@ pub struct MeshBindGroups { model_only: Option, skinned: Option, morph_targets: HashMap, MeshBindGroupPair>, - lightmaps: HashMap, BindGroup>, + lightmaps: HashMap, } pub struct MeshBindGroupPair { @@ -2348,7 +2373,7 @@ impl MeshBindGroups { pub fn get( &self, asset_id: AssetId, - lightmap: Option>, + lightmap: Option, is_skinned: bool, morph: bool, motion_vectors: bool, @@ -2362,7 +2387,7 @@ impl MeshBindGroups { .skinned .as_ref() .map(|bind_group_pair| bind_group_pair.get(motion_vectors)), - (false, false, Some(lightmap)) => self.lightmaps.get(&lightmap), + (false, false, Some(lightmap_slab)) => self.lightmaps.get(&lightmap_slab), (false, false, None) => self.model_only.as_ref(), } } @@ -2381,7 +2406,6 @@ impl MeshBindGroupPair { #[allow(clippy::too_many_arguments)] pub fn prepare_mesh_bind_group( meshes: Res>, - images: Res>, mut groups: ResMut, mesh_pipeline: Res, render_device: Res, @@ -2393,7 +2417,7 @@ pub fn prepare_mesh_bind_group( >, skins_uniform: Res, weights_uniform: Res, - render_lightmaps: Res, + mut render_lightmaps: ResMut, ) { groups.reset(); @@ -2475,13 +2499,13 @@ pub fn prepare_mesh_bind_group( } } - // Create lightmap bindgroups. - for &image_id in &render_lightmaps.all_lightmap_images { - if let (Entry::Vacant(entry), Some(image)) = - (groups.lightmaps.entry(image_id), images.get(image_id)) - { - entry.insert(layouts.lightmapped(&render_device, &model, image)); - } + // Create lightmap bindgroups. There will be one bindgroup for each slab. + let bindless_supported = render_lightmaps.bindless_supported; + for (lightmap_slab_id, lightmap_slab) in render_lightmaps.slabs.iter_mut().enumerate() { + groups.lightmaps.insert( + LightmapSlabIndex(NonMaxU32::new(lightmap_slab_id as u32).unwrap()), + layouts.lightmapped(&render_device, &model, lightmap_slab, bindless_supported), + ); } } @@ -2581,14 +2605,14 @@ impl RenderCommand

for SetMeshBindGroup { let is_skinned = current_skin_index.is_some(); let is_morphed = current_morph_index.is_some(); - let lightmap = lightmaps + let lightmap_slab_index = lightmaps .render_lightmaps .get(entity) - .map(|render_lightmap| render_lightmap.image); + .map(|render_lightmap| render_lightmap.slab_index); let Some(bind_group) = bind_groups.get( mesh_asset_id, - lightmap, + lightmap_slab_index, is_skinned, is_morphed, has_motion_vector_prepass, diff --git a/crates/bevy_pbr/src/render/mesh_bindings.rs b/crates/bevy_pbr/src/render/mesh_bindings.rs index cc1c5bec23bb7..3e3210a026325 100644 --- a/crates/bevy_pbr/src/render/mesh_bindings.rs +++ b/crates/bevy_pbr/src/render/mesh_bindings.rs @@ -1,11 +1,9 @@ //! Bind group layout related definitions for the mesh pipeline. use bevy_math::Mat4; -use bevy_render::{ - mesh::morph::MAX_MORPH_WEIGHTS, render_resource::*, renderer::RenderDevice, texture::GpuImage, -}; +use bevy_render::{mesh::morph::MAX_MORPH_WEIGHTS, render_resource::*, renderer::RenderDevice}; -use crate::render::skin::MAX_JOINTS; +use crate::{binding_arrays_are_usable, render::skin::MAX_JOINTS, LightmapSlab}; const MORPH_WEIGHT_SIZE: usize = size_of::(); @@ -21,8 +19,10 @@ pub(crate) const JOINT_BUFFER_SIZE: usize = MAX_JOINTS * JOINT_SIZE; /// Individual layout entries. mod layout_entry { + use core::num::NonZeroU32; + use super::{JOINT_BUFFER_SIZE, MORPH_BUFFER_SIZE}; - use crate::{render::skin, MeshUniform}; + use crate::{render::skin, MeshUniform, LIGHTMAPS_PER_SLAB}; use bevy_render::{ render_resource::{ binding_types::{ @@ -61,6 +61,16 @@ mod layout_entry { pub(super) fn lightmaps_sampler() -> BindGroupLayoutEntryBuilder { sampler(SamplerBindingType::Filtering).visibility(ShaderStages::FRAGMENT) } + pub(super) fn lightmaps_texture_view_array() -> BindGroupLayoutEntryBuilder { + texture_2d(TextureSampleType::Float { filterable: true }) + .visibility(ShaderStages::FRAGMENT) + .count(NonZeroU32::new(LIGHTMAPS_PER_SLAB as u32).unwrap()) + } + pub(super) fn lightmaps_sampler_array() -> BindGroupLayoutEntryBuilder { + sampler(SamplerBindingType::Filtering) + .visibility(ShaderStages::FRAGMENT) + .count(NonZeroU32::new(LIGHTMAPS_PER_SLAB as u32).unwrap()) + } } /// Individual [`BindGroupEntry`] @@ -72,7 +82,7 @@ mod entry { use bevy_render::{ render_resource::{ BindGroupEntry, BindingResource, Buffer, BufferBinding, BufferSize, Sampler, - TextureView, + TextureView, WgpuSampler, WgpuTextureView, }, renderer::RenderDevice, }; @@ -123,6 +133,24 @@ mod entry { resource: BindingResource::Sampler(sampler), } } + pub(super) fn lightmaps_texture_view_array<'a>( + binding: u32, + textures: &'a [&'a WgpuTextureView], + ) -> BindGroupEntry<'a> { + BindGroupEntry { + binding, + resource: BindingResource::TextureViewArray(textures), + } + } + pub(super) fn lightmaps_sampler_array<'a>( + binding: u32, + samplers: &'a [&'a WgpuSampler], + ) -> BindGroupEntry<'a> { + BindGroupEntry { + binding, + resource: BindingResource::SamplerArray(samplers), + } + } } /// All possible [`BindGroupLayout`]s in bevy's default mesh shader (`mesh.wgsl`). @@ -302,17 +330,31 @@ impl MeshLayouts { } fn lightmapped_layout(render_device: &RenderDevice) -> BindGroupLayout { - render_device.create_bind_group_layout( - "lightmapped_mesh_layout", - &BindGroupLayoutEntries::with_indices( - ShaderStages::VERTEX, - ( - (0, layout_entry::model(render_device)), - (4, layout_entry::lightmaps_texture_view()), - (5, layout_entry::lightmaps_sampler()), + if binding_arrays_are_usable(render_device) { + render_device.create_bind_group_layout( + "lightmapped_mesh_layout", + &BindGroupLayoutEntries::with_indices( + ShaderStages::VERTEX, + ( + (0, layout_entry::model(render_device)), + (4, layout_entry::lightmaps_texture_view_array()), + (5, layout_entry::lightmaps_sampler_array()), + ), ), - ), - ) + ) + } else { + render_device.create_bind_group_layout( + "lightmapped_mesh_layout", + &BindGroupLayoutEntries::with_indices( + ShaderStages::VERTEX, + ( + (0, layout_entry::model(render_device)), + (4, layout_entry::lightmaps_texture_view()), + (5, layout_entry::lightmaps_sampler()), + ), + ), + ) + } } // ---------- BindGroup methods ---------- @@ -329,17 +371,32 @@ impl MeshLayouts { &self, render_device: &RenderDevice, model: &BindingResource, - lightmap: &GpuImage, + lightmap_slab: &LightmapSlab, + bindless_lightmaps: bool, ) -> BindGroup { - render_device.create_bind_group( - "lightmapped_mesh_bind_group", - &self.lightmapped, - &[ - entry::model(0, model.clone()), - entry::lightmaps_texture_view(4, &lightmap.texture_view), - entry::lightmaps_sampler(5, &lightmap.sampler), - ], - ) + if bindless_lightmaps { + let (texture_views, samplers) = lightmap_slab.build_binding_arrays(); + render_device.create_bind_group( + "lightmapped_mesh_bind_group", + &self.lightmapped, + &[ + entry::model(0, model.clone()), + entry::lightmaps_texture_view_array(4, &texture_views), + entry::lightmaps_sampler_array(5, &samplers), + ], + ) + } else { + let (texture_view, sampler) = lightmap_slab.bindings_for_first_lightmap(); + render_device.create_bind_group( + "lightmapped_mesh_bind_group", + &self.lightmapped, + &[ + entry::model(0, model.clone()), + entry::lightmaps_texture_view(4, texture_view), + entry::lightmaps_sampler(5, sampler), + ], + ) + } } /// Creates the bind group for skinned meshes with no morph targets. diff --git a/crates/bevy_pbr/src/render/mesh_preprocess.wgsl b/crates/bevy_pbr/src/render/mesh_preprocess.wgsl index 9836077a66614..6e19f6b8004b0 100644 --- a/crates/bevy_pbr/src/render/mesh_preprocess.wgsl +++ b/crates/bevy_pbr/src/render/mesh_preprocess.wgsl @@ -25,8 +25,9 @@ struct MeshInput { first_vertex_index: u32, current_skin_index: u32, previous_skin_index: u32, - // Index of the material inside the bind group data. - material_bind_group_slot: u32, + // Low 16 bits: index of the material inside the bind group data. + // High 16 bits: index of the lightmap in the binding array. + material_and_lightmap_bind_group_slot: u32, } // Information about each mesh instance needed to cull it on GPU. @@ -196,6 +197,6 @@ fn main(@builtin(global_invocation_id) global_invocation_id: vec3) { output[mesh_output_index].first_vertex_index = current_input[input_index].first_vertex_index; output[mesh_output_index].current_skin_index = current_input[input_index].current_skin_index; output[mesh_output_index].previous_skin_index = current_input[input_index].previous_skin_index; - output[mesh_output_index].material_bind_group_slot = - current_input[input_index].material_bind_group_slot; + output[mesh_output_index].material_and_lightmap_bind_group_slot = + current_input[input_index].material_and_lightmap_bind_group_slot; } diff --git a/crates/bevy_pbr/src/render/mesh_types.wgsl b/crates/bevy_pbr/src/render/mesh_types.wgsl index 9a5e064c825a5..f0258770c6da0 100644 --- a/crates/bevy_pbr/src/render/mesh_types.wgsl +++ b/crates/bevy_pbr/src/render/mesh_types.wgsl @@ -19,8 +19,9 @@ struct Mesh { first_vertex_index: u32, current_skin_index: u32, previous_skin_index: u32, - // Index of the material inside the bind group data. - material_bind_group_slot: u32, + // Low 16 bits: index of the material inside the bind group data. + // High 16 bits: index of the lightmap in the binding array. + material_and_lightmap_bind_group_slot: u32, }; #ifdef SKINNED diff --git a/crates/bevy_pbr/src/render/parallax_mapping.wgsl b/crates/bevy_pbr/src/render/parallax_mapping.wgsl index d6fe4f9bbec95..706c96850e172 100644 --- a/crates/bevy_pbr/src/render/parallax_mapping.wgsl +++ b/crates/bevy_pbr/src/render/parallax_mapping.wgsl @@ -6,7 +6,7 @@ } fn sample_depth_map(uv: vec2, instance_index: u32) -> f32 { - let slot = mesh[instance_index].material_bind_group_slot; + let slot = mesh[instance_index].material_and_lightmap_bind_group_slot & 0xffffu; // We use `textureSampleLevel` over `textureSample` because the wgpu DX12 // backend (Fxc) panics when using "gradient instructions" inside a loop. // It results in the whole loop being unrolled by the shader compiler, diff --git a/crates/bevy_pbr/src/render/pbr_fragment.wgsl b/crates/bevy_pbr/src/render/pbr_fragment.wgsl index 7bdc8632ec334..cd7500d1ac054 100644 --- a/crates/bevy_pbr/src/render/pbr_fragment.wgsl +++ b/crates/bevy_pbr/src/render/pbr_fragment.wgsl @@ -71,7 +71,7 @@ fn pbr_input_from_standard_material( is_front: bool, ) -> pbr_types::PbrInput { #ifdef BINDLESS - let slot = mesh[in.instance_index].material_bind_group_slot; + let slot = mesh[in.instance_index].material_and_lightmap_bind_group_slot & 0xffffu; let flags = pbr_bindings::material[slot].flags; let base_color = pbr_bindings::material[slot].base_color; let deferred_lighting_pass_id = pbr_bindings::material[slot].deferred_lighting_pass_id; diff --git a/crates/bevy_pbr/src/render/pbr_prepass_functions.wgsl b/crates/bevy_pbr/src/render/pbr_prepass_functions.wgsl index 493e56f27781d..e3391126aa76d 100644 --- a/crates/bevy_pbr/src/render/pbr_prepass_functions.wgsl +++ b/crates/bevy_pbr/src/render/pbr_prepass_functions.wgsl @@ -17,7 +17,7 @@ fn prepass_alpha_discard(in: VertexOutput) { #ifdef MAY_DISCARD #ifdef BINDLESS - let slot = mesh[in.instance_index].material_bind_group_slot; + let slot = mesh[in.instance_index].material_and_lightmap_bind_group_slot & 0xffffu; var output_color: vec4 = pbr_bindings::material[slot].base_color; #else // BINDLESS var output_color: vec4 = pbr_bindings::material.base_color; diff --git a/crates/bevy_render/src/render_resource/bind_group.rs b/crates/bevy_render/src/render_resource/bind_group.rs index d7033d660b85a..8d0ed47f8395f 100644 --- a/crates/bevy_render/src/render_resource/bind_group.rs +++ b/crates/bevy_render/src/render_resource/bind_group.rs @@ -256,7 +256,8 @@ impl Deref for BindGroup { /// shader Bevy will instead present a *binding array* of `COUNT` elements. /// In your shader, the index of the element of each binding array /// corresponding to the mesh currently being drawn can be retrieved with -/// `mesh[in.instance_index].material_bind_group_slot`. +/// `mesh[in.instance_index].material_and_lightmap_bind_group_slot & +/// 0xffffu`. /// * Bindless uniforms don't exist, so in bindless mode all uniforms and /// uniform buffers are automatically replaced with read-only storage /// buffers. diff --git a/examples/shader/custom_phase_item.rs b/examples/shader/custom_phase_item.rs index 360c664d9ec86..67e3e16d51d8e 100644 --- a/examples/shader/custom_phase_item.rs +++ b/examples/shader/custom_phase_item.rs @@ -274,7 +274,7 @@ fn queue_custom_phase_item( draw_function: draw_custom_phase_item, pipeline: pipeline_id, material_bind_group_index: None, - lightmap_image: None, + lightmap_slab: None, vertex_slab: default(), index_slab: None, }, diff --git a/examples/shader/specialized_mesh_pipeline.rs b/examples/shader/specialized_mesh_pipeline.rs index 5df65a2befa29..8fe2eeb00e357 100644 --- a/examples/shader/specialized_mesh_pipeline.rs +++ b/examples/shader/specialized_mesh_pipeline.rs @@ -341,7 +341,7 @@ fn queue_custom_mesh_pipeline( material_bind_group_index: None, vertex_slab: default(), index_slab: None, - lightmap_image: None, + lightmap_slab: None, }, // The asset ID is arbitrary; we simply use [`AssetId::invalid`], // but you can use anything you like. Note that the asset ID need