Skip to main content

bevy_render/
pipelined_rendering.rs

1use async_channel::{Receiver, Sender};
2
3use bevy_app::{App, AppExit, AppLabel, Plugin, SubApp};
4use bevy_ecs::{
5    resource::Resource,
6    schedule::MainThreadExecutor,
7    world::{Mut, World},
8};
9use bevy_tasks::ComputeTaskPool;
10
11use crate::RenderApp;
12
13/// A Label for the sub app that runs the parts of pipelined rendering that need to run on the main thread.
14///
15/// The Main schedule of this app can be used to run logic after the render schedule starts, but
16/// before I/O processing. This can be useful for something like frame pacing.
17#[derive(#[automatically_derived]
impl ::core::fmt::Debug for RenderExtractApp {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::write_str(f, "RenderExtractApp")
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for RenderExtractApp {
    #[inline]
    fn clone(&self) -> RenderExtractApp { *self }
}Clone, #[automatically_derived]
impl ::core::marker::Copy for RenderExtractApp { }Copy, #[automatically_derived]
impl ::core::hash::Hash for RenderExtractApp {
    #[inline]
    fn hash<__H: ::core::hash::Hasher>(&self, state: &mut __H) {}
}Hash, #[automatically_derived]
impl ::core::cmp::PartialEq for RenderExtractApp {
    #[inline]
    fn eq(&self, other: &RenderExtractApp) -> bool { true }
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for RenderExtractApp {
    #[inline]
    #[doc(hidden)]
    #[coverage(off)]
    fn assert_fields_are_eq(&self) {}
}Eq, const _: () =
    {
        extern crate alloc;
        impl bevy_app::AppLabel for RenderExtractApp where Self: 'static +
            ::core::marker::Send + ::core::marker::Sync +
            ::core::clone::Clone + ::core::cmp::Eq + ::core::fmt::Debug +
            ::core::hash::Hash {
            fn dyn_clone(&self) -> alloc::boxed::Box<dyn bevy_app::AppLabel> {
                alloc::boxed::Box::new(::core::clone::Clone::clone(self))
            }
        }
    };AppLabel)]
18pub struct RenderExtractApp;
19
20/// Channels used by the main app to send and receive the render app.
21#[derive(impl bevy_ecs::resource::Resource for RenderAppChannels where
    Self: ::core::marker::Send + ::core::marker::Sync + 'static {}Resource)]
22pub struct RenderAppChannels {
23    app_to_render_sender: Sender<SubApp>,
24    render_to_app_receiver: Receiver<SubApp>,
25    render_app_in_render_thread: bool,
26}
27
28impl RenderAppChannels {
29    /// Create a `RenderAppChannels` from a [`async_channel::Receiver`] and [`async_channel::Sender`]
30    pub fn new(
31        app_to_render_sender: Sender<SubApp>,
32        render_to_app_receiver: Receiver<SubApp>,
33    ) -> Self {
34        Self {
35            app_to_render_sender,
36            render_to_app_receiver,
37            render_app_in_render_thread: false,
38        }
39    }
40
41    /// Send the `render_app` to the rendering thread.
42    pub fn send_blocking(&mut self, render_app: SubApp) {
43        self.app_to_render_sender.send_blocking(render_app).unwrap();
44        self.render_app_in_render_thread = true;
45    }
46
47    /// Receive the `render_app` from the rendering thread.
48    /// Return `None` if the render thread has panicked.
49    pub async fn recv(&mut self) -> Option<SubApp> {
50        let render_app = self.render_to_app_receiver.recv().await.ok()?;
51        self.render_app_in_render_thread = false;
52        Some(render_app)
53    }
54}
55
56impl Drop for RenderAppChannels {
57    fn drop(&mut self) {
58        if self.render_app_in_render_thread {
59            // Any non-send data in the render world was initialized on the main thread.
60            // So on dropping the main world and ending the app, we block and wait for
61            // the render world to return to drop it. Which allows the non-send data
62            // drop methods to run on the correct thread.
63            self.render_to_app_receiver.recv_blocking().ok();
64        }
65    }
66}
67
68/// The [`PipelinedRenderingPlugin`] can be added to your application to enable pipelined rendering.
69///
70/// This moves rendering into a different thread, so that the Nth frame's rendering can
71/// be run at the same time as the N + 1 frame's simulation.
72///
73/// ```text
74/// |--------------------|--------------------|--------------------|--------------------|
75/// | simulation thread  | frame 1 simulation | frame 2 simulation | frame 3 simulation |
76/// |--------------------|--------------------|--------------------|--------------------|
77/// | rendering thread   |                    | frame 1 rendering  | frame 2 rendering  |
78/// |--------------------|--------------------|--------------------|--------------------|
79/// ```
80///
81/// The plugin is dependent on the [`RenderApp`] added by [`crate::RenderPlugin`] and so must
82/// be added after that plugin. If it is not added after, the plugin will do nothing.
83///
84/// A single frame of execution looks something like below
85///
86/// ```text
87/// |---------------------------------------------------------------------------|
88/// |      |         | RenderExtractApp schedule | winit events | main schedule |
89/// | sync | extract |----------------------------------------------------------|
90/// |      |         | extract commands | rendering schedule                    |
91/// |---------------------------------------------------------------------------|
92/// ```
93///
94/// - `sync` is the step where the entity-entity mapping between the main and render world is updated.
95///   This is run on the main app's thread. For more information checkout [`SyncWorldPlugin`].
96/// - `extract` is the step where data is copied from the main world to the render world.
97///   This is run on the main app's thread.
98/// - On the render thread, we first apply the `extract commands`. This is not run during extract, so the
99///   main schedule can start sooner.
100/// - Then the `rendering schedule` is run. See [`RenderSystems`](crate::RenderSystems) for the standard steps in this process.
101/// - In parallel to the rendering thread the [`RenderExtractApp`] schedule runs. By
102///   default, this schedule is empty. But it is useful if you need something to run before I/O processing.
103/// - Next all the `winit events` are processed.
104/// - And finally the `main app schedule` is run.
105/// - Once both the `main app schedule` and the `render schedule` are finished running, `extract` is run again.
106///
107/// [`SyncWorldPlugin`]: crate::sync_world::SyncWorldPlugin
108#[derive(#[automatically_derived]
impl ::core::default::Default for PipelinedRenderingPlugin {
    #[inline]
    fn default() -> PipelinedRenderingPlugin { PipelinedRenderingPlugin {} }
}Default)]
109pub struct PipelinedRenderingPlugin;
110
111impl Plugin for PipelinedRenderingPlugin {
112    fn build(&self, app: &mut App) {
113        // Don't add RenderExtractApp if RenderApp isn't initialized.
114        if app.get_sub_app(RenderApp).is_none() {
115            return;
116        }
117        app.insert_resource(MainThreadExecutor::new());
118
119        let mut sub_app = SubApp::new();
120        sub_app.set_extract(renderer_extract);
121        app.insert_sub_app(RenderExtractApp, sub_app);
122    }
123
124    // Sets up the render thread and inserts resources into the main app used for controlling the render thread.
125    fn cleanup(&self, app: &mut App) {
126        // skip setting up when headless
127        if app.get_sub_app(RenderExtractApp).is_none() {
128            return;
129        }
130
131        let (app_to_render_sender, app_to_render_receiver) = async_channel::bounded::<SubApp>(1);
132        let (render_to_app_sender, render_to_app_receiver) = async_channel::bounded::<SubApp>(1);
133
134        let mut render_app = app
135            .remove_sub_app(RenderApp)
136            .expect("Unable to get RenderApp. Another plugin may have removed the RenderApp before PipelinedRenderingPlugin");
137
138        // clone main thread executor to render world
139        let executor = app.world().get_resource::<MainThreadExecutor>().unwrap();
140        render_app.world_mut().insert_resource(executor.clone());
141
142        render_to_app_sender.send_blocking(render_app).unwrap();
143
144        app.insert_resource(RenderAppChannels::new(
145            app_to_render_sender,
146            render_to_app_receiver,
147        ));
148
149        std::thread::spawn(move || {
150            #[cfg(feature = "trace")]
151            let _span = {
    use ::tracing::__macro_support::Callsite as _;
    static __CALLSITE: ::tracing::callsite::DefaultCallsite =
        {
            static META: ::tracing::Metadata<'static> =
                {
                    ::tracing_core::metadata::Metadata::new("render thread",
                        "bevy_render::pipelined_rendering", ::tracing::Level::INFO,
                        ::tracing_core::__macro_support::Option::Some("src/pipelined_rendering.rs"),
                        ::tracing_core::__macro_support::Option::Some(151u32),
                        ::tracing_core::__macro_support::Option::Some("bevy_render::pipelined_rendering"),
                        ::tracing_core::field::FieldSet::new(&[],
                            ::tracing_core::callsite::Identifier(&__CALLSITE)),
                        ::tracing::metadata::Kind::SPAN)
                };
            ::tracing::callsite::DefaultCallsite::new(&META)
        };
    let mut interest = ::tracing::subscriber::Interest::never();
    if ::tracing::Level::INFO <= ::tracing::level_filters::STATIC_MAX_LEVEL &&
                    ::tracing::Level::INFO <=
                        ::tracing::level_filters::LevelFilter::current() &&
                { interest = __CALLSITE.interest(); !interest.is_never() } &&
            ::tracing::__macro_support::__is_enabled(__CALLSITE.metadata(),
                interest) {
        let meta = __CALLSITE.metadata();
        ::tracing::Span::new(meta, &{ meta.fields().value_set_all(&[]) })
    } else {
        let span =
            ::tracing::__macro_support::__disabled_span(__CALLSITE.metadata());
        {};
        span
    }
}bevy_log::info_span!("render thread").entered();
152
153            let compute_task_pool = ComputeTaskPool::get();
154            loop {
155                // run a scope here to allow main world to use this thread while it's waiting for the render app
156                let sent_app = compute_task_pool
157                    .scope(|s| {
158                        s.spawn(async { app_to_render_receiver.recv().await });
159                    })
160                    .pop();
161                let Some(Ok(mut render_app)) = sent_app else {
162                    break;
163                };
164
165                {
166                    #[cfg(feature = "trace")]
167                    let _sub_app_span =
168                        {
    use ::tracing::__macro_support::Callsite as _;
    static __CALLSITE: ::tracing::callsite::DefaultCallsite =
        {
            static META: ::tracing::Metadata<'static> =
                {
                    ::tracing_core::metadata::Metadata::new("sub app",
                        "bevy_render::pipelined_rendering", ::tracing::Level::INFO,
                        ::tracing_core::__macro_support::Option::Some("src/pipelined_rendering.rs"),
                        ::tracing_core::__macro_support::Option::Some(168u32),
                        ::tracing_core::__macro_support::Option::Some("bevy_render::pipelined_rendering"),
                        ::tracing_core::field::FieldSet::new(&[{
                                            const NAME:
                                                ::tracing::__macro_support::FieldName<{
                                                    ::tracing::__macro_support::FieldName::len("name")
                                                }> =
                                                ::tracing::__macro_support::FieldName::new("name");
                                            NAME.as_str()
                                        }], ::tracing_core::callsite::Identifier(&__CALLSITE)),
                        ::tracing::metadata::Kind::SPAN)
                };
            ::tracing::callsite::DefaultCallsite::new(&META)
        };
    let mut interest = ::tracing::subscriber::Interest::never();
    if ::tracing::Level::INFO <= ::tracing::level_filters::STATIC_MAX_LEVEL &&
                    ::tracing::Level::INFO <=
                        ::tracing::level_filters::LevelFilter::current() &&
                { interest = __CALLSITE.interest(); !interest.is_never() } &&
            ::tracing::__macro_support::__is_enabled(__CALLSITE.metadata(),
                interest) {
        let meta = __CALLSITE.metadata();
        ::tracing::Span::new(meta,
            &{
                    #[allow(unused_imports)]
                    use ::tracing::field::{debug, display, Value};
                    meta.fields().value_set_all(&[(::tracing::__macro_support::Option::Some(&::tracing::field::debug(&RenderApp)
                                                as &dyn ::tracing::field::Value))])
                })
    } else {
        let span =
            ::tracing::__macro_support::__disabled_span(__CALLSITE.metadata());
        {};
        span
    }
}bevy_log::info_span!("sub app", name = ?RenderApp).entered();
169                    render_app.update();
170                }
171
172                if render_to_app_sender.send_blocking(render_app).is_err() {
173                    break;
174                }
175            }
176
177            {
    use ::tracing::__macro_support::Callsite as _;
    static __CALLSITE: ::tracing::callsite::DefaultCallsite =
        {
            static META: ::tracing::Metadata<'static> =
                {
                    ::tracing_core::metadata::Metadata::new("event src/pipelined_rendering.rs:177",
                        "bevy_render::pipelined_rendering", ::tracing::Level::DEBUG,
                        ::tracing_core::__macro_support::Option::Some("src/pipelined_rendering.rs"),
                        ::tracing_core::__macro_support::Option::Some(177u32),
                        ::tracing_core::__macro_support::Option::Some("bevy_render::pipelined_rendering"),
                        ::tracing_core::field::FieldSet::new(&["message"],
                            ::tracing_core::callsite::Identifier(&__CALLSITE)),
                        ::tracing::metadata::Kind::EVENT)
                };
            ::tracing::callsite::DefaultCallsite::new(&META)
        };
    let enabled =
        ::tracing::Level::DEBUG <= ::tracing::level_filters::STATIC_MAX_LEVEL
                &&
                ::tracing::Level::DEBUG <=
                    ::tracing::level_filters::LevelFilter::current() &&
            {
                let interest = __CALLSITE.interest();
                !interest.is_never() &&
                    ::tracing::__macro_support::__is_enabled(__CALLSITE.metadata(),
                        interest)
            };
    if enabled {
        (|value_set: ::tracing::field::ValueSet|
                    {
                        let meta = __CALLSITE.metadata();
                        ::tracing::Event::dispatch(meta, &value_set);
                        ;
                    })({
                #[allow(unused_imports)]
                use ::tracing::field::{debug, display, Value};
                __CALLSITE.metadata().fields().value_set_all(&[(::tracing::__macro_support::Option::Some(&format_args!("exiting pipelined rendering thread")
                                            as &dyn ::tracing::field::Value))])
            });
    } else { ; }
};bevy_log::debug!("exiting pipelined rendering thread");
178        });
179    }
180}
181
182// This function waits for the rendering world to be received,
183// runs extract, and then sends the rendering world back to the render thread.
184fn renderer_extract(app_world: &mut World, _world: &mut World) {
185    app_world.resource_scope(|world, main_thread_executor: Mut<MainThreadExecutor>| {
186        world.resource_scope(|world, mut render_channels: Mut<RenderAppChannels>| {
187            // we use a scope here to run any main thread tasks that the render world still needs to run
188            // while we wait for the render world to be received.
189            if let Some(mut render_app) = ComputeTaskPool::get()
190                .scope_with_executor(true, Some(&*main_thread_executor.0), |s| {
191                    s.spawn(async { render_channels.recv().await });
192                })
193                .pop()
194                .unwrap()
195            {
196                render_app.extract(world);
197
198                render_channels.send_blocking(render_app);
199            } else {
200                // Renderer thread panicked
201                world.write_message(AppExit::error());
202            }
203        });
204    });
205}