Prevent black frames during startup (#9826)

# Objective

This PR addresses the issue where Bevy displays one or several black
frames before the scene is first rendered. This is particularly
noticeable on iOS, where the black frames disrupt the transition from
the launch screen to the game UI. I have written about my search to
solve this issue on the Bevy discord:
https://discord.com/channels/691052431525675048/1151047604520632352

While I can attest this PR works on both iOS and Linux/Wayland (and even
seems to resolve a slight flicker during startup with the latter as
well), I'm not familiar enough with Bevy to judge the full implications
of these changes. I hope a reviewer or tester can help me confirm
whether this is the right approach, or what might be a cleaner solution
to resolve this issue.

## Solution

I have moved the "startup phase" as well as the plugin finalization into
the `app.run()` function so those things finish synchronously before the
"main schedule" starts. I even move one frame forward as well, using
`app.update()`, to make sure the rendering has caught up with the state
of the finalized plugins as well.

I admit that part of this was achieved through trial-and-error, since
not doing the "startup phase" *before* `app.finish()` resulted in
panics, while not calling an extra `app.update()` didn't fully resolve
the issue.

What I *can* say, is that the iOS launch screen animation works in such
a way that the OS initiates the transition once the framework's
[`didFinishLaunching()`](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622921-application)
returns, meaning app developers **must** finish setting up their UI
before that function returns. This is what basically led me on the path
to try to "finish stuff earlier" :)

## Changelog

### Changed

- The startup phase and the first frame are rendered synchronously when
calling `app.run()`, before the "main schedule" is started. This fixes
black frames during the iOS launch transition and possible flickering on
other platforms, but may affect initialization order in your
application.

## Migration Guide

Because of this change, the timing of the first few frames might have
changed, and I think it could be that some things one may expect to be
initialized in a system may no longer be. To be honest, I feel out of my
depth to judge the exact impact here.
This commit is contained in:
Arend van Beelen jr 2023-10-19 01:24:19 +02:00 committed by GitHub
parent 4b65a533f1
commit 5d110eb96e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 16 additions and 6 deletions

View file

@ -288,6 +288,14 @@ impl App {
panic!("App::run() was called from within Plugin::build(), which is not allowed."); panic!("App::run() was called from within Plugin::build(), which is not allowed.");
} }
if app.ready() {
// If we're already ready, we finish up now and advance one frame.
// This prevents black frames during the launch transition on iOS.
app.finish();
app.cleanup();
app.update();
}
let runner = std::mem::replace(&mut app.runner, Box::new(run_once)); let runner = std::mem::replace(&mut app.runner, Box::new(run_once));
(runner)(app); (runner)(app);
} }

View file

@ -71,12 +71,14 @@ impl Plugin for ScheduleRunnerPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
let run_mode = self.run_mode; let run_mode = self.run_mode;
app.set_runner(move |mut app: App| { app.set_runner(move |mut app: App| {
while !app.ready() { if !app.ready() {
#[cfg(not(target_arch = "wasm32"))] while !app.ready() {
bevy_tasks::tick_global_task_pools_on_main_thread(); #[cfg(not(target_arch = "wasm32"))]
bevy_tasks::tick_global_task_pools_on_main_thread();
}
app.finish();
app.cleanup();
} }
app.finish();
app.cleanup();
let mut app_exit_event_reader = ManualEventReader::<AppExit>::default(); let mut app_exit_event_reader = ManualEventReader::<AppExit>::default();
match run_mode { match run_mode {

View file

@ -378,7 +378,7 @@ pub fn winit_runner(mut app: App) {
ResMut<CanvasParentResizeEventChannel>, ResMut<CanvasParentResizeEventChannel>,
)> = SystemState::from_world(&mut app.world); )> = SystemState::from_world(&mut app.world);
let mut finished_and_setup_done = false; let mut finished_and_setup_done = app.ready();
// setup up the event loop // setup up the event loop
let event_handler = move |event: Event<()>, let event_handler = move |event: Event<()>,