Skip to content

Commit

Permalink
Prevent black frames during startup (#9826)
Browse files Browse the repository at this point in the history
# 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.
  • Loading branch information
arendjr authored Oct 18, 2023
1 parent 4b65a53 commit 5d110eb
Show file tree
Hide file tree
Showing 3 changed files with 16 additions and 6 deletions.
8 changes: 8 additions & 0 deletions crates/bevy_app/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,14 @@ impl App {
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));
(runner)(app);
}
Expand Down
12 changes: 7 additions & 5 deletions crates/bevy_app/src/schedule_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,14 @@ impl Plugin for ScheduleRunnerPlugin {
fn build(&self, app: &mut App) {
let run_mode = self.run_mode;
app.set_runner(move |mut app: App| {
while !app.ready() {
#[cfg(not(target_arch = "wasm32"))]
bevy_tasks::tick_global_task_pools_on_main_thread();
if !app.ready() {
while !app.ready() {
#[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();
match run_mode {
Expand Down
2 changes: 1 addition & 1 deletion crates/bevy_winit/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@ pub fn winit_runner(mut app: App) {
ResMut<CanvasParentResizeEventChannel>,
)> = 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
let event_handler = move |event: Event<()>,
Expand Down

0 comments on commit 5d110eb

Please sign in to comment.