Make the specialized pipeline cache two-level. (#17915)
Currently, the specialized pipeline cache maps a (view entity, mesh entity) tuple to the retained pipeline for that entity. This causes two problems: 1. Using the view entity is incorrect, because the view entity isn't stable from frame to frame. 2. Switching the view entity to a `RetainedViewEntity`, which is necessary for correctness, significantly regresses performance of `specialize_material_meshes` and `specialize_shadows` because of the loss of the fast `EntityHash`. This patch fixes both problems by switching to a *two-level* hash table. The outer level of the table maps each `RetainedViewEntity` to an inner table, which maps each `MainEntity` to its pipeline ID and change tick. Because we loop over views first and, within that loop, loop over entities visible from that view, we hoist the slow lookup of the view entity out of the inner entity loop. Additionally, this patch fixes a bug whereby pipeline IDs were leaked when removing the view. We still have a problem with leaking pipeline IDs for deleted entities, but that won't be fixed until the specialized pipeline cache is retained. This patch improves performance of the [Caldera benchmark] from 7.8× faster than 0.14 to 9.0× faster than 0.14, when applied on top of the global binding arrays PR, #17898. [Caldera benchmark]: https://github.com/DGriffin91/bevy_caldera_scene
This commit is contained in:
parent
8976a45199
commit
5e569af2d0
@ -19,7 +19,6 @@ use bevy_core_pipeline::{
|
|||||||
};
|
};
|
||||||
use bevy_derive::{Deref, DerefMut};
|
use bevy_derive::{Deref, DerefMut};
|
||||||
use bevy_ecs::component::Tick;
|
use bevy_ecs::component::Tick;
|
||||||
use bevy_ecs::entity::EntityHash;
|
|
||||||
use bevy_ecs::system::SystemChangeTick;
|
use bevy_ecs::system::SystemChangeTick;
|
||||||
use bevy_ecs::{
|
use bevy_ecs::{
|
||||||
prelude::*,
|
prelude::*,
|
||||||
@ -28,7 +27,8 @@ use bevy_ecs::{
|
|||||||
SystemParamItem,
|
SystemParamItem,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use bevy_platform_support::collections::HashMap;
|
use bevy_platform_support::collections::{HashMap, HashSet};
|
||||||
|
use bevy_platform_support::hash::FixedHasher;
|
||||||
use bevy_reflect::std_traits::ReflectDefault;
|
use bevy_reflect::std_traits::ReflectDefault;
|
||||||
use bevy_reflect::Reflect;
|
use bevy_reflect::Reflect;
|
||||||
use bevy_render::mesh::mark_3d_meshes_as_changed_if_their_assets_changed;
|
use bevy_render::mesh::mark_3d_meshes_as_changed_if_their_assets_changed;
|
||||||
@ -41,7 +41,7 @@ use bevy_render::{
|
|||||||
render_resource::*,
|
render_resource::*,
|
||||||
renderer::RenderDevice,
|
renderer::RenderDevice,
|
||||||
sync_world::MainEntity,
|
sync_world::MainEntity,
|
||||||
view::{ExtractedView, Msaa, RenderVisibilityRanges, ViewVisibility},
|
view::{ExtractedView, Msaa, RenderVisibilityRanges, RetainedViewEntity, ViewVisibility},
|
||||||
Extract,
|
Extract,
|
||||||
};
|
};
|
||||||
use bevy_render::{mesh::allocator::MeshAllocator, sync_world::MainEntityHashMap};
|
use bevy_render::{mesh::allocator::MeshAllocator, sync_world::MainEntityHashMap};
|
||||||
@ -735,11 +735,22 @@ impl<M> Default for EntitySpecializationTicks<M> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Stores the [`SpecializedMaterialViewPipelineCache`] for each view.
|
||||||
#[derive(Resource, Deref, DerefMut)]
|
#[derive(Resource, Deref, DerefMut)]
|
||||||
pub struct SpecializedMaterialPipelineCache<M> {
|
pub struct SpecializedMaterialPipelineCache<M> {
|
||||||
// (view_entity, material_entity) -> (tick, pipeline_id)
|
// view entity -> view pipeline cache
|
||||||
#[deref]
|
#[deref]
|
||||||
map: HashMap<(MainEntity, MainEntity), (Tick, CachedRenderPipelineId), EntityHash>,
|
map: HashMap<RetainedViewEntity, SpecializedMaterialViewPipelineCache<M>>,
|
||||||
|
marker: PhantomData<M>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stores the cached render pipeline ID for each entity in a single view, as
|
||||||
|
/// well as the last time it was changed.
|
||||||
|
#[derive(Deref, DerefMut)]
|
||||||
|
pub struct SpecializedMaterialViewPipelineCache<M> {
|
||||||
|
// material entity -> (tick, pipeline_id)
|
||||||
|
#[deref]
|
||||||
|
map: MainEntityHashMap<(Tick, CachedRenderPipelineId)>,
|
||||||
marker: PhantomData<M>,
|
marker: PhantomData<M>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -752,6 +763,15 @@ impl<M> Default for SpecializedMaterialPipelineCache<M> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<M> Default for SpecializedMaterialViewPipelineCache<M> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
map: MainEntityHashMap::default(),
|
||||||
|
marker: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn check_entities_needing_specialization<M>(
|
pub fn check_entities_needing_specialization<M>(
|
||||||
needs_specialization: Query<
|
needs_specialization: Query<
|
||||||
Entity,
|
Entity,
|
||||||
@ -792,7 +812,7 @@ pub fn specialize_material_meshes<M: Material>(
|
|||||||
Res<ViewSortedRenderPhases<Transmissive3d>>,
|
Res<ViewSortedRenderPhases<Transmissive3d>>,
|
||||||
Res<ViewSortedRenderPhases<Transparent3d>>,
|
Res<ViewSortedRenderPhases<Transparent3d>>,
|
||||||
),
|
),
|
||||||
views: Query<(&MainEntity, &ExtractedView, &RenderVisibleEntities)>,
|
views: Query<(&ExtractedView, &RenderVisibleEntities)>,
|
||||||
view_key_cache: Res<ViewKeyCache>,
|
view_key_cache: Res<ViewKeyCache>,
|
||||||
entity_specialization_ticks: Res<EntitySpecializationTicks<M>>,
|
entity_specialization_ticks: Res<EntitySpecializationTicks<M>>,
|
||||||
view_specialization_ticks: Res<ViewSpecializationTicks>,
|
view_specialization_ticks: Res<ViewSpecializationTicks>,
|
||||||
@ -804,7 +824,13 @@ pub fn specialize_material_meshes<M: Material>(
|
|||||||
) where
|
) where
|
||||||
M::Data: PartialEq + Eq + Hash + Clone,
|
M::Data: PartialEq + Eq + Hash + Clone,
|
||||||
{
|
{
|
||||||
for (view_entity, view, visible_entities) in &views {
|
// Record the retained IDs of all shadow views so that we can expire old
|
||||||
|
// pipeline IDs.
|
||||||
|
let mut all_views: HashSet<RetainedViewEntity, FixedHasher> = HashSet::default();
|
||||||
|
|
||||||
|
for (view, visible_entities) in &views {
|
||||||
|
all_views.insert(view.retained_view_entity);
|
||||||
|
|
||||||
if !transparent_render_phases.contains_key(&view.retained_view_entity)
|
if !transparent_render_phases.contains_key(&view.retained_view_entity)
|
||||||
&& !opaque_render_phases.contains_key(&view.retained_view_entity)
|
&& !opaque_render_phases.contains_key(&view.retained_view_entity)
|
||||||
&& !alpha_mask_render_phases.contains_key(&view.retained_view_entity)
|
&& !alpha_mask_render_phases.contains_key(&view.retained_view_entity)
|
||||||
@ -813,15 +839,21 @@ pub fn specialize_material_meshes<M: Material>(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(view_key) = view_key_cache.get(view_entity) else {
|
let Some(view_key) = view_key_cache.get(&view.retained_view_entity) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let view_tick = view_specialization_ticks
|
||||||
|
.get(&view.retained_view_entity)
|
||||||
|
.unwrap();
|
||||||
|
let view_specialized_material_pipeline_cache = specialized_material_pipeline_cache
|
||||||
|
.entry(view.retained_view_entity)
|
||||||
|
.or_default();
|
||||||
|
|
||||||
for (_, visible_entity) in visible_entities.iter::<Mesh3d>() {
|
for (_, visible_entity) in visible_entities.iter::<Mesh3d>() {
|
||||||
let view_tick = view_specialization_ticks.get(view_entity).unwrap();
|
|
||||||
let entity_tick = entity_specialization_ticks.get(visible_entity).unwrap();
|
let entity_tick = entity_specialization_ticks.get(visible_entity).unwrap();
|
||||||
let last_specialized_tick = specialized_material_pipeline_cache
|
let last_specialized_tick = view_specialized_material_pipeline_cache
|
||||||
.get(&(*view_entity, *visible_entity))
|
.get(visible_entity)
|
||||||
.map(|(tick, _)| *tick);
|
.map(|(tick, _)| *tick);
|
||||||
let needs_specialization = last_specialized_tick.is_none_or(|tick| {
|
let needs_specialization = last_specialized_tick.is_none_or(|tick| {
|
||||||
view_tick.is_newer_than(tick, ticks.this_run())
|
view_tick.is_newer_than(tick, ticks.this_run())
|
||||||
@ -901,12 +933,14 @@ pub fn specialize_material_meshes<M: Material>(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
specialized_material_pipeline_cache.insert(
|
view_specialized_material_pipeline_cache
|
||||||
(*view_entity, *visible_entity),
|
.insert(*visible_entity, (ticks.this_run(), pipeline_id));
|
||||||
(ticks.this_run(), pipeline_id),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete specialized pipelines belonging to views that have expired.
|
||||||
|
specialized_material_pipeline_cache
|
||||||
|
.retain(|retained_view_entity, _| all_views.contains(retained_view_entity));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// For each view, iterates over all the meshes visible from that view and adds
|
/// For each view, iterates over all the meshes visible from that view and adds
|
||||||
@ -921,12 +955,12 @@ pub fn queue_material_meshes<M: Material>(
|
|||||||
mut alpha_mask_render_phases: ResMut<ViewBinnedRenderPhases<AlphaMask3d>>,
|
mut alpha_mask_render_phases: ResMut<ViewBinnedRenderPhases<AlphaMask3d>>,
|
||||||
mut transmissive_render_phases: ResMut<ViewSortedRenderPhases<Transmissive3d>>,
|
mut transmissive_render_phases: ResMut<ViewSortedRenderPhases<Transmissive3d>>,
|
||||||
mut transparent_render_phases: ResMut<ViewSortedRenderPhases<Transparent3d>>,
|
mut transparent_render_phases: ResMut<ViewSortedRenderPhases<Transparent3d>>,
|
||||||
views: Query<(&MainEntity, &ExtractedView, &RenderVisibleEntities)>,
|
views: Query<(&ExtractedView, &RenderVisibleEntities)>,
|
||||||
specialized_material_pipeline_cache: ResMut<SpecializedMaterialPipelineCache<M>>,
|
specialized_material_pipeline_cache: ResMut<SpecializedMaterialPipelineCache<M>>,
|
||||||
) where
|
) where
|
||||||
M::Data: PartialEq + Eq + Hash + Clone,
|
M::Data: PartialEq + Eq + Hash + Clone,
|
||||||
{
|
{
|
||||||
for (view_entity, view, visible_entities) in &views {
|
for (view, visible_entities) in &views {
|
||||||
let (
|
let (
|
||||||
Some(opaque_phase),
|
Some(opaque_phase),
|
||||||
Some(alpha_mask_phase),
|
Some(alpha_mask_phase),
|
||||||
@ -942,10 +976,16 @@ pub fn queue_material_meshes<M: Material>(
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let Some(view_specialized_material_pipeline_cache) =
|
||||||
|
specialized_material_pipeline_cache.get(&view.retained_view_entity)
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
let rangefinder = view.rangefinder3d();
|
let rangefinder = view.rangefinder3d();
|
||||||
for (render_entity, visible_entity) in visible_entities.iter::<Mesh3d>() {
|
for (render_entity, visible_entity) in visible_entities.iter::<Mesh3d>() {
|
||||||
let Some((current_change_tick, pipeline_id)) = specialized_material_pipeline_cache
|
let Some((current_change_tick, pipeline_id)) = view_specialized_material_pipeline_cache
|
||||||
.get(&(*view_entity, *visible_entity))
|
.get(visible_entity)
|
||||||
.map(|(current_change_tick, pipeline_id)| (*current_change_tick, *pipeline_id))
|
.map(|(current_change_tick, pipeline_id)| (*current_change_tick, *pipeline_id))
|
||||||
else {
|
else {
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@ -18,7 +18,7 @@ use bevy_render::{
|
|||||||
render_resource::binding_types::uniform_buffer,
|
render_resource::binding_types::uniform_buffer,
|
||||||
renderer::RenderAdapter,
|
renderer::RenderAdapter,
|
||||||
sync_world::RenderEntity,
|
sync_world::RenderEntity,
|
||||||
view::{RenderVisibilityRanges, VISIBILITY_RANGES_STORAGE_BUFFER_COUNT},
|
view::{RenderVisibilityRanges, RetainedViewEntity, VISIBILITY_RANGES_STORAGE_BUFFER_COUNT},
|
||||||
ExtractSchedule, Render, RenderApp, RenderDebugFlags, RenderSet,
|
ExtractSchedule, Render, RenderApp, RenderDebugFlags, RenderSet,
|
||||||
};
|
};
|
||||||
pub use prepass_bindings::*;
|
pub use prepass_bindings::*;
|
||||||
@ -56,10 +56,9 @@ use crate::meshlet::{
|
|||||||
|
|
||||||
use bevy_derive::{Deref, DerefMut};
|
use bevy_derive::{Deref, DerefMut};
|
||||||
use bevy_ecs::component::Tick;
|
use bevy_ecs::component::Tick;
|
||||||
use bevy_ecs::entity::EntityHash;
|
|
||||||
use bevy_ecs::system::SystemChangeTick;
|
use bevy_ecs::system::SystemChangeTick;
|
||||||
use bevy_platform_support::collections::HashMap;
|
use bevy_platform_support::collections::HashMap;
|
||||||
use bevy_render::sync_world::{MainEntity, MainEntityHashMap};
|
use bevy_render::sync_world::MainEntityHashMap;
|
||||||
use bevy_render::view::RenderVisibleEntities;
|
use bevy_render::view::RenderVisibleEntities;
|
||||||
use bevy_render::RenderSet::{PrepareAssets, PrepareResources};
|
use bevy_render::RenderSet::{PrepareAssets, PrepareResources};
|
||||||
use core::{hash::Hash, marker::PhantomData};
|
use core::{hash::Hash, marker::PhantomData};
|
||||||
@ -807,11 +806,22 @@ pub fn prepare_prepass_view_bind_group<M: Material>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Stores the [`SpecializedPrepassMaterialViewPipelineCache`] for each view.
|
||||||
#[derive(Resource, Deref, DerefMut)]
|
#[derive(Resource, Deref, DerefMut)]
|
||||||
pub struct SpecializedPrepassMaterialPipelineCache<M> {
|
pub struct SpecializedPrepassMaterialPipelineCache<M> {
|
||||||
// (view_entity, material_entity) -> (tick, pipeline_id)
|
// view_entity -> view pipeline cache
|
||||||
#[deref]
|
#[deref]
|
||||||
map: HashMap<(MainEntity, MainEntity), (Tick, CachedRenderPipelineId), EntityHash>,
|
map: HashMap<RetainedViewEntity, SpecializedPrepassMaterialViewPipelineCache<M>>,
|
||||||
|
marker: PhantomData<M>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stores the cached render pipeline ID for each entity in a single view, as
|
||||||
|
/// well as the last time it was changed.
|
||||||
|
#[derive(Deref, DerefMut)]
|
||||||
|
pub struct SpecializedPrepassMaterialViewPipelineCache<M> {
|
||||||
|
// material entity -> (tick, pipeline_id)
|
||||||
|
#[deref]
|
||||||
|
map: MainEntityHashMap<(Tick, CachedRenderPipelineId)>,
|
||||||
marker: PhantomData<M>,
|
marker: PhantomData<M>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -824,17 +834,26 @@ impl<M> Default for SpecializedPrepassMaterialPipelineCache<M> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Resource, Deref, DerefMut, Default, Clone)]
|
impl<M> Default for SpecializedPrepassMaterialViewPipelineCache<M> {
|
||||||
pub struct ViewKeyPrepassCache(MainEntityHashMap<MeshPipelineKey>);
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
map: HashMap::default(),
|
||||||
|
marker: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Resource, Deref, DerefMut, Default, Clone)]
|
#[derive(Resource, Deref, DerefMut, Default, Clone)]
|
||||||
pub struct ViewPrepassSpecializationTicks(MainEntityHashMap<Tick>);
|
pub struct ViewKeyPrepassCache(HashMap<RetainedViewEntity, MeshPipelineKey>);
|
||||||
|
|
||||||
|
#[derive(Resource, Deref, DerefMut, Default, Clone)]
|
||||||
|
pub struct ViewPrepassSpecializationTicks(HashMap<RetainedViewEntity, Tick>);
|
||||||
|
|
||||||
pub fn check_prepass_views_need_specialization(
|
pub fn check_prepass_views_need_specialization(
|
||||||
mut view_key_cache: ResMut<ViewKeyPrepassCache>,
|
mut view_key_cache: ResMut<ViewKeyPrepassCache>,
|
||||||
mut view_specialization_ticks: ResMut<ViewPrepassSpecializationTicks>,
|
mut view_specialization_ticks: ResMut<ViewPrepassSpecializationTicks>,
|
||||||
mut views: Query<(
|
mut views: Query<(
|
||||||
&MainEntity,
|
&ExtractedView,
|
||||||
&Msaa,
|
&Msaa,
|
||||||
Option<&DepthPrepass>,
|
Option<&DepthPrepass>,
|
||||||
Option<&NormalPrepass>,
|
Option<&NormalPrepass>,
|
||||||
@ -842,9 +861,7 @@ pub fn check_prepass_views_need_specialization(
|
|||||||
)>,
|
)>,
|
||||||
ticks: SystemChangeTick,
|
ticks: SystemChangeTick,
|
||||||
) {
|
) {
|
||||||
for (view_entity, msaa, depth_prepass, normal_prepass, motion_vector_prepass) in
|
for (view, msaa, depth_prepass, normal_prepass, motion_vector_prepass) in views.iter_mut() {
|
||||||
views.iter_mut()
|
|
||||||
{
|
|
||||||
let mut view_key = MeshPipelineKey::from_msaa_samples(msaa.samples());
|
let mut view_key = MeshPipelineKey::from_msaa_samples(msaa.samples());
|
||||||
if depth_prepass.is_some() {
|
if depth_prepass.is_some() {
|
||||||
view_key |= MeshPipelineKey::DEPTH_PREPASS;
|
view_key |= MeshPipelineKey::DEPTH_PREPASS;
|
||||||
@ -856,14 +873,14 @@ pub fn check_prepass_views_need_specialization(
|
|||||||
view_key |= MeshPipelineKey::MOTION_VECTOR_PREPASS;
|
view_key |= MeshPipelineKey::MOTION_VECTOR_PREPASS;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(current_key) = view_key_cache.get_mut(view_entity) {
|
if let Some(current_key) = view_key_cache.get_mut(&view.retained_view_entity) {
|
||||||
if *current_key != view_key {
|
if *current_key != view_key {
|
||||||
view_key_cache.insert(*view_entity, view_key);
|
view_key_cache.insert(view.retained_view_entity, view_key);
|
||||||
view_specialization_ticks.insert(*view_entity, ticks.this_run());
|
view_specialization_ticks.insert(view.retained_view_entity, ticks.this_run());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
view_key_cache.insert(*view_entity, view_key);
|
view_key_cache.insert(view.retained_view_entity, view_key);
|
||||||
view_specialization_ticks.insert(*view_entity, ticks.this_run());
|
view_specialization_ticks.insert(view.retained_view_entity, ticks.this_run());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -878,7 +895,6 @@ pub fn specialize_prepass_material_meshes<M>(
|
|||||||
material_bind_group_allocator: Res<MaterialBindGroupAllocator<M>>,
|
material_bind_group_allocator: Res<MaterialBindGroupAllocator<M>>,
|
||||||
view_key_cache: Res<ViewKeyPrepassCache>,
|
view_key_cache: Res<ViewKeyPrepassCache>,
|
||||||
views: Query<(
|
views: Query<(
|
||||||
&MainEntity,
|
|
||||||
&ExtractedView,
|
&ExtractedView,
|
||||||
&RenderVisibleEntities,
|
&RenderVisibleEntities,
|
||||||
&Msaa,
|
&Msaa,
|
||||||
@ -917,14 +933,7 @@ pub fn specialize_prepass_material_meshes<M>(
|
|||||||
M: Material,
|
M: Material,
|
||||||
M::Data: PartialEq + Eq + Hash + Clone,
|
M::Data: PartialEq + Eq + Hash + Clone,
|
||||||
{
|
{
|
||||||
for (
|
for (extracted_view, visible_entities, msaa, motion_vector_prepass, deferred_prepass) in &views
|
||||||
view_entity,
|
|
||||||
extracted_view,
|
|
||||||
visible_entities,
|
|
||||||
msaa,
|
|
||||||
motion_vector_prepass,
|
|
||||||
deferred_prepass,
|
|
||||||
) in &views
|
|
||||||
{
|
{
|
||||||
if !opaque_deferred_render_phases.contains_key(&extracted_view.retained_view_entity)
|
if !opaque_deferred_render_phases.contains_key(&extracted_view.retained_view_entity)
|
||||||
&& !alpha_mask_deferred_render_phases.contains_key(&extracted_view.retained_view_entity)
|
&& !alpha_mask_deferred_render_phases.contains_key(&extracted_view.retained_view_entity)
|
||||||
@ -934,15 +943,21 @@ pub fn specialize_prepass_material_meshes<M>(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(view_key) = view_key_cache.get(view_entity) else {
|
let Some(view_key) = view_key_cache.get(&extracted_view.retained_view_entity) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let view_tick = view_specialization_ticks
|
||||||
|
.get(&extracted_view.retained_view_entity)
|
||||||
|
.unwrap();
|
||||||
|
let view_specialized_material_pipeline_cache = specialized_material_pipeline_cache
|
||||||
|
.entry(extracted_view.retained_view_entity)
|
||||||
|
.or_default();
|
||||||
|
|
||||||
for (_, visible_entity) in visible_entities.iter::<Mesh3d>() {
|
for (_, visible_entity) in visible_entities.iter::<Mesh3d>() {
|
||||||
let view_tick = view_specialization_ticks.get(view_entity).unwrap();
|
|
||||||
let entity_tick = entity_specialization_ticks.get(visible_entity).unwrap();
|
let entity_tick = entity_specialization_ticks.get(visible_entity).unwrap();
|
||||||
let last_specialized_tick = specialized_material_pipeline_cache
|
let last_specialized_tick = view_specialized_material_pipeline_cache
|
||||||
.get(&(*view_entity, *visible_entity))
|
.get(visible_entity)
|
||||||
.map(|(tick, _)| *tick);
|
.map(|(tick, _)| *tick);
|
||||||
let needs_specialization = last_specialized_tick.is_none_or(|tick| {
|
let needs_specialization = last_specialized_tick.is_none_or(|tick| {
|
||||||
view_tick.is_newer_than(tick, ticks.this_run())
|
view_tick.is_newer_than(tick, ticks.this_run())
|
||||||
@ -1054,10 +1069,8 @@ pub fn specialize_prepass_material_meshes<M>(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
specialized_material_pipeline_cache.insert(
|
view_specialized_material_pipeline_cache
|
||||||
(*view_entity, *visible_entity),
|
.insert(*visible_entity, (ticks.this_run(), pipeline_id));
|
||||||
(ticks.this_run(), pipeline_id),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1072,12 +1085,12 @@ pub fn queue_prepass_material_meshes<M: Material>(
|
|||||||
mut alpha_mask_prepass_render_phases: ResMut<ViewBinnedRenderPhases<AlphaMask3dPrepass>>,
|
mut alpha_mask_prepass_render_phases: ResMut<ViewBinnedRenderPhases<AlphaMask3dPrepass>>,
|
||||||
mut opaque_deferred_render_phases: ResMut<ViewBinnedRenderPhases<Opaque3dDeferred>>,
|
mut opaque_deferred_render_phases: ResMut<ViewBinnedRenderPhases<Opaque3dDeferred>>,
|
||||||
mut alpha_mask_deferred_render_phases: ResMut<ViewBinnedRenderPhases<AlphaMask3dDeferred>>,
|
mut alpha_mask_deferred_render_phases: ResMut<ViewBinnedRenderPhases<AlphaMask3dDeferred>>,
|
||||||
views: Query<(&MainEntity, &ExtractedView, &RenderVisibleEntities)>,
|
views: Query<(&ExtractedView, &RenderVisibleEntities)>,
|
||||||
specialized_material_pipeline_cache: Res<SpecializedPrepassMaterialPipelineCache<M>>,
|
specialized_material_pipeline_cache: Res<SpecializedPrepassMaterialPipelineCache<M>>,
|
||||||
) where
|
) where
|
||||||
M::Data: PartialEq + Eq + Hash + Clone,
|
M::Data: PartialEq + Eq + Hash + Clone,
|
||||||
{
|
{
|
||||||
for (view_entity, extracted_view, visible_entities) in &views {
|
for (extracted_view, visible_entities) in &views {
|
||||||
let (
|
let (
|
||||||
mut opaque_phase,
|
mut opaque_phase,
|
||||||
mut alpha_mask_phase,
|
mut alpha_mask_phase,
|
||||||
@ -1090,6 +1103,12 @@ pub fn queue_prepass_material_meshes<M: Material>(
|
|||||||
alpha_mask_deferred_render_phases.get_mut(&extracted_view.retained_view_entity),
|
alpha_mask_deferred_render_phases.get_mut(&extracted_view.retained_view_entity),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let Some(view_specialized_material_pipeline_cache) =
|
||||||
|
specialized_material_pipeline_cache.get(&extracted_view.retained_view_entity)
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
// Skip if there's no place to put the mesh.
|
// Skip if there's no place to put the mesh.
|
||||||
if opaque_phase.is_none()
|
if opaque_phase.is_none()
|
||||||
&& alpha_mask_phase.is_none()
|
&& alpha_mask_phase.is_none()
|
||||||
@ -1101,7 +1120,7 @@ pub fn queue_prepass_material_meshes<M: Material>(
|
|||||||
|
|
||||||
for (render_entity, visible_entity) in visible_entities.iter::<Mesh3d>() {
|
for (render_entity, visible_entity) in visible_entities.iter::<Mesh3d>() {
|
||||||
let Some((current_change_tick, pipeline_id)) =
|
let Some((current_change_tick, pipeline_id)) =
|
||||||
specialized_material_pipeline_cache.get(&(*view_entity, *visible_entity))
|
view_specialized_material_pipeline_cache.get(visible_entity)
|
||||||
else {
|
else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -14,6 +14,8 @@ use bevy_ecs::{
|
|||||||
};
|
};
|
||||||
use bevy_math::{ops, Mat4, UVec4, Vec2, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles};
|
use bevy_math::{ops, Mat4, UVec4, Vec2, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles};
|
||||||
use bevy_platform_support::collections::{HashMap, HashSet};
|
use bevy_platform_support::collections::{HashMap, HashSet};
|
||||||
|
use bevy_platform_support::hash::FixedHasher;
|
||||||
|
use bevy_render::sync_world::MainEntityHashMap;
|
||||||
use bevy_render::{
|
use bevy_render::{
|
||||||
batching::gpu_preprocessing::{GpuPreprocessingMode, GpuPreprocessingSupport},
|
batching::gpu_preprocessing::{GpuPreprocessingMode, GpuPreprocessingSupport},
|
||||||
camera::SortedCameras,
|
camera::SortedCameras,
|
||||||
@ -1613,9 +1615,16 @@ pub struct LightSpecializationTicks(HashMap<RetainedViewEntity, Tick>);
|
|||||||
|
|
||||||
#[derive(Resource, Deref, DerefMut)]
|
#[derive(Resource, Deref, DerefMut)]
|
||||||
pub struct SpecializedShadowMaterialPipelineCache<M> {
|
pub struct SpecializedShadowMaterialPipelineCache<M> {
|
||||||
// (view_light_entity, visible_entity) -> (tick, pipeline_id)
|
// view light entity -> view pipeline cache
|
||||||
#[deref]
|
#[deref]
|
||||||
map: HashMap<(RetainedViewEntity, MainEntity), (Tick, CachedRenderPipelineId)>,
|
map: HashMap<RetainedViewEntity, SpecializedShadowMaterialViewPipelineCache<M>>,
|
||||||
|
marker: PhantomData<M>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deref, DerefMut)]
|
||||||
|
pub struct SpecializedShadowMaterialViewPipelineCache<M> {
|
||||||
|
#[deref]
|
||||||
|
map: MainEntityHashMap<(Tick, CachedRenderPipelineId)>,
|
||||||
marker: PhantomData<M>,
|
marker: PhantomData<M>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1628,6 +1637,15 @@ impl<M> Default for SpecializedShadowMaterialPipelineCache<M> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<M> Default for SpecializedShadowMaterialViewPipelineCache<M> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
map: MainEntityHashMap::default(),
|
||||||
|
marker: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn check_views_lights_need_specialization(
|
pub fn check_views_lights_need_specialization(
|
||||||
view_lights: Query<&ViewLightEntities, With<ExtractedView>>,
|
view_lights: Query<&ViewLightEntities, With<ExtractedView>>,
|
||||||
view_light_entities: Query<(&LightEntity, &ExtractedView)>,
|
view_light_entities: Query<(&LightEntity, &ExtractedView)>,
|
||||||
@ -1702,6 +1720,10 @@ pub fn specialize_shadows<M: Material>(
|
|||||||
) where
|
) where
|
||||||
M::Data: PartialEq + Eq + Hash + Clone,
|
M::Data: PartialEq + Eq + Hash + Clone,
|
||||||
{
|
{
|
||||||
|
// Record the retained IDs of all shadow views so that we can expire old
|
||||||
|
// pipeline IDs.
|
||||||
|
let mut all_shadow_views: HashSet<RetainedViewEntity, FixedHasher> = HashSet::default();
|
||||||
|
|
||||||
for (entity, view_lights) in &view_lights {
|
for (entity, view_lights) in &view_lights {
|
||||||
for view_light_entity in view_lights.lights.iter().copied() {
|
for view_light_entity in view_lights.lights.iter().copied() {
|
||||||
let Ok((light_entity, extracted_view_light)) =
|
let Ok((light_entity, extracted_view_light)) =
|
||||||
@ -1709,6 +1731,9 @@ pub fn specialize_shadows<M: Material>(
|
|||||||
else {
|
else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
all_shadow_views.insert(extracted_view_light.retained_view_entity);
|
||||||
|
|
||||||
if !shadow_render_phases.contains_key(&extracted_view_light.retained_view_entity) {
|
if !shadow_render_phases.contains_key(&extracted_view_light.retained_view_entity) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -1744,13 +1769,17 @@ pub fn specialize_shadows<M: Material>(
|
|||||||
// NOTE: Lights with shadow mapping disabled will have no visible entities
|
// NOTE: Lights with shadow mapping disabled will have no visible entities
|
||||||
// so no meshes will be queued
|
// so no meshes will be queued
|
||||||
|
|
||||||
for (_, visible_entity) in visible_entities.iter().copied() {
|
|
||||||
let view_tick = light_specialization_ticks
|
let view_tick = light_specialization_ticks
|
||||||
.get(&extracted_view_light.retained_view_entity)
|
.get(&extracted_view_light.retained_view_entity)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
let view_specialized_material_pipeline_cache = specialized_material_pipeline_cache
|
||||||
|
.entry(extracted_view_light.retained_view_entity)
|
||||||
|
.or_default();
|
||||||
|
|
||||||
|
for (_, visible_entity) in visible_entities.iter().copied() {
|
||||||
let entity_tick = entity_specialization_ticks.get(&visible_entity).unwrap();
|
let entity_tick = entity_specialization_ticks.get(&visible_entity).unwrap();
|
||||||
let last_specialized_tick = specialized_material_pipeline_cache
|
let last_specialized_tick = view_specialized_material_pipeline_cache
|
||||||
.get(&(extracted_view_light.retained_view_entity, visible_entity))
|
.get(&visible_entity)
|
||||||
.map(|(tick, _)| *tick);
|
.map(|(tick, _)| *tick);
|
||||||
let needs_specialization = last_specialized_tick.is_none_or(|tick| {
|
let needs_specialization = last_specialized_tick.is_none_or(|tick| {
|
||||||
view_tick.is_newer_than(tick, ticks.this_run())
|
view_tick.is_newer_than(tick, ticks.this_run())
|
||||||
@ -1829,13 +1858,14 @@ pub fn specialize_shadows<M: Material>(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
specialized_material_pipeline_cache.insert(
|
view_specialized_material_pipeline_cache
|
||||||
(extracted_view_light.retained_view_entity, visible_entity),
|
.insert(visible_entity, (ticks.this_run(), pipeline_id));
|
||||||
(ticks.this_run(), pipeline_id),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete specialized pipelines belonging to views that have expired.
|
||||||
|
specialized_material_pipeline_cache.retain(|view, _| all_shadow_views.contains(view));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// For each shadow cascade, iterates over all the meshes "visible" from it and
|
/// For each shadow cascade, iterates over all the meshes "visible" from it and
|
||||||
@ -1875,6 +1905,12 @@ pub fn queue_shadows<M: Material>(
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let Some(view_specialized_material_pipeline_cache) =
|
||||||
|
specialized_material_pipeline_cache.get(&extracted_view_light.retained_view_entity)
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
let visible_entities = match light_entity {
|
let visible_entities = match light_entity {
|
||||||
LightEntity::Directional {
|
LightEntity::Directional {
|
||||||
light_entity,
|
light_entity,
|
||||||
@ -1900,8 +1936,8 @@ pub fn queue_shadows<M: Material>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
for (entity, main_entity) in visible_entities.iter().copied() {
|
for (entity, main_entity) in visible_entities.iter().copied() {
|
||||||
let Some((current_change_tick, pipeline_id)) = specialized_material_pipeline_cache
|
let Some((current_change_tick, pipeline_id)) =
|
||||||
.get(&(extracted_view_light.retained_view_entity, main_entity))
|
view_specialized_material_pipeline_cache.get(&main_entity)
|
||||||
else {
|
else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -37,8 +37,8 @@ use bevy_render::{
|
|||||||
renderer::{RenderAdapter, RenderDevice, RenderQueue},
|
renderer::{RenderAdapter, RenderDevice, RenderQueue},
|
||||||
texture::DefaultImageSampler,
|
texture::DefaultImageSampler,
|
||||||
view::{
|
view::{
|
||||||
self, NoFrustumCulling, NoIndirectDrawing, RenderVisibilityRanges, ViewTarget,
|
self, NoFrustumCulling, NoIndirectDrawing, RenderVisibilityRanges, RetainedViewEntity,
|
||||||
ViewUniformOffset, ViewVisibility, VisibilityRange,
|
ViewTarget, ViewUniformOffset, ViewVisibility, VisibilityRange,
|
||||||
},
|
},
|
||||||
Extract,
|
Extract,
|
||||||
};
|
};
|
||||||
@ -317,16 +317,15 @@ impl Plugin for MeshRenderPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Resource, Deref, DerefMut, Default, Debug, Clone)]
|
#[derive(Resource, Deref, DerefMut, Default, Debug, Clone)]
|
||||||
pub struct ViewKeyCache(MainEntityHashMap<MeshPipelineKey>);
|
pub struct ViewKeyCache(HashMap<RetainedViewEntity, MeshPipelineKey>);
|
||||||
|
|
||||||
#[derive(Resource, Deref, DerefMut, Default, Debug, Clone)]
|
#[derive(Resource, Deref, DerefMut, Default, Debug, Clone)]
|
||||||
pub struct ViewSpecializationTicks(MainEntityHashMap<Tick>);
|
pub struct ViewSpecializationTicks(HashMap<RetainedViewEntity, Tick>);
|
||||||
|
|
||||||
pub fn check_views_need_specialization(
|
pub fn check_views_need_specialization(
|
||||||
mut view_key_cache: ResMut<ViewKeyCache>,
|
mut view_key_cache: ResMut<ViewKeyCache>,
|
||||||
mut view_specialization_ticks: ResMut<ViewSpecializationTicks>,
|
mut view_specialization_ticks: ResMut<ViewSpecializationTicks>,
|
||||||
mut views: Query<(
|
mut views: Query<(
|
||||||
&MainEntity,
|
|
||||||
&ExtractedView,
|
&ExtractedView,
|
||||||
&Msaa,
|
&Msaa,
|
||||||
Option<&Tonemapping>,
|
Option<&Tonemapping>,
|
||||||
@ -352,7 +351,6 @@ pub fn check_views_need_specialization(
|
|||||||
ticks: SystemChangeTick,
|
ticks: SystemChangeTick,
|
||||||
) {
|
) {
|
||||||
for (
|
for (
|
||||||
view_entity,
|
|
||||||
view,
|
view,
|
||||||
msaa,
|
msaa,
|
||||||
tonemapping,
|
tonemapping,
|
||||||
@ -444,11 +442,11 @@ pub fn check_views_need_specialization(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if !view_key_cache
|
if !view_key_cache
|
||||||
.get_mut(view_entity)
|
.get_mut(&view.retained_view_entity)
|
||||||
.is_some_and(|current_key| *current_key == view_key)
|
.is_some_and(|current_key| *current_key == view_key)
|
||||||
{
|
{
|
||||||
view_key_cache.insert(*view_entity, view_key);
|
view_key_cache.insert(view.retained_view_entity, view_key);
|
||||||
view_specialization_ticks.insert(*view_entity, ticks.this_run());
|
view_specialization_ticks.insert(view.retained_view_entity, ticks.this_run());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,6 @@ use bevy_core_pipeline::{
|
|||||||
};
|
};
|
||||||
use bevy_derive::{Deref, DerefMut};
|
use bevy_derive::{Deref, DerefMut};
|
||||||
use bevy_ecs::component::Tick;
|
use bevy_ecs::component::Tick;
|
||||||
use bevy_ecs::entity::EntityHash;
|
|
||||||
use bevy_ecs::system::SystemChangeTick;
|
use bevy_ecs::system::SystemChangeTick;
|
||||||
use bevy_ecs::{
|
use bevy_ecs::{
|
||||||
prelude::*,
|
prelude::*,
|
||||||
@ -590,11 +589,22 @@ impl<M> Default for EntitySpecializationTicks<M> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Stores the [`SpecializedMaterial2dViewPipelineCache`] for each view.
|
||||||
#[derive(Resource, Deref, DerefMut)]
|
#[derive(Resource, Deref, DerefMut)]
|
||||||
pub struct SpecializedMaterial2dPipelineCache<M> {
|
pub struct SpecializedMaterial2dPipelineCache<M> {
|
||||||
// (view_entity, material_entity) -> (tick, pipeline_id)
|
// view_entity -> view pipeline cache
|
||||||
#[deref]
|
#[deref]
|
||||||
map: HashMap<(MainEntity, MainEntity), (Tick, CachedRenderPipelineId), EntityHash>,
|
map: MainEntityHashMap<SpecializedMaterial2dViewPipelineCache<M>>,
|
||||||
|
marker: PhantomData<M>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stores the cached render pipeline ID for each entity in a single view, as
|
||||||
|
/// well as the last time it was changed.
|
||||||
|
#[derive(Deref, DerefMut)]
|
||||||
|
pub struct SpecializedMaterial2dViewPipelineCache<M> {
|
||||||
|
// material entity -> (tick, pipeline_id)
|
||||||
|
#[deref]
|
||||||
|
map: MainEntityHashMap<(Tick, CachedRenderPipelineId)>,
|
||||||
marker: PhantomData<M>,
|
marker: PhantomData<M>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -607,6 +617,15 @@ impl<M> Default for SpecializedMaterial2dPipelineCache<M> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<M> Default for SpecializedMaterial2dViewPipelineCache<M> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
map: HashMap::default(),
|
||||||
|
marker: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn check_entities_needing_specialization<M>(
|
pub fn check_entities_needing_specialization<M>(
|
||||||
needs_specialization: Query<
|
needs_specialization: Query<
|
||||||
Entity,
|
Entity,
|
||||||
@ -665,11 +684,15 @@ pub fn specialize_material2d_meshes<M: Material2d>(
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
for (_, visible_entity) in visible_entities.iter::<Mesh2d>() {
|
|
||||||
let view_tick = view_specialization_ticks.get(view_entity).unwrap();
|
let view_tick = view_specialization_ticks.get(view_entity).unwrap();
|
||||||
|
let view_specialized_material_pipeline_cache = specialized_material_pipeline_cache
|
||||||
|
.entry(*view_entity)
|
||||||
|
.or_default();
|
||||||
|
|
||||||
|
for (_, visible_entity) in visible_entities.iter::<Mesh2d>() {
|
||||||
let entity_tick = entity_specialization_ticks.get(visible_entity).unwrap();
|
let entity_tick = entity_specialization_ticks.get(visible_entity).unwrap();
|
||||||
let last_specialized_tick = specialized_material_pipeline_cache
|
let last_specialized_tick = view_specialized_material_pipeline_cache
|
||||||
.get(&(*view_entity, *visible_entity))
|
.get(visible_entity)
|
||||||
.map(|(tick, _)| *tick);
|
.map(|(tick, _)| *tick);
|
||||||
let needs_specialization = last_specialized_tick.is_none_or(|tick| {
|
let needs_specialization = last_specialized_tick.is_none_or(|tick| {
|
||||||
view_tick.is_newer_than(tick, ticks.this_run())
|
view_tick.is_newer_than(tick, ticks.this_run())
|
||||||
@ -713,10 +736,8 @@ pub fn specialize_material2d_meshes<M: Material2d>(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
specialized_material_pipeline_cache.insert(
|
view_specialized_material_pipeline_cache
|
||||||
(*view_entity, *visible_entity),
|
.insert(*visible_entity, (ticks.this_run(), pipeline_id));
|
||||||
(ticks.this_run(), pipeline_id),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -741,6 +762,12 @@ pub fn queue_material2d_meshes<M: Material2d>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (view_entity, view, visible_entities) in &views {
|
for (view_entity, view, visible_entities) in &views {
|
||||||
|
let Some(view_specialized_material_pipeline_cache) =
|
||||||
|
specialized_material_pipeline_cache.get(view_entity)
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
let Some(transparent_phase) = transparent_render_phases.get_mut(&view.retained_view_entity)
|
let Some(transparent_phase) = transparent_render_phases.get_mut(&view.retained_view_entity)
|
||||||
else {
|
else {
|
||||||
continue;
|
continue;
|
||||||
@ -754,8 +781,8 @@ pub fn queue_material2d_meshes<M: Material2d>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
for (render_entity, visible_entity) in visible_entities.iter::<Mesh2d>() {
|
for (render_entity, visible_entity) in visible_entities.iter::<Mesh2d>() {
|
||||||
let Some((current_change_tick, pipeline_id)) = specialized_material_pipeline_cache
|
let Some((current_change_tick, pipeline_id)) = view_specialized_material_pipeline_cache
|
||||||
.get(&(*view_entity, *visible_entity))
|
.get(visible_entity)
|
||||||
.map(|(current_change_tick, pipeline_id)| (*current_change_tick, *pipeline_id))
|
.map(|(current_change_tick, pipeline_id)| (*current_change_tick, *pipeline_id))
|
||||||
else {
|
else {
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user