Path: blob/main/crates/bevy_render/src/pipelined_rendering.rs
6595 views
use async_channel::{Receiver, Sender};12use bevy_app::{App, AppExit, AppLabel, Plugin, SubApp};3use bevy_ecs::{4resource::Resource,5schedule::MainThreadExecutor,6world::{Mut, World},7};8use bevy_tasks::ComputeTaskPool;910use crate::RenderApp;1112/// A Label for the sub app that runs the parts of pipelined rendering that need to run on the main thread.13///14/// The Main schedule of this app can be used to run logic after the render schedule starts, but15/// before I/O processing. This can be useful for something like frame pacing.16#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, AppLabel)]17pub struct RenderExtractApp;1819/// Channels used by the main app to send and receive the render app.20#[derive(Resource)]21pub struct RenderAppChannels {22app_to_render_sender: Sender<SubApp>,23render_to_app_receiver: Receiver<SubApp>,24render_app_in_render_thread: bool,25}2627impl RenderAppChannels {28/// Create a `RenderAppChannels` from a [`async_channel::Receiver`] and [`async_channel::Sender`]29pub fn new(30app_to_render_sender: Sender<SubApp>,31render_to_app_receiver: Receiver<SubApp>,32) -> Self {33Self {34app_to_render_sender,35render_to_app_receiver,36render_app_in_render_thread: false,37}38}3940/// Send the `render_app` to the rendering thread.41pub fn send_blocking(&mut self, render_app: SubApp) {42self.app_to_render_sender.send_blocking(render_app).unwrap();43self.render_app_in_render_thread = true;44}4546/// Receive the `render_app` from the rendering thread.47/// Return `None` if the render thread has panicked.48pub async fn recv(&mut self) -> Option<SubApp> {49let render_app = self.render_to_app_receiver.recv().await.ok()?;50self.render_app_in_render_thread = false;51Some(render_app)52}53}5455impl Drop for RenderAppChannels {56fn drop(&mut self) {57if self.render_app_in_render_thread {58// Any non-send data in the render world was initialized on the main thread.59// So on dropping the main world and ending the app, we block and wait for60// the render world to return to drop it. Which allows the non-send data61// drop methods to run on the correct thread.62self.render_to_app_receiver.recv_blocking().ok();63}64}65}6667/// The [`PipelinedRenderingPlugin`] can be added to your application to enable pipelined rendering.68///69/// This moves rendering into a different thread, so that the Nth frame's rendering can70/// be run at the same time as the N + 1 frame's simulation.71///72/// ```text73/// |--------------------|--------------------|--------------------|--------------------|74/// | simulation thread | frame 1 simulation | frame 2 simulation | frame 3 simulation |75/// |--------------------|--------------------|--------------------|--------------------|76/// | rendering thread | | frame 1 rendering | frame 2 rendering |77/// |--------------------|--------------------|--------------------|--------------------|78/// ```79///80/// The plugin is dependent on the [`RenderApp`] added by [`crate::RenderPlugin`] and so must81/// be added after that plugin. If it is not added after, the plugin will do nothing.82///83/// A single frame of execution looks something like below84///85/// ```text86/// |---------------------------------------------------------------------------|87/// | | | RenderExtractApp schedule | winit events | main schedule |88/// | sync | extract |----------------------------------------------------------|89/// | | | extract commands | rendering schedule |90/// |---------------------------------------------------------------------------|91/// ```92///93/// - `sync` is the step where the entity-entity mapping between the main and render world is updated.94/// This is run on the main app's thread. For more information checkout [`SyncWorldPlugin`].95/// - `extract` is the step where data is copied from the main world to the render world.96/// This is run on the main app's thread.97/// - On the render thread, we first apply the `extract commands`. This is not run during extract, so the98/// main schedule can start sooner.99/// - Then the `rendering schedule` is run. See [`RenderSystems`](crate::RenderSystems) for the standard steps in this process.100/// - In parallel to the rendering thread the [`RenderExtractApp`] schedule runs. By101/// default, this schedule is empty. But it is useful if you need something to run before I/O processing.102/// - Next all the `winit events` are processed.103/// - And finally the `main app schedule` is run.104/// - Once both the `main app schedule` and the `render schedule` are finished running, `extract` is run again.105///106/// [`SyncWorldPlugin`]: crate::sync_world::SyncWorldPlugin107#[derive(Default)]108pub struct PipelinedRenderingPlugin;109110impl Plugin for PipelinedRenderingPlugin {111fn build(&self, app: &mut App) {112// Don't add RenderExtractApp if RenderApp isn't initialized.113if app.get_sub_app(RenderApp).is_none() {114return;115}116app.insert_resource(MainThreadExecutor::new());117118let mut sub_app = SubApp::new();119sub_app.set_extract(renderer_extract);120app.insert_sub_app(RenderExtractApp, sub_app);121}122123// Sets up the render thread and inserts resources into the main app used for controlling the render thread.124fn cleanup(&self, app: &mut App) {125// skip setting up when headless126if app.get_sub_app(RenderExtractApp).is_none() {127return;128}129130let (app_to_render_sender, app_to_render_receiver) = async_channel::bounded::<SubApp>(1);131let (render_to_app_sender, render_to_app_receiver) = async_channel::bounded::<SubApp>(1);132133let mut render_app = app134.remove_sub_app(RenderApp)135.expect("Unable to get RenderApp. Another plugin may have removed the RenderApp before PipelinedRenderingPlugin");136137// clone main thread executor to render world138let executor = app.world().get_resource::<MainThreadExecutor>().unwrap();139render_app.world_mut().insert_resource(executor.clone());140141render_to_app_sender.send_blocking(render_app).unwrap();142143app.insert_resource(RenderAppChannels::new(144app_to_render_sender,145render_to_app_receiver,146));147148std::thread::spawn(move || {149#[cfg(feature = "trace")]150let _span = tracing::info_span!("render thread").entered();151152let compute_task_pool = ComputeTaskPool::get();153loop {154// run a scope here to allow main world to use this thread while it's waiting for the render app155let sent_app = compute_task_pool156.scope(|s| {157s.spawn(async { app_to_render_receiver.recv().await });158})159.pop();160let Some(Ok(mut render_app)) = sent_app else {161break;162};163164{165#[cfg(feature = "trace")]166let _sub_app_span = tracing::info_span!("sub app", name = ?RenderApp).entered();167render_app.update();168}169170if render_to_app_sender.send_blocking(render_app).is_err() {171break;172}173}174175tracing::debug!("exiting pipelined rendering thread");176});177}178}179180// This function waits for the rendering world to be received,181// runs extract, and then sends the rendering world back to the render thread.182fn renderer_extract(app_world: &mut World, _world: &mut World) {183app_world.resource_scope(|world, main_thread_executor: Mut<MainThreadExecutor>| {184world.resource_scope(|world, mut render_channels: Mut<RenderAppChannels>| {185// we use a scope here to run any main thread tasks that the render world still needs to run186// while we wait for the render world to be received.187if let Some(mut render_app) = ComputeTaskPool::get()188.scope_with_executor(true, Some(&*main_thread_executor.0), |s| {189s.spawn(async { render_channels.recv().await });190})191.pop()192.unwrap()193{194render_app.extract(world);195196render_channels.send_blocking(render_app);197} else {198// Renderer thread panicked199world.write_event(AppExit::error());200}201});202});203}204205206