Bevy version and features
- Bevy
0.19.0 (crates.io).
- Default features (plus
debug, only so the panic prints the resource's
full type name — the bug reproduces with default features too).
[Optional] Relevant system information
cargo 1.96.0 (30a34c682 2026-05-25)
- macOS 15.5 (Sequoia), Apple M4.
- No GPU / adapter is involved: the app is configured with
WgpuSettings { backends: None, .. }, so no wgpu adapter is requested and no
render world is ever created. The crash is purely in the main world and is
platform-independent.
What you did
Ran a minimal headless Bevy app: DefaultPlugins with no window, no winit
event loop, and no wgpu backend (backends: None), driven by
ScheduleRunnerPlugin. A startup system spawns a single Camera3d entity; an
update system despawns it a few frames later.
src/main.rs (full, single file, no assets)
//! Repro: despawning a render-synced entity (e.g. `Camera3d`) in a headless
//! app (`WgpuSettings { backends: None }`) panics with
//! «Requested resource bevy_render::sync_world::PendingSyncEntity does not exist».
//!
//! Cause: `SyncComponentPlugin::<C>` registers an `on_remove` component hook
//! unconditionally, and that hook does `world.resource_mut::<PendingSyncEntity>()`.
//! The resource is only inserted by `SyncWorldPlugin`, which is only added when
//! the render world is created — and with `backends: None` it never is.
//!
//! Run: `cargo run`. Expected: the app panics at frame 3, right at the despawn.
use std::time::Duration;
use bevy::{
app::ScheduleRunnerPlugin,
prelude::*,
render::{
settings::{WgpuSettings, RenderCreation},
RenderPlugin,
},
window::ExitCondition,
winit::WinitPlugin,
};
fn main() {
App::new()
.add_plugins(
DefaultPlugins
// No window / no winit event loop: pure headless.
.set(WindowPlugin {
primary_window: None,
primary_cursor_options: None,
exit_condition: ExitCondition::DontExit,
close_when_requested: false,
})
// The crux: request no wgpu backend, so no render world is
// ever created.
.set(RenderPlugin {
render_creation: RenderCreation::from(WgpuSettings {
backends: None,
..default()
}),
..default()
})
.disable::<WinitPlugin>(),
)
// Drive the schedule ourselves since there is no winit loop.
.add_plugins(ScheduleRunnerPlugin::run_loop(Duration::ZERO))
.add_systems(Startup, spawn_camera)
.add_systems(Update, despawn_on_frame_3)
.run();
}
fn spawn_camera(mut commands: Commands) {
let entity = commands.spawn(Camera3d::default()).id();
info!("spawned camera entity {entity}");
}
fn despawn_on_frame_3(
mut commands: Commands,
q: Query<Entity, With<Camera3d>>,
mut frame: Local<u32>,
) {
*frame += 1;
info!("frame {}", *frame);
if *frame == 3 {
for entity in &q {
info!("about to despawn camera {entity} ...");
commands.entity(entity).despawn();
info!("... despawn command queued (panic expected as it applies)");
}
}
}
Cargo.toml:
[dependencies]
bevy = { version = "0.19", features = ["debug"] }
What went wrong
Expected: one of two consistent behaviors:
- Fail fast at
App build time. Arguably RenderPlugin with
backends: None while render-consuming plugins (camera/mesh/light) are
present is a configuration the engine does not want to support — then
building the App should panic/error right away with a clear message,
not succeed and blow up minutes later.
- Degrade gracefully. The render world is never created, so the sync
machinery has nothing to do: despawning cameras, meshes, lights, etc.
should simply work.
Either is fine; what should not happen is the current middle ground — the app
builds and runs normally, then panics at an arbitrary later point, the first
time any render-synced entity is despawned.
Actual: the app panics the moment the despawn command is applied:
Why this scenario matters. Headless is usefull for CI testing of game, which far faster without render. And wgpu setting without backend looks like normal way to run headless (if there are no panic in RenderPlugin creation)
thread 'main' panicked at
.../bevy_render-0.19.0/src/sync_component.rs:55:41:
Requested resource bevy_render::sync_world::PendingSyncEntity does not exist in the `World`.
Did you forget to add it using `app.insert_resource` / `app.init_resource`?
Resources are also implicitly added via `app.add_message`,
and can be added by plugins.
Encountered a panic when applying buffers for system `bevy_headless_sync_repro::despawn_on_frame_3`!
(Without the debug feature the message is the same panic at the same location,
just with <Enable the debug feature to see the name> instead of the resolved
PendingSyncEntity type name.)
Additional information
Root cause. SyncComponentPlugin::<C>::build() registers an on_remove
component hook unconditionally, and that hook unconditionally reads the
PendingSyncEntity resource:
bevy_render-0.19.0/src/sync_component.rs (build):
impl<C: SyncComponent<F>, F: Send + Sync + 'static> Plugin for SyncComponentPlugin<C, F> {
fn build(&self, app: &mut App) {
app.register_required_components::<C, SyncToRenderWorld>();
app.world_mut()
.register_component_hooks::<C>()
.on_remove(|mut world, context| {
let mut pending = world.resource_mut::<PendingSyncEntity>(); // <-- panics
pending.push(EntityRecord::ComponentRemoved(
context.entity,
|mut entity| { entity.remove::<C::Target>(); },
));
});
}
}
But PendingSyncEntity is only inserted by SyncWorldPlugin
(bevy_render-0.19.0/src/sync_world.rs: app.init_resource::<PendingSyncEntity>()),
and SyncWorldPlugin is only added through ExtractPlugin, which
RenderPlugin::build() adds only when a rendering backend is available
(bevy_render-0.19.0/src/lib.rs):
if insert_future_resources(&self.render_creation, app.world_mut()) {
// We only create the render world and set up extraction if we
// have a rendering backend available.
app.add_plugins(ExtractPlugin { .. });
};
app.add_plugins((
WindowRenderPlugin,
CameraPlugin, // <-- adds SyncComponentPlugin::<Camera> → registers the on_remove hook
...
));
So with backends: None, insert_future_resources returns false, no render
world / SyncWorldPlugin / PendingSyncEntity — yet CameraPlugin (and the
mesh/light plugins) still add SyncComponentPlugin, which still registers the
on_remove hook. Despawning any entity carrying a synced component
(Camera3d, Mesh3d, DirectionalLight/PointLight/SpotLight, …) fires the
hook, which reads a resource that does not exist → panic.
This means any backends: None headless app crashes on the first despawn of a
camera / mesh / light. (For the record, Bevy's own CI is not affected — it
runs examples against real software GPUs, mesa/dx12 — but backends: None is a
documented WgpuSettings option used by downstream headless simulation/test
setups.) In a real app it surfaces easily, e.g. via DespawnOnExit tearing
down a scene that contains a camera.
Suggested fix. The hook has nothing to do when there is no render world, so
it should degrade gracefully instead of asserting the resource's presence — use
get_resource_mut with an early return:
.on_remove(|mut world, context| {
// No render world → nothing to sync.
let Some(mut pending) = world.get_resource_mut::<PendingSyncEntity>() else {
return;
};
pending.push(EntityRecord::ComponentRemoved(
context.entity,
|mut entity| { entity.remove::<C::Target>(); },
));
});
(Alternatively, only register the hook when the render world / SyncWorldPlugin
is present.)
Workaround. Patch bevy_render locally ([patch.crates-io]) to make the
on_remove hook use get_resource_mut with an early return as above; this fully
avoids the crash with no behavioral change for apps that do have a render world.
Bevy version and features
0.19.0(crates.io).debug, only so the panic prints the resource'sfull type name — the bug reproduces with default features too).
[Optional] Relevant system information
cargo 1.96.0 (30a34c682 2026-05-25)WgpuSettings { backends: None, .. }, so no wgpu adapter is requested and norender world is ever created. The crash is purely in the main world and is
platform-independent.
What you did
Ran a minimal headless Bevy app:
DefaultPluginswith no window, no winitevent loop, and no wgpu backend (
backends: None), driven byScheduleRunnerPlugin. A startup system spawns a singleCamera3dentity; anupdate system despawns it a few frames later.
src/main.rs(full, single file, no assets)Cargo.toml:What went wrong
Expected: one of two consistent behaviors:
Appbuild time. ArguablyRenderPluginwithbackends: Nonewhile render-consuming plugins (camera/mesh/light) arepresent is a configuration the engine does not want to support — then
building the
Appshould panic/error right away with a clear message,not succeed and blow up minutes later.
machinery has nothing to do: despawning cameras, meshes, lights, etc.
should simply work.
Either is fine; what should not happen is the current middle ground — the app
builds and runs normally, then panics at an arbitrary later point, the first
time any render-synced entity is despawned.
Actual: the app panics the moment the despawn command is applied:
Why this scenario matters. Headless is usefull for CI testing of game, which far faster without render. And wgpu setting without backend looks like normal way to run headless (if there are no panic in RenderPlugin creation)
(Without the
debugfeature the message is the same panic at the same location,just with
<Enable the debug feature to see the name>instead of the resolvedPendingSyncEntitytype name.)Additional information
Root cause.
SyncComponentPlugin::<C>::build()registers anon_removecomponent hook unconditionally, and that hook unconditionally reads the
PendingSyncEntityresource:bevy_render-0.19.0/src/sync_component.rs(build):But
PendingSyncEntityis only inserted bySyncWorldPlugin(
bevy_render-0.19.0/src/sync_world.rs:app.init_resource::<PendingSyncEntity>()),and
SyncWorldPluginis only added throughExtractPlugin, whichRenderPlugin::build()adds only when a rendering backend is available(
bevy_render-0.19.0/src/lib.rs):So with
backends: None,insert_future_resourcesreturnsfalse, no renderworld /
SyncWorldPlugin/PendingSyncEntity— yetCameraPlugin(and themesh/light plugins) still add
SyncComponentPlugin, which still registers theon_removehook. Despawning any entity carrying a synced component(
Camera3d,Mesh3d,DirectionalLight/PointLight/SpotLight, …) fires thehook, which reads a resource that does not exist → panic.
This means any
backends: Noneheadless app crashes on the first despawn of acamera / mesh / light. (For the record, Bevy's own CI is not affected — it
runs examples against real software GPUs, mesa/dx12 — but
backends: Noneis adocumented
WgpuSettingsoption used by downstream headless simulation/testsetups.) In a real app it surfaces easily, e.g. via
DespawnOnExittearingdown a scene that contains a camera.
Suggested fix. The hook has nothing to do when there is no render world, so
it should degrade gracefully instead of asserting the resource's presence — use
get_resource_mutwith an early return:(Alternatively, only register the hook when the render world /
SyncWorldPluginis present.)
Workaround. Patch
bevy_renderlocally ([patch.crates-io]) to make theon_removehook useget_resource_mutwith an early return as above; this fullyavoids the crash with no behavioral change for apps that do have a render world.