Skip to content

Move render app init and extraction to separate plugin#22758

Open
kristoff3r wants to merge 7 commits intobevyengine:mainfrom
kristoff3r:ks/extract_plugin
Open

Move render app init and extraction to separate plugin#22758
kristoff3r wants to merge 7 commits intobevyengine:mainfrom
kristoff3r:ks/extract_plugin

Conversation

@kristoff3r
Copy link
Contributor

Objective

Currently in bevy_render the renderer initialization (e.g. creating a surface, registering rendering systems) is very intertwined with setting up the extraction logic itself (e.g. how is data moved between the worlds, how are they kept in sync). This makes it harder to understand both things, and also makes it very hard to test the extraction logic, as you don't want to start the renderer in unit test.

In the future this functionality might even be useful for the ECS outside bevy_render, e.g. for custom renderers that want to use a render world, or for multi world setups.

Solution

This PR splits out creation of the render subapp plus the extraction logic itself into ExtractPlugin, and uses the new found flexibility to finally add a test for it.

This is the first part in a series of PRs I plan to do to:

  • Separate the extraction logic so it's possible to test (this one)
  • Fix the bug that's currently commented out in the test
  • Rework the Extract trait to bypass the orphan rules, so we can use it everywhere (speculative but I have an idea)

Testing

Added a test to show that extraction is working.

I also ran a bunch of examples requiring rendering and they still work.

@kristoff3r kristoff3r added A-Rendering Drawing game state to the screen C-Code-Quality A section of code that is hard to understand or change S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Jan 31, 2026
@github-project-automation github-project-automation bot moved this to Needs SME Triage in Rendering (2026 Proposal) Jan 31, 2026
@alice-i-cecile alice-i-cecile requested a review from atlv24 January 31, 2026 22:06
@alice-i-cecile alice-i-cecile added the A-Windowing Platform-agnostic interface layer to run your app in label Jan 31, 2026
@kristoff3r kristoff3r removed the A-Windowing Platform-agnostic interface layer to run your app in label Feb 1, 2026
@alice-i-cecile alice-i-cecile added the M-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide label Feb 2, 2026
Copy link
Member

@alice-i-cecile alice-i-cecile left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like separating these, and this appears to have been done correctly.

/// We need both the main and render world to properly handle errors, so we wedge ourselves into [extract](bevy_app::SubApp::set_extract).
pub(crate) fn update_state(main_world: &mut World, render_world: &mut World) {
if let Some(error) = render_world.resource::<DeviceErrorHandler>().poll() {
if let Some(handler) = render_world.get_resource::<DeviceErrorHandler>()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not quite right as it creates a possibility for a false default: removing the DeviceErrorHandler does not behave the same as DeviceErrorHandler::default(). Can you use .unwrap_or_default() instead, or better yet just not make this change and require the presence + init_resource?

Comment on lines +106 to +109
/// The render recovery schedule. This schedule runs the [`Render`] schedule if
/// we are in [`RenderState::Ready`], and is otherwise hidden from users.
#[derive(ScheduleLabel, Debug, Hash, PartialEq, Eq, Clone)]
struct RenderRecovery;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this make sense to have in extract?

Comment on lines +45 to +65
render_app.add_systems(RenderRecovery, move |world: &mut World| {
if matches!(world.resource::<RenderState>(), RenderState::Ready) {
world.run_schedule(Render);
}

// update the time and send it to the app world regardless of whether we render
let time_sender = world.resource::<TimeSender>();
if let Err(error) = time_sender.0.try_send(Instant::now()) {
match error {
bevy_time::TrySendError::Full(_) => {
panic!(
"The TimeSender channel should always be empty during render. \
You might need to add the bevy::core::time_system to your app."
);
}
bevy_time::TrySendError::Disconnected(_) => {
// ignore disconnected errors, the main world probably just got dropped during shutdown
}
}
}
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this doesn't feel like extract logic

reset_render_asset_bytes_per_frame.in_set(RenderSystems::Cleanup),
);
),
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you put the RenderRecovery init stuff here instead?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-Rendering Drawing game state to the screen C-Code-Quality A section of code that is hard to understand or change M-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide S-Needs-Review Needs reviewer attention (from anyone!) to move forward

Projects

Status: No status
Status: Needs SME Triage

Development

Successfully merging this pull request may close these issues.

3 participants