summarylogtreecommitdiffstats
diff options
context:
space:
mode:
authorohno10522024-02-05 02:01:35 +0500
committerohno10522024-02-05 02:01:35 +0500
commit5ea4c58e8a9fb3d2d356c9f4609ce19415b69dc6 (patch)
tree739ede5bb4e3e66ac99dcb1247da4909c3183850
parent06684b07d19edef6181d6946dcd86c2f3f8c379f (diff)
downloadaur-5ea4c58e8a9fb3d2d356c9f4609ce19415b69dc6.tar.gz
minor pkgbuild changes, add dynamic buffering patch
-rw-r--r--.SRCINFO5
-rw-r--r--PKGBUILD5
-rw-r--r--mr1441.patch2312
3 files changed, 2321 insertions, 1 deletions
diff --git a/.SRCINFO b/.SRCINFO
index 9eb6f035a4ce..f676d7549cb1 100644
--- a/.SRCINFO
+++ b/.SRCINFO
@@ -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
diff --git a/PKGBUILD b/PKGBUILD
index 5373712df0eb..da47b76329fe 100644
--- a/PKGBUILD
+++ b/PKGBUILD
@@ -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),
+ };
+