Skip to content

Commit 5d110eb

Browse files
authored
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.
1 parent 4b65a53 commit 5d110eb

File tree

3 files changed

+16
-6
lines changed

3 files changed

+16
-6
lines changed

crates/bevy_app/src/app.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,14 @@ impl App {
288288
panic!("App::run() was called from within Plugin::build(), which is not allowed.");
289289
}
290290

291+
if app.ready() {
292+
// If we're already ready, we finish up now and advance one frame.
293+
// This prevents black frames during the launch transition on iOS.
294+
app.finish();
295+
app.cleanup();
296+
app.update();
297+
}
298+
291299
let runner = std::mem::replace(&mut app.runner, Box::new(run_once));
292300
(runner)(app);
293301
}

crates/bevy_app/src/schedule_runner.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,14 @@ impl Plugin for ScheduleRunnerPlugin {
7171
fn build(&self, app: &mut App) {
7272
let run_mode = self.run_mode;
7373
app.set_runner(move |mut app: App| {
74-
while !app.ready() {
75-
#[cfg(not(target_arch = "wasm32"))]
76-
bevy_tasks::tick_global_task_pools_on_main_thread();
74+
if !app.ready() {
75+
while !app.ready() {
76+
#[cfg(not(target_arch = "wasm32"))]
77+
bevy_tasks::tick_global_task_pools_on_main_thread();
78+
}
79+
app.finish();
80+
app.cleanup();
7781
}
78-
app.finish();
79-
app.cleanup();
8082

8183
let mut app_exit_event_reader = ManualEventReader::<AppExit>::default();
8284
match run_mode {

crates/bevy_winit/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,7 @@ pub fn winit_runner(mut app: App) {
378378
ResMut<CanvasParentResizeEventChannel>,
379379
)> = SystemState::from_world(&mut app.world);
380380

381-
let mut finished_and_setup_done = false;
381+
let mut finished_and_setup_done = app.ready();
382382

383383
// setup up the event loop
384384
let event_handler = move |event: Event<()>,

0 commit comments

Comments
 (0)