Skip to content

Fix the crash in Bistro by avoiding binning mesh instances multiple times.#24984

Merged
alice-i-cecile merged 5 commits into
bevyengine:mainfrom
pcwalton:bistro-crash
Jul 16, 2026
Merged

Fix the crash in Bistro by avoiding binning mesh instances multiple times.#24984
alice-i-cecile merged 5 commits into
bevyengine:mainfrom
pcwalton:bistro-crash

Conversation

@pcwalton

@pcwalton pcwalton commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

At the moment, DirtySpecializations::iter_to_queue attempts to avoid yielding the same mesh instance multiple times in a complex way. Unfortunately, this logic is incorrect for mesh instances that couldn't be queued because their materials haven't loaded yet (i.e. the contents of last_frame_view_pending_queues). The Bistro example tests this code path, as it has many materials that take a significant amount of time to load.

Code exists that tries to handle the case in which a mesh is re-binned, and this is why no crash occurred before PR #23662. PR #23662 made this incorrect code more incorrect because it bumps the instance count regardless of whether the mesh instance was already present in the bin; this causes a crash later with an out-of-bounds array access. However, it's always illegal to try to bin a binned entity; the entity must be removed from its bin first. Therefore, moving the instance count bump to the case in which the entity wasn't already in a bin is the wrong fix.

This commit contains the correct fix, which is to change DirtySpecializations::iter_to_queue so that it doesn't yield duplicate entities. This PR removes the complex logic that failed to account for all cases of duplicate mesh instances in favor of a simple hash set that tracks the mesh instances yielded so far. Additionally, this PR deletes the code path that tried to handle the case in which a mesh that's already binned is binned again and replaces it with a panic in debug mode.

I verified that this change fixes the crash in Bistro via cargo run --features=bevy/debug -p bistro --bin bistro. Note that on my NVIDIA GeForce RTX 2060, I still get an OOM error on that test case, but that's a separate issue.

Fixes #24990.

times.

At the moment, `DirtySpecializations::iter_to_queue` attempts to avoid
yielding the same mesh instance multiple times in a complex way.
Unfortunately, this logic is incorrect for mesh instances that couldn't
be queued because their materials haven't loaded yet (i.e. the contents
of `last_frame_view_pending_queues`). The Bistro example tests this code
path, as it has many materials that take a significant amount of time to
load.

Code exists that tries to handle the case in which a mesh is re-binned,
and this is why no crash occurred before PR bevyengine#23662. PR bevyengine#23662 made this
incorrect code *more* incorrect because it bumps the instance count
regardless of whether the mesh instance was already present in the bin;
this causes a crash later with an out-of-bounds array access. However,
it's always illegal to try to bin a binned entity; the entity must be
removed from its bin first. Therefore, moving the instance count bump to
the case in which the entity wasn't already in a bin is the wrong fix.

This commit contains the correct fix, which is to change
`DirtySpecializations::iter_to_queue` so that it doesn't yield duplicate
entities. This PR removes the complex logic that failed to account for
all cases of duplicate mesh instances in favor of a simple hash set that
tracks the mesh instances yielded so far. Additionally, this PR deletes
the code path that tried to handle the case in which a mesh that's
already binned is binned again and replaces it with a panic in debug
mode.

I verified that this change fixes the crash in Bistro via `cargo run
--features=bevy/debug -p bistro --bin bistro`. Note that on my NVIDIA
GeForce RTX 2060, I still get an OOM error on that test case, but that's
a separate issue.
@pcwalton pcwalton added P-Crash A sudden unexpected crash P-Regression Functionality that used to work but no longer does. Add a test for this! A-Rendering Drawing game state to the screen labels Jul 14, 2026
@github-project-automation github-project-automation Bot moved this to Needs SME Triage in Rendering Jul 14, 2026
@pcwalton pcwalton added the C-Bug An unexpected or incorrect behavior label Jul 14, 2026
@alice-i-cecile alice-i-cecile added this to the 0.19.1 milestone Jul 14, 2026
@pcwalton

Copy link
Copy Markdown
Contributor Author

This doesn’t need to be in 0.19.1 as PR #23662 isn’t in 0.19.

@kfc35 kfc35 modified the milestones: 0.19.1, 0.20 Jul 14, 2026

@kfc35 kfc35 left a comment

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.

This fixes the startup errors for mixed_lighting and clustered_decal_maps, but switching light types in pcss (cargo run --example pcss —features="experimental_pbr_pcss") still errors when switching light types, now with the new error. That example removes a Light component from an entity and sticks a different Light on the same entity when the light type is switched. It results in the new debug_assert! failing in render_phase/mod.rs:

thread 'Compute Task Pool (1)' (3041233) panicked at crates/bevy_render/src/render_phase/mod.rs:295:9:
assertion failed: maybe_previous_binned_mesh_instance_buffer_index.is_none()
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

thread 'Compute Task Pool (1)' (3041233) panicked at crates/bevy_ecs/src/error/handler.rs:141:1:
Encountered an error in system `bevy_pbr::render::light::queue_shadows`: System panicked

Encountered a panic in system `bevy_pbr::render::light::queue_shadows`!

thread 'Render thread' (3041323) panicked at crates/bevy_ecs/src/error/handler.rs:141:1:
Encountered an error in system `bevy_render::run_render_schedule`: Exclusive system panicked

Encountered a panic in system `bevy_render::run_render_schedule`!

I’m approving since this does fix several crashes. This probably supercedes #24980 but the issue with pcss still needs to be resolved.

})
.chain(last_frame_view_pending_queues.iter().filter_map(
|(entity, main_entity)| {
if render_visible_mesh_entities.entity_pair_is_visible(*entity, *main_entity) {

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.

Is removing the entity_pair_is_visible check intentional here? It seems like the check could be kept. iter_to_specialize continues to keep it

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.

It shouldn’t be needed anymore because mesh_instances_queued_this_frame handles that case. i.e. it existed before to avoid yielding a mesh instance twice, but now we have a more general solution for that, so there’s no need for the check.

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.

Oh, and also iter_to_specialize doesn’t have mesh_instances_queued_this_frame, so it still needs the check since it has no other way to avoid yielding a mesh instance twice.

Comment thread crates/bevy_render/src/camera.rs Outdated
last_frame_view_pending_queues: &'a HashSet<(Entity, MainEntity)>,
mesh_instances_queued_this_frame: &'a mut MainEntityHashSet,
) -> impl Iterator<Item = (&'a Entity, &'a MainEntity)> {
mesh_instances_queued_this_frame.clear();

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.

nit: mesh_instances_queued_this_frame is maybe misleading name given it is cleared on every iter_to_queue call -- is more of a queued scratch space

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.

Renamed it to mesh_instances_queued_this_iteration_scratch_space.

@alice-i-cecile alice-i-cecile added the S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it label Jul 14, 2026
@alice-i-cecile
alice-i-cecile added this pull request to the merge queue Jul 14, 2026
@alice-i-cecile
alice-i-cecile removed this pull request from the merge queue due to a manual request Jul 14, 2026
@alice-i-cecile alice-i-cecile added S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged and removed S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it labels Jul 14, 2026
@alice-i-cecile

Copy link
Copy Markdown
Member

Waiting on brief replies to @stuartparmenter then I'll merge :)

@pcwalton pcwalton mentioned this pull request Jul 14, 2026

@ChristopherBiscardi ChristopherBiscardi left a comment

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.

fixes world_serialization and custom_skinned_mesh as well. thus closes #24990

@alice-i-cecile alice-i-cecile 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-Waiting-on-Author The author needs to make changes or address concerns before this can be merged labels Jul 14, 2026
@alice-i-cecile

Copy link
Copy Markdown
Member

@pcwalton lemme know when you'd like me to merge this and I'll press the button for you :)

@pcwalton pcwalton mentioned this pull request Jul 15, 2026
@pcwalton

pcwalton commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

Merged on top of #24980. I kept the change from #24980 that avoids panicking or corrupting the bins if we yield the same mesh instance twice, but we now log an error.

Additionally, I fixed the crash in PCSS at the source. The problem in that case was that RenderExtractedShadowMapVisibleEntities and RenderShadowMapVisibleEntities were part of the SyncComponent::Target for DirectionalLight, PointLight, and SpotLight. So if a light changes from one type to another, the system that handles SyncComponent will run and delete the RenderExtractedShadowMapVisibleEntities and RenderShadowMapVisibleEntities components. That system runs during extraction, and the scheduler might decide to schedule it after extract_lights. If it does, then the RenderExtractedShadowMapVisibleEntities and RenderShadowMapVisibleEntities components that extract_lights updated will be deleted right after the light changes type. Because those components must be retained in order to detect whether entities are newly visible or retained from the previous frame, this ultimately causes us to bin mesh instances in the shadow map twice. The solution was to remove RenderExtractedShadowMapVisibleEntities and RenderShadowMapVisibleEntities from the SyncComponent::Target associated type for DirectionalLight, PointLight, and SpotLight and to centralize the management of those components in extract_lights.

@pcwalton pcwalton added S-Needs-Review Needs reviewer attention (from anyone!) to move forward and removed S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it labels Jul 16, 2026
@alice-i-cecile
alice-i-cecile added this pull request to the merge queue Jul 16, 2026
Merged via the queue into bevyengine:main with commit 244e0e6 Jul 16, 2026
40 checks passed
@github-project-automation github-project-automation Bot moved this from Needs SME Triage to Done in Rendering Jul 16, 2026
stuartparmenter added a commit to stuartparmenter/bevy that referenced this pull request Jul 18, 2026
An entity whose material is still loading lives in ViewPendingQueues —
the sole retry mechanism for specialization and queueing. Both iterators
matched those entries against the current visible lists by exact
(render entity, main entity) pair (iter_to_specialize), or not at all
(iter_to_queue, since bevyengine#24984). The render-entity half of a stored pair
can go stale while the main entity stays continuously visible, e.g.
when a component insertion changes how the entity is mirrored to the
render world. The specialize side then silently evicts the entry, the
main entity never re-enters via added_entities (it was never invisible),
and once the material loads nothing specializes or queues it: on
deferred views the prepass bin item is the entire draw, so the mesh is
permanently invisible with no error.

Key both pending chains on the main entity and re-resolve the render
entity through entity_pair_from_visible_main_entity, restoring the
queue/specialize symmetry bevyengine#24984 dropped while keeping its
never-bin-twice dedup as the final stage.
dreamersilly pushed a commit to dreamersilly/bevy that referenced this pull request Jul 19, 2026
…imes. (bevyengine#24984)

At the moment, `DirtySpecializations::iter_to_queue` attempts to avoid
yielding the same mesh instance multiple times in a complex way.
Unfortunately, this logic is incorrect for mesh instances that couldn't
be queued because their materials haven't loaded yet (i.e. the contents
of `last_frame_view_pending_queues`). The Bistro example tests this code
path, as it has many materials that take a significant amount of time to
load.

Code exists that tries to handle the case in which a mesh is re-binned,
and this is why no crash occurred before PR bevyengine#23662. PR bevyengine#23662 made this
incorrect code *more* incorrect because it bumps the instance count
regardless of whether the mesh instance was already present in the bin;
this causes a crash later with an out-of-bounds array access. However,
it's always illegal to try to bin a binned entity; the entity must be
removed from its bin first. Therefore, moving the instance count bump to
the case in which the entity wasn't already in a bin is the wrong fix.

This commit contains the correct fix, which is to change
`DirtySpecializations::iter_to_queue` so that it doesn't yield duplicate
entities. This PR removes the complex logic that failed to account for
all cases of duplicate mesh instances in favor of a simple hash set that
tracks the mesh instances yielded so far. Additionally, this PR deletes
the code path that tried to handle the case in which a mesh that's
already binned is binned again and replaces it with a panic in debug
mode.

I verified that this change fixes the crash in Bistro via `cargo run
--features=bevy/debug -p bistro --bin bistro`. Note that on my NVIDIA
GeForce RTX 2060, I still get an OOM error on that test case, but that's
a separate issue.

Fixes bevyengine#24990.
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 P-Crash A sudden unexpected crash P-Regression Functionality that used to work but no longer does. Add a test for this! S-Needs-Review Needs reviewer attention (from anyone!) to move forward

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

binned panics

6 participants