Skip to content

Make the specialized pipeline cache two-level.#17915

Merged
superdump merged 4 commits intobevyengine:mainfrom
pcwalton:two-level-pipeline-cache
Feb 18, 2025
Merged

Make the specialized pipeline cache two-level.#17915
superdump merged 4 commits intobevyengine:mainfrom
pcwalton:two-level-pipeline-cache

Conversation

@pcwalton
Copy link
Copy Markdown
Contributor

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.

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, bevyengine#17898.

[Caldera benchmark]: https://github.com/DGriffin91/bevy_caldera_scene
@pcwalton pcwalton added A-Rendering Drawing game state to the screen C-Bug An unexpected or incorrect behavior C-Performance A change motivated by improving speed, memory usage or compile times S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Feb 17, 2025
Copy link
Copy Markdown
Member

@tychedelia tychedelia left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Much improved, thanks. Can you just do the same for the 2d and prepass specialization checks as well?

let view_specialized_material_pipeline_cache = specialized_material_pipeline_cache
.entry(view.retained_view_entity)
.or_default();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add an early-out here to avoid looping visible entities if not needed? Ish:

if !entity_specialization_ticks.is_changed() && !view_tick.is_newer_than(view_specialized_material_pipeline_cache.tick, ticks.this_run())  {
    continue;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure that's correct if entities have been added since the last time entity_specialization_ticks was updated?

Really, the solution here is going to be to use change detection with special cases for when entities are added and removed, like our other retained passes.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could be missing something, but entity_specialization_ticks is always updated with new entities every frame. Driven by:

pub fn check_entities_needing_specialization<M>(
    needs_specialization: Query<
        Entity,
        Or<(
            Changed<Mesh3d>, // <-- Will pick up new entities
            AssetChanged<Mesh3d>,
            Changed<MeshMaterial3d<M>>, // <-- Will pick up new entities
            AssetChanged<MeshMaterial3d<M>>,
        )>,
    >,
    mut entities_needing_specialization: ResMut<EntitiesNeedingSpecialization<M>>,
) where
    M: Material,
{
    entities_needing_specialization.clear();
    for entity in &needs_specialization {
        // Feeds entity_specialization_ticks
        entities_needing_specialization.push(entity);
    }
}

Regardless, this can be deferred to another PR. Just thought it could be a big quick win for static scenes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, let's defer this to another PR; this has already gotten big.

let view_specialized_material_pipeline_cache = specialized_material_pipeline_cache
.entry(extracted_view_light.retained_view_entity)
.or_default();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

`MainEntity`/`Entity` where `RetainedViewEntity` is more appropriate
@pcwalton
Copy link
Copy Markdown
Contributor Author

@tychedelia Done. I changed the ViewSpecializationTicks and ViewKeyCache to key off the RetainedViewEntity as well.

@tychedelia tychedelia added S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Feb 17, 2025
@superdump
Copy link
Copy Markdown
Contributor

This can be merged as soon as CI passes.

@superdump superdump added this pull request to the merge queue Feb 18, 2025
Merged via the queue into bevyengine:main with commit 5e569af Feb 18, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-Rendering Drawing game state to the screen C-Bug An unexpected or incorrect behavior C-Performance A change motivated by improving speed, memory usage or compile times S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it

Projects

No open projects
Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants