
# Objective - Fixes #17960 ## Solution - Followed the [edition upgrade guide](https://doc.rust-lang.org/edition-guide/editions/transitioning-an-existing-project-to-a-new-edition.html) ## Testing - CI --- ## Summary of Changes ### Documentation Indentation When using lists in documentation, proper indentation is now linted for. This means subsequent lines within the same list item must start at the same indentation level as the item. ```rust /* Valid */ /// - Item 1 /// Run-on sentence. /// - Item 2 struct Foo; /* Invalid */ /// - Item 1 /// Run-on sentence. /// - Item 2 struct Foo; ``` ### Implicit `!` to `()` Conversion `!` (the never return type, returned by `panic!`, etc.) no longer implicitly converts to `()`. This is particularly painful for systems with `todo!` or `panic!` statements, as they will no longer be functions returning `()` (or `Result<()>`), making them invalid systems for functions like `add_systems`. The ideal fix would be to accept functions returning `!` (or rather, _not_ returning), but this is blocked on the [stabilisation of the `!` type itself](https://doc.rust-lang.org/std/primitive.never.html), which is not done. The "simple" fix would be to add an explicit `-> ()` to system signatures (e.g., `|| { todo!() }` becomes `|| -> () { todo!() }`). However, this is _also_ banned, as there is an existing lint which (IMO, incorrectly) marks this as an unnecessary annotation. So, the "fix" (read: workaround) is to put these kinds of `|| -> ! { ... }` closuers into variables and give the variable an explicit type (e.g., `fn()`). ```rust // Valid let system: fn() = || todo!("Not implemented yet!"); app.add_systems(..., system); // Invalid app.add_systems(..., || todo!("Not implemented yet!")); ``` ### Temporary Variable Lifetimes The order in which temporary variables are dropped has changed. The simple fix here is _usually_ to just assign temporaries to a named variable before use. ### `gen` is a keyword We can no longer use the name `gen` as it is reserved for a future generator syntax. This involved replacing uses of the name `gen` with `r#gen` (the raw-identifier syntax). ### Formatting has changed Use statements have had the order of imports changed, causing a substantial +/-3,000 diff when applied. For now, I have opted-out of this change by amending `rustfmt.toml` ```toml style_edition = "2021" ``` This preserves the original formatting for now, reducing the size of this PR. It would be a simple followup to update this to 2024 and run `cargo fmt`. ### New `use<>` Opt-Out Syntax Lifetimes are now implicitly included in RPIT types. There was a handful of instances where it needed to be added to satisfy the borrow checker, but there may be more cases where it _should_ be added to avoid breakages in user code. ### `MyUnitStruct { .. }` is an invalid pattern Previously, you could match against unit structs (and unit enum variants) with a `{ .. }` destructuring. This is no longer valid. ### Pretty much every use of `ref` and `mut` are gone Pattern binding has changed to the point where these terms are largely unused now. They still serve a purpose, but it is far more niche now. ### `iter::repeat(...).take(...)` is bad New lint recommends using the more explicit `iter::repeat_n(..., ...)` instead. ## Migration Guide The lifetimes of functions using return-position impl-trait (RPIT) are likely _more_ conservative than they had been previously. If you encounter lifetime issues with such a function, please create an issue to investigate the addition of `+ use<...>`. ## Notes - Check the individual commits for a clearer breakdown for what _actually_ changed. --------- Co-authored-by: François Mockers <francois.mockers@vleue.com>
205 lines
8.4 KiB
Rust
205 lines
8.4 KiB
Rust
use async_channel::{Receiver, Sender};
|
|
|
|
use bevy_app::{App, AppExit, AppLabel, Plugin, SubApp};
|
|
use bevy_ecs::{
|
|
resource::Resource,
|
|
schedule::MainThreadExecutor,
|
|
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 |
|
|
/// | sync | extract |----------------------------------------------------------|
|
|
/// | | | extract commands | rendering schedule |
|
|
/// |---------------------------------------------------------------------------|
|
|
/// ```
|
|
///
|
|
/// - `sync` is the step where the entity-entity mapping between the main and render world is updated.
|
|
/// This is run on the main app's thread. For more information checkout [`SyncWorldPlugin`].
|
|
/// - `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.
|
|
///
|
|
/// [`SyncWorldPlugin`]: crate::sync_world::SyncWorldPlugin
|
|
#[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_none() {
|
|
return;
|
|
}
|
|
app.insert_resource(MainThreadExecutor::new());
|
|
|
|
let mut sub_app = SubApp::new();
|
|
sub_app.set_extract(renderer_extract);
|
|
app.insert_sub_app(RenderExtractApp, sub_app);
|
|
}
|
|
|
|
// 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_none() {
|
|
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.world_mut().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 = 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 = tracing::info_span!("sub app", name = ?RenderApp).entered();
|
|
render_app.update();
|
|
}
|
|
|
|
if render_to_app_sender.send_blocking(render_app).is_err() {
|
|
break;
|
|
}
|
|
}
|
|
|
|
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 renderer_extract(app_world: &mut World, _world: &mut World) {
|
|
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::error());
|
|
}
|
|
});
|
|
});
|
|
}
|