diff options
author | ohno1052 | 2024-02-05 02:01:35 +0500 |
---|---|---|
committer | ohno1052 | 2024-02-05 02:01:35 +0500 |
commit | 5ea4c58e8a9fb3d2d356c9f4609ce19415b69dc6 (patch) | |
tree | 739ede5bb4e3e66ac99dcb1247da4909c3183850 | |
parent | 06684b07d19edef6181d6946dcd86c2f3f8c379f (diff) | |
download | aur-5ea4c58e8a9fb3d2d356c9f4609ce19415b69dc6.tar.gz |
minor pkgbuild changes, add dynamic buffering patch
-rw-r--r-- | .SRCINFO | 5 | ||||
-rw-r--r-- | PKGBUILD | 5 | ||||
-rw-r--r-- | mr1441.patch | 2312 |
3 files changed, 2321 insertions, 1 deletions
@@ -40,9 +40,14 @@ pkgbase = mutter-auto-rotation provides = libmutter-13.so provides = mutter conflicts = mutter + options = strip + options = !docs + options = lto source = git+https://gitlab.gnome.org/GNOME/mutter.git#tag=45.3 + source = mr1441.patch source = 0001-Revert-backends-native-Disable-touch-mode-with-point.patch sha256sums = SKIP + sha256sums = 5b0e927eb2873256c7999ebc711f5f0db2296550d7dbe51e757335e2b77d016c sha256sums = b11e3e1c9a14b9d9a3941e4495f7f16cfe84c53c6b8e571df049546840a91a46 pkgname = mutter-auto-rotation @@ -55,11 +55,13 @@ makedepends=( source=( "git+https://gitlab.gnome.org/GNOME/mutter.git#tag=$_tag" + "mr1441.patch" "0001-Revert-backends-native-Disable-touch-mode-with-point.patch" ) sha256sums=( 'SKIP' + '5b0e927eb2873256c7999ebc711f5f0db2296550d7dbe51e757335e2b77d016c' 'b11e3e1c9a14b9d9a3941e4495f7f16cfe84c53c6b8e571df049546840a91a46' ) @@ -70,7 +72,7 @@ pkgver() { prepare() { cd mutter - + git apply ../mr1441.patch git apply ../0001-Revert-backends-native-Disable-touch-mode-with-point.patch } @@ -82,6 +84,7 @@ build() { -D installed_tests=false -D libdisplay_info=true -D wayland_eglstream=true + -D tests=false --buildtype=release ) diff --git a/mr1441.patch b/mr1441.patch new file mode 100644 index 000000000000..84f8d47715a3 --- /dev/null +++ b/mr1441.patch @@ -0,0 +1,2312 @@ +Author: Daniel van Vugt <daniel.van.vugt@canonical.com> +Source: https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/1441 +Commit: 0b896518b2028d9c4d6ea44806d093fd33793689 +Rebase: Fri Dec 8 16:31:18 2023 +0800 + +diff --git a/clutter/clutter/clutter-frame-clock.c b/clutter/clutter/clutter-frame-clock.c +index ab493e0b0..905d8c926 100644 +--- a/clutter/clutter/clutter-frame-clock.c ++++ b/clutter/clutter/clutter-frame-clock.c +@@ -35,6 +35,15 @@ enum + + static guint signals[N_SIGNALS]; + ++typedef enum ++{ ++ TRIPLE_BUFFERING_MODE_NEVER, ++ TRIPLE_BUFFERING_MODE_AUTO, ++ TRIPLE_BUFFERING_MODE_ALWAYS, ++} TripleBufferingMode; ++ ++static TripleBufferingMode triple_buffering_mode = TRIPLE_BUFFERING_MODE_AUTO; ++ + #define SYNC_DELAY_FALLBACK_FRACTION 0.875 + + typedef struct _ClutterFrameListener +@@ -55,8 +64,9 @@ typedef enum _ClutterFrameClockState + CLUTTER_FRAME_CLOCK_STATE_INIT, + CLUTTER_FRAME_CLOCK_STATE_IDLE, + CLUTTER_FRAME_CLOCK_STATE_SCHEDULED, +- CLUTTER_FRAME_CLOCK_STATE_DISPATCHING, +- CLUTTER_FRAME_CLOCK_STATE_PENDING_PRESENTED, ++ CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE, ++ CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE_AND_SCHEDULED, ++ CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_TWO, + } ClutterFrameClockState; + + struct _ClutterFrameClock +@@ -73,6 +83,7 @@ struct _ClutterFrameClock + + ClutterFrameClockState state; + int64_t last_dispatch_time_us; ++ int64_t prev_last_dispatch_time_us; + int64_t last_dispatch_lateness_us; + int64_t last_presentation_time_us; + int64_t next_update_time_us; +@@ -87,6 +98,9 @@ struct _ClutterFrameClock + int64_t vblank_duration_us; + /* Last KMS buffer submission time. */ + int64_t last_flip_time_us; ++ int64_t prev_last_flip_time_us; ++ ++ ClutterFrameHint last_flip_hints; + + /* Last time we promoted short-term maximum to long-term one */ + int64_t longterm_promotion_us; +@@ -219,10 +233,6 @@ static void + maybe_update_longterm_max_duration_us (ClutterFrameClock *frame_clock, + ClutterFrameInfo *frame_info) + { +- /* Do not update long-term max if there has been no measurement */ +- if (!frame_clock->shortterm_max_update_duration_us) +- return; +- + if ((frame_info->presentation_time - frame_clock->longterm_promotion_us) < + G_USEC_PER_SEC) + return; +@@ -249,6 +259,12 @@ void + clutter_frame_clock_notify_presented (ClutterFrameClock *frame_clock, + ClutterFrameInfo *frame_info) + { ++#ifdef CLUTTER_ENABLE_DEBUG ++ const char *debug_state = ++ frame_clock->state == CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_TWO ? ++ "Triple buffering" : "Double buffering"; ++#endif ++ + COGL_TRACE_BEGIN_SCOPED (ClutterFrameClockNotifyPresented, + "Frame Clock (presented)"); + +@@ -328,31 +344,58 @@ clutter_frame_clock_notify_presented (ClutterFrameClock *frame_clock, + + frame_clock->got_measurements_last_frame = FALSE; + +- if (frame_info->cpu_time_before_buffer_swap_us != 0) ++ if (frame_info->cpu_time_before_buffer_swap_us != 0 || ++ frame_clock->ever_got_measurements) + { + int64_t dispatch_to_swap_us, swap_to_rendering_done_us, swap_to_flip_us; ++ int64_t dispatch_time_us = 0, flip_time_us = 0; + +- dispatch_to_swap_us = +- frame_info->cpu_time_before_buffer_swap_us - +- frame_clock->last_dispatch_time_us; ++ switch (frame_clock->state) ++ { ++ case CLUTTER_FRAME_CLOCK_STATE_INIT: ++ case CLUTTER_FRAME_CLOCK_STATE_IDLE: ++ case CLUTTER_FRAME_CLOCK_STATE_SCHEDULED: ++ g_warn_if_reached (); ++ G_GNUC_FALLTHROUGH; ++ case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE: ++ case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE_AND_SCHEDULED: ++ dispatch_time_us = frame_clock->last_dispatch_time_us; ++ flip_time_us = frame_clock->last_flip_time_us; ++ break; ++ case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_TWO: ++ dispatch_time_us = frame_clock->prev_last_dispatch_time_us; ++ flip_time_us = frame_clock->prev_last_flip_time_us; ++ break; ++ } ++ ++ if (frame_info->cpu_time_before_buffer_swap_us == 0) ++ { ++ /* Cursor-only updates with no "swap" or "flip" */ ++ dispatch_to_swap_us = 0; ++ swap_to_flip_us = 0; ++ } ++ else ++ { ++ dispatch_to_swap_us = frame_info->cpu_time_before_buffer_swap_us - ++ dispatch_time_us; ++ swap_to_flip_us = flip_time_us - ++ frame_info->cpu_time_before_buffer_swap_us; ++ } + swap_to_rendering_done_us = + frame_info->gpu_rendering_duration_ns / 1000; +- swap_to_flip_us = +- frame_clock->last_flip_time_us - +- frame_info->cpu_time_before_buffer_swap_us; + + CLUTTER_NOTE (FRAME_TIMINGS, +- "update2dispatch %ld µs, dispatch2swap %ld µs, swap2render %ld µs, swap2flip %ld µs", ++ "%s: update2dispatch %ld µs, dispatch2swap %ld µs, swap2render %ld µs, swap2flip %ld µs", ++ debug_state, + frame_clock->last_dispatch_lateness_us, + dispatch_to_swap_us, + swap_to_rendering_done_us, + swap_to_flip_us); + + frame_clock->shortterm_max_update_duration_us = +- CLAMP (frame_clock->last_dispatch_lateness_us + dispatch_to_swap_us + +- MAX (swap_to_rendering_done_us, swap_to_flip_us), +- frame_clock->shortterm_max_update_duration_us, +- frame_clock->refresh_interval_us); ++ MAX (frame_clock->shortterm_max_update_duration_us, ++ frame_clock->last_dispatch_lateness_us + dispatch_to_swap_us + ++ MAX (swap_to_rendering_done_us, swap_to_flip_us)); + + maybe_update_longterm_max_duration_us (frame_clock, frame_info); + +@@ -361,7 +404,8 @@ clutter_frame_clock_notify_presented (ClutterFrameClock *frame_clock, + } + else + { +- CLUTTER_NOTE (FRAME_TIMINGS, "update2dispatch %ld µs", ++ CLUTTER_NOTE (FRAME_TIMINGS, "%s: update2dispatch %ld µs", ++ debug_state, + frame_clock->last_dispatch_lateness_us); + } + +@@ -378,11 +422,18 @@ clutter_frame_clock_notify_presented (ClutterFrameClock *frame_clock, + case CLUTTER_FRAME_CLOCK_STATE_SCHEDULED: + g_warn_if_reached (); + break; +- case CLUTTER_FRAME_CLOCK_STATE_DISPATCHING: +- case CLUTTER_FRAME_CLOCK_STATE_PENDING_PRESENTED: ++ case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE: + frame_clock->state = CLUTTER_FRAME_CLOCK_STATE_IDLE; + maybe_reschedule_update (frame_clock); + break; ++ case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE_AND_SCHEDULED: ++ frame_clock->state = CLUTTER_FRAME_CLOCK_STATE_SCHEDULED; ++ maybe_reschedule_update (frame_clock); ++ break; ++ case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_TWO: ++ frame_clock->state = CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE; ++ maybe_reschedule_update (frame_clock); ++ break; + } + } + +@@ -398,11 +449,18 @@ clutter_frame_clock_notify_ready (ClutterFrameClock *frame_clock) + case CLUTTER_FRAME_CLOCK_STATE_SCHEDULED: + g_warn_if_reached (); + break; +- case CLUTTER_FRAME_CLOCK_STATE_DISPATCHING: +- case CLUTTER_FRAME_CLOCK_STATE_PENDING_PRESENTED: ++ case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE: + frame_clock->state = CLUTTER_FRAME_CLOCK_STATE_IDLE; + maybe_reschedule_update (frame_clock); + break; ++ case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE_AND_SCHEDULED: ++ frame_clock->state = CLUTTER_FRAME_CLOCK_STATE_SCHEDULED; ++ maybe_reschedule_update (frame_clock); ++ break; ++ case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_TWO: ++ frame_clock->state = CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE; ++ maybe_reschedule_update (frame_clock); ++ break; + } + } + +@@ -417,7 +475,14 @@ clutter_frame_clock_compute_max_render_time_us (ClutterFrameClock *frame_clock) + if (!frame_clock->ever_got_measurements || + G_UNLIKELY (clutter_paint_debug_flags & + CLUTTER_DEBUG_DISABLE_DYNAMIC_MAX_RENDER_TIME)) +- return refresh_interval_us * SYNC_DELAY_FALLBACK_FRACTION; ++ { ++ int64_t ret = refresh_interval_us * SYNC_DELAY_FALLBACK_FRACTION; ++ ++ if (frame_clock->state == CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE) ++ ret += refresh_interval_us; ++ ++ return ret; ++ } + + /* Max render time shows how early the frame clock needs to be dispatched + * to make it to the predicted next presentation time. It is an estimate of +@@ -437,8 +502,6 @@ clutter_frame_clock_compute_max_render_time_us (ClutterFrameClock *frame_clock) + frame_clock->vblank_duration_us + + clutter_max_render_time_constant_us; + +- max_render_time_us = CLAMP (max_render_time_us, 0, refresh_interval_us); +- + return max_render_time_us; + } + +@@ -453,8 +516,9 @@ calculate_next_update_time_us (ClutterFrameClock *frame_clock, + int64_t refresh_interval_us; + int64_t min_render_time_allowed_us; + int64_t max_render_time_allowed_us; +- int64_t next_presentation_time_us; ++ int64_t next_presentation_time_us = 0; + int64_t next_update_time_us; ++ gboolean skipped_frames = FALSE; + + now_us = g_get_monotonic_time (); + +@@ -498,7 +562,24 @@ calculate_next_update_time_us (ClutterFrameClock *frame_clock, + * + */ + last_presentation_time_us = frame_clock->last_presentation_time_us; +- next_presentation_time_us = last_presentation_time_us + refresh_interval_us; ++ switch (frame_clock->state) ++ { ++ case CLUTTER_FRAME_CLOCK_STATE_INIT: ++ case CLUTTER_FRAME_CLOCK_STATE_IDLE: ++ case CLUTTER_FRAME_CLOCK_STATE_SCHEDULED: ++ next_presentation_time_us = last_presentation_time_us + ++ refresh_interval_us; ++ break; ++ case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE: ++ case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE_AND_SCHEDULED: ++ next_presentation_time_us = last_presentation_time_us + ++ 2 * refresh_interval_us; ++ break; ++ case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_TWO: ++ next_presentation_time_us = last_presentation_time_us + ++ 3 * refresh_interval_us; ++ break; ++ } + + /* + * However, the last presentation could have happened more than a frame ago. +@@ -534,6 +615,7 @@ calculate_next_update_time_us (ClutterFrameClock *frame_clock, + + current_phase_us = (now_us - last_presentation_time_us) % refresh_interval_us; + next_presentation_time_us = now_us - current_phase_us + refresh_interval_us; ++ skipped_frames = TRUE; + } + + if (frame_clock->is_next_presentation_time_valid) +@@ -566,7 +648,7 @@ calculate_next_update_time_us (ClutterFrameClock *frame_clock, + } + } + +- if (next_presentation_time_us != last_presentation_time_us + refresh_interval_us) ++ if (skipped_frames) + { + /* There was an idle period since the last presentation, so there seems + * be no constantly updating actor. In this case it's best to start +@@ -607,8 +689,12 @@ clutter_frame_clock_inhibit (ClutterFrameClock *frame_clock) + frame_clock->pending_reschedule = TRUE; + frame_clock->state = CLUTTER_FRAME_CLOCK_STATE_IDLE; + break; +- case CLUTTER_FRAME_CLOCK_STATE_DISPATCHING: +- case CLUTTER_FRAME_CLOCK_STATE_PENDING_PRESENTED: ++ case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE_AND_SCHEDULED: ++ frame_clock->pending_reschedule = TRUE; ++ frame_clock->state = CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE; ++ break; ++ case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE: ++ case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_TWO: + break; + } + +@@ -645,9 +731,15 @@ clutter_frame_clock_schedule_update_now (ClutterFrameClock *frame_clock) + case CLUTTER_FRAME_CLOCK_STATE_IDLE: + case CLUTTER_FRAME_CLOCK_STATE_SCHEDULED: + next_update_time_us = g_get_monotonic_time (); ++ frame_clock->state = CLUTTER_FRAME_CLOCK_STATE_SCHEDULED; ++ break; ++ case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE: ++ case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE_AND_SCHEDULED: ++ next_update_time_us = g_get_monotonic_time (); ++ frame_clock->state = ++ CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE_AND_SCHEDULED; + break; +- case CLUTTER_FRAME_CLOCK_STATE_DISPATCHING: +- case CLUTTER_FRAME_CLOCK_STATE_PENDING_PRESENTED: ++ case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_TWO: + frame_clock->pending_reschedule = TRUE; + frame_clock->pending_reschedule_now = TRUE; + return; +@@ -657,7 +749,6 @@ clutter_frame_clock_schedule_update_now (ClutterFrameClock *frame_clock) + + frame_clock->next_update_time_us = next_update_time_us; + g_source_set_ready_time (frame_clock->source, next_update_time_us); +- frame_clock->state = CLUTTER_FRAME_CLOCK_STATE_SCHEDULED; + frame_clock->is_next_presentation_time_valid = FALSE; + } + +@@ -665,6 +756,12 @@ void + clutter_frame_clock_schedule_update (ClutterFrameClock *frame_clock) + { + int64_t next_update_time_us = -1; ++ TripleBufferingMode current_mode = triple_buffering_mode; ++ ++ if (current_mode == TRIPLE_BUFFERING_MODE_AUTO && ++ (frame_clock->last_flip_hints & ++ CLUTTER_FRAME_HINT_DIRECT_SCANOUT_ATTEMPTED)) ++ current_mode = TRIPLE_BUFFERING_MODE_NEVER; + + if (frame_clock->inhibit_count > 0) + { +@@ -676,6 +773,7 @@ clutter_frame_clock_schedule_update (ClutterFrameClock *frame_clock) + { + case CLUTTER_FRAME_CLOCK_STATE_INIT: + next_update_time_us = g_get_monotonic_time (); ++ frame_clock->state = CLUTTER_FRAME_CLOCK_STATE_SCHEDULED; + break; + case CLUTTER_FRAME_CLOCK_STATE_IDLE: + calculate_next_update_time_us (frame_clock, +@@ -684,11 +782,37 @@ clutter_frame_clock_schedule_update (ClutterFrameClock *frame_clock) + &frame_clock->min_render_time_allowed_us); + frame_clock->is_next_presentation_time_valid = + (frame_clock->next_presentation_time_us != 0); ++ frame_clock->state = CLUTTER_FRAME_CLOCK_STATE_SCHEDULED; + break; + case CLUTTER_FRAME_CLOCK_STATE_SCHEDULED: ++ case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE_AND_SCHEDULED: + return; +- case CLUTTER_FRAME_CLOCK_STATE_DISPATCHING: +- case CLUTTER_FRAME_CLOCK_STATE_PENDING_PRESENTED: ++ case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE: ++ switch (current_mode) ++ { ++ case TRIPLE_BUFFERING_MODE_NEVER: ++ frame_clock->pending_reschedule = TRUE; ++ return; ++ case TRIPLE_BUFFERING_MODE_AUTO: ++ calculate_next_update_time_us (frame_clock, ++ &next_update_time_us, ++ &frame_clock->next_presentation_time_us, ++ &frame_clock->min_render_time_allowed_us); ++ frame_clock->is_next_presentation_time_valid = ++ (frame_clock->next_presentation_time_us != 0); ++ frame_clock->state = ++ CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE_AND_SCHEDULED; ++ break; ++ case TRIPLE_BUFFERING_MODE_ALWAYS: ++ next_update_time_us = g_get_monotonic_time (); ++ frame_clock->next_presentation_time_us = 0; ++ frame_clock->is_next_presentation_time_valid = FALSE; ++ frame_clock->state = ++ CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE_AND_SCHEDULED; ++ break; ++ } ++ break; ++ case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_TWO: + frame_clock->pending_reschedule = TRUE; + return; + } +@@ -697,7 +821,6 @@ clutter_frame_clock_schedule_update (ClutterFrameClock *frame_clock) + + frame_clock->next_update_time_us = next_update_time_us; + g_source_set_ready_time (frame_clock->source, next_update_time_us); +- frame_clock->state = CLUTTER_FRAME_CLOCK_STATE_SCHEDULED; + } + + static void +@@ -728,7 +851,7 @@ clutter_frame_clock_dispatch (ClutterFrameClock *frame_clock, + frame_clock->refresh_interval_us; + + lateness_us = time_us - ideal_dispatch_time_us; +- if (lateness_us < 0 || lateness_us >= frame_clock->refresh_interval_us) ++ if (lateness_us < 0 || lateness_us >= frame_clock->refresh_interval_us / 4) + frame_clock->last_dispatch_lateness_us = 0; + else + frame_clock->last_dispatch_lateness_us = lateness_us; +@@ -749,10 +872,25 @@ clutter_frame_clock_dispatch (ClutterFrameClock *frame_clock, + } + #endif + ++ frame_clock->prev_last_dispatch_time_us = frame_clock->last_dispatch_time_us; + frame_clock->last_dispatch_time_us = time_us; + g_source_set_ready_time (frame_clock->source, -1); + +- frame_clock->state = CLUTTER_FRAME_CLOCK_STATE_DISPATCHING; ++ switch (frame_clock->state) ++ { ++ case CLUTTER_FRAME_CLOCK_STATE_INIT: ++ case CLUTTER_FRAME_CLOCK_STATE_IDLE: ++ case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE: ++ case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_TWO: ++ g_warn_if_reached (); ++ return; ++ case CLUTTER_FRAME_CLOCK_STATE_SCHEDULED: ++ frame_clock->state = CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE; ++ break; ++ case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE_AND_SCHEDULED: ++ frame_clock->state = CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_TWO; ++ break; ++ } + + frame_count = frame_clock->frame_count++; + +@@ -781,25 +919,31 @@ clutter_frame_clock_dispatch (ClutterFrameClock *frame_clock, + result = iface->frame (frame_clock, frame, frame_clock->listener.user_data); + COGL_TRACE_END (ClutterFrameClockFrame); + +- switch (frame_clock->state) ++ switch (result) + { +- case CLUTTER_FRAME_CLOCK_STATE_INIT: +- case CLUTTER_FRAME_CLOCK_STATE_PENDING_PRESENTED: +- g_warn_if_reached (); +- break; +- case CLUTTER_FRAME_CLOCK_STATE_IDLE: +- case CLUTTER_FRAME_CLOCK_STATE_SCHEDULED: ++ case CLUTTER_FRAME_RESULT_PENDING_PRESENTED: + break; +- case CLUTTER_FRAME_CLOCK_STATE_DISPATCHING: +- switch (result) ++ case CLUTTER_FRAME_RESULT_IDLE: ++ /* The frame was aborted; nothing to paint/present */ ++ switch (frame_clock->state) + { +- case CLUTTER_FRAME_RESULT_PENDING_PRESENTED: +- frame_clock->state = CLUTTER_FRAME_CLOCK_STATE_PENDING_PRESENTED; ++ case CLUTTER_FRAME_CLOCK_STATE_INIT: ++ case CLUTTER_FRAME_CLOCK_STATE_IDLE: ++ case CLUTTER_FRAME_CLOCK_STATE_SCHEDULED: ++ g_warn_if_reached (); + break; +- case CLUTTER_FRAME_RESULT_IDLE: ++ case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE: + frame_clock->state = CLUTTER_FRAME_CLOCK_STATE_IDLE; + maybe_reschedule_update (frame_clock); + break; ++ case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE_AND_SCHEDULED: ++ frame_clock->state = CLUTTER_FRAME_CLOCK_STATE_SCHEDULED; ++ maybe_reschedule_update (frame_clock); ++ break; ++ case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_TWO: ++ frame_clock->state = CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE; ++ maybe_reschedule_update (frame_clock); ++ break; + } + break; + } +@@ -832,10 +976,13 @@ frame_clock_source_dispatch (GSource *source, + } + + void +-clutter_frame_clock_record_flip_time (ClutterFrameClock *frame_clock, +- int64_t flip_time_us) ++clutter_frame_clock_record_flip (ClutterFrameClock *frame_clock, ++ int64_t flip_time_us, ++ ClutterFrameHint hints) + { ++ frame_clock->prev_last_flip_time_us = frame_clock->last_flip_time_us; + frame_clock->last_flip_time_us = flip_time_us; ++ frame_clock->last_flip_hints = hints; + } + + GString * +@@ -929,8 +1076,6 @@ clutter_frame_clock_dispose (GObject *object) + { + ClutterFrameClock *frame_clock = CLUTTER_FRAME_CLOCK (object); + +- g_warn_if_fail (frame_clock->state != CLUTTER_FRAME_CLOCK_STATE_DISPATCHING); +- + if (frame_clock->source) + { + g_signal_emit (frame_clock, signals[DESTROY], 0); +@@ -951,6 +1096,15 @@ static void + clutter_frame_clock_class_init (ClutterFrameClockClass *klass) + { + GObjectClass *object_class = G_OBJECT_CLASS (klass); ++ const char *mode_str; ++ ++ mode_str = g_getenv ("MUTTER_DEBUG_TRIPLE_BUFFERING"); ++ if (!g_strcmp0 (mode_str, "never")) ++ triple_buffering_mode = TRIPLE_BUFFERING_MODE_NEVER; ++ else if (!g_strcmp0 (mode_str, "auto")) ++ triple_buffering_mode = TRIPLE_BUFFERING_MODE_AUTO; ++ else if (!g_strcmp0 (mode_str, "always")) ++ triple_buffering_mode = TRIPLE_BUFFERING_MODE_ALWAYS; + + object_class->dispose = clutter_frame_clock_dispose; + +diff --git a/clutter/clutter/clutter-frame-clock.h b/clutter/clutter/clutter-frame-clock.h +index 93ebc9438..e1fd6b986 100644 +--- a/clutter/clutter/clutter-frame-clock.h ++++ b/clutter/clutter/clutter-frame-clock.h +@@ -33,6 +33,12 @@ typedef enum _ClutterFrameResult + CLUTTER_FRAME_RESULT_IDLE, + } ClutterFrameResult; + ++typedef enum _ClutterFrameHint ++{ ++ CLUTTER_FRAME_HINT_NONE = 0, ++ CLUTTER_FRAME_HINT_DIRECT_SCANOUT_ATTEMPTED = 1 << 0, ++} ClutterFrameHint; ++ + #define CLUTTER_TYPE_FRAME_CLOCK (clutter_frame_clock_get_type ()) + CLUTTER_EXPORT + G_DECLARE_FINAL_TYPE (ClutterFrameClock, clutter_frame_clock, +@@ -91,7 +97,8 @@ void clutter_frame_clock_remove_timeline (ClutterFrameClock *frame_clock, + CLUTTER_EXPORT + float clutter_frame_clock_get_refresh_rate (ClutterFrameClock *frame_clock); + +-void clutter_frame_clock_record_flip_time (ClutterFrameClock *frame_clock, +- int64_t flip_time_us); ++void clutter_frame_clock_record_flip (ClutterFrameClock *frame_clock, ++ int64_t flip_time_us, ++ ClutterFrameHint hints); + + GString * clutter_frame_clock_get_max_render_time_debug_info (ClutterFrameClock *frame_clock); +diff --git a/clutter/clutter/clutter-frame-private.h b/clutter/clutter/clutter-frame-private.h +index 0a0226b0a..55c76df72 100644 +--- a/clutter/clutter/clutter-frame-private.h ++++ b/clutter/clutter/clutter-frame-private.h +@@ -34,6 +34,7 @@ struct _ClutterFrame + + gboolean has_result; + ClutterFrameResult result; ++ ClutterFrameHint hints; + }; + + CLUTTER_EXPORT +diff --git a/clutter/clutter/clutter-frame.c b/clutter/clutter/clutter-frame.c +index 85baef274..413ce9c2b 100644 +--- a/clutter/clutter/clutter-frame.c ++++ b/clutter/clutter/clutter-frame.c +@@ -113,3 +113,16 @@ clutter_frame_set_result (ClutterFrame *frame, + frame->result = result; + frame->has_result = TRUE; + } ++ ++void ++clutter_frame_set_hint (ClutterFrame *frame, ++ ClutterFrameHint hint) ++{ ++ frame->hints |= hint; ++} ++ ++ClutterFrameHint ++clutter_frame_get_hints (ClutterFrame *frame) ++{ ++ return frame->hints; ++} +diff --git a/clutter/clutter/clutter-frame.h b/clutter/clutter/clutter-frame.h +index 1d5660d68..0e7f618a4 100644 +--- a/clutter/clutter/clutter-frame.h ++++ b/clutter/clutter/clutter-frame.h +@@ -54,4 +54,11 @@ void clutter_frame_set_result (ClutterFrame *frame, + CLUTTER_EXPORT + gboolean clutter_frame_has_result (ClutterFrame *frame); + ++CLUTTER_EXPORT ++void clutter_frame_set_hint (ClutterFrame *frame, ++ ClutterFrameHint hint); ++ ++CLUTTER_EXPORT ++ClutterFrameHint clutter_frame_get_hints (ClutterFrame *frame); ++ + G_DEFINE_AUTOPTR_CLEANUP_FUNC (ClutterFrame, clutter_frame_unref) +diff --git a/clutter/clutter/clutter-stage-view.c b/clutter/clutter/clutter-stage-view.c +index 168746dd4..f0e36619e 100644 +--- a/clutter/clutter/clutter-stage-view.c ++++ b/clutter/clutter/clutter-stage-view.c +@@ -1264,14 +1264,21 @@ handle_frame_clock_frame (ClutterFrameClock *frame_clock, + + _clutter_stage_window_redraw_view (stage_window, view, frame); + +- clutter_frame_clock_record_flip_time (frame_clock, +- g_get_monotonic_time ()); ++ clutter_frame_clock_record_flip (frame_clock, ++ g_get_monotonic_time (), ++ clutter_frame_get_hints (frame)); + + clutter_stage_emit_after_paint (stage, view, frame); + + if (_clutter_context_get_show_fps ()) + end_frame_timing_measurement (view); + } ++ else ++ { ++ clutter_frame_clock_record_flip (frame_clock, ++ g_get_monotonic_time (), ++ clutter_frame_get_hints (frame)); ++ } + + _clutter_stage_window_finish_frame (stage_window, view, frame); + +diff --git a/cogl/cogl/cogl-onscreen-private.h b/cogl/cogl/cogl-onscreen-private.h +index 9dbecfd0c..681d91d2b 100644 +--- a/cogl/cogl/cogl-onscreen-private.h ++++ b/cogl/cogl/cogl-onscreen-private.h +@@ -95,3 +95,6 @@ cogl_onscreen_peek_tail_frame_info (CoglOnscreen *onscreen); + + COGL_EXPORT CoglFrameInfo * + cogl_onscreen_pop_head_frame_info (CoglOnscreen *onscreen); ++ ++COGL_EXPORT unsigned int ++cogl_onscreen_count_pending_frames (CoglOnscreen *onscreen); +diff --git a/cogl/cogl/cogl-onscreen.c b/cogl/cogl/cogl-onscreen.c +index 73425e498..02c4474b2 100644 +--- a/cogl/cogl/cogl-onscreen.c ++++ b/cogl/cogl/cogl-onscreen.c +@@ -508,6 +508,14 @@ cogl_onscreen_pop_head_frame_info (CoglOnscreen *onscreen) + return g_queue_pop_head (&priv->pending_frame_infos); + } + ++unsigned int ++cogl_onscreen_count_pending_frames (CoglOnscreen *onscreen) ++{ ++ CoglOnscreenPrivate *priv = cogl_onscreen_get_instance_private (onscreen); ++ ++ return g_queue_get_length (&priv->pending_frame_infos); ++} ++ + CoglFrameClosure * + cogl_onscreen_add_frame_callback (CoglOnscreen *onscreen, + CoglFrameCallback callback, +diff --git a/src/backends/meta-stage-impl.c b/src/backends/meta-stage-impl.c +index c35cb36e3..2130e4042 100644 +--- a/src/backends/meta-stage-impl.c ++++ b/src/backends/meta-stage-impl.c +@@ -775,6 +775,8 @@ meta_stage_impl_redraw_view (ClutterStageWindow *stage_window, + { + g_autoptr (GError) error = NULL; + ++ clutter_frame_set_hint (frame, CLUTTER_FRAME_HINT_DIRECT_SCANOUT_ATTEMPTED); ++ + if (meta_stage_impl_scanout_view (stage_impl, + stage_view, + scanout, +diff --git a/src/backends/native/meta-kms-crtc.c b/src/backends/native/meta-kms-crtc.c +index d89b12598..b17e8460d 100644 +--- a/src/backends/native/meta-kms-crtc.c ++++ b/src/backends/native/meta-kms-crtc.c +@@ -48,6 +48,8 @@ struct _MetaKmsCrtc + MetaKmsCrtcState current_state; + + MetaKmsCrtcPropTable prop_table; ++ ++ MetaSwapChain *swap_chain; + }; + + G_DEFINE_TYPE (MetaKmsCrtc, meta_kms_crtc, G_TYPE_OBJECT) +@@ -99,6 +101,12 @@ meta_kms_crtc_get_prop_drm_value (MetaKmsCrtc *crtc, + return meta_kms_prop_convert_value (prop, value); + } + ++MetaSwapChain * ++meta_kms_crtc_get_swap_chain (MetaKmsCrtc *crtc) ++{ ++ return crtc->swap_chain; ++} ++ + gboolean + meta_kms_crtc_is_active (MetaKmsCrtc *crtc) + { +@@ -465,12 +473,23 @@ meta_kms_crtc_new (MetaKmsImplDevice *impl_device, + return crtc; + } + ++static void ++meta_kms_crtc_dispose (GObject *object) ++{ ++ MetaKmsCrtc *crtc = META_KMS_CRTC (object); ++ ++ meta_swap_chain_release_buffers (crtc->swap_chain); ++ ++ G_OBJECT_CLASS (meta_kms_crtc_parent_class)->dispose (object); ++} ++ + static void + meta_kms_crtc_finalize (GObject *object) + { + MetaKmsCrtc *crtc = META_KMS_CRTC (object); + + g_clear_pointer (&crtc->current_state.gamma.value, meta_gamma_lut_free); ++ g_clear_object (&crtc->swap_chain); + + G_OBJECT_CLASS (meta_kms_crtc_parent_class)->finalize (object); + } +@@ -480,6 +499,7 @@ meta_kms_crtc_init (MetaKmsCrtc *crtc) + { + crtc->current_state.gamma.size = 0; + crtc->current_state.gamma.value = NULL; ++ crtc->swap_chain = meta_swap_chain_new (); + } + + static void +@@ -487,6 +507,7 @@ meta_kms_crtc_class_init (MetaKmsCrtcClass *klass) + { + GObjectClass *object_class = G_OBJECT_CLASS (klass); + ++ object_class->dispose = meta_kms_crtc_dispose; + object_class->finalize = meta_kms_crtc_finalize; + } + +diff --git a/src/backends/native/meta-kms-crtc.h b/src/backends/native/meta-kms-crtc.h +index b26b682dd..a30a6de6e 100644 +--- a/src/backends/native/meta-kms-crtc.h ++++ b/src/backends/native/meta-kms-crtc.h +@@ -22,6 +22,7 @@ + #include <xf86drmMode.h> + + #include "backends/native/meta-kms-types.h" ++#include "backends/native/meta-swap-chain.h" + #include "backends/meta-backend-types.h" + #include "core/util-private.h" + #include "meta/boxes.h" +@@ -60,3 +61,5 @@ int meta_kms_crtc_get_idx (MetaKmsCrtc *crtc); + + META_EXPORT_TEST + gboolean meta_kms_crtc_is_active (MetaKmsCrtc *crtc); ++ ++MetaSwapChain * meta_kms_crtc_get_swap_chain (MetaKmsCrtc *crtc); +diff --git a/src/backends/native/meta-kms-impl-device-atomic.c b/src/backends/native/meta-kms-impl-device-atomic.c +index 2ca70326f..80c01413c 100644 +--- a/src/backends/native/meta-kms-impl-device-atomic.c ++++ b/src/backends/native/meta-kms-impl-device-atomic.c +@@ -505,6 +505,7 @@ process_plane_assignment (MetaKmsImplDevice *impl_device, + { + MetaKmsPlaneAssignment *plane_assignment = update_entry; + MetaKmsPlane *plane = plane_assignment->plane; ++ MetaKmsUpdateFlag flags = (MetaKmsUpdateFlag) user_data; + MetaDrmBuffer *buffer; + MetaKmsFbDamage *fb_damage; + uint32_t prop_id; +@@ -657,6 +658,12 @@ process_plane_assignment (MetaKmsImplDevice *impl_device, + error)) + return FALSE; + } ++ ++ if (!(flags & META_KMS_UPDATE_FLAG_TEST_ONLY)) ++ meta_swap_chain_push_buffer (meta_kms_crtc_get_swap_chain (plane_assignment->crtc), ++ meta_kms_plane_get_id (plane), ++ G_OBJECT (buffer)); ++ + return TRUE; + } + +@@ -1005,7 +1012,7 @@ meta_kms_impl_device_atomic_process_update (MetaKmsImplDevice *impl_device, + req, + blob_ids, + meta_kms_update_get_plane_assignments (update), +- NULL, ++ GUINT_TO_POINTER (flags), + process_plane_assignment, + &error)) + goto err; +diff --git a/src/backends/native/meta-kms-impl-device-simple.c b/src/backends/native/meta-kms-impl-device-simple.c +index 2d68ba11f..f4e23df07 100644 +--- a/src/backends/native/meta-kms-impl-device-simple.c ++++ b/src/backends/native/meta-kms-impl-device-simple.c +@@ -485,6 +485,8 @@ process_mode_set (MetaKmsImplDevice *impl_device, + return FALSE; + } + ++ meta_swap_chain_swap_buffers (meta_kms_crtc_get_swap_chain (crtc)); ++ + if (drm_mode) + { + g_hash_table_replace (impl_device_simple->cached_mode_sets, +@@ -554,7 +556,7 @@ is_timestamp_earlier_than (uint64_t ts1, + typedef struct _RetryPageFlipData + { + MetaKmsCrtc *crtc; +- uint32_t fb_id; ++ MetaDrmBuffer *fb; + MetaKmsPageFlipData *page_flip_data; + float refresh_rate; + uint64_t retry_time_us; +@@ -567,6 +569,7 @@ retry_page_flip_data_free (RetryPageFlipData *retry_page_flip_data) + g_assert (!retry_page_flip_data->page_flip_data); + g_clear_pointer (&retry_page_flip_data->custom_page_flip, + meta_kms_custom_page_flip_free); ++ g_clear_object (&retry_page_flip_data->fb); + g_free (retry_page_flip_data); + } + +@@ -634,16 +637,21 @@ retry_page_flips (gpointer user_data) + } + else + { ++ uint32_t fb_id = ++ retry_page_flip_data->fb ? ++ meta_drm_buffer_get_fb_id (retry_page_flip_data->fb) : ++ 0; ++ + meta_topic (META_DEBUG_KMS, + "[simple] Retrying page flip on CRTC %u (%s) with %u", + meta_kms_crtc_get_id (crtc), + meta_kms_impl_device_get_path (impl_device), +- retry_page_flip_data->fb_id); ++ fb_id); + + fd = meta_kms_impl_device_get_fd (impl_device); + ret = drmModePageFlip (fd, + meta_kms_crtc_get_id (crtc), +- retry_page_flip_data->fb_id, ++ fb_id, + DRM_MODE_PAGE_FLIP_EVENT, + retry_page_flip_data->page_flip_data); + } +@@ -730,7 +738,7 @@ retry_page_flips (gpointer user_data) + static void + schedule_retry_page_flip (MetaKmsImplDeviceSimple *impl_device_simple, + MetaKmsCrtc *crtc, +- uint32_t fb_id, ++ MetaDrmBuffer *fb, + float refresh_rate, + MetaKmsPageFlipData *page_flip_data, + MetaKmsCustomPageFlip *custom_page_flip) +@@ -745,7 +753,7 @@ schedule_retry_page_flip (MetaKmsImplDeviceSimple *impl_device_simple, + retry_page_flip_data = g_new0 (RetryPageFlipData, 1); + *retry_page_flip_data = (RetryPageFlipData) { + .crtc = crtc, +- .fb_id = fb_id, ++ .fb = fb ? g_object_ref (fb) : NULL, + .page_flip_data = page_flip_data, + .refresh_rate = refresh_rate, + .retry_time_us = retry_time_us, +@@ -877,6 +885,8 @@ mode_set_fallback (MetaKmsImplDeviceSimple *impl_device_simple, + return FALSE; + } + ++ meta_swap_chain_swap_buffers (meta_kms_crtc_get_swap_chain (crtc)); ++ + if (!impl_device_simple->mode_set_fallback_feedback_source) + { + MetaKmsImpl *impl = meta_kms_impl_device_get_impl (impl_device); +@@ -1003,20 +1013,20 @@ dispatch_page_flip (MetaKmsImplDevice *impl_device, + cached_mode_set = get_cached_mode_set (impl_device_simple, crtc); + if (cached_mode_set) + { +- uint32_t fb_id; ++ MetaDrmBuffer *fb; + drmModeModeInfo *drm_mode; + float refresh_rate; + + if (plane_assignment) +- fb_id = meta_drm_buffer_get_fb_id (plane_assignment->buffer); ++ fb = plane_assignment->buffer; + else +- fb_id = 0; ++ fb = NULL; + drm_mode = cached_mode_set->drm_mode; + refresh_rate = meta_calculate_drm_mode_refresh_rate (drm_mode); + meta_kms_impl_device_hold_fd (impl_device); + schedule_retry_page_flip (impl_device_simple, + crtc, +- fb_id, ++ fb, + refresh_rate, + page_flip_data, + g_steal_pointer (&custom_page_flip)); +@@ -1299,7 +1309,7 @@ process_plane_assignment (MetaKmsImplDevice *impl_device, + { + case META_KMS_PLANE_TYPE_PRIMARY: + /* Handled as part of the mode-set and page flip. */ +- return TRUE; ++ goto assigned; + case META_KMS_PLANE_TYPE_CURSOR: + if (!process_cursor_plane_assignment (impl_device, update, + plane_assignment, +@@ -1313,7 +1323,7 @@ process_plane_assignment (MetaKmsImplDevice *impl_device, + } + else + { +- return TRUE; ++ goto assigned; + } + case META_KMS_PLANE_TYPE_OVERLAY: + error = g_error_new_literal (G_IO_ERROR, G_IO_ERROR_FAILED, +@@ -1326,6 +1336,12 @@ process_plane_assignment (MetaKmsImplDevice *impl_device, + } + + g_assert_not_reached (); ++ ++assigned: ++ meta_swap_chain_push_buffer (meta_kms_crtc_get_swap_chain (plane_assignment->crtc), ++ meta_kms_plane_get_id (plane), ++ G_OBJECT (plane_assignment->buffer)); ++ return TRUE; + } + + static gboolean +diff --git a/src/backends/native/meta-kms-impl-device.c b/src/backends/native/meta-kms-impl-device.c +index bce64d309..85441f47b 100644 +--- a/src/backends/native/meta-kms-impl-device.c ++++ b/src/backends/native/meta-kms-impl-device.c +@@ -1483,9 +1483,11 @@ meta_kms_impl_device_handle_update (MetaKmsImplDevice *impl_device, + meta_kms_update_merge_from (crtc_frame->pending_update, update); + meta_kms_update_free (update); + update = g_steal_pointer (&crtc_frame->pending_update); +- disarm_crtc_frame_deadline_timer (crtc_frame); + } + ++ if (crtc_frame->deadline.armed) ++ disarm_crtc_frame_deadline_timer (crtc_frame); ++ + meta_kms_device_handle_flush (priv->device, latch_crtc); + + feedback = do_process (impl_device, latch_crtc, update, flags); +@@ -1862,6 +1864,16 @@ meta_kms_impl_device_init_mode_setting (MetaKmsImplDevice *impl_device, + return TRUE; + } + ++static void ++release_buffers (gpointer data, ++ gpointer user_data) ++{ ++ MetaKmsCrtc *crtc = data; ++ MetaSwapChain *swap_chain = meta_kms_crtc_get_swap_chain (crtc); ++ ++ meta_swap_chain_release_buffers (swap_chain); ++} ++ + void + meta_kms_impl_device_prepare_shutdown (MetaKmsImplDevice *impl_device) + { +@@ -1869,6 +1881,8 @@ meta_kms_impl_device_prepare_shutdown (MetaKmsImplDevice *impl_device) + meta_kms_impl_device_get_instance_private (impl_device); + MetaKmsImplDeviceClass *klass = META_KMS_IMPL_DEVICE_GET_CLASS (impl_device); + ++ g_list_foreach (priv->crtcs, release_buffers, NULL); ++ + if (klass->prepare_shutdown) + klass->prepare_shutdown (impl_device); + +diff --git a/src/backends/native/meta-kms-update.c b/src/backends/native/meta-kms-update.c +index 5189c5ab3..bb7349ecf 100644 +--- a/src/backends/native/meta-kms-update.c ++++ b/src/backends/native/meta-kms-update.c +@@ -190,6 +190,7 @@ static void + meta_kms_plane_assignment_free (MetaKmsPlaneAssignment *plane_assignment) + { + g_clear_pointer (&plane_assignment->fb_damage, meta_kms_fb_damage_free); ++ g_clear_object (&plane_assignment->buffer); + g_free (plane_assignment); + } + +@@ -292,7 +293,7 @@ meta_kms_update_assign_plane (MetaKmsUpdate *update, + .update = update, + .crtc = crtc, + .plane = plane, +- .buffer = buffer, ++ .buffer = g_object_ref (buffer), + .src_rect = src_rect, + .dst_rect = dst_rect, + .flags = flags, +diff --git a/src/backends/native/meta-kms.c b/src/backends/native/meta-kms.c +index ec009ec8c..c6708946e 100644 +--- a/src/backends/native/meta-kms.c ++++ b/src/backends/native/meta-kms.c +@@ -155,6 +155,8 @@ struct _MetaKms + int kernel_thread_inhibit_count; + + MetaKmsCursorManager *cursor_manager; ++ ++ gboolean shutting_down; + }; + + G_DEFINE_TYPE (MetaKms, meta_kms, META_TYPE_THREAD) +@@ -433,6 +435,7 @@ static void + on_prepare_shutdown (MetaBackend *backend, + MetaKms *kms) + { ++ kms->shutting_down = TRUE; + meta_kms_run_impl_task_sync (kms, prepare_shutdown_in_impl, NULL, NULL); + meta_thread_flush_callbacks (META_THREAD (kms)); + +@@ -487,6 +490,12 @@ meta_kms_new (MetaBackend *backend, + return kms; + } + ++gboolean ++meta_kms_is_shutting_down (MetaKms *kms) ++{ ++ return kms->shutting_down; ++} ++ + static void + meta_kms_finalize (GObject *object) + { +diff --git a/src/backends/native/meta-kms.h b/src/backends/native/meta-kms.h +index 743401406..f6b19520b 100644 +--- a/src/backends/native/meta-kms.h ++++ b/src/backends/native/meta-kms.h +@@ -60,6 +60,8 @@ MetaKmsDevice * meta_kms_create_device (MetaKms *kms, + MetaKmsDeviceFlag flags, + GError **error); + ++gboolean meta_kms_is_shutting_down (MetaKms *kms); ++ + MetaKms * meta_kms_new (MetaBackend *backend, + MetaKmsFlags flags, + GError **error); +diff --git a/src/backends/native/meta-onscreen-native.c b/src/backends/native/meta-onscreen-native.c +index 2388a44a2..14d727c55 100644 +--- a/src/backends/native/meta-onscreen-native.c ++++ b/src/backends/native/meta-onscreen-native.c +@@ -72,7 +72,7 @@ typedef struct _MetaOnscreenNativeSecondaryGpuState + + struct { + MetaDrmBufferDumb *current_dumb_fb; +- MetaDrmBufferDumb *dumb_fbs[2]; ++ MetaDrmBufferDumb *dumb_fbs[3]; + } cpu; + + gboolean noted_primary_gpu_copy_ok; +@@ -93,8 +93,13 @@ struct _MetaOnscreenNative + + struct { + struct gbm_surface *surface; +- MetaDrmBuffer *current_fb; + MetaDrmBuffer *next_fb; ++ MetaDrmBuffer *stalled_fb; ++ ++ /* Temporary workaround for the scanout-failed signal wanting the buffer ++ * to live longer than it does, and then it doesn't use it anyway... ++ */ ++ MetaDrmBuffer *direct_fb; + } gbm; + + #ifdef HAVE_EGL_DEVICE +@@ -116,6 +121,16 @@ struct _MetaOnscreenNative + gulong privacy_screen_changed_handler_id; + gulong color_space_changed_handler_id; + gulong hdr_metadata_changed_handler_id; ++ ++ gboolean needs_flush; ++ ++ unsigned int swaps_pending; ++ ++ struct { ++ int *rectangles; /* 4 x n_rectangles */ ++ int n_rectangles; ++ ClutterFrame *frame; ++ } next_post; + }; + + G_DEFINE_TYPE (MetaOnscreenNative, meta_onscreen_native, +@@ -123,40 +138,17 @@ G_DEFINE_TYPE (MetaOnscreenNative, meta_onscreen_native, + + static GQuark blit_source_quark = 0; + +-static gboolean +-init_secondary_gpu_state (MetaRendererNative *renderer_native, +- CoglOnscreen *onscreen, +- GError **error); +- + static void +-free_current_bo (CoglOnscreen *onscreen) +-{ +- MetaOnscreenNative *onscreen_native = META_ONSCREEN_NATIVE (onscreen); +- +- g_clear_object (&onscreen_native->gbm.current_fb); +-} +- +-static void +-meta_onscreen_native_swap_drm_fb (CoglOnscreen *onscreen) +-{ +- MetaOnscreenNative *onscreen_native = META_ONSCREEN_NATIVE (onscreen); +- +- if (!onscreen_native->gbm.next_fb) +- return; +- +- free_current_bo (onscreen); +- +- g_set_object (&onscreen_native->gbm.current_fb, onscreen_native->gbm.next_fb); +- g_clear_object (&onscreen_native->gbm.next_fb); +-} ++try_post_latest_swap (CoglOnscreen *onscreen); + + static void +-meta_onscreen_native_clear_next_fb (CoglOnscreen *onscreen) +-{ +- MetaOnscreenNative *onscreen_native = META_ONSCREEN_NATIVE (onscreen); ++post_finish_frame (MetaOnscreenNative *onscreen_native, ++ MetaKmsUpdate *kms_update); + +- g_clear_object (&onscreen_native->gbm.next_fb); +-} ++static gboolean ++init_secondary_gpu_state (MetaRendererNative *renderer_native, ++ CoglOnscreen *onscreen, ++ GError **error); + + static void + maybe_update_frame_info (MetaCrtc *crtc, +@@ -193,7 +185,7 @@ meta_onscreen_native_notify_frame_complete (CoglOnscreen *onscreen) + + info = cogl_onscreen_pop_head_frame_info (onscreen); + +- g_assert (!cogl_onscreen_peek_head_frame_info (onscreen)); ++ g_assert (info); + + _cogl_onscreen_notify_frame_sync (onscreen, info); + _cogl_onscreen_notify_complete (onscreen, info); +@@ -228,7 +220,8 @@ notify_view_crtc_presented (MetaRendererView *view, + maybe_update_frame_info (crtc, frame_info, time_us, flags, sequence); + + meta_onscreen_native_notify_frame_complete (onscreen); +- meta_onscreen_native_swap_drm_fb (onscreen); ++ meta_swap_chain_swap_buffers (meta_kms_crtc_get_swap_chain (kms_crtc)); ++ try_post_latest_swap (onscreen); + } + + static void +@@ -278,15 +271,13 @@ page_flip_feedback_ready (MetaKmsCrtc *kms_crtc, + CoglFramebuffer *framebuffer = + clutter_stage_view_get_onscreen (CLUTTER_STAGE_VIEW (view)); + CoglOnscreen *onscreen = COGL_ONSCREEN (framebuffer); +- MetaOnscreenNative *onscreen_native = META_ONSCREEN_NATIVE (onscreen); + CoglFrameInfo *frame_info; + + frame_info = cogl_onscreen_peek_head_frame_info (onscreen); + frame_info->flags |= COGL_FRAME_INFO_FLAG_SYMBOLIC; + +- g_warn_if_fail (!onscreen_native->gbm.next_fb); +- + meta_onscreen_native_notify_frame_complete (onscreen); ++ try_post_latest_swap (onscreen); + } + + static void +@@ -336,7 +327,7 @@ page_flip_feedback_discarded (MetaKmsCrtc *kms_crtc, + frame_info->flags |= COGL_FRAME_INFO_FLAG_SYMBOLIC; + + meta_onscreen_native_notify_frame_complete (onscreen); +- meta_onscreen_native_clear_next_fb (onscreen); ++ try_post_latest_swap (onscreen); + } + + static const MetaKmsPageFlipListenerVtable page_flip_listener_vtable = { +@@ -397,18 +388,40 @@ custom_egl_stream_page_flip (gpointer custom_page_flip_data, + } + #endif /* HAVE_EGL_DEVICE */ + +-void +-meta_onscreen_native_dummy_power_save_page_flip (CoglOnscreen *onscreen) ++static void ++drop_stalled_swap (CoglOnscreen *onscreen) + { + CoglFrameInfo *frame_info; ++ MetaOnscreenNative *onscreen_native = META_ONSCREEN_NATIVE (onscreen); + +- meta_onscreen_native_swap_drm_fb (onscreen); ++ /* Remember we can't compare stalled_fb because it's not used by ++ * META_RENDERER_NATIVE_MODE_EGL_DEVICE. So we judge stalled to be whenever ++ * swaps_pending > 1. ++ */ ++ if (onscreen_native->swaps_pending <= 1) ++ return; ++ ++ onscreen_native->swaps_pending--; ++ ++ g_clear_object (&onscreen_native->gbm.stalled_fb); + + frame_info = cogl_onscreen_peek_tail_frame_info (onscreen); + frame_info->flags |= COGL_FRAME_INFO_FLAG_SYMBOLIC; + meta_onscreen_native_notify_frame_complete (onscreen); + } + ++void ++meta_onscreen_native_dummy_power_save_page_flip (CoglOnscreen *onscreen) ++{ ++ drop_stalled_swap (onscreen); ++ ++ /* If the monitor just woke up and the shell is fully idle (has nothing ++ * more to swap) then we just woke to an indefinitely black screen. Let's ++ * fix that using the last swap (which is never classified as "stalled"). ++ */ ++ try_post_latest_swap (onscreen); ++} ++ + static void + meta_onscreen_native_flip_crtc (CoglOnscreen *onscreen, + MetaRendererView *view, +@@ -425,7 +438,7 @@ meta_onscreen_native_flip_crtc (CoglOnscreen *onscreen, + MetaKmsCrtc *kms_crtc = meta_crtc_kms_get_kms_crtc (crtc_kms); + MetaRendererNativeGpuData *renderer_gpu_data; + MetaGpuKms *gpu_kms; +- MetaDrmBuffer *buffer; ++ g_autoptr (MetaDrmBuffer) buffer = NULL; + MetaKmsPlaneAssignment *plane_assignment; + + COGL_TRACE_BEGIN_SCOPED (MetaOnscreenNativeFlipCrtcs, +@@ -440,7 +453,7 @@ meta_onscreen_native_flip_crtc (CoglOnscreen *onscreen, + switch (renderer_gpu_data->mode) + { + case META_RENDERER_NATIVE_MODE_GBM: +- buffer = onscreen_native->gbm.next_fb; ++ buffer = g_steal_pointer (&onscreen_native->gbm.next_fb); + + plane_assignment = meta_crtc_kms_assign_primary_plane (crtc_kms, + buffer, +@@ -596,6 +609,16 @@ import_shared_framebuffer (CoglOnscreen *onscreen, + return imported_buffer; + } + ++static void ++reference_owning_gbm_surface (CoglOnscreen *onscreen, ++ MetaDrmBufferGbm *buffer_gbm) ++{ ++ g_object_set_data_full (G_OBJECT (buffer_gbm), ++ "gbm_surface owner", ++ g_object_ref (onscreen), ++ (GDestroyNotify) g_object_unref); ++} ++ + static MetaDrmBuffer * + copy_shared_framebuffer_gpu (CoglOnscreen *onscreen, + MetaOnscreenNativeSecondaryGpuState *secondary_gpu_state, +@@ -681,6 +704,8 @@ copy_shared_framebuffer_gpu (CoglOnscreen *onscreen, + return NULL; + } + ++ reference_owning_gbm_surface (onscreen, buffer_gbm); ++ + g_object_set_qdata_full (G_OBJECT (buffer_gbm), + blit_source_quark, + g_object_ref (primary_gpu_fb), +@@ -693,12 +718,17 @@ static MetaDrmBufferDumb * + secondary_gpu_get_next_dumb_buffer (MetaOnscreenNativeSecondaryGpuState *secondary_gpu_state) + { + MetaDrmBufferDumb *current_dumb_fb; ++ const int n_dumb_fbs = G_N_ELEMENTS (secondary_gpu_state->cpu.dumb_fbs); ++ int i; + + current_dumb_fb = secondary_gpu_state->cpu.current_dumb_fb; +- if (current_dumb_fb == secondary_gpu_state->cpu.dumb_fbs[0]) +- return secondary_gpu_state->cpu.dumb_fbs[1]; +- else +- return secondary_gpu_state->cpu.dumb_fbs[0]; ++ for (i = 0; i < n_dumb_fbs; i++) ++ { ++ if (current_dumb_fb == secondary_gpu_state->cpu.dumb_fbs[i]) ++ return secondary_gpu_state->cpu.dumb_fbs[(i + 1) % n_dumb_fbs]; ++ } ++ ++ return secondary_gpu_state->cpu.dumb_fbs[0]; + } + + static MetaDrmBuffer * +@@ -1029,10 +1059,15 @@ swap_buffer_result_feedback (const MetaKmsFeedback *kms_feedback, + g_warning ("Page flip failed: %s", error->message); + + frame_info = cogl_onscreen_peek_head_frame_info (onscreen); +- frame_info->flags |= COGL_FRAME_INFO_FLAG_SYMBOLIC; + +- meta_onscreen_native_notify_frame_complete (onscreen); +- meta_onscreen_native_clear_next_fb (onscreen); ++ /* After resuming from suspend, drop_stalled_swap might have done this ++ * already and emptied the frame_info queue. ++ */ ++ if (frame_info) ++ { ++ frame_info->flags |= COGL_FRAME_INFO_FLAG_SYMBOLIC; ++ meta_onscreen_native_notify_frame_complete (onscreen); ++ } + } + + static const MetaKmsResultListenerVtable swap_buffer_result_listener_vtable = { +@@ -1053,30 +1088,35 @@ meta_onscreen_native_swap_buffers_with_damage (CoglOnscreen *onscreen, + CoglRendererEGL *cogl_renderer_egl = cogl_renderer->winsys; + MetaRendererNativeGpuData *renderer_gpu_data = cogl_renderer_egl->platform; + MetaRendererNative *renderer_native = renderer_gpu_data->renderer_native; +- MetaRenderer *renderer = META_RENDERER (renderer_native); +- MetaBackend *backend = meta_renderer_get_backend (renderer); +- MetaMonitorManager *monitor_manager = +- meta_backend_get_monitor_manager (backend); + MetaOnscreenNative *onscreen_native = META_ONSCREEN_NATIVE (onscreen); + MetaGpuKms *render_gpu = onscreen_native->render_gpu; + MetaDeviceFile *render_device_file; + ClutterFrame *frame = user_data; +- MetaFrameNative *frame_native = meta_frame_native_from_frame (frame); +- MetaKmsUpdate *kms_update; + CoglOnscreenClass *parent_class; + gboolean egl_context_changed = FALSE; +- MetaPowerSave power_save_mode; + g_autoptr (GError) error = NULL; + MetaDrmBufferFlags buffer_flags; + MetaDrmBufferGbm *buffer_gbm; + g_autoptr (MetaDrmBuffer) primary_gpu_fb = NULL; + g_autoptr (MetaDrmBuffer) secondary_gpu_fb = NULL; +- MetaKmsCrtc *kms_crtc; +- MetaKmsDevice *kms_device; ++ size_t rectangles_size; + + COGL_TRACE_BEGIN_SCOPED (MetaRendererNativeSwapBuffers, + "Onscreen (swap-buffers)"); + ++ if (meta_is_topic_enabled (META_DEBUG_KMS)) ++ { ++ unsigned int frames_pending = ++ cogl_onscreen_count_pending_frames (onscreen); ++ ++ meta_topic (META_DEBUG_KMS, ++ "Swap buffers: %u frames pending (%s-buffering)", ++ frames_pending, ++ frames_pending == 1 ? "double" : ++ frames_pending == 2 ? "triple" : ++ "?"); ++ } ++ + secondary_gpu_fb = + update_secondary_gpu_state_pre_swap_buffers (onscreen, + rectangles, +@@ -1113,6 +1153,7 @@ meta_onscreen_native_swap_buffers_with_damage (CoglOnscreen *onscreen, + return; + } + ++ reference_owning_gbm_surface (onscreen, buffer_gbm); + primary_gpu_fb = META_DRM_BUFFER (g_steal_pointer (&buffer_gbm)); + break; + case META_RENDERER_NATIVE_MODE_SURFACELESS: +@@ -1132,7 +1173,15 @@ meta_onscreen_native_swap_buffers_with_damage (CoglOnscreen *onscreen, + switch (renderer_gpu_data->mode) + { + case META_RENDERER_NATIVE_MODE_GBM: +- g_warn_if_fail (onscreen_native->gbm.next_fb == NULL); ++ if (onscreen_native->gbm.next_fb != NULL) ++ { ++ g_warn_if_fail (onscreen_native->gbm.stalled_fb == NULL); ++ drop_stalled_swap (onscreen); ++ g_assert (onscreen_native->gbm.stalled_fb == NULL); ++ onscreen_native->gbm.stalled_fb = ++ g_steal_pointer (&onscreen_native->gbm.next_fb); ++ } ++ + if (onscreen_native->secondary_gpu_state) + g_set_object (&onscreen_native->gbm.next_fb, secondary_gpu_fb); + else +@@ -1146,6 +1195,9 @@ meta_onscreen_native_swap_buffers_with_damage (CoglOnscreen *onscreen, + #endif + } + ++ clutter_frame_set_result (frame, ++ CLUTTER_FRAME_RESULT_PENDING_PRESENTED); ++ + /* + * If we changed EGL context, cogl will have the wrong idea about what is + * current, making it fail to set it when it needs to. Avoid that by making +@@ -1155,12 +1207,83 @@ meta_onscreen_native_swap_buffers_with_damage (CoglOnscreen *onscreen, + if (egl_context_changed) + _cogl_winsys_egl_ensure_current (cogl_display); + +- kms_crtc = meta_crtc_kms_get_kms_crtc (META_CRTC_KMS (onscreen_native->crtc)); +- kms_device = meta_kms_crtc_get_device (kms_crtc); ++ rectangles_size = n_rectangles * 4 * sizeof (int); ++ onscreen_native->next_post.rectangles = ++ g_realloc (onscreen_native->next_post.rectangles, rectangles_size); ++ memcpy (onscreen_native->next_post.rectangles, rectangles, rectangles_size); ++ onscreen_native->next_post.n_rectangles = n_rectangles; ++ ++ g_clear_pointer (&onscreen_native->next_post.frame, clutter_frame_unref); ++ onscreen_native->next_post.frame = clutter_frame_ref (frame); ++ ++ onscreen_native->swaps_pending++; ++ try_post_latest_swap (onscreen); ++} ++ ++static void ++try_post_latest_swap (CoglOnscreen *onscreen) ++{ ++ CoglFramebuffer *framebuffer = COGL_FRAMEBUFFER (onscreen); ++ CoglContext *cogl_context = cogl_framebuffer_get_context (framebuffer); ++ CoglRenderer *cogl_renderer = cogl_context->display->renderer; ++ CoglRendererEGL *cogl_renderer_egl = cogl_renderer->winsys; ++ MetaRendererNativeGpuData *renderer_gpu_data = cogl_renderer_egl->platform; ++ MetaRendererNative *renderer_native = renderer_gpu_data->renderer_native; ++ MetaRenderer *renderer = META_RENDERER (renderer_native); ++ MetaBackend *backend = meta_renderer_get_backend (renderer); ++ MetaBackendNative *backend_native = META_BACKEND_NATIVE (backend); ++ MetaKms *kms = meta_backend_native_get_kms (backend_native); ++ MetaMonitorManager *monitor_manager = ++ meta_backend_get_monitor_manager (backend); ++ MetaOnscreenNative *onscreen_native = META_ONSCREEN_NATIVE (onscreen); ++ MetaPowerSave power_save_mode; ++ MetaCrtcKms *crtc_kms = META_CRTC_KMS (onscreen_native->crtc); ++ MetaKmsCrtc *kms_crtc = meta_crtc_kms_get_kms_crtc (crtc_kms); ++ MetaKmsDevice *kms_device = meta_kms_crtc_get_device (kms_crtc); ++ MetaKmsUpdate *kms_update; ++ g_autoptr (MetaKmsFeedback) kms_feedback = NULL; ++ g_autoptr (ClutterFrame) frame = NULL; ++ MetaFrameNative *frame_native; ++ ++ if (onscreen_native->next_post.frame == NULL || ++ onscreen_native->view == NULL) ++ return; ++ ++ if (meta_kms_is_shutting_down (kms)) ++ { ++ meta_onscreen_native_discard_pending_swaps (onscreen); ++ return; ++ } + + power_save_mode = meta_monitor_manager_get_power_save_mode (monitor_manager); + if (power_save_mode == META_POWER_SAVE_ON) + { ++ unsigned int frames_pending = ++ cogl_onscreen_count_pending_frames (onscreen); ++ unsigned int posts_pending; ++ ++ g_assert (frames_pending >= onscreen_native->swaps_pending); ++ posts_pending = frames_pending - onscreen_native->swaps_pending; ++ if (posts_pending > 0) ++ return; /* wait for the next frame notification and then try again */ ++ ++ frame = g_steal_pointer (&onscreen_native->next_post.frame); ++ frame_native = meta_frame_native_from_frame (frame); ++ ++ if (onscreen_native->swaps_pending == 0) ++ { ++ if (frame_native) ++ { ++ kms_update = meta_frame_native_steal_kms_update (frame_native); ++ if (kms_update) ++ post_finish_frame (onscreen_native, kms_update); ++ } ++ return; ++ } ++ ++ drop_stalled_swap (onscreen); ++ onscreen_native->swaps_pending--; ++ + kms_update = meta_frame_native_ensure_kms_update (frame_native, + kms_device); + meta_kms_update_add_result_listener (kms_update, +@@ -1175,15 +1298,13 @@ meta_onscreen_native_swap_buffers_with_damage (CoglOnscreen *onscreen, + onscreen_native->crtc, + kms_update, + META_KMS_PAGE_FLIP_LISTENER_FLAG_NONE, +- rectangles, +- n_rectangles); ++ onscreen_native->next_post.rectangles, ++ onscreen_native->next_post.n_rectangles); + } + else + { + meta_renderer_native_queue_power_save_page_flip (renderer_native, + onscreen); +- clutter_frame_set_result (frame, +- CLUTTER_FRAME_RESULT_PENDING_PRESENTED); + return; + } + +@@ -1203,8 +1324,6 @@ meta_onscreen_native_swap_buffers_with_damage (CoglOnscreen *onscreen, + kms_update = meta_frame_native_steal_kms_update (frame_native); + meta_renderer_native_queue_mode_set_update (renderer_native, + kms_update); +- clutter_frame_set_result (frame, +- CLUTTER_FRAME_RESULT_PENDING_PRESENTED); + return; + } + else if (meta_renderer_native_has_pending_mode_set (renderer_native)) +@@ -1218,8 +1337,6 @@ meta_onscreen_native_swap_buffers_with_damage (CoglOnscreen *onscreen, + + meta_frame_native_steal_kms_update (frame_native); + meta_renderer_native_post_mode_set_updates (renderer_native); +- clutter_frame_set_result (frame, +- CLUTTER_FRAME_RESULT_PENDING_PRESENTED); + return; + } + break; +@@ -1235,8 +1352,6 @@ meta_onscreen_native_swap_buffers_with_damage (CoglOnscreen *onscreen, + kms_update); + + meta_renderer_native_post_mode_set_updates (renderer_native); +- clutter_frame_set_result (frame, +- CLUTTER_FRAME_RESULT_PENDING_PRESENTED); + return; + } + break; +@@ -1251,7 +1366,6 @@ meta_onscreen_native_swap_buffers_with_damage (CoglOnscreen *onscreen, + kms_update = meta_frame_native_steal_kms_update (frame_native); + meta_kms_device_post_update (kms_device, kms_update, + META_KMS_UPDATE_FLAG_NONE); +- clutter_frame_set_result (frame, CLUTTER_FRAME_RESULT_PENDING_PRESENTED); + } + + gboolean +@@ -1296,6 +1410,7 @@ scanout_result_feedback (const MetaKmsFeedback *kms_feedback, + CoglOnscreen *onscreen = COGL_ONSCREEN (onscreen_native); + const GError *error; + CoglFrameInfo *frame_info; ++ g_autoptr (MetaDrmBuffer) direct_fb = g_steal_pointer (&onscreen_native->gbm.direct_fb); + + error = meta_kms_feedback_get_error (kms_feedback); + if (!error) +@@ -1309,8 +1424,7 @@ scanout_result_feedback (const MetaKmsFeedback *kms_feedback, + + g_warning ("Direct scanout page flip failed: %s", error->message); + +- cogl_scanout_notify_failed (COGL_SCANOUT (onscreen_native->gbm.next_fb), +- onscreen); ++ cogl_scanout_notify_failed (COGL_SCANOUT (direct_fb), onscreen); + clutter_stage_view_add_redraw_clip (view, NULL); + clutter_stage_view_schedule_update_now (view); + } +@@ -1319,7 +1433,6 @@ scanout_result_feedback (const MetaKmsFeedback *kms_feedback, + frame_info->flags |= COGL_FRAME_INFO_FLAG_SYMBOLIC; + + meta_onscreen_native_notify_frame_complete (onscreen); +- meta_onscreen_native_clear_next_fb (onscreen); + } + + static const MetaKmsResultListenerVtable scanout_result_listener_vtable = { +@@ -1371,6 +1484,18 @@ meta_onscreen_native_direct_scanout (CoglOnscreen *onscreen, + return FALSE; + } + ++ /* Our direct scanout frame counts as 1, so more than that means we would ++ * be jumping the queue (and post would fail). ++ */ ++ if (cogl_onscreen_count_pending_frames (onscreen) > 1) ++ { ++ g_set_error_literal (error, ++ COGL_SCANOUT_ERROR, ++ COGL_SCANOUT_ERROR_INHIBITED, ++ "Direct scanout is inhibited during triple buffering"); ++ return FALSE; ++ } ++ + renderer_gpu_data = meta_renderer_native_get_gpu_data (renderer_native, + render_gpu); + +@@ -1385,6 +1510,8 @@ meta_onscreen_native_direct_scanout (CoglOnscreen *onscreen, + kms_device = meta_kms_crtc_get_device (kms_crtc); + kms_update = meta_frame_native_ensure_kms_update (frame_native, kms_device); + ++ g_set_object (&onscreen_native->gbm.direct_fb, ++ onscreen_native->gbm.next_fb); + meta_kms_update_add_result_listener (kms_update, + &scanout_result_listener_vtable, + NULL, +@@ -1430,12 +1557,6 @@ void + meta_onscreen_native_before_redraw (CoglOnscreen *onscreen, + ClutterFrame *frame) + { +- MetaOnscreenNative *onscreen_native = META_ONSCREEN_NATIVE (onscreen); +- MetaCrtcKms *crtc_kms = META_CRTC_KMS (onscreen_native->crtc); +- MetaKmsCrtc *kms_crtc = meta_crtc_kms_get_kms_crtc (crtc_kms); +- +- meta_kms_device_await_flush (meta_kms_crtc_get_device (kms_crtc), +- kms_crtc); + } + + void +@@ -1555,22 +1676,79 @@ meta_onscreen_native_finish_frame (CoglOnscreen *onscreen, + MetaKmsDevice *kms_device = meta_kms_crtc_get_device (kms_crtc); + MetaFrameNative *frame_native = meta_frame_native_from_frame (frame); + MetaKmsUpdate *kms_update; ++ unsigned int frames_pending = cogl_onscreen_count_pending_frames (onscreen); ++ unsigned int swaps_pending = onscreen_native->swaps_pending; ++ unsigned int posts_pending = frames_pending - swaps_pending; + +- kms_update = meta_frame_native_steal_kms_update (frame_native); +- if (!kms_update) ++ onscreen_native->needs_flush |= meta_kms_device_handle_flush (kms_device, ++ kms_crtc); ++ ++ if (!meta_frame_native_has_kms_update (frame_native)) + { +- if (meta_kms_device_handle_flush (kms_device, kms_crtc)) +- { +- kms_update = meta_kms_update_new (kms_device); +- meta_kms_update_set_flushing (kms_update, kms_crtc); +- } +- else ++ if (!onscreen_native->needs_flush || posts_pending) + { + clutter_frame_set_result (frame, CLUTTER_FRAME_RESULT_IDLE); + return; + } + } + ++ if (posts_pending && !swaps_pending) ++ { ++ g_return_if_fail (meta_frame_native_has_kms_update (frame_native)); ++ g_warn_if_fail (onscreen_native->next_post.frame == NULL); ++ ++ g_clear_pointer (&onscreen_native->next_post.frame, clutter_frame_unref); ++ onscreen_native->next_post.frame = clutter_frame_ref (frame); ++ clutter_frame_set_result (frame, CLUTTER_FRAME_RESULT_PENDING_PRESENTED); ++ return; ++ } ++ ++ kms_update = meta_frame_native_steal_kms_update (frame_native); ++ ++ if (posts_pending && swaps_pending) ++ { ++ MetaFrameNative *older_frame_native; ++ MetaKmsUpdate *older_kms_update; ++ ++ g_return_if_fail (kms_update); ++ g_return_if_fail (onscreen_native->next_post.frame != NULL); ++ ++ older_frame_native = ++ meta_frame_native_from_frame (onscreen_native->next_post.frame); ++ older_kms_update = ++ meta_frame_native_ensure_kms_update (older_frame_native, kms_device); ++ meta_kms_update_merge_from (older_kms_update, kms_update); ++ meta_kms_update_free (kms_update); ++ clutter_frame_set_result (frame, CLUTTER_FRAME_RESULT_IDLE); ++ return; ++ } ++ ++ if (!kms_update) ++ { ++ kms_update = meta_kms_update_new (kms_device); ++ g_warn_if_fail (onscreen_native->needs_flush); ++ } ++ ++ if (onscreen_native->needs_flush) ++ { ++ meta_kms_update_set_flushing (kms_update, kms_crtc); ++ onscreen_native->needs_flush = FALSE; ++ } ++ ++ post_finish_frame (onscreen_native, kms_update); ++ ++ clutter_frame_set_result (frame, CLUTTER_FRAME_RESULT_PENDING_PRESENTED); ++} ++ ++static void ++post_finish_frame (MetaOnscreenNative *onscreen_native, ++ MetaKmsUpdate *kms_update) ++{ ++ MetaCrtc *crtc = onscreen_native->crtc; ++ MetaKmsCrtc *kms_crtc = meta_crtc_kms_get_kms_crtc (META_CRTC_KMS (crtc)); ++ MetaKmsDevice *kms_device = meta_kms_crtc_get_device (kms_crtc); ++ g_autoptr (MetaKmsFeedback) kms_feedback = NULL; ++ + meta_kms_update_add_result_listener (kms_update, + &finish_frame_result_listener_vtable, + NULL, +@@ -1594,7 +1772,17 @@ meta_onscreen_native_finish_frame (CoglOnscreen *onscreen, + meta_kms_update_set_flushing (kms_update, kms_crtc); + meta_kms_device_post_update (kms_device, kms_update, + META_KMS_UPDATE_FLAG_NONE); +- clutter_frame_set_result (frame, CLUTTER_FRAME_RESULT_PENDING_PRESENTED); ++} ++ ++void ++meta_onscreen_native_discard_pending_swaps (CoglOnscreen *onscreen) ++{ ++ MetaOnscreenNative *onscreen_native = META_ONSCREEN_NATIVE (onscreen); ++ ++ onscreen_native->swaps_pending = 0; ++ ++ g_clear_object (&onscreen_native->gbm.stalled_fb); ++ g_clear_object (&onscreen_native->gbm.next_fb); + } + + static gboolean +@@ -2421,7 +2609,7 @@ meta_onscreen_native_dispose (GObject *object) + { + case META_RENDERER_NATIVE_MODE_GBM: + g_clear_object (&onscreen_native->gbm.next_fb); +- free_current_bo (onscreen); ++ g_clear_object (&onscreen_native->gbm.direct_fb); + break; + case META_RENDERER_NATIVE_MODE_SURFACELESS: + g_assert_not_reached (); +@@ -2455,6 +2643,10 @@ meta_onscreen_native_dispose (GObject *object) + + g_clear_object (&onscreen_native->output); + g_clear_object (&onscreen_native->crtc); ++ ++ g_clear_pointer (&onscreen_native->next_post.rectangles, g_free); ++ g_clear_pointer (&onscreen_native->next_post.frame, clutter_frame_unref); ++ onscreen_native->next_post.n_rectangles = 0; + } + + static void +diff --git a/src/backends/native/meta-onscreen-native.h b/src/backends/native/meta-onscreen-native.h +index 91eb7b533..11bb5ba56 100644 +--- a/src/backends/native/meta-onscreen-native.h ++++ b/src/backends/native/meta-onscreen-native.h +@@ -45,6 +45,8 @@ void meta_onscreen_native_finish_frame (CoglOnscreen *onscreen, + + void meta_onscreen_native_dummy_power_save_page_flip (CoglOnscreen *onscreen); + ++void meta_onscreen_native_discard_pending_swaps (CoglOnscreen *onscreen); ++ + gboolean meta_onscreen_native_is_buffer_scanout_compatible (CoglOnscreen *onscreen, + MetaDrmBuffer *fb); + +diff --git a/src/backends/native/meta-renderer-native.c b/src/backends/native/meta-renderer-native.c +index e6c653e26..7e39889bc 100644 +--- a/src/backends/native/meta-renderer-native.c ++++ b/src/backends/native/meta-renderer-native.c +@@ -99,6 +99,7 @@ struct _MetaRendererNative + + GList *detached_onscreens; + GList *lingering_onscreens; ++ GList *disabled_crtcs; + guint release_unused_gpus_idle_id; + + GList *power_save_page_flip_onscreens; +@@ -683,6 +684,9 @@ configure_disabled_crtcs (MetaKmsDevice *kms_device, + + kms_update = ensure_mode_set_update (renderer_native, kms_device); + meta_kms_update_mode_set (kms_update, kms_crtc, NULL, NULL); ++ ++ renderer_native->disabled_crtcs = ++ g_list_prepend (renderer_native->disabled_crtcs, kms_crtc); + } + } + +@@ -690,12 +694,18 @@ static gboolean + dummy_power_save_page_flip_cb (gpointer user_data) + { + MetaRendererNative *renderer_native = user_data; ++ GList *old_list = ++ g_steal_pointer (&renderer_native->power_save_page_flip_onscreens); + +- g_list_foreach (renderer_native->power_save_page_flip_onscreens, ++ g_list_foreach (old_list, + (GFunc) meta_onscreen_native_dummy_power_save_page_flip, + NULL); +- g_clear_list (&renderer_native->power_save_page_flip_onscreens, ++ g_clear_list (&old_list, + g_object_unref); ++ ++ if (renderer_native->power_save_page_flip_onscreens != NULL) ++ return G_SOURCE_CONTINUE; ++ + renderer_native->power_save_page_flip_source_id = 0; + + return G_SOURCE_REMOVE; +@@ -707,6 +717,9 @@ meta_renderer_native_queue_power_save_page_flip (MetaRendererNative *renderer_na + { + const unsigned int timeout_ms = 100; + ++ if (g_list_find (renderer_native->power_save_page_flip_onscreens, onscreen)) ++ return; ++ + if (!renderer_native->power_save_page_flip_source_id) + { + renderer_native->power_save_page_flip_source_id = +@@ -817,6 +830,22 @@ clear_detached_onscreens (MetaRendererNative *renderer_native) + g_object_unref); + } + ++static void ++clear_disabled_crtcs (MetaRendererNative *renderer_native) ++{ ++ GList *l; ++ ++ for (l = renderer_native->disabled_crtcs; l; l = l->next) ++ { ++ MetaKmsCrtc *kms_crtc = l->data; ++ MetaSwapChain *swap_chain = meta_kms_crtc_get_swap_chain (kms_crtc); ++ ++ meta_swap_chain_release_buffers (swap_chain); ++ } ++ ++ g_clear_list (&renderer_native->disabled_crtcs, NULL); ++} ++ + static void + mode_sets_update_result_feedback (const MetaKmsFeedback *kms_feedback, + gpointer user_data) +@@ -878,6 +907,7 @@ meta_renderer_native_post_mode_set_updates (MetaRendererNative *renderer_native) + post_mode_set_updates (renderer_native); + + clear_detached_onscreens (renderer_native); ++ clear_disabled_crtcs (renderer_native); + + meta_kms_notify_modes_set (kms); + +@@ -1467,6 +1497,26 @@ detach_onscreens (MetaRenderer *renderer) + } + } + ++static void ++discard_pending_swaps (MetaRenderer *renderer) ++{ ++ GList *views = meta_renderer_get_views (renderer);; ++ GList *l; ++ ++ for (l = views; l; l = l->next) ++ { ++ ClutterStageView *stage_view = l->data; ++ CoglFramebuffer *fb = clutter_stage_view_get_onscreen (stage_view); ++ CoglOnscreen *onscreen; ++ ++ if (!COGL_IS_ONSCREEN (fb)) ++ continue; ++ ++ onscreen = COGL_ONSCREEN (fb); ++ meta_onscreen_native_discard_pending_swaps (onscreen); ++ } ++} ++ + static void + meta_renderer_native_rebuild_views (MetaRenderer *renderer) + { +@@ -1477,6 +1527,7 @@ meta_renderer_native_rebuild_views (MetaRenderer *renderer) + MetaRendererClass *parent_renderer_class = + META_RENDERER_CLASS (meta_renderer_native_parent_class); + ++ discard_pending_swaps (renderer); + meta_kms_discard_pending_page_flips (kms); + g_hash_table_remove_all (renderer_native->mode_set_updates); + +@@ -2239,6 +2290,7 @@ meta_renderer_native_finalize (GObject *object) + g_clear_handle_id (&renderer_native->release_unused_gpus_idle_id, + g_source_remove); + clear_detached_onscreens (renderer_native); ++ clear_disabled_crtcs (renderer_native); + + g_hash_table_destroy (renderer_native->gpu_datas); + g_clear_object (&renderer_native->gles3); +diff --git a/src/backends/native/meta-swap-chain.c b/src/backends/native/meta-swap-chain.c +new file mode 100644 +index 000000000..c3bed569d +--- /dev/null ++++ b/src/backends/native/meta-swap-chain.c +@@ -0,0 +1,149 @@ ++/* ++ * Copyright (C) 2022 Canonical Ltd. ++ * ++ * This program is free software; you can redistribute it and/or ++ * modify it under the terms of the GNU General Public License as ++ * published by the Free Software Foundation; either version 2 of the ++ * License, or (at your option) any later version. ++ * ++ * This program is distributed in the hope that it will be useful, but ++ * WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ++ * General Public License for more details. ++ * ++ * You should have received a copy of the GNU General Public License ++ * along with this program; if not, write to the Free Software ++ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA ++ * 02111-1307, USA. ++ * ++ * Author: Daniel van Vugt <daniel.van.vugt@canonical.com> ++ */ ++ ++#include "backends/native/meta-swap-chain.h" ++ ++typedef struct ++{ ++ GObject *front, *back; ++ gboolean back_is_set; ++} PlaneState; ++ ++typedef struct _MetaSwapChainPrivate MetaSwapChainPrivate; ++struct _MetaSwapChainPrivate ++{ ++ GHashTable *plane_states; ++}; ++ ++G_DEFINE_TYPE_WITH_PRIVATE (MetaSwapChain, meta_swap_chain, G_TYPE_OBJECT) ++ ++MetaSwapChain * ++meta_swap_chain_new (void) ++{ ++ return g_object_new (META_TYPE_SWAP_CHAIN, NULL); ++} ++ ++void ++meta_swap_chain_push_buffer (MetaSwapChain *swap_chain, ++ unsigned int plane_id, ++ GObject *buffer) ++{ ++ MetaSwapChainPrivate *priv = ++ meta_swap_chain_get_instance_private (swap_chain); ++ gpointer key = GUINT_TO_POINTER (plane_id); ++ PlaneState *plane_state; ++ ++ plane_state = g_hash_table_lookup (priv->plane_states, key); ++ if (plane_state == NULL) ++ { ++ plane_state = g_new0 (PlaneState, 1); ++ g_hash_table_insert (priv->plane_states, key, plane_state); ++ } ++ ++ plane_state->back_is_set = TRUE; /* note buffer may be NULL */ ++ g_set_object (&plane_state->back, buffer); ++} ++ ++static void ++swap_plane_buffers (gpointer key, ++ gpointer value, ++ gpointer user_data) ++{ ++ PlaneState *plane_state = value; ++ ++ if (plane_state->back_is_set) ++ { ++ g_set_object (&plane_state->front, plane_state->back); ++ g_clear_object (&plane_state->back); ++ plane_state->back_is_set = FALSE; ++ } ++} ++ ++void ++meta_swap_chain_swap_buffers (MetaSwapChain *swap_chain) ++{ ++ MetaSwapChainPrivate *priv = ++ meta_swap_chain_get_instance_private (swap_chain); ++ ++ g_hash_table_foreach (priv->plane_states, swap_plane_buffers, NULL); ++} ++ ++void ++meta_swap_chain_release_buffers (MetaSwapChain *swap_chain) ++{ ++ MetaSwapChainPrivate *priv = ++ meta_swap_chain_get_instance_private (swap_chain); ++ ++ g_hash_table_remove_all (priv->plane_states); ++} ++ ++static void ++meta_swap_chain_dispose (GObject *object) ++{ ++ MetaSwapChain *swap_chain = META_SWAP_CHAIN (object); ++ ++ meta_swap_chain_release_buffers (swap_chain); ++ ++ G_OBJECT_CLASS (meta_swap_chain_parent_class)->dispose (object); ++} ++ ++static void ++meta_swap_chain_finalize (GObject *object) ++{ ++ MetaSwapChain *swap_chain = META_SWAP_CHAIN (object); ++ MetaSwapChainPrivate *priv = ++ meta_swap_chain_get_instance_private (swap_chain); ++ ++ g_hash_table_unref (priv->plane_states); ++ ++ G_OBJECT_CLASS (meta_swap_chain_parent_class)->finalize (object); ++} ++ ++static void ++destroy_plane_state (gpointer data) ++{ ++ PlaneState *plane_state = data; ++ ++ g_clear_object (&plane_state->front); ++ g_clear_object (&plane_state->back); ++ g_free (plane_state); ++} ++ ++static void ++meta_swap_chain_init (MetaSwapChain *swap_chain) ++{ ++ MetaSwapChainPrivate *priv = ++ meta_swap_chain_get_instance_private (swap_chain); ++ ++ priv->plane_states = g_hash_table_new_full (NULL, ++ NULL, ++ NULL, ++ destroy_plane_state); ++} ++ ++static void ++meta_swap_chain_class_init (MetaSwapChainClass *klass) ++{ ++ GObjectClass *object_class = G_OBJECT_CLASS (klass); ++ ++ object_class->dispose = meta_swap_chain_dispose; ++ object_class->finalize = meta_swap_chain_finalize; ++} +diff --git a/src/backends/native/meta-swap-chain.h b/src/backends/native/meta-swap-chain.h +new file mode 100644 +index 000000000..bad772b89 +--- /dev/null ++++ b/src/backends/native/meta-swap-chain.h +@@ -0,0 +1,48 @@ ++/* ++ * Copyright (C) 2022 Canonical Ltd. ++ * ++ * This program is free software; you can redistribute it and/or ++ * modify it under the terms of the GNU General Public License as ++ * published by the Free Software Foundation; either version 2 of the ++ * License, or (at your option) any later version. ++ * ++ * This program is distributed in the hope that it will be useful, but ++ * WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ++ * General Public License for more details. ++ * ++ * You should have received a copy of the GNU General Public License ++ * along with this program; if not, write to the Free Software ++ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA ++ * 02111-1307, USA. ++ * ++ * Author: Daniel van Vugt <daniel.van.vugt@canonical.com> ++ */ ++ ++#ifndef META_SWAP_CHAIN_H ++#define META_SWAP_CHAIN_H ++ ++#include <glib-object.h> ++ ++#define META_TYPE_SWAP_CHAIN (meta_swap_chain_get_type ()) ++G_DECLARE_DERIVABLE_TYPE (MetaSwapChain, ++ meta_swap_chain, ++ META, SWAP_CHAIN, ++ GObject) ++ ++struct _MetaSwapChainClass ++{ ++ GObjectClass parent_class; ++}; ++ ++MetaSwapChain * meta_swap_chain_new (void); ++ ++void meta_swap_chain_push_buffer (MetaSwapChain *swap_chain, ++ unsigned int plane_id, ++ GObject *buffer); ++ ++void meta_swap_chain_swap_buffers (MetaSwapChain *swap_chain); ++ ++void meta_swap_chain_release_buffers (MetaSwapChain *swap_chain); ++ ++#endif /* META_SWAP_CHAIN_H */ +diff --git a/src/meson.build b/src/meson.build +index ca2ef166c..0038988bd 100644 +--- a/src/meson.build ++++ b/src/meson.build +@@ -850,6 +850,8 @@ if have_native_backend + 'backends/native/meta-seat-native.h', + 'backends/native/meta-stage-native.c', + 'backends/native/meta-stage-native.h', ++ 'backends/native/meta-swap-chain.c', ++ 'backends/native/meta-swap-chain.h', + 'backends/native/meta-thread-impl.c', + 'backends/native/meta-thread-impl.h', + 'backends/native/meta-thread-private.h', +diff --git a/src/tests/native-kms-render.c b/src/tests/native-kms-render.c +index 90ea9b581..aafa682bd 100644 +--- a/src/tests/native-kms-render.c ++++ b/src/tests/native-kms-render.c +@@ -39,6 +39,8 @@ + #include "tests/meta-wayland-test-driver.h" + #include "tests/meta-wayland-test-utils.h" + ++#define N_FRAMES_PER_TEST 30 ++ + typedef struct + { + int number_of_frames_left; +@@ -46,12 +48,15 @@ typedef struct + + struct { + int n_paints; +- uint32_t fb_id; ++ int n_presentations; ++ int n_direct_scanouts; ++ GList *fb_ids; + } scanout; + + gboolean wait_for_scanout; + + struct { ++ int scanouts_attempted; + gboolean scanout_sabotaged; + gboolean fallback_painted; + guint repaint_guard_id; +@@ -101,7 +106,7 @@ meta_test_kms_render_basic (void) + gulong handler_id; + + test = (KmsRenderingTest) { +- .number_of_frames_left = 10, ++ .number_of_frames_left = N_FRAMES_PER_TEST, + .loop = g_main_loop_new (NULL, FALSE), + }; + handler_id = g_signal_connect (stage, "after-update", +@@ -123,7 +128,6 @@ on_scanout_before_update (ClutterStage *stage, + KmsRenderingTest *test) + { + test->scanout.n_paints = 0; +- test->scanout.fb_id = 0; + } + + static void +@@ -134,6 +138,7 @@ on_scanout_before_paint (ClutterStage *stage, + { + CoglScanout *scanout; + MetaDrmBuffer *buffer; ++ uint32_t fb_id; + + scanout = clutter_stage_view_peek_scanout (stage_view); + if (!scanout) +@@ -141,8 +146,14 @@ on_scanout_before_paint (ClutterStage *stage, + + g_assert_true (META_IS_DRM_BUFFER (scanout)); + buffer = META_DRM_BUFFER (scanout); +- test->scanout.fb_id = meta_drm_buffer_get_fb_id (buffer); +- g_assert_cmpuint (test->scanout.fb_id, >, 0); ++ ++ fb_id = meta_drm_buffer_get_fb_id (buffer); ++ g_assert_cmpuint (fb_id, >, 0); ++ test->scanout.fb_ids = g_list_append (test->scanout.fb_ids, ++ GUINT_TO_POINTER (fb_id)); ++ ++ /* Triple buffering, but no higher */ ++ g_assert_cmpuint (g_list_length (test->scanout.fb_ids), <=, 2); + } + + static void +@@ -171,12 +182,12 @@ on_scanout_presented (ClutterStage *stage, + MetaDeviceFile *device_file; + GError *error = NULL; + drmModeCrtc *drm_crtc; ++ uint32_t first_fb_id_expected; + +- if (test->wait_for_scanout && test->scanout.n_paints > 0) ++ if (test->wait_for_scanout && test->scanout.fb_ids == NULL) + return; + +- if (test->wait_for_scanout && test->scanout.fb_id == 0) +- return; ++ test->scanout.n_presentations++; + + device_pool = meta_backend_native_get_device_pool (backend_native); + +@@ -195,15 +206,41 @@ on_scanout_presented (ClutterStage *stage, + drm_crtc = drmModeGetCrtc (meta_device_file_get_fd (device_file), + meta_kms_crtc_get_id (kms_crtc)); + g_assert_nonnull (drm_crtc); +- if (test->scanout.fb_id == 0) +- g_assert_cmpuint (drm_crtc->buffer_id, !=, test->scanout.fb_id); ++ ++ if (test->scanout.fb_ids) ++ { ++ test->scanout.n_direct_scanouts++; ++ first_fb_id_expected = GPOINTER_TO_UINT (test->scanout.fb_ids->data); ++ test->scanout.fb_ids = g_list_delete_link (test->scanout.fb_ids, ++ test->scanout.fb_ids); ++ } + else +- g_assert_cmpuint (drm_crtc->buffer_id, ==, test->scanout.fb_id); ++ { ++ first_fb_id_expected = 0; ++ } ++ ++ /* The buffer ID won't match on the first frame because switching from ++ * triple buffered compositing to double buffered direct scanout takes ++ * an extra frame to drain the queue. Thereafter we are in direct scanout ++ * mode and expect the buffer IDs to match. ++ */ ++ if (test->scanout.n_presentations > 1) ++ { ++ if (first_fb_id_expected == 0) ++ g_assert_cmpuint (drm_crtc->buffer_id, !=, first_fb_id_expected); ++ else ++ g_assert_cmpuint (drm_crtc->buffer_id, ==, first_fb_id_expected); ++ } ++ + drmModeFreeCrtc (drm_crtc); + + meta_device_file_release (device_file); + +- g_main_loop_quit (test->loop); ++ test->number_of_frames_left--; ++ if (test->number_of_frames_left <= 0) ++ g_main_loop_quit (test->loop); ++ else ++ clutter_actor_queue_redraw (CLUTTER_ACTOR (stage)); + } + + typedef enum +@@ -242,7 +279,9 @@ meta_test_kms_render_client_scanout (void) + g_assert_nonnull (wayland_test_client); + + test = (KmsRenderingTest) { ++ .number_of_frames_left = N_FRAMES_PER_TEST, + .loop = g_main_loop_new (NULL, FALSE), ++ .scanout = {0}, + .wait_for_scanout = TRUE, + }; + +@@ -268,7 +307,8 @@ meta_test_kms_render_client_scanout (void) + clutter_actor_queue_redraw (CLUTTER_ACTOR (stage)); + g_main_loop_run (test.loop); + +- g_assert_cmpuint (test.scanout.fb_id, >, 0); ++ g_assert_cmpint (test.scanout.n_presentations, ==, N_FRAMES_PER_TEST); ++ g_assert_cmpint (test.scanout.n_direct_scanouts, ==, N_FRAMES_PER_TEST); + + g_debug ("Unmake fullscreen"); + window = meta_find_window_from_title (test_context, "dma-buf-scanout-test"); +@@ -290,10 +330,15 @@ meta_test_kms_render_client_scanout (void) + g_assert_cmpint (buffer_rect.y, ==, 10); + + test.wait_for_scanout = FALSE; ++ test.number_of_frames_left = N_FRAMES_PER_TEST; ++ test.scanout.n_presentations = 0; ++ test.scanout.n_direct_scanouts = 0; ++ + clutter_actor_queue_redraw (CLUTTER_ACTOR (stage)); + g_main_loop_run (test.loop); + +- g_assert_cmpuint (test.scanout.fb_id, ==, 0); ++ g_assert_cmpint (test.scanout.n_presentations, ==, N_FRAMES_PER_TEST); ++ g_assert_cmpint (test.scanout.n_direct_scanouts, ==, 0); + + g_debug ("Moving back to 0, 0"); + meta_window_move_frame (window, TRUE, 0, 0); +@@ -305,10 +350,15 @@ meta_test_kms_render_client_scanout (void) + g_assert_cmpint (buffer_rect.y, ==, 0); + + test.wait_for_scanout = TRUE; ++ test.number_of_frames_left = N_FRAMES_PER_TEST; ++ test.scanout.n_presentations = 0; ++ test.scanout.n_direct_scanouts = 0; ++ + clutter_actor_queue_redraw (CLUTTER_ACTOR (stage)); + g_main_loop_run (test.loop); + +- g_assert_cmpuint (test.scanout.fb_id, >, 0); ++ g_assert_cmpint (test.scanout.n_presentations, ==, N_FRAMES_PER_TEST); ++ g_assert_cmpint (test.scanout.n_direct_scanouts, ==, N_FRAMES_PER_TEST); + + g_signal_handler_disconnect (stage, before_update_handler_id); + g_signal_handler_disconnect (stage, before_paint_handler_id); +@@ -362,6 +412,15 @@ on_scanout_fallback_before_paint (ClutterStage *stage, + if (!scanout) + return; + ++ test->scanout_fallback.scanouts_attempted++; ++ ++ /* The first scanout candidate frame will get composited due to triple ++ * buffering draining the queue to drop to double buffering. So don't ++ * sabotage that first frame. ++ */ ++ if (test->scanout_fallback.scanouts_attempted < 2) ++ return; ++ + g_assert_false (test->scanout_fallback.scanout_sabotaged); + + if (is_atomic_mode_setting (kms_device)) +@@ -399,6 +458,15 @@ on_scanout_fallback_paint_view (ClutterStage *stage, + g_clear_handle_id (&test->scanout_fallback.repaint_guard_id, + g_source_remove); + test->scanout_fallback.fallback_painted = TRUE; ++ test->scanout_fallback.scanout_sabotaged = FALSE; ++ } ++ else if (test->scanout_fallback.scanouts_attempted == 1) ++ { ++ /* Now that we've seen the first scanout attempt that was inhibited by ++ * triple buffering, try a second frame. The second one should scanout ++ * and will be sabotaged. ++ */ ++ clutter_actor_queue_redraw (CLUTTER_ACTOR (stage)); + } + } + +@@ -408,11 +476,11 @@ on_scanout_fallback_presented (ClutterStage *stage, + ClutterFrameInfo *frame_info, + KmsRenderingTest *test) + { +- if (!test->scanout_fallback.scanout_sabotaged) +- return; ++ if (test->scanout_fallback.fallback_painted) ++ g_main_loop_quit (test->loop); + +- g_assert_true (test->scanout_fallback.fallback_painted); +- g_main_loop_quit (test->loop); ++ test->number_of_frames_left--; ++ g_assert_cmpint (test->number_of_frames_left, >, 0); + } + + static void +@@ -441,6 +509,7 @@ meta_test_kms_render_client_scanout_fallback (void) + g_assert_nonnull (wayland_test_client); + + test = (KmsRenderingTest) { ++ .number_of_frames_left = N_FRAMES_PER_TEST, + .loop = g_main_loop_new (NULL, FALSE), + }; + |