diff --git a/crates/bevy_ecs/src/schedule/executor/mod.rs b/crates/bevy_ecs/src/schedule/executor/mod.rs index cc61c6969936c..585ffa6358324 100644 --- a/crates/bevy_ecs/src/schedule/executor/mod.rs +++ b/crates/bevy_ecs/src/schedule/executor/mod.rs @@ -119,3 +119,54 @@ pub(super) fn is_apply_deferred(system: &BoxedSystem) -> bool { // deref to use `System::type_id` instead of `Any::type_id` system.as_ref().type_id() == apply_deferred.system_type_id() } + +/// These functions hide the bottom of the callstack from `RUST_BACKTRACE=1` (assuming the default panic handler is used). +/// The full callstack will still be visible with `RUST_BACKTRACE=full`. +/// They are specialized for `System::run` & co instead of being generic over closures because this avoids an +/// extra frame in the backtrace. +/// +/// This is reliant on undocumented behavior in Rust's default panic handler, which checks the call stack for symbols +/// containing the string `__rust_begin_short_backtrace` in their mangled name. +mod __rust_begin_short_backtrace { + use std::hint::black_box; + + use crate::{ + system::{ReadOnlySystem, System}, + world::{unsafe_world_cell::UnsafeWorldCell, World}, + }; + + /// # Safety + /// See `System::run_unsafe`. + #[inline(never)] + pub(super) unsafe fn run_unsafe( + system: &mut dyn System, + world: UnsafeWorldCell, + ) { + system.run_unsafe((), world); + black_box(()); + } + + /// # Safety + /// See `ReadOnlySystem::run_unsafe`. + #[inline(never)] + pub(super) unsafe fn readonly_run_unsafe( + system: &mut dyn ReadOnlySystem, + world: UnsafeWorldCell, + ) -> O { + black_box(system.run_unsafe((), world)) + } + + #[inline(never)] + pub(super) fn run(system: &mut dyn System, world: &mut World) { + system.run((), world); + black_box(()); + } + + #[inline(never)] + pub(super) fn readonly_run( + system: &mut dyn ReadOnlySystem, + world: &mut World, + ) -> O { + black_box(system.run((), world)) + } +} diff --git a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs index 82a3e207e995f..e6e2037748339 100644 --- a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs +++ b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs @@ -24,6 +24,8 @@ use crate::{ use crate as bevy_ecs; +use super::__rust_begin_short_backtrace; + /// Borrowed data used by the [`MultiThreadedExecutor`]. struct Environment<'env, 'sys> { executor: &'env MultiThreadedExecutor, @@ -618,7 +620,12 @@ impl ExecutorState { // - The caller ensures that we have permission to // access the world data used by the system. // - `update_archetype_component_access` has been called. - unsafe { system.run_unsafe((), context.environment.world_cell) }; + unsafe { + __rust_begin_short_backtrace::run_unsafe( + &mut **system, + context.environment.world_cell, + ); + }; })); context.system_completed(system_index, res, system); }; @@ -668,7 +675,7 @@ impl ExecutorState { let res = std::panic::catch_unwind(AssertUnwindSafe(|| { #[cfg(feature = "trace")] let _span = system_span.enter(); - system.run((), world); + __rust_begin_short_backtrace::run(&mut **system, world); })); context.system_completed(system_index, res, system); }; @@ -780,7 +787,7 @@ unsafe fn evaluate_and_fold_conditions( .map(|condition| { // SAFETY: The caller ensures that `world` has permission to // access any data required by the condition. - unsafe { condition.run_unsafe((), world) } + unsafe { __rust_begin_short_backtrace::readonly_run_unsafe(&mut **condition, world) } }) .fold(true, |acc, res| acc && res) } diff --git a/crates/bevy_ecs/src/schedule/executor/simple.rs b/crates/bevy_ecs/src/schedule/executor/simple.rs index 7e5ce65a2022e..c3d7a630217fe 100644 --- a/crates/bevy_ecs/src/schedule/executor/simple.rs +++ b/crates/bevy_ecs/src/schedule/executor/simple.rs @@ -10,6 +10,8 @@ use crate::{ world::World, }; +use super::__rust_begin_short_backtrace; + /// A variant of [`SingleThreadedExecutor`](crate::schedule::SingleThreadedExecutor) that calls /// [`apply_deferred`](crate::system::System::apply_deferred) immediately after running each system. #[derive(Default)] @@ -93,7 +95,7 @@ impl SystemExecutor for SimpleExecutor { } let res = std::panic::catch_unwind(AssertUnwindSafe(|| { - system.run((), world); + __rust_begin_short_backtrace::run(&mut **system, world); })); if let Err(payload) = res { eprintln!("Encountered a panic in system `{}`!", &*system.name()); @@ -126,7 +128,7 @@ fn evaluate_and_fold_conditions(conditions: &mut [BoxedCondition], world: &mut W #[allow(clippy::unnecessary_fold)] conditions .iter_mut() - .map(|condition| condition.run((), world)) + .map(|condition| __rust_begin_short_backtrace::readonly_run(&mut **condition, world)) .fold(true, |acc, res| acc && res) } diff --git a/crates/bevy_ecs/src/schedule/executor/single_threaded.rs b/crates/bevy_ecs/src/schedule/executor/single_threaded.rs index 3c88b463df43f..b7419bf66aedf 100644 --- a/crates/bevy_ecs/src/schedule/executor/single_threaded.rs +++ b/crates/bevy_ecs/src/schedule/executor/single_threaded.rs @@ -8,6 +8,8 @@ use crate::{ world::World, }; +use super::__rust_begin_short_backtrace; + /// Runs the schedule using a single thread. /// /// Useful if you're dealing with a single-threaded environment, saving your threads for @@ -101,14 +103,14 @@ impl SystemExecutor for SingleThreadedExecutor { let res = std::panic::catch_unwind(AssertUnwindSafe(|| { if system.is_exclusive() { - system.run((), world); + __rust_begin_short_backtrace::run(&mut **system, world); } else { // Use run_unsafe to avoid immediately applying deferred buffers let world = world.as_unsafe_world_cell(); system.update_archetype_component_access(world); // SAFETY: We have exclusive, single-threaded access to the world and // update_archetype_component_access is being called immediately before this. - unsafe { system.run_unsafe((), world) }; + unsafe { __rust_begin_short_backtrace::run_unsafe(&mut **system, world) }; } })); if let Err(payload) = res { @@ -158,6 +160,6 @@ fn evaluate_and_fold_conditions(conditions: &mut [BoxedCondition], world: &mut W #[allow(clippy::unnecessary_fold)] conditions .iter_mut() - .map(|condition| condition.run((), world)) + .map(|condition| __rust_begin_short_backtrace::readonly_run(&mut **condition, world)) .fold(true, |acc, res| acc && res) } diff --git a/crates/bevy_render/src/pipelined_rendering.rs b/crates/bevy_render/src/pipelined_rendering.rs index 0688c90095f95..00ffc3b01e2f9 100644 --- a/crates/bevy_render/src/pipelined_rendering.rs +++ b/crates/bevy_render/src/pipelined_rendering.rs @@ -1,6 +1,6 @@ use async_channel::{Receiver, Sender}; -use bevy_app::{App, AppLabel, Main, Plugin, SubApp}; +use bevy_app::{App, AppExit, AppLabel, Main, Plugin, SubApp}; use bevy_ecs::{ schedule::MainThreadExecutor, system::Resource, @@ -45,10 +45,11 @@ impl RenderAppChannels { } /// Receive the `render_app` from the rendering thread. - pub async fn recv(&mut self) -> SubApp { - let render_app = self.render_to_app_receiver.recv().await.unwrap(); + /// Return `None` if the render thread has panicked. + pub async fn recv(&mut self) -> Option { + let render_app = self.render_to_app_receiver.recv().await.ok()?; self.render_app_in_render_thread = false; - render_app + Some(render_app) } } @@ -180,16 +181,20 @@ fn update_rendering(app_world: &mut World, _sub_app: &mut App) { world.resource_scope(|world, mut render_channels: Mut| { // we use a scope here to run any main thread tasks that the render world still needs to run // while we wait for the render world to be received. - let mut render_app = ComputeTaskPool::get() + if let Some(mut render_app) = ComputeTaskPool::get() .scope_with_executor(true, Some(&*main_thread_executor.0), |s| { s.spawn(async { render_channels.recv().await }); }) .pop() - .unwrap(); - - render_app.extract(world); - - render_channels.send_blocking(render_app); + .unwrap() + { + render_app.extract(world); + + render_channels.send_blocking(render_app); + } else { + // Renderer thread panicked + world.send_event(AppExit); + } }); }); }