# Objective
Remove Bevy internals from backtraces
## Solution
Executors insert `__rust_begin_short_backtrace` into the callstack
before running a system.
<details>
<summary>Example current output</summary>
```
thread 'Compute Task Pool (3)' panicked at src/main.rs:7:33:
Foo
stack backtrace:
0: rust_begin_unwind
at /rustc/8ace7ea1f7cbba7b4f031e66c54ca237a0d65de6/library/std/src/panicking.rs:647:5
1: core::panicking::panic_fmt
at /rustc/8ace7ea1f7cbba7b4f031e66c54ca237a0d65de6/library/core/src/panicking.rs:72:14
2: foo::main::{{closure}}
at ./src/main.rs:7:33
3: core::ops::function::impls::<impl core::ops::function::FnMut<A> for &mut F>::call_mut
at /rustc/8ace7ea1f7cbba7b4f031e66c54ca237a0d65de6/library/core/src/ops/function.rs:294:13
4: <Func as bevy_ecs::system::function_system::SystemParamFunction<fn() .> Out>>::run::call_inner
at /home/vj/workspace/rust/bevy/crates/bevy_ecs/src/system/function_system.rs:661:21
5: <Func as bevy_ecs::system::function_system::SystemParamFunction<fn() .> Out>>::run
at /home/vj/workspace/rust/bevy/crates/bevy_ecs/src/system/function_system.rs:664:17
6: <bevy_ecs::system::function_system::FunctionSystem<Marker,F> as bevy_ecs::system::system::System>::run_unsafe
at /home/vj/workspace/rust/bevy/crates/bevy_ecs/src/system/function_system.rs:504:19
7: bevy_ecs::schedule::executor::multi_threaded::ExecutorState::spawn_system_task::{{closure}}::{{closure}}
at /home/vj/workspace/rust/bevy/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs:621:26
8: core::ops::function::FnOnce::call_once
at /rustc/8ace7ea1f7cbba7b4f031e66c54ca237a0d65de6/library/core/src/ops/function.rs:250:5
9: <core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once
at /rustc/8ace7ea1f7cbba7b4f031e66c54ca237a0d65de6/library/core/src/panic/unwind_safe.rs:272:9
10: std::panicking::try::do_call
at /rustc/8ace7ea1f7cbba7b4f031e66c54ca237a0d65de6/library/std/src/panicking.rs:554:40
11: __rust_try
12: std::panicking::try
at /rustc/8ace7ea1f7cbba7b4f031e66c54ca237a0d65de6/library/std/src/panicking.rs:518:19
13: std::panic::catch_unwind
at /rustc/8ace7ea1f7cbba7b4f031e66c54ca237a0d65de6/library/std/src/panic.rs:142:14
14: bevy_ecs::schedule::executor::multi_threaded::ExecutorState::spawn_system_task::{{closure}}
at /home/vj/workspace/rust/bevy/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs:614:23
15: <core::panic::unwind_safe::AssertUnwindSafe<F> as core::future::future::Future>::poll
at /rustc/8ace7ea1f7cbba7b4f031e66c54ca237a0d65de6/library/core/src/panic/unwind_safe.rs:297:9
16: <futures_lite::future::CatchUnwind<F> as core::future::future::Future>::poll::{{closure}}
at /home/vj/.cargo/registry/src/index.crates.io-6f17d22bba15001f/futures-lite-2.2.0/src/future.rs:588:42
17: <core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once
at /rustc/8ace7ea1f7cbba7b4f031e66c54ca237a0d65de6/library/core/src/panic/unwind_safe.rs:272:9
18: std::panicking::try::do_call
at /rustc/8ace7ea1f7cbba7b4f031e66c54ca237a0d65de6/library/std/src/panicking.rs:554:40
19: __rust_try
20: std::panicking::try
at /rustc/8ace7ea1f7cbba7b4f031e66c54ca237a0d65de6/library/std/src/panicking.rs:518:19
21: std::panic::catch_unwind
at /rustc/8ace7ea1f7cbba7b4f031e66c54ca237a0d65de6/library/std/src/panic.rs:142:14
22: <futures_lite::future::CatchUnwind<F> as core::future::future::Future>::poll
at /home/vj/.cargo/registry/src/index.crates.io-6f17d22bba15001f/futures-lite-2.2.0/src/future.rs:588:9
23: async_executor::Executor::spawn::{{closure}}
at /home/vj/.cargo/registry/src/index.crates.io-6f17d22bba15001f/async-executor-1.8.0/src/lib.rs:158:20
24: async_task::raw::RawTask<F,T,S,M>::run::{{closure}}
at /home/vj/.cargo/registry/src/index.crates.io-6f17d22bba15001f/async-task-4.7.0/src/raw.rs:550:21
25: core::ops::function::FnOnce::call_once
at /rustc/8ace7ea1f7cbba7b4f031e66c54ca237a0d65de6/library/core/src/ops/function.rs:250:5
26: <core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once
at /rustc/8ace7ea1f7cbba7b4f031e66c54ca237a0d65de6/library/core/src/panic/unwind_safe.rs:272:9
27: std::panicking::try::do_call
at /rustc/8ace7ea1f7cbba7b4f031e66c54ca237a0d65de6/library/std/src/panicking.rs:554:40
28: __rust_try
29: std::panicking::try
at /rustc/8ace7ea1f7cbba7b4f031e66c54ca237a0d65de6/library/std/src/panicking.rs:518:19
30: std::panic::catch_unwind
at /rustc/8ace7ea1f7cbba7b4f031e66c54ca237a0d65de6/library/std/src/panic.rs:142:14
31: async_task::raw::RawTask<F,T,S,M>::run
at /home/vj/.cargo/registry/src/index.crates.io-6f17d22bba15001f/async-task-4.7.0/src/raw.rs:549:23
32: async_task::runnable::Runnable<M>::run
at /home/vj/.cargo/registry/src/index.crates.io-6f17d22bba15001f/async-task-4.7.0/src/runnable.rs:781:18
33: async_executor::Executor::run::{{closure}}::{{closure}}
at /home/vj/.cargo/registry/src/index.crates.io-6f17d22bba15001f/async-executor-1.8.0/src/lib.rs:254:21
34: <futures_lite::future::Or<F1,F2> as core::future::future::Future>::poll
at /home/vj/.cargo/registry/src/index.crates.io-6f17d22bba15001f/futures-lite-2.2.0/src/future.rs:449:33
35: async_executor::Executor::run::{{closure}}
at /home/vj/.cargo/registry/src/index.crates.io-6f17d22bba15001f/async-executor-1.8.0/src/lib.rs:261:32
36: futures_lite::future::block_on::{{closure}}
at /home/vj/.cargo/registry/src/index.crates.io-6f17d22bba15001f/futures-lite-2.2.0/src/future.rs:99:19
37: std:🧵:local::LocalKey<T>::try_with
at /rustc/8ace7ea1f7cbba7b4f031e66c54ca237a0d65de6/library/std/src/thread/local.rs:286:16
38: std:🧵:local::LocalKey<T>::with
at /rustc/8ace7ea1f7cbba7b4f031e66c54ca237a0d65de6/library/std/src/thread/local.rs:262:9
39: futures_lite::future::block_on
at /home/vj/.cargo/registry/src/index.crates.io-6f17d22bba15001f/futures-lite-2.2.0/src/future.rs:78:5
40: bevy_tasks::task_pool::TaskPool::new_internal::{{closure}}::{{closure}}::{{closure}}::{{closure}}
at /home/vj/workspace/rust/bevy/crates/bevy_tasks/src/task_pool.rs:180:37
41: std::panicking::try::do_call
at /rustc/8ace7ea1f7cbba7b4f031e66c54ca237a0d65de6/library/std/src/panicking.rs:554:40
42: __rust_try
43: std::panicking::try
at /rustc/8ace7ea1f7cbba7b4f031e66c54ca237a0d65de6/library/std/src/panicking.rs:518:19
44: std::panic::catch_unwind
at /rustc/8ace7ea1f7cbba7b4f031e66c54ca237a0d65de6/library/std/src/panic.rs:142:14
45: bevy_tasks::task_pool::TaskPool::new_internal::{{closure}}::{{closure}}::{{closure}}
at /home/vj/workspace/rust/bevy/crates/bevy_tasks/src/task_pool.rs:174:43
46: std:🧵:local::LocalKey<T>::try_with
at /rustc/8ace7ea1f7cbba7b4f031e66c54ca237a0d65de6/library/std/src/thread/local.rs:286:16
47: std:🧵:local::LocalKey<T>::with
at /rustc/8ace7ea1f7cbba7b4f031e66c54ca237a0d65de6/library/std/src/thread/local.rs:262:9
48: bevy_tasks::task_pool::TaskPool::new_internal::{{closure}}::{{closure}}
at /home/vj/workspace/rust/bevy/crates/bevy_tasks/src/task_pool.rs:167:25
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
Encountered a panic in system `foo::main::{{closure}}`!
Encountered a panic in system `bevy_app::main_schedule::Main::run_main`!
get on your knees and beg mommy for forgiveness you pervert~ 💖
```
</details>
<details>
<summary>Example output with this PR</summary>
```
Panic at src/main.rs:7:33:
Foo
stack backtrace:
0: rust_begin_unwind
at /rustc/8ace7ea1f7cbba7b4f031e66c54ca237a0d65de6/library/std/src/panicking.rs:647:5
1: core::panicking::panic_fmt
at /rustc/8ace7ea1f7cbba7b4f031e66c54ca237a0d65de6/library/core/src/panicking.rs:72:14
2: foo::main::{{closure}}
at ./src/main.rs:7:59
3: core::ops::function::impls::<impl core::ops::function::FnMut<A> for &mut F>::call_mut
at /rustc/8ace7ea1f7cbba7b4f031e66c54ca237a0d65de6/library/core/src/ops/function.rs:294:13
4: <Func as bevy_ecs::system::function_system::SystemParamFunction<fn() .> Out>>::run::call_inner
at /home/vj/workspace/rust/bevy/crates/bevy_ecs/src/system/function_system.rs:661:21
5: <Func as bevy_ecs::system::function_system::SystemParamFunction<fn() .> Out>>::run
at /home/vj/workspace/rust/bevy/crates/bevy_ecs/src/system/function_system.rs:664:17
6: <bevy_ecs::system::function_system::FunctionSystem<Marker,F> as bevy_ecs::system::system::System>::run_unsafe
at /home/vj/workspace/rust/bevy/crates/bevy_ecs/src/system/function_system.rs:504:19
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
Encountered a panic in system `foo::main::{{closure}}`!
Encountered a panic in system `bevy_app::main_schedule::Main::run_main`!
```
</details>
Full backtraces (`RUST_BACKTRACE=full`) are unchanged.
## Alternative solutions
Write a custom panic hook. This could potentially let use exclude a few
more callstack frames but requires a dependency on `backtrace` and is
incompatible with user-provided panic hooks.
---
## Changelog
- Backtraces now exclude many Bevy internals (unless
`RUST_BACKTRACE=full` is used)
---------
Co-authored-by: James Liu <contact@jamessliu.com>
201 lines
8.2 KiB
Rust
201 lines
8.2 KiB
Rust
use async_channel::{Receiver, Sender};
|
|
|
|
use bevy_app::{App, AppExit, AppLabel, Main, Plugin, SubApp};
|
|
use bevy_ecs::{
|
|
schedule::MainThreadExecutor,
|
|
system::Resource,
|
|
world::{Mut, World},
|
|
};
|
|
use bevy_tasks::ComputeTaskPool;
|
|
|
|
use crate::RenderApp;
|
|
|
|
/// A Label for the sub app that runs the parts of pipelined rendering that need to run on the main thread.
|
|
///
|
|
/// The Main schedule of this app can be used to run logic after the render schedule starts, but
|
|
/// before I/O processing. This can be useful for something like frame pacing.
|
|
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, AppLabel)]
|
|
pub struct RenderExtractApp;
|
|
|
|
/// Channels used by the main app to send and receive the render app.
|
|
#[derive(Resource)]
|
|
pub struct RenderAppChannels {
|
|
app_to_render_sender: Sender<SubApp>,
|
|
render_to_app_receiver: Receiver<SubApp>,
|
|
render_app_in_render_thread: bool,
|
|
}
|
|
|
|
impl RenderAppChannels {
|
|
/// Create a `RenderAppChannels` from a [`async_channel::Receiver`] and [`async_channel::Sender`]
|
|
pub fn new(
|
|
app_to_render_sender: Sender<SubApp>,
|
|
render_to_app_receiver: Receiver<SubApp>,
|
|
) -> Self {
|
|
Self {
|
|
app_to_render_sender,
|
|
render_to_app_receiver,
|
|
render_app_in_render_thread: false,
|
|
}
|
|
}
|
|
|
|
/// Send the `render_app` to the rendering thread.
|
|
pub fn send_blocking(&mut self, render_app: SubApp) {
|
|
self.app_to_render_sender.send_blocking(render_app).unwrap();
|
|
self.render_app_in_render_thread = true;
|
|
}
|
|
|
|
/// Receive the `render_app` from the rendering thread.
|
|
/// Return `None` if the render thread has panicked.
|
|
pub async fn recv(&mut self) -> Option<SubApp> {
|
|
let render_app = self.render_to_app_receiver.recv().await.ok()?;
|
|
self.render_app_in_render_thread = false;
|
|
Some(render_app)
|
|
}
|
|
}
|
|
|
|
impl Drop for RenderAppChannels {
|
|
fn drop(&mut self) {
|
|
if self.render_app_in_render_thread {
|
|
// Any non-send data in the render world was initialized on the main thread.
|
|
// So on dropping the main world and ending the app, we block and wait for
|
|
// the render world to return to drop it. Which allows the non-send data
|
|
// drop methods to run on the correct thread.
|
|
self.render_to_app_receiver.recv_blocking().ok();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The [`PipelinedRenderingPlugin`] can be added to your application to enable pipelined rendering.
|
|
/// This moves rendering into a different thread, so that the Nth frame's rendering can
|
|
/// be run at the same time as the N + 1 frame's simulation.
|
|
///
|
|
/// ```text
|
|
/// |--------------------|--------------------|--------------------|--------------------|
|
|
/// | simulation thread | frame 1 simulation | frame 2 simulation | frame 3 simulation |
|
|
/// |--------------------|--------------------|--------------------|--------------------|
|
|
/// | rendering thread | | frame 1 rendering | frame 2 rendering |
|
|
/// |--------------------|--------------------|--------------------|--------------------|
|
|
/// ```
|
|
///
|
|
/// The plugin is dependent on the [`RenderApp`] added by [`crate::RenderPlugin`] and so must
|
|
/// be added after that plugin. If it is not added after, the plugin will do nothing.
|
|
///
|
|
/// A single frame of execution looks something like below
|
|
///
|
|
/// ```text
|
|
/// |--------------------------------------------------------------------|
|
|
/// | | RenderExtractApp schedule | winit events | main schedule |
|
|
/// | extract |----------------------------------------------------------|
|
|
/// | | extract commands | rendering schedule |
|
|
/// |--------------------------------------------------------------------|
|
|
/// ```
|
|
///
|
|
/// - `extract` is the step where data is copied from the main world to the render world.
|
|
/// This is run on the main app's thread.
|
|
/// - On the render thread, we first apply the `extract commands`. This is not run during extract, so the
|
|
/// main schedule can start sooner.
|
|
/// - Then the `rendering schedule` is run. See [`RenderSet`](crate::RenderSet) for the standard steps in this process.
|
|
/// - In parallel to the rendering thread the [`RenderExtractApp`] schedule runs. By
|
|
/// default this schedule is empty. But it is useful if you need something to run before I/O processing.
|
|
/// - Next all the `winit events` are processed.
|
|
/// - And finally the `main app schedule` is run.
|
|
/// - Once both the `main app schedule` and the `render schedule` are finished running, `extract` is run again.
|
|
#[derive(Default)]
|
|
pub struct PipelinedRenderingPlugin;
|
|
|
|
impl Plugin for PipelinedRenderingPlugin {
|
|
fn build(&self, app: &mut App) {
|
|
// Don't add RenderExtractApp if RenderApp isn't initialized.
|
|
if app.get_sub_app(RenderApp).is_err() {
|
|
return;
|
|
}
|
|
app.insert_resource(MainThreadExecutor::new());
|
|
|
|
let mut sub_app = App::empty();
|
|
sub_app.init_schedule(Main);
|
|
app.insert_sub_app(RenderExtractApp, SubApp::new(sub_app, update_rendering));
|
|
}
|
|
|
|
// Sets up the render thread and inserts resources into the main app used for controlling the render thread.
|
|
fn cleanup(&self, app: &mut App) {
|
|
// skip setting up when headless
|
|
if app.get_sub_app(RenderExtractApp).is_err() {
|
|
return;
|
|
}
|
|
|
|
let (app_to_render_sender, app_to_render_receiver) = async_channel::bounded::<SubApp>(1);
|
|
let (render_to_app_sender, render_to_app_receiver) = async_channel::bounded::<SubApp>(1);
|
|
|
|
let mut render_app = app
|
|
.remove_sub_app(RenderApp)
|
|
.expect("Unable to get RenderApp. Another plugin may have removed the RenderApp before PipelinedRenderingPlugin");
|
|
|
|
// clone main thread executor to render world
|
|
let executor = app.world.get_resource::<MainThreadExecutor>().unwrap();
|
|
render_app.app.world.insert_resource(executor.clone());
|
|
|
|
render_to_app_sender.send_blocking(render_app).unwrap();
|
|
|
|
app.insert_resource(RenderAppChannels::new(
|
|
app_to_render_sender,
|
|
render_to_app_receiver,
|
|
));
|
|
|
|
std::thread::spawn(move || {
|
|
#[cfg(feature = "trace")]
|
|
let _span = bevy_utils::tracing::info_span!("render thread").entered();
|
|
|
|
let compute_task_pool = ComputeTaskPool::get();
|
|
loop {
|
|
// run a scope here to allow main world to use this thread while it's waiting for the render app
|
|
let sent_app = compute_task_pool
|
|
.scope(|s| {
|
|
s.spawn(async { app_to_render_receiver.recv().await });
|
|
})
|
|
.pop();
|
|
let Some(Ok(mut render_app)) = sent_app else {
|
|
break;
|
|
};
|
|
|
|
{
|
|
#[cfg(feature = "trace")]
|
|
let _sub_app_span =
|
|
bevy_utils::tracing::info_span!("sub app", name = ?RenderApp).entered();
|
|
render_app.run();
|
|
}
|
|
|
|
if render_to_app_sender.send_blocking(render_app).is_err() {
|
|
break;
|
|
}
|
|
}
|
|
|
|
bevy_utils::tracing::debug!("exiting pipelined rendering thread");
|
|
});
|
|
}
|
|
}
|
|
|
|
// This function waits for the rendering world to be received,
|
|
// runs extract, and then sends the rendering world back to the render thread.
|
|
fn update_rendering(app_world: &mut World, _sub_app: &mut App) {
|
|
app_world.resource_scope(|world, main_thread_executor: Mut<MainThreadExecutor>| {
|
|
world.resource_scope(|world, mut render_channels: Mut<RenderAppChannels>| {
|
|
// 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.
|
|
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);
|
|
} else {
|
|
// Renderer thread panicked
|
|
world.send_event(AppExit);
|
|
}
|
|
});
|
|
});
|
|
}
|