From: Daniel van Vugt <daniel.van.vugt@canonical.com>
Date: Wed, 10 Nov 2021 18:55:53 +0800
Subject: Support Dynamic triple/double buffering

Use triple buffering if and when the previous frame is running late.
This means the next frame will be dispatched on time instead of also starting
late.

It also triggers a GPU clock boost if deemed necessary by the driver.
Although frequency scaling is not required to get a performance gain here
because even a fixed frequency GPU will benefit from not over-sleeping anymore.
If the previous frame is not running late then we stick to double buffering so
there's no latency penalty when the system is able to maintain full frame rate.

Origin: https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/1441
---
 clutter/clutter/clutter-frame-clock.c             | 165 ++++++--
 clutter/clutter/clutter-frame-clock.h             |  11 +-
 clutter/clutter/clutter-frame-private.h           |   1 +
 clutter/clutter/clutter-frame.c                   |  13 +
 clutter/clutter/clutter-frame.h                   |   7 +
 clutter/clutter/clutter-stage-view.c              |   5 +-
 cogl/cogl/cogl-onscreen-private.h                 |   3 +
 cogl/cogl/cogl-onscreen.c                         |   8 +
 src/backends/meta-stage-impl.c                    |   2 +
 src/backends/native/meta-crtc-kms.c               |   5 +-
 src/backends/native/meta-cursor-renderer-native.c | 146 +------
 src/backends/native/meta-kms-crtc.c               |  80 ++++
 src/backends/native/meta-kms-crtc.h               |   9 +
 src/backends/native/meta-kms-impl-device-atomic.c |   9 +-
 src/backends/native/meta-kms-impl-device-simple.c |  38 +-
 src/backends/native/meta-kms-impl-device.c        |   4 +
 src/backends/native/meta-kms-page-flip.c          |   3 +
 src/backends/native/meta-kms-update-private.h     |   6 +
 src/backends/native/meta-kms-update.c             |  47 ++-
 src/backends/native/meta-kms.c                    | 103 ++++-
 src/backends/native/meta-kms.h                    |  10 +
 src/backends/native/meta-onscreen-native.c        | 486 ++++++++++++++--------
 src/backends/native/meta-onscreen-native.h        |   2 +
 src/backends/native/meta-renderer-native.c        |  34 +-
 24 files changed, 836 insertions(+), 361 deletions(-)

diff --git a/clutter/clutter/clutter-frame-clock.c b/clutter/clutter/clutter-frame-clock.c
index 6fa2b25..7a6444e 100644
--- a/clutter/clutter/clutter-frame-clock.c
+++ b/clutter/clutter/clutter-frame-clock.c
@@ -45,6 +45,8 @@ typedef struct _EstimateQueue
   int next_index;
 } EstimateQueue;
 
+static gboolean triple_buffering_disabled = FALSE;
+
 #define SYNC_DELAY_FALLBACK_FRACTION 0.875
 
 typedef struct _ClutterFrameListener
@@ -65,8 +67,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
@@ -96,6 +99,8 @@ struct _ClutterFrameClock
   /* Last KMS buffer submission time. */
   int64_t last_flip_time_us;
 
+  ClutterFrameHint last_flip_hints;
+
   /* Last few durations between dispatch start and buffer swap. */
   EstimateQueue dispatch_to_swap_us;
   /* Last few durations between buffer swap and GPU rendering finish. */
@@ -228,6 +233,10 @@ void
 clutter_frame_clock_notify_presented (ClutterFrameClock *frame_clock,
                                       ClutterFrameInfo  *frame_info)
 {
+  const char *debug_state =
+    frame_clock->state == CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_TWO ?
+    "Triple buffering" : "Double buffering";
+
   COGL_TRACE_BEGIN_SCOPED (ClutterFrameClockNotifyPresented,
                            "Frame Clock (presented)");
 
@@ -289,7 +298,8 @@ clutter_frame_clock_notify_presented (ClutterFrameClock *frame_clock,
         frame_info->cpu_time_before_buffer_swap_us;
 
       CLUTTER_NOTE (FRAME_TIMINGS,
-                    "dispatch2swap %ld µs, swap2render %ld µs, swap2flip %ld µs",
+                    "%s: dispatch2swap %ld µs, swap2render %ld µs, swap2flip %ld µs",
+                    debug_state,
                     dispatch_to_swap_us,
                     swap_to_rendering_done_us,
                     swap_to_flip_us);
@@ -303,6 +313,10 @@ clutter_frame_clock_notify_presented (ClutterFrameClock *frame_clock,
 
       frame_clock->got_measurements_last_frame = TRUE;
     }
+  else
+    {
+      CLUTTER_NOTE (FRAME_TIMINGS, "%s", debug_state);
+    }
 
   if (frame_info->refresh_rate > 1)
     {
@@ -317,11 +331,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;
     }
 }
 
@@ -337,11 +358,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;
     }
 }
 
@@ -353,6 +381,7 @@ clutter_frame_clock_compute_max_render_time_us (ClutterFrameClock *frame_clock)
   int64_t max_swap_to_rendering_done_us = 0;
   int64_t max_swap_to_flip_us = 0;
   int64_t max_render_time_us;
+  int buffer_queue_latency_frames = 0;
   int i;
 
   refresh_interval_us = frame_clock->refresh_interval_us;
@@ -375,6 +404,27 @@ clutter_frame_clock_compute_max_render_time_us (ClutterFrameClock *frame_clock)
              frame_clock->swap_to_flip_us.values[i]);
     }
 
+  switch (frame_clock->state)
+    {
+    case CLUTTER_FRAME_CLOCK_STATE_INIT:
+    case CLUTTER_FRAME_CLOCK_STATE_IDLE:
+    case CLUTTER_FRAME_CLOCK_STATE_SCHEDULED:
+      buffer_queue_latency_frames = 0;
+      break;
+    case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE:
+    case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE_AND_SCHEDULED:
+      buffer_queue_latency_frames = 1;
+      break;
+    case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_TWO:
+      g_warn_if_reached ();
+      buffer_queue_latency_frames = 2;
+      break;
+    }
+
+  max_swap_to_flip_us -= refresh_interval_us * buffer_queue_latency_frames;
+  if (max_swap_to_flip_us < 0)
+    max_swap_to_flip_us = 0;
+
   /* Max render time shows how early the frame clock needs to be dispatched
    * to make it to the predicted next presentation time. It is composed of:
    * - An estimate of duration from dispatch start to buffer swap.
@@ -391,8 +441,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;
 }
 
@@ -558,8 +606,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;
         }
 
@@ -595,11 +647,17 @@ clutter_frame_clock_schedule_update_now (ClutterFrameClock *frame_clock)
     case CLUTTER_FRAME_CLOCK_STATE_INIT:
     case CLUTTER_FRAME_CLOCK_STATE_IDLE:
       next_update_time_us = g_get_monotonic_time ();
+      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:
+      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_DISPATCHED_TWO:
       frame_clock->pending_reschedule = TRUE;
       frame_clock->pending_reschedule_now = TRUE;
       return;
@@ -608,7 +666,6 @@ clutter_frame_clock_schedule_update_now (ClutterFrameClock *frame_clock)
   g_warn_if_fail (next_update_time_us != -1);
 
   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;
 }
 
@@ -627,6 +684,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,
@@ -634,11 +692,28 @@ clutter_frame_clock_schedule_update (ClutterFrameClock *frame_clock)
                                      &frame_clock->next_presentation_time_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:
+      if (frame_clock->last_flip_hints & CLUTTER_FRAME_HINT_DIRECT_SCANOUT_ATTEMPTED ||
+          triple_buffering_disabled)
+        {
+          /* Force double buffering, disable triple buffering */
+          frame_clock->pending_reschedule = TRUE;
+          return;
+        }
+
+      calculate_next_update_time_us (frame_clock,
+                                     &next_update_time_us,
+                                     &frame_clock->next_presentation_time_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 CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_TWO:
       frame_clock->pending_reschedule = TRUE;
       return;
     }
@@ -646,7 +721,6 @@ clutter_frame_clock_schedule_update (ClutterFrameClock *frame_clock)
   g_warn_if_fail (next_update_time_us != -1);
 
   g_source_set_ready_time (frame_clock->source, next_update_time_us);
-  frame_clock->state = CLUTTER_FRAME_CLOCK_STATE_SCHEDULED;
 }
 
 static void
@@ -670,7 +744,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;
@@ -678,7 +752,21 @@ clutter_frame_clock_dispatch (ClutterFrameClock *frame_clock,
   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++;
 
@@ -703,25 +791,31 @@ clutter_frame_clock_dispatch (ClutterFrameClock *frame_clock,
                                                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 ();
+    case CLUTTER_FRAME_RESULT_PENDING_PRESENTED:
       break;
-    case CLUTTER_FRAME_CLOCK_STATE_IDLE:
-    case CLUTTER_FRAME_CLOCK_STATE_SCHEDULED:
-      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;
     }
@@ -754,10 +848,12 @@ 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->last_flip_time_us = flip_time_us;
+  frame_clock->last_flip_hints = hints;
 }
 
 GString *
@@ -888,6 +984,9 @@ clutter_frame_clock_class_init (ClutterFrameClockClass *klass)
 {
   GObjectClass *object_class = G_OBJECT_CLASS (klass);
 
+  if (!g_strcmp0 (g_getenv ("MUTTER_DEBUG_DISABLE_TRIPLE_BUFFERING"), "1"))
+    triple_buffering_disabled = TRUE;
+
   object_class->dispose = clutter_frame_clock_dispose;
 
   signals[DESTROY] =
diff --git a/clutter/clutter/clutter-frame-clock.h b/clutter/clutter/clutter-frame-clock.h
index 91e6b3a..d750404 100644
--- a/clutter/clutter/clutter-frame-clock.h
+++ b/clutter/clutter/clutter-frame-clock.h
@@ -34,6 +34,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,
@@ -90,8 +96,9 @@ 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 e008856..0658149 100644
--- a/clutter/clutter/clutter-frame-private.h
+++ b/clutter/clutter/clutter-frame-private.h
@@ -24,6 +24,7 @@ struct _ClutterFrame
 {
   gboolean has_result;
   ClutterFrameResult result;
+  ClutterFrameHint hints;
 };
 
 #define CLUTTER_FRAME_INIT ((ClutterFrame) { 0 })
diff --git a/clutter/clutter/clutter-frame.c b/clutter/clutter/clutter-frame.c
index 3c708da..63ae302 100644
--- a/clutter/clutter/clutter-frame.c
+++ b/clutter/clutter/clutter-frame.c
@@ -40,3 +40,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 d3608e8..06c5f7f 100644
--- a/clutter/clutter/clutter-frame.h
+++ b/clutter/clutter/clutter-frame.h
@@ -33,4 +33,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);
+
 #endif /* CLUTTER_FRAME_H */
diff --git a/clutter/clutter/clutter-stage-view.c b/clutter/clutter/clutter-stage-view.c
index 8a82de7..45d0093 100644
--- a/clutter/clutter/clutter-stage-view.c
+++ b/clutter/clutter/clutter-stage-view.c
@@ -1190,8 +1190,9 @@ 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);
 
diff --git a/cogl/cogl/cogl-onscreen-private.h b/cogl/cogl/cogl-onscreen-private.h
index dffe018..e0215f7 100644
--- a/cogl/cogl/cogl-onscreen-private.h
+++ b/cogl/cogl/cogl-onscreen-private.h
@@ -97,4 +97,7 @@ 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);
+
 #endif /* __COGL_ONSCREEN_PRIVATE_H */
diff --git a/cogl/cogl/cogl-onscreen.c b/cogl/cogl/cogl-onscreen.c
index ff9e174..b25b4af 100644
--- a/cogl/cogl/cogl-onscreen.c
+++ b/cogl/cogl/cogl-onscreen.c
@@ -510,6 +510,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 c45aaf8..683f4ff 100644
--- a/src/backends/meta-stage-impl.c
+++ b/src/backends/meta-stage-impl.c
@@ -720,6 +720,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-crtc-kms.c b/src/backends/native/meta-crtc-kms.c
index 584a780..773b540 100644
--- a/src/backends/native/meta-crtc-kms.c
+++ b/src/backends/native/meta-crtc-kms.c
@@ -211,6 +211,7 @@ meta_crtc_kms_maybe_set_gamma (MetaCrtcKms   *crtc_kms,
   MetaMonitorManagerNative *monitor_manager_native =
     META_MONITOR_MANAGER_NATIVE (monitor_manager);
   MetaKms *kms = meta_kms_device_get_kms (kms_device);
+  MetaKmsCrtc *kms_crtc = meta_crtc_kms_get_kms_crtc (crtc_kms);
   MetaKmsUpdate *kms_update;
   MetaKmsCrtcGamma *gamma;
 
@@ -222,9 +223,9 @@ meta_crtc_kms_maybe_set_gamma (MetaCrtcKms   *crtc_kms,
   if (!gamma)
     return;
 
-  kms_update = meta_kms_ensure_pending_update (kms, kms_device);
+  kms_update = meta_kms_ensure_pending_update_for_crtc (kms, kms_crtc);
   meta_kms_update_set_crtc_gamma (kms_update,
-                                  meta_crtc_kms_get_kms_crtc (crtc_kms),
+                                  kms_crtc,
                                   gamma->size,
                                   gamma->red,
                                   gamma->green,
diff --git a/src/backends/native/meta-cursor-renderer-native.c b/src/backends/native/meta-cursor-renderer-native.c
index df5862c..606fcad 100644
--- a/src/backends/native/meta-cursor-renderer-native.c
+++ b/src/backends/native/meta-cursor-renderer-native.c
@@ -64,19 +64,6 @@
 #define DRM_CAP_CURSOR_HEIGHT 0x9
 #endif
 
-/* When animating a cursor, we usually call drmModeSetCursor2 once per frame.
- * Though, testing shows that we need to triple buffer the cursor buffer in
- * order to avoid glitches when animating the cursor, at least when running on
- * Intel. The reason for this might be (but is not confirmed to be) due to
- * the user space gbm_bo cache, making us reuse and overwrite the kernel side
- * buffer content before it was scanned out. To avoid this, we keep a user space
- * reference to each buffer we set until at least one frame after it was drawn.
- * In effect, this means we three active cursor gbm_bo's: one that that just has
- * been set, one that was previously set and may or may not have been scanned
- * out, and one pending that will be replaced if the cursor sprite changes.
- */
-#define HW_CURSOR_BUFFER_COUNT 3
-
 static GQuark quark_cursor_sprite = 0;
 
 typedef struct _CrtcCursorData
@@ -110,19 +97,10 @@ typedef struct _MetaCursorRendererNativeGpuData
   uint64_t cursor_height;
 } MetaCursorRendererNativeGpuData;
 
-typedef enum _MetaCursorBufferState
-{
-  META_CURSOR_BUFFER_STATE_NONE,
-  META_CURSOR_BUFFER_STATE_SET,
-  META_CURSOR_BUFFER_STATE_INVALIDATED,
-} MetaCursorBufferState;
-
 typedef struct _MetaCursorNativeGpuState
 {
   MetaGpu *gpu;
-  unsigned int active_buffer_idx;
-  MetaCursorBufferState pending_buffer_state;
-  MetaDrmBuffer *buffers[HW_CURSOR_BUFFER_COUNT];
+  MetaDrmBuffer *buffer;
 } MetaCursorNativeGpuState;
 
 typedef struct _MetaCursorNativePrivate
@@ -199,44 +177,17 @@ meta_cursor_renderer_native_finalize (GObject *object)
   G_OBJECT_CLASS (meta_cursor_renderer_native_parent_class)->finalize (object);
 }
 
-static unsigned int
-get_pending_cursor_sprite_buffer_index (MetaCursorNativeGpuState *cursor_gpu_state)
-{
-  return (cursor_gpu_state->active_buffer_idx + 1) % HW_CURSOR_BUFFER_COUNT;
-}
-
-static MetaDrmBuffer *
-get_pending_cursor_sprite_buffer (MetaCursorNativeGpuState *cursor_gpu_state)
-{
-  unsigned int pending_buffer_idx;
-
-  pending_buffer_idx =
-    get_pending_cursor_sprite_buffer_index (cursor_gpu_state);
-  return cursor_gpu_state->buffers[pending_buffer_idx];
-}
-
-static MetaDrmBuffer *
-get_active_cursor_sprite_buffer (MetaCursorNativeGpuState *cursor_gpu_state)
-{
-  return cursor_gpu_state->buffers[cursor_gpu_state->active_buffer_idx];
-}
-
 static void
-set_pending_cursor_sprite_buffer (MetaCursorSprite *cursor_sprite,
-                                  MetaGpuKms       *gpu_kms,
-                                  MetaDrmBuffer    *buffer)
+set_cursor_sprite_buffer (MetaCursorSprite *cursor_sprite,
+                          MetaGpuKms       *gpu_kms,
+                          MetaDrmBuffer    *buffer)
 {
   MetaCursorNativePrivate *cursor_priv;
   MetaCursorNativeGpuState *cursor_gpu_state;
-  unsigned int pending_buffer_idx;
 
   cursor_priv = ensure_cursor_priv (cursor_sprite);
   cursor_gpu_state = ensure_cursor_gpu_state (cursor_priv, gpu_kms);
-
-  pending_buffer_idx =
-    get_pending_cursor_sprite_buffer_index (cursor_gpu_state);
-  cursor_gpu_state->buffers[pending_buffer_idx] = buffer;
-  cursor_gpu_state->pending_buffer_state = META_CURSOR_BUFFER_STATE_SET;
+  cursor_gpu_state->buffer = buffer;
 }
 
 static void
@@ -311,10 +262,7 @@ assign_cursor_plane (MetaCursorRendererNative *native,
   MetaKmsUpdate *kms_update;
   MetaKmsPlaneAssignment *plane_assignment;
 
-  if (cursor_gpu_state->pending_buffer_state == META_CURSOR_BUFFER_STATE_SET)
-    buffer = get_pending_cursor_sprite_buffer (cursor_gpu_state);
-  else
-    buffer = get_active_cursor_sprite_buffer (cursor_gpu_state);
+  buffer = cursor_gpu_state->buffer;
 
   kms_crtc = meta_crtc_kms_get_kms_crtc (crtc_kms);
   kms_device = meta_kms_crtc_get_device (kms_crtc);
@@ -343,8 +291,8 @@ assign_cursor_plane (MetaCursorRendererNative *native,
     flags |= META_KMS_ASSIGN_PLANE_FLAG_FB_UNCHANGED;
 
   kms_update =
-    meta_kms_ensure_pending_update (meta_kms_device_get_kms (kms_device),
-                                    meta_kms_crtc_get_device (kms_crtc));
+    meta_kms_ensure_pending_update_for_crtc (meta_kms_device_get_kms (kms_device),
+                                             kms_crtc);
   plane_assignment = meta_kms_update_assign_plane (kms_update,
                                                    kms_crtc,
                                                    cursor_plane,
@@ -365,13 +313,6 @@ assign_cursor_plane (MetaCursorRendererNative *native,
                                        native);
 
   crtc_cursor_data->buffer = buffer;
-
-  if (cursor_gpu_state->pending_buffer_state == META_CURSOR_BUFFER_STATE_SET)
-    {
-      cursor_gpu_state->active_buffer_idx =
-        (cursor_gpu_state->active_buffer_idx + 1) % HW_CURSOR_BUFFER_COUNT;
-      cursor_gpu_state->pending_buffer_state = META_CURSOR_BUFFER_STATE_NONE;
-    }
 }
 
 static float
@@ -499,7 +440,7 @@ unset_crtc_cursor (MetaCursorRendererNative *native,
       MetaKms *kms = meta_kms_device_get_kms (kms_device);
       MetaKmsUpdate *kms_update;
 
-      kms_update = meta_kms_ensure_pending_update (kms, kms_device);
+      kms_update = meta_kms_ensure_pending_update_for_crtc (kms, kms_crtc);
       meta_kms_update_unassign_plane (kms_update, kms_crtc, cursor_plane);
     }
 
@@ -599,19 +540,7 @@ has_valid_cursor_sprite_buffer (MetaCursorSprite *cursor_sprite,
   if (!cursor_gpu_state)
     return FALSE;
 
-  switch (cursor_gpu_state->pending_buffer_state)
-    {
-    case META_CURSOR_BUFFER_STATE_NONE:
-      return get_active_cursor_sprite_buffer (cursor_gpu_state) != NULL;
-    case META_CURSOR_BUFFER_STATE_SET:
-      return TRUE;
-    case META_CURSOR_BUFFER_STATE_INVALIDATED:
-      return FALSE;
-    }
-
-  g_assert_not_reached ();
-
-  return FALSE;
+  return cursor_gpu_state->buffer != NULL;
 }
 
 static void
@@ -1115,16 +1044,14 @@ unset_crtc_cursor_renderer_privates (MetaGpu       *gpu,
 static void
 cursor_gpu_state_free (MetaCursorNativeGpuState *cursor_gpu_state)
 {
-  int i;
   MetaDrmBuffer *active_buffer;
 
-  active_buffer = get_active_cursor_sprite_buffer (cursor_gpu_state);
+  active_buffer = cursor_gpu_state->buffer;
   if (active_buffer)
     unset_crtc_cursor_renderer_privates (cursor_gpu_state->gpu,
                                          active_buffer);
 
-  for (i = 0; i < HW_CURSOR_BUFFER_COUNT; i++)
-    g_clear_object (&cursor_gpu_state->buffers[i]);
+  g_clear_object (&cursor_gpu_state->buffer);
   g_free (cursor_gpu_state);
 }
 
@@ -1161,14 +1088,7 @@ invalidate_cursor_gpu_state (MetaCursorSprite *cursor_sprite)
 
   g_hash_table_iter_init (&iter, cursor_priv->gpu_states);
   while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &cursor_gpu_state))
-    {
-      unsigned int pending_buffer_idx;
-
-      pending_buffer_idx = get_pending_cursor_sprite_buffer_index (cursor_gpu_state);
-      g_clear_object (&cursor_gpu_state->buffers[pending_buffer_idx]);
-      cursor_gpu_state->pending_buffer_state =
-        META_CURSOR_BUFFER_STATE_INVALIDATED;
-    }
+    g_clear_object (&cursor_gpu_state->buffer);
 }
 
 static void
@@ -1306,8 +1226,8 @@ load_cursor_sprite_gbm_buffer_for_gpu (MetaCursorRendererNative *native,
           return;
         }
 
-      set_pending_cursor_sprite_buffer (cursor_sprite, gpu_kms,
-                                        META_DRM_BUFFER (buffer_gbm));
+      set_cursor_sprite_buffer (cursor_sprite, gpu_kms,
+                                META_DRM_BUFFER (buffer_gbm));
     }
   else
     {
@@ -1315,34 +1235,6 @@ load_cursor_sprite_gbm_buffer_for_gpu (MetaCursorRendererNative *native,
     }
 }
 
-static gboolean
-is_cursor_hw_state_valid (MetaCursorSprite *cursor_sprite,
-                          MetaGpuKms       *gpu_kms)
-{
-  MetaCursorNativePrivate *cursor_priv;
-  MetaCursorNativeGpuState *cursor_gpu_state;
-
-  cursor_priv = get_cursor_priv (cursor_sprite);
-  if (!cursor_priv)
-    return FALSE;
-
-  cursor_gpu_state = get_cursor_gpu_state (cursor_priv, gpu_kms);
-  if (!cursor_gpu_state)
-    return FALSE;
-
-  switch (cursor_gpu_state->pending_buffer_state)
-    {
-    case META_CURSOR_BUFFER_STATE_SET:
-    case META_CURSOR_BUFFER_STATE_NONE:
-      return TRUE;
-    case META_CURSOR_BUFFER_STATE_INVALIDATED:
-      return FALSE;
-    }
-
-  g_assert_not_reached ();
-  return FALSE;
-}
-
 static gboolean
 is_cursor_scale_and_transform_valid (MetaCursorRenderer *renderer,
                                      MetaCursorSprite   *cursor_sprite)
@@ -1507,7 +1399,7 @@ realize_cursor_sprite_from_wl_buffer_for_gpu (MetaCursorRenderer      *renderer,
   if (!cursor_renderer_gpu_data || cursor_renderer_gpu_data->hw_cursor_broken)
     return;
 
-  if (is_cursor_hw_state_valid (cursor_sprite, gpu_kms) &&
+  if (has_valid_cursor_sprite_buffer (cursor_sprite, gpu_kms) &&
       is_cursor_scale_and_transform_valid (renderer, cursor_sprite))
     return;
 
@@ -1649,8 +1541,8 @@ realize_cursor_sprite_from_wl_buffer_for_gpu (MetaCursorRenderer      *renderer,
           return;
         }
 
-      set_pending_cursor_sprite_buffer (cursor_sprite, gpu_kms,
-                                        META_DRM_BUFFER (buffer_gbm));
+      set_cursor_sprite_buffer (cursor_sprite, gpu_kms,
+                                META_DRM_BUFFER (buffer_gbm));
     }
 }
 #endif
@@ -1674,7 +1566,7 @@ realize_cursor_sprite_from_xcursor_for_gpu (MetaCursorRenderer      *renderer,
   if (!cursor_renderer_gpu_data || cursor_renderer_gpu_data->hw_cursor_broken)
     return;
 
-  if (is_cursor_hw_state_valid (cursor_sprite, gpu_kms) &&
+  if (has_valid_cursor_sprite_buffer (cursor_sprite, gpu_kms) &&
       is_cursor_scale_and_transform_valid (renderer, cursor_sprite))
     return;
 
diff --git a/src/backends/native/meta-kms-crtc.c b/src/backends/native/meta-kms-crtc.c
index ab83b5a..88651c5 100644
--- a/src/backends/native/meta-kms-crtc.c
+++ b/src/backends/native/meta-kms-crtc.c
@@ -32,6 +32,12 @@ typedef struct _MetaKmsCrtcPropTable
   MetaKmsProp props[META_KMS_CRTC_N_PROPS];
 } MetaKmsCrtcPropTable;
 
+typedef struct
+{
+  MetaDrmBuffer *front, *back;
+  gboolean back_is_set;
+} PlaneState;
+
 struct _MetaKmsCrtc
 {
   GObject parent;
@@ -44,6 +50,8 @@ struct _MetaKmsCrtc
   MetaKmsCrtcState current_state;
 
   MetaKmsCrtcPropTable prop_table;
+
+  GHashTable *plane_states;
 };
 
 G_DEFINE_TYPE (MetaKmsCrtc, meta_kms_crtc, G_TYPE_OBJECT)
@@ -448,20 +456,91 @@ meta_kms_crtc_new (MetaKmsImplDevice  *impl_device,
   return crtc;
 }
 
+void
+meta_kms_crtc_remember_plane_buffer (MetaKmsCrtc   *crtc,
+                                     uint32_t       plane_id,
+                                     MetaDrmBuffer *buffer)
+{
+  gpointer key = GUINT_TO_POINTER (plane_id);
+  PlaneState *plane_state;
+
+  plane_state = g_hash_table_lookup (crtc->plane_states, key);
+  if (plane_state == NULL)
+    {
+      plane_state = g_new0 (PlaneState, 1);
+      g_hash_table_insert (crtc->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_kms_crtc_on_scanout_started (MetaKmsCrtc *crtc)
+{
+  g_hash_table_foreach (crtc->plane_states, swap_plane_buffers, NULL);
+}
+
+void
+meta_kms_crtc_release_buffers (MetaKmsCrtc *crtc)
+{
+  g_hash_table_remove_all (crtc->plane_states);
+}
+
+static void
+meta_kms_crtc_dispose (GObject *object)
+{
+  MetaKmsCrtc *crtc = META_KMS_CRTC (object);
+
+  meta_kms_crtc_release_buffers (crtc);
+
+  G_OBJECT_CLASS (meta_kms_crtc_parent_class)->dispose (object);
+}
+
 static void
 meta_kms_crtc_finalize (GObject *object)
 {
   MetaKmsCrtc *crtc = META_KMS_CRTC (object);
 
   clear_gamma_state (&crtc->current_state);
+  g_hash_table_unref (crtc->plane_states);
 
   G_OBJECT_CLASS (meta_kms_crtc_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_kms_crtc_init (MetaKmsCrtc *crtc)
 {
   crtc->current_state.gamma.size = 0;
+  crtc->plane_states = g_hash_table_new_full (NULL,
+                                              NULL,
+                                              NULL,
+                                              destroy_plane_state);
 }
 
 static void
@@ -469,5 +548,6 @@ 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 218bec9..29fdfcb 100644
--- a/src/backends/native/meta-kms-crtc.h
+++ b/src/backends/native/meta-kms-crtc.h
@@ -25,6 +25,7 @@
 #include <xf86drmMode.h>
 
 #include "backends/native/meta-kms-types.h"
+#include "backends/native/meta-drm-buffer.h"
 #include "core/util-private.h"
 #include "meta/boxes.h"
 
@@ -82,4 +83,12 @@ MetaKmsCrtcGamma * meta_kms_crtc_gamma_new (MetaKmsCrtc    *crtc,
                                             const uint16_t *green,
                                             const uint16_t *blue);
 
+void meta_kms_crtc_remember_plane_buffer (MetaKmsCrtc   *crtc,
+                                          uint32_t       plane_id,
+                                          MetaDrmBuffer *buffer);
+
+void meta_kms_crtc_on_scanout_started (MetaKmsCrtc *crtc);
+
+void meta_kms_crtc_release_buffers (MetaKmsCrtc *crtc);
+
 #endif /* META_KMS_CRTC_H */
diff --git a/src/backends/native/meta-kms-impl-device-atomic.c b/src/backends/native/meta-kms-impl-device-atomic.c
index 73dd8e6..787d05a 100644
--- a/src/backends/native/meta-kms-impl-device-atomic.c
+++ b/src/backends/native/meta-kms-impl-device-atomic.c
@@ -431,6 +431,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;
@@ -586,6 +587,12 @@ process_plane_assignment (MetaKmsImplDevice  *impl_device,
                                error))
         return FALSE;
     }
+
+  if (!(flags & META_KMS_UPDATE_FLAG_TEST_ONLY))
+    meta_kms_crtc_remember_plane_buffer (plane_assignment->crtc,
+                                         meta_kms_plane_get_id (plane),
+                                         buffer);
+
   return TRUE;
 }
 
@@ -963,7 +970,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 882cd97..8aa7834 100644
--- a/src/backends/native/meta-kms-impl-device-simple.c
+++ b/src/backends/native/meta-kms-impl-device-simple.c
@@ -470,6 +470,8 @@ process_mode_set (MetaKmsImplDevice  *impl_device,
       return FALSE;
     }
 
+  meta_kms_crtc_on_scanout_started (crtc);
+
   if (drm_mode)
     {
       g_hash_table_replace (impl_device_simple->cached_mode_sets,
@@ -534,7 +536,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;
@@ -547,6 +549,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);
 }
 
@@ -614,16 +617,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);
         }
@@ -710,7 +718,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)
@@ -725,7 +733,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,
@@ -859,6 +867,8 @@ mode_set_fallback (MetaKmsImplDeviceSimple  *impl_device_simple,
       return FALSE;
     }
 
+  meta_kms_crtc_on_scanout_started (crtc);
+
   if (!impl_device_simple->mode_set_fallback_feedback_source)
     {
       GSource *source;
@@ -983,20 +993,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));
@@ -1290,7 +1300,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,
@@ -1304,7 +1314,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,
@@ -1317,6 +1327,12 @@ process_plane_assignment (MetaKmsImplDevice       *impl_device,
     }
 
   g_assert_not_reached ();
+
+assigned:
+  meta_kms_crtc_remember_plane_buffer (plane_assignment->crtc,
+                                       meta_kms_plane_get_id (plane),
+                                       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 ec1a0e5..afea2c4 100644
--- a/src/backends/native/meta-kms-impl-device.c
+++ b/src/backends/native/meta-kms-impl-device.c
@@ -1028,8 +1028,12 @@ meta_kms_impl_device_init_mode_setting (MetaKmsImplDevice  *impl_device,
 void
 meta_kms_impl_device_prepare_shutdown (MetaKmsImplDevice *impl_device)
 {
+  MetaKmsImplDevicePrivate *priv =
+    meta_kms_impl_device_get_instance_private (impl_device);
   MetaKmsImplDeviceClass *klass = META_KMS_IMPL_DEVICE_GET_CLASS (impl_device);
 
+  g_list_foreach (priv->crtcs, (GFunc) meta_kms_crtc_release_buffers, NULL);
+
   if (klass->prepare_shutdown)
     klass->prepare_shutdown (impl_device);
 
diff --git a/src/backends/native/meta-kms-page-flip.c b/src/backends/native/meta-kms-page-flip.c
index 817f4e7..148d237 100644
--- a/src/backends/native/meta-kms-page-flip.c
+++ b/src/backends/native/meta-kms-page-flip.c
@@ -24,6 +24,7 @@
 #include "backends/native/meta-kms-impl.h"
 #include "backends/native/meta-kms-private.h"
 #include "backends/native/meta-kms-update.h"
+#include "backends/native/meta-kms-crtc.h"
 
 typedef struct _MetaKmsPageFlipClosure
 {
@@ -149,6 +150,8 @@ meta_kms_page_flip_data_flipped (MetaKms  *kms,
 
   meta_assert_not_in_kms_impl (kms);
 
+  meta_kms_crtc_on_scanout_started (page_flip_data->crtc);
+
   for (l = page_flip_data->closures; l; l = l->next)
     {
       MetaKmsPageFlipClosure *closure = l->data;
diff --git a/src/backends/native/meta-kms-update-private.h b/src/backends/native/meta-kms-update-private.h
index a613cbc..1d964ff 100644
--- a/src/backends/native/meta-kms-update-private.h
+++ b/src/backends/native/meta-kms-update-private.h
@@ -132,6 +132,12 @@ uint64_t meta_kms_update_get_sequence_number (MetaKmsUpdate *update);
 META_EXPORT_TEST
 MetaKmsDevice * meta_kms_update_get_device (MetaKmsUpdate *update);
 
+gboolean meta_kms_update_includes_crtc (MetaKmsUpdate *update,
+                                        MetaKmsCrtc   *crtc);
+
+void meta_kms_update_include_crtc (MetaKmsUpdate *update,
+                                   MetaKmsCrtc   *crtc);
+
 void meta_kms_plane_assignment_set_rotation (MetaKmsPlaneAssignment *plane_assignment,
                                              uint64_t                rotation);
 
diff --git a/src/backends/native/meta-kms-update.c b/src/backends/native/meta-kms-update.c
index 53fc92e..f977571 100644
--- a/src/backends/native/meta-kms-update.c
+++ b/src/backends/native/meta-kms-update.c
@@ -25,12 +25,14 @@
 #include "backends/meta-display-config-shared.h"
 #include "backends/native/meta-kms-connector.h"
 #include "backends/native/meta-kms-crtc.h"
+#include "backends/native/meta-kms-device.h"
 #include "backends/native/meta-kms-mode-private.h"
 #include "backends/native/meta-kms-plane.h"
 
 struct _MetaKmsUpdate
 {
   MetaKmsDevice *device;
+  GHashTable *crtcs;
 
   gboolean is_locked;
   uint64_t sequence_number;
@@ -149,6 +151,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);
 }
 
@@ -228,7 +231,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,
@@ -237,6 +240,8 @@ meta_kms_update_assign_plane (MetaKmsUpdate          *update,
   update->plane_assignments = g_list_prepend (update->plane_assignments,
                                               plane_assignment);
 
+  g_hash_table_insert (update->crtcs, crtc, NULL);
+
   return plane_assignment;
 }
 
@@ -262,6 +267,8 @@ meta_kms_update_unassign_plane (MetaKmsUpdate *update,
   update->plane_assignments = g_list_prepend (update->plane_assignments,
                                               plane_assignment);
 
+  g_hash_table_insert (update->crtcs, crtc, NULL);
+
   return plane_assignment;
 }
 
@@ -284,6 +291,8 @@ meta_kms_update_mode_set (MetaKmsUpdate *update,
   };
 
   update->mode_sets = g_list_prepend (update->mode_sets, mode_set);
+
+  g_hash_table_insert (update->crtcs, crtc, NULL);
 }
 
 static MetaKmsConnectorUpdate *
@@ -292,6 +301,8 @@ ensure_connector_update (MetaKmsUpdate    *update,
 {
   GList *l;
   MetaKmsConnectorUpdate *connector_update;
+  MetaKmsDevice *device;
+  const MetaKmsConnectorState *state;
 
   for (l = update->connector_updates; l; l = l->next)
     {
@@ -307,6 +318,21 @@ ensure_connector_update (MetaKmsUpdate    *update,
   update->connector_updates = g_list_prepend (update->connector_updates,
                                               connector_update);
 
+  device = meta_kms_connector_get_device (connector);
+  state = meta_kms_connector_get_current_state (connector);
+  if (state && device && state->current_crtc_id)
+    {
+      GList *l;
+
+      for (l = meta_kms_device_get_crtcs (device); l; l = l->next)
+        {
+          MetaKmsCrtc *kms_crtc = l->data;
+
+          if (meta_kms_crtc_get_id (kms_crtc) == state->current_crtc_id)
+            g_hash_table_insert (update->crtcs, kms_crtc, NULL);
+        }
+    }
+
   return connector_update;
 }
 
@@ -402,6 +428,8 @@ meta_kms_update_set_crtc_gamma (MetaKmsUpdate  *update,
   gamma = meta_kms_crtc_gamma_new (crtc, size, red, green, blue);
 
   update->crtc_gammas = g_list_prepend (update->crtc_gammas, gamma);
+
+  g_hash_table_insert (update->crtcs, crtc, NULL);
 }
 
 void
@@ -665,6 +693,20 @@ meta_kms_update_get_device (MetaKmsUpdate *update)
   return update->device;
 }
 
+gboolean
+meta_kms_update_includes_crtc (MetaKmsUpdate *update,
+                               MetaKmsCrtc   *crtc)
+{
+  return g_hash_table_lookup_extended (update->crtcs, crtc, NULL, NULL);
+}
+
+void
+meta_kms_update_include_crtc (MetaKmsUpdate *update,
+                              MetaKmsCrtc   *crtc)
+{
+  g_hash_table_insert (update->crtcs, crtc, NULL);
+}
+
 MetaKmsCustomPageFlip *
 meta_kms_update_take_custom_page_flip_func (MetaKmsUpdate *update)
 {
@@ -693,12 +735,15 @@ meta_kms_update_new (MetaKmsDevice *device)
   update->device = device;
   update->sequence_number = sequence_number++;
 
+  update->crtcs = g_hash_table_new (NULL, NULL);
+
   return update;
 }
 
 void
 meta_kms_update_free (MetaKmsUpdate *update)
 {
+  g_hash_table_destroy (update->crtcs);
   g_list_free_full (update->result_listeners,
                     (GDestroyNotify) meta_kms_result_listener_free);
   g_list_free_full (update->plane_assignments,
diff --git a/src/backends/native/meta-kms.c b/src/backends/native/meta-kms.c
index 052ec8a..9acc17b 100644
--- a/src/backends/native/meta-kms.c
+++ b/src/backends/native/meta-kms.c
@@ -23,6 +23,7 @@
 #include "backends/native/meta-kms-private.h"
 
 #include "backends/native/meta-backend-native.h"
+#include "backends/native/meta-kms-crtc.h"
 #include "backends/native/meta-kms-device-private.h"
 #include "backends/native/meta-kms-impl.h"
 #include "backends/native/meta-kms-update-private.h"
@@ -181,6 +182,11 @@ struct _MetaKms
 
 G_DEFINE_TYPE (MetaKms, meta_kms, G_TYPE_OBJECT)
 
+static MetaKmsFeedback *
+meta_kms_post_update_sync (MetaKms           *kms,
+                           MetaKmsUpdate     *update,
+                           MetaKmsUpdateFlag  flags);
+
 void
 meta_kms_discard_pending_updates (MetaKms *kms)
 {
@@ -247,12 +253,105 @@ meta_kms_take_pending_update (MetaKms       *kms,
   return NULL;
 }
 
+MetaKmsUpdate *
+meta_kms_ensure_pending_update_for_crtc (MetaKms     *kms,
+                                         MetaKmsCrtc *crtc)
+{
+  MetaKmsUpdate *update;
+
+  update = meta_kms_get_pending_update_for_crtc (kms, crtc);
+  if (update == NULL)
+    {
+      update = meta_kms_update_new (meta_kms_crtc_get_device (crtc));
+      meta_kms_update_include_crtc (update, crtc);
+      meta_kms_add_pending_update (kms, update);
+    }
+
+  return update;
+}
+
+static MetaKmsUpdate *
+meta_kms_find_compatible_update_for_crtc (MetaKms     *kms,
+                                          MetaKmsCrtc *crtc,
+                                          gboolean     take)
+{
+  MetaKmsDevice *device;
+  MetaKmsUpdate *update;
+  GList *l;
+
+  for (l = kms->pending_updates; l; l = l->next)
+    {
+      update = l->data;
+      if (meta_kms_update_includes_crtc (update, crtc))
+        goto found;
+    }
+
+  device = meta_kms_crtc_get_device (crtc);
+
+  for (l = kms->pending_updates; l; l = l->next)
+    {
+      update = l->data;
+      if (meta_kms_update_get_device (update) == device &&
+          meta_kms_update_get_mode_sets (update))
+        goto found;
+    }
+
+  return NULL;
+
+found:
+  if (take)
+    kms->pending_updates = g_list_delete_link (kms->pending_updates, l);
+  return update;
+}
+
+MetaKmsUpdate *
+meta_kms_get_pending_update_for_crtc (MetaKms     *kms,
+                                      MetaKmsCrtc *crtc)
+{
+  return meta_kms_find_compatible_update_for_crtc (kms, crtc, FALSE);
+}
+
+static MetaKmsUpdate *
+meta_kms_take_pending_update_for_crtc (MetaKms     *kms,
+                                       MetaKmsCrtc *crtc)
+{
+  return meta_kms_find_compatible_update_for_crtc (kms, crtc, TRUE);
+}
+
 MetaKmsFeedback *
 meta_kms_post_pending_update_sync (MetaKms           *kms,
                                    MetaKmsDevice     *device,
                                    MetaKmsUpdateFlag  flags)
 {
   MetaKmsUpdate *update;
+
+  update = meta_kms_take_pending_update (kms, device);
+  if (!update)
+    return NULL;
+
+  return meta_kms_post_update_sync (kms, update, flags);
+}
+
+MetaKmsFeedback *
+meta_kms_post_pending_update_for_crtc_sync (MetaKms           *kms,
+                                            MetaKmsCrtc       *crtc,
+                                            MetaKmsUpdateFlag  flags)
+{
+  MetaKmsUpdate *update;
+
+  update = meta_kms_take_pending_update_for_crtc (kms, crtc);
+  if (!update)
+    return NULL;
+
+  return meta_kms_post_update_sync (kms, update, flags);
+}
+
+static MetaKmsFeedback *
+meta_kms_post_update_sync (MetaKms           *kms,
+                           MetaKmsUpdate     *update,
+                           MetaKmsUpdateFlag  flags)
+{
+  MetaKmsDevice *device = meta_kms_update_get_device (update);
   MetaKmsFeedback *feedback;
   GList *result_listeners;
   GList *l;
@@ -260,10 +359,6 @@ meta_kms_post_pending_update_sync (MetaKms           *kms,
   COGL_TRACE_BEGIN_SCOPED (MetaKmsPostUpdateSync,
                            "KMS (post update)");
 
-  update = meta_kms_take_pending_update (kms, device);
-  if (!update)
-    return NULL;
-
   meta_kms_update_lock (update);
 
   feedback = meta_kms_device_process_update_sync (device, update, flags);
diff --git a/src/backends/native/meta-kms.h b/src/backends/native/meta-kms.h
index bd9fe5c..84f1bed 100644
--- a/src/backends/native/meta-kms.h
+++ b/src/backends/native/meta-kms.h
@@ -39,9 +39,15 @@ void meta_kms_discard_pending_updates (MetaKms *kms);
 MetaKmsUpdate * meta_kms_ensure_pending_update (MetaKms       *kms,
                                                 MetaKmsDevice *device);
 
+MetaKmsUpdate * meta_kms_ensure_pending_update_for_crtc (MetaKms     *kms,
+                                                         MetaKmsCrtc *crtc);
+
 MetaKmsUpdate * meta_kms_get_pending_update (MetaKms       *kms,
                                              MetaKmsDevice *device);
 
+MetaKmsUpdate * meta_kms_get_pending_update_for_crtc (MetaKms     *kms,
+                                                      MetaKmsCrtc *crtc);
+
 MetaKmsFeedback * meta_kms_post_pending_update_sync (MetaKms           *kms,
                                                      MetaKmsDevice     *device,
                                                      MetaKmsUpdateFlag  flags);
@@ -49,6 +55,10 @@ MetaKmsFeedback * meta_kms_post_pending_update_sync (MetaKms           *kms,
 MetaKmsFeedback * meta_kms_post_test_update_sync (MetaKms       *kms,
                                                   MetaKmsUpdate *update);
 
+MetaKmsFeedback * meta_kms_post_pending_update_for_crtc_sync (MetaKms           *kms,
+                                                              MetaKmsCrtc       *device,
+                                                              MetaKmsUpdateFlag  flags);
+
 void meta_kms_discard_pending_page_flips (MetaKms *kms);
 
 void meta_kms_notify_modes_set (MetaKms *kms);
diff --git a/src/backends/native/meta-onscreen-native.c b/src/backends/native/meta-onscreen-native.c
index 43b9549..196a8a9 100644
--- a/src/backends/native/meta-onscreen-native.c
+++ b/src/backends/native/meta-onscreen-native.c
@@ -67,13 +67,12 @@ typedef struct _MetaOnscreenNativeSecondaryGpuState
 
   struct {
     struct gbm_surface *surface;
-    MetaDrmBuffer *current_fb;
-    MetaDrmBuffer *next_fb;
   } gbm;
 
   struct {
     MetaDrmBufferDumb *current_dumb_fb;
     MetaDrmBufferDumb *dumb_fbs[2];
+    MetaDrmBuffer *source_fbs[2];
   } cpu;
 
   gboolean noted_primary_gpu_copy_ok;
@@ -94,8 +93,8 @@ struct _MetaOnscreenNative
 
   struct {
     struct gbm_surface *surface;
-    MetaDrmBuffer *current_fb;
     MetaDrmBuffer *next_fb;
+    MetaDrmBuffer *stalled_fb;
   } gbm;
 
 #ifdef HAVE_EGL_DEVICE
@@ -107,69 +106,25 @@ struct _MetaOnscreenNative
 #endif
 
   MetaRendererView *view;
+
+  unsigned int swaps_pending;
+  struct {
+    int *rectangles;  /* 4 x n_rectangles */
+    int n_rectangles;
+  } next_post;
 };
 
 G_DEFINE_TYPE (MetaOnscreenNative, meta_onscreen_native,
                COGL_TYPE_ONSCREEN_EGL)
 
+static void
+try_post_latest_swap (CoglOnscreen *onscreen);
+
 static gboolean
 init_secondary_gpu_state (MetaRendererNative  *renderer_native,
                           CoglOnscreen        *onscreen,
                           GError             **error);
 
-static void
-swap_secondary_drm_fb (CoglOnscreen *onscreen)
-{
-  MetaOnscreenNative *onscreen_native = META_ONSCREEN_NATIVE (onscreen);
-  MetaOnscreenNativeSecondaryGpuState *secondary_gpu_state;
-
-  secondary_gpu_state = onscreen_native->secondary_gpu_state;
-  if (!secondary_gpu_state)
-    return;
-
-  g_set_object (&secondary_gpu_state->gbm.current_fb,
-                secondary_gpu_state->gbm.next_fb);
-  g_clear_object (&secondary_gpu_state->gbm.next_fb);
-}
-
-static void
-free_current_secondary_bo (CoglOnscreen *onscreen)
-{
-  MetaOnscreenNative *onscreen_native = META_ONSCREEN_NATIVE (onscreen);
-  MetaOnscreenNativeSecondaryGpuState *secondary_gpu_state;
-
-  secondary_gpu_state = onscreen_native->secondary_gpu_state;
-  if (!secondary_gpu_state)
-    return;
-
-  g_clear_object (&secondary_gpu_state->gbm.current_fb);
-}
-
-static void
-free_current_bo (CoglOnscreen *onscreen)
-{
-  MetaOnscreenNative *onscreen_native = META_ONSCREEN_NATIVE (onscreen);
-
-  g_clear_object (&onscreen_native->gbm.current_fb);
-  free_current_secondary_bo (onscreen);
-}
-
-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);
-
-  swap_secondary_drm_fb (onscreen);
-}
-
 static void
 maybe_update_frame_info (MetaCrtc         *crtc,
                          CoglFrameInfo    *frame_info,
@@ -205,7 +160,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);
@@ -234,7 +189,7 @@ 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);
+  try_post_latest_swap (onscreen);
 }
 
 static int64_t
@@ -296,6 +251,7 @@ page_flip_feedback_ready (MetaKmsCrtc *kms_crtc,
   frame_info->flags |= COGL_FRAME_INFO_FLAG_SYMBOLIC;
 
   meta_onscreen_native_notify_frame_complete (onscreen);
+  try_post_latest_swap (onscreen);
 }
 
 static void
@@ -345,7 +301,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_swap_drm_fb (onscreen);
+  try_post_latest_swap (onscreen);
 }
 
 static const MetaKmsPageFlipListenerVtable page_flip_listener_vtable = {
@@ -406,18 +362,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)
 {
+  MetaOnscreenNative *onscreen_native = META_ONSCREEN_NATIVE (onscreen);
   CoglFrameInfo *frame_info;
 
-  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,
@@ -436,8 +414,7 @@ meta_onscreen_native_flip_crtc (CoglOnscreen                *onscreen,
   MetaKmsDevice *kms_device;
   MetaKms *kms;
   MetaKmsUpdate *kms_update;
-  MetaOnscreenNativeSecondaryGpuState *secondary_gpu_state = NULL;
-  MetaDrmBuffer *buffer;
+  g_autoptr (MetaDrmBuffer) buffer = NULL;
   MetaKmsPlaneAssignment *plane_assignment;
 
   COGL_TRACE_BEGIN_SCOPED (MetaOnscreenNativeFlipCrtcs,
@@ -446,7 +423,7 @@ meta_onscreen_native_flip_crtc (CoglOnscreen                *onscreen,
   gpu_kms = META_GPU_KMS (meta_crtc_get_gpu (crtc));
   kms_device = meta_gpu_kms_get_kms_device (gpu_kms);
   kms = meta_kms_device_get_kms (kms_device);
-  kms_update = meta_kms_ensure_pending_update (kms, kms_device);
+  kms_update = meta_kms_ensure_pending_update_for_crtc (kms, kms_crtc);
 
   g_assert (meta_gpu_kms_is_crtc_active (gpu_kms, crtc));
 
@@ -455,15 +432,7 @@ meta_onscreen_native_flip_crtc (CoglOnscreen                *onscreen,
   switch (renderer_gpu_data->mode)
     {
     case META_RENDERER_NATIVE_MODE_GBM:
-      if (gpu_kms == render_gpu)
-        {
-          buffer = onscreen_native->gbm.next_fb;
-        }
-      else
-        {
-          secondary_gpu_state = onscreen_native->secondary_gpu_state;
-          buffer = secondary_gpu_state->gbm.next_fb;
-        }
+      buffer = g_steal_pointer (&onscreen_native->gbm.next_fb);
 
       plane_assignment = meta_crtc_kms_assign_primary_plane (crtc_kms,
                                                              buffer,
@@ -509,7 +478,7 @@ meta_onscreen_native_set_crtc_mode (CoglOnscreen              *onscreen,
   COGL_TRACE_BEGIN_SCOPED (MetaOnscreenNativeSetCrtcModes,
                            "Onscreen (set CRTC modes)");
 
-  kms_update = meta_kms_ensure_pending_update (kms, kms_device);
+  kms_update = meta_kms_ensure_pending_update_for_crtc (kms, kms_crtc);
 
   switch (renderer_gpu_data->mode)
     {
@@ -535,13 +504,41 @@ meta_onscreen_native_set_crtc_mode (CoglOnscreen              *onscreen,
                                  kms_update);
 }
 
+static void
+hold_primary_gpu_fb_for_secondary_gpu_scanout (MetaOnscreenNativeSecondaryGpuState *secondary_gpu_state,
+                                               MetaDrmBuffer                       *primary_gpu_fb,
+                                               MetaDrmBuffer                       *secondary_gpu_fb)
+{
+  if (META_IS_DRM_BUFFER_DUMB (secondary_gpu_fb))
+    {
+      MetaDrmBufferDumb *dumb_fb = META_DRM_BUFFER_DUMB (secondary_gpu_fb);
+      int i;
+      const int n = G_N_ELEMENTS (secondary_gpu_state->cpu.dumb_fbs);
+
+      for (i = 0; i < n; i++)
+        {
+          if (dumb_fb == secondary_gpu_state->cpu.dumb_fbs[i])
+            {
+              g_set_object (&secondary_gpu_state->cpu.source_fbs[i],
+                            primary_gpu_fb);
+              break;
+            }
+        }
+
+      g_warn_if_fail (i < n);
+    }
+}
+
 static void
 secondary_gpu_release_dumb (MetaOnscreenNativeSecondaryGpuState *secondary_gpu_state)
 {
   unsigned i;
 
   for (i = 0; i < G_N_ELEMENTS (secondary_gpu_state->cpu.dumb_fbs); i++)
-    g_clear_object (&secondary_gpu_state->cpu.dumb_fbs[i]);
+    {
+      g_clear_object (&secondary_gpu_state->cpu.dumb_fbs[i]);
+      g_clear_object (&secondary_gpu_state->cpu.source_fbs[i]);
+    }
 }
 
 static void
@@ -566,8 +563,6 @@ secondary_gpu_state_free (MetaOnscreenNativeSecondaryGpuState *secondary_gpu_sta
                                 NULL);
     }
 
-  g_clear_object (&secondary_gpu_state->gbm.current_fb);
-  g_clear_object (&secondary_gpu_state->gbm.next_fb);
   g_clear_pointer (&secondary_gpu_state->gbm.surface, gbm_surface_destroy);
 
   secondary_gpu_release_dumb (secondary_gpu_state);
@@ -575,11 +570,11 @@ secondary_gpu_state_free (MetaOnscreenNativeSecondaryGpuState *secondary_gpu_sta
   g_free (secondary_gpu_state);
 }
 
-static gboolean
+static MetaDrmBuffer *
 import_shared_framebuffer (CoglOnscreen                        *onscreen,
-                           MetaOnscreenNativeSecondaryGpuState *secondary_gpu_state)
+                           MetaOnscreenNativeSecondaryGpuState *secondary_gpu_state,
+                           MetaDrmBuffer                       *primary_gpu_fb)
 {
-  MetaOnscreenNative *onscreen_native = META_ONSCREEN_NATIVE (onscreen);
   MetaRenderDevice *render_device;
   g_autoptr (GError) error = NULL;
   MetaDrmBuffer *imported_buffer;
@@ -587,7 +582,7 @@ import_shared_framebuffer (CoglOnscreen                        *onscreen,
   render_device = secondary_gpu_state->renderer_gpu_data->render_device;
   imported_buffer =
     meta_render_device_import_dma_buf (render_device,
-                                       onscreen_native->gbm.next_fb,
+                                       primary_gpu_fb,
                                        &error);
   if (!imported_buffer)
     {
@@ -601,16 +596,9 @@ import_shared_framebuffer (CoglOnscreen                        *onscreen,
                       META_SHARED_FRAMEBUFFER_IMPORT_STATUS_NONE);
       secondary_gpu_state->import_status =
         META_SHARED_FRAMEBUFFER_IMPORT_STATUS_FAILED;
-      return FALSE;
+      return NULL;
     }
 
-  /*
-   * next_fb may already contain a fallback buffer, so clear it only
-   * when we are sure to succeed.
-   */
-  g_clear_object (&secondary_gpu_state->gbm.next_fb);
-  secondary_gpu_state->gbm.next_fb = imported_buffer;
-
   if (secondary_gpu_state->import_status ==
       META_SHARED_FRAMEBUFFER_IMPORT_STATUS_NONE)
     {
@@ -627,16 +615,16 @@ import_shared_framebuffer (CoglOnscreen                        *onscreen,
 
   secondary_gpu_state->import_status =
     META_SHARED_FRAMEBUFFER_IMPORT_STATUS_OK;
-  return TRUE;
+  return imported_buffer;
 }
 
-static void
+static MetaDrmBuffer *
 copy_shared_framebuffer_gpu (CoglOnscreen                        *onscreen,
                              MetaOnscreenNativeSecondaryGpuState *secondary_gpu_state,
                              MetaRendererNativeGpuData           *renderer_gpu_data,
-                             gboolean                            *egl_context_changed)
+                             gboolean                            *egl_context_changed,
+                             MetaDrmBuffer                       *primary_gpu_fb)
 {
-  MetaOnscreenNative *onscreen_native = META_ONSCREEN_NATIVE (onscreen);
   MetaRendererNative *renderer_native = renderer_gpu_data->renderer_native;
   MetaEgl *egl = meta_renderer_native_get_egl (renderer_native);
   MetaGles3 *gles3 = meta_renderer_native_get_gles3 (renderer_native);
@@ -652,9 +640,6 @@ copy_shared_framebuffer_gpu (CoglOnscreen                        *onscreen,
   COGL_TRACE_BEGIN_SCOPED (CopySharedFramebufferSecondaryGpu,
                            "FB Copy (secondary GPU)");
 
-  g_warn_if_fail (secondary_gpu_state->gbm.next_fb == NULL);
-  g_clear_object (&secondary_gpu_state->gbm.next_fb);
-
   render_device = renderer_gpu_data->render_device;
   egl_display = meta_render_device_get_egl_display (render_device);
 
@@ -667,13 +652,13 @@ copy_shared_framebuffer_gpu (CoglOnscreen                        *onscreen,
     {
       g_warning ("Failed to make current: %s", error->message);
       g_error_free (error);
-      return;
+      return NULL;
     }
 
   *egl_context_changed = TRUE;
 
 
-  buffer_gbm = META_DRM_BUFFER_GBM (onscreen_native->gbm.next_fb);
+  buffer_gbm = META_DRM_BUFFER_GBM (primary_gpu_fb);
   bo = meta_drm_buffer_gbm_get_bo (buffer_gbm);
   if (!meta_renderer_native_gles3_blit_shared_bo (egl,
                                                   gles3,
@@ -685,7 +670,7 @@ copy_shared_framebuffer_gpu (CoglOnscreen                        *onscreen,
     {
       g_warning ("Failed to blit shared framebuffer: %s", error->message);
       g_error_free (error);
-      return;
+      return NULL;
     }
 
   if (!meta_egl_swap_buffers (egl,
@@ -695,7 +680,7 @@ copy_shared_framebuffer_gpu (CoglOnscreen                        *onscreen,
     {
       g_warning ("Failed to swap buffers: %s", error->message);
       g_error_free (error);
-      return;
+      return NULL;
     }
 
   use_modifiers = meta_renderer_native_use_modifiers (renderer_native);
@@ -715,10 +700,10 @@ copy_shared_framebuffer_gpu (CoglOnscreen                        *onscreen,
       g_warning ("meta_drm_buffer_gbm_new_lock_front failed: %s",
                  error->message);
       g_error_free (error);
-      return;
+      return NULL;
     }
 
-  secondary_gpu_state->gbm.next_fb = META_DRM_BUFFER (buffer_gbm);
+  return META_DRM_BUFFER (buffer_gbm);
 }
 
 static MetaDrmBufferDumb *
@@ -733,7 +718,7 @@ secondary_gpu_get_next_dumb_buffer (MetaOnscreenNativeSecondaryGpuState *seconda
     return secondary_gpu_state->cpu.dumb_fbs[0];
 }
 
-static gboolean
+static MetaDrmBuffer *
 copy_shared_framebuffer_primary_gpu (CoglOnscreen                        *onscreen,
                                      MetaOnscreenNativeSecondaryGpuState *secondary_gpu_state,
                                      const int                           *rectangles,
@@ -765,7 +750,7 @@ copy_shared_framebuffer_primary_gpu (CoglOnscreen                        *onscre
   primary_gpu_data =
     meta_renderer_native_get_gpu_data (renderer_native, primary_gpu);
   if (!primary_gpu_data->secondary.has_EGL_EXT_image_dma_buf_import_modifiers)
-    return FALSE;
+    return NULL;
 
   buffer_dumb = secondary_gpu_get_next_dumb_buffer (secondary_gpu_state);
   buffer = META_DRM_BUFFER (buffer_dumb);
@@ -788,7 +773,7 @@ copy_shared_framebuffer_primary_gpu (CoglOnscreen                        *onscre
     {
       meta_topic (META_DEBUG_KMS,
                   "Failed to create DMA buffer: %s", error->message);
-      return FALSE;
+      return NULL;
     }
 
   dmabuf_fb =
@@ -806,7 +791,7 @@ copy_shared_framebuffer_primary_gpu (CoglOnscreen                        *onscre
       meta_topic (META_DEBUG_KMS,
                   "Failed to create DMA buffer for blitting: %s",
                   error->message);
-      return FALSE;
+      return NULL;
     }
   /* Limit the number of individual copies to 16 */
 #define MAX_RECTS 16
@@ -819,7 +804,7 @@ copy_shared_framebuffer_primary_gpu (CoglOnscreen                        *onscre
                                   &error))
         {
           g_object_unref (dmabuf_fb);
-          return FALSE;
+          return NULL;
         }
     }
   else
@@ -836,20 +821,19 @@ copy_shared_framebuffer_primary_gpu (CoglOnscreen                        *onscre
                                       &error))
             {
               g_object_unref (dmabuf_fb);
-              return FALSE;
+              return NULL;
             }
         }
     }
 
   g_object_unref (dmabuf_fb);
 
-  g_set_object (&secondary_gpu_state->gbm.next_fb, buffer);
   secondary_gpu_state->cpu.current_dumb_fb = buffer_dumb;
 
-  return TRUE;
+  return g_object_ref (buffer);
 }
 
-static void
+static MetaDrmBuffer *
 copy_shared_framebuffer_cpu (CoglOnscreen                        *onscreen,
                              MetaOnscreenNativeSecondaryGpuState *secondary_gpu_state,
                              MetaRendererNativeGpuData           *renderer_gpu_data)
@@ -901,17 +885,19 @@ copy_shared_framebuffer_cpu (CoglOnscreen                        *onscreen,
 
   cogl_object_unref (dumb_bitmap);
 
-  g_set_object (&secondary_gpu_state->gbm.next_fb, buffer);
   secondary_gpu_state->cpu.current_dumb_fb = buffer_dumb;
+
+  return g_object_ref (buffer);
 }
 
-static void
+static MetaDrmBuffer *
 update_secondary_gpu_state_pre_swap_buffers (CoglOnscreen *onscreen,
                                              const int    *rectangles,
                                              int           n_rectangles)
 {
   MetaOnscreenNative *onscreen_native = META_ONSCREEN_NATIVE (onscreen);
   MetaOnscreenNativeSecondaryGpuState *secondary_gpu_state;
+  MetaDrmBuffer *copy = NULL;
 
   COGL_TRACE_BEGIN_SCOPED (MetaRendererNativeGpuStatePreSwapBuffers,
                            "Onscreen (secondary gpu pre-swap-buffers)");
@@ -937,10 +923,11 @@ update_secondary_gpu_state_pre_swap_buffers (CoglOnscreen *onscreen,
           /* prepare fallback */
           G_GNUC_FALLTHROUGH;
         case META_SHARED_FRAMEBUFFER_COPY_MODE_PRIMARY:
-          if (!copy_shared_framebuffer_primary_gpu (onscreen,
-                                                    secondary_gpu_state,
-                                                    rectangles,
-                                                    n_rectangles))
+          copy = copy_shared_framebuffer_primary_gpu (onscreen,
+                                                      secondary_gpu_state,
+                                                      rectangles,
+                                                      n_rectangles);
+          if (!copy)
             {
               if (!secondary_gpu_state->noted_primary_gpu_copy_failed)
                 {
@@ -950,9 +937,9 @@ update_secondary_gpu_state_pre_swap_buffers (CoglOnscreen *onscreen,
                   secondary_gpu_state->noted_primary_gpu_copy_failed = TRUE;
                 }
 
-              copy_shared_framebuffer_cpu (onscreen,
-                                           secondary_gpu_state,
-                                           renderer_gpu_data);
+              copy = copy_shared_framebuffer_cpu (onscreen,
+                                                  secondary_gpu_state,
+                                                  renderer_gpu_data);
             }
           else if (!secondary_gpu_state->noted_primary_gpu_copy_ok)
             {
@@ -964,11 +951,15 @@ update_secondary_gpu_state_pre_swap_buffers (CoglOnscreen *onscreen,
           break;
         }
     }
+
+  return copy;
 }
 
 static void
-update_secondary_gpu_state_post_swap_buffers (CoglOnscreen *onscreen,
-                                              gboolean     *egl_context_changed)
+update_secondary_gpu_state_post_swap_buffers (CoglOnscreen   *onscreen,
+                                              gboolean       *egl_context_changed,
+                                              MetaDrmBuffer  *primary_gpu_fb,
+                                              MetaDrmBuffer **secondary_gpu_fb)
 {
   MetaOnscreenNative *onscreen_native = META_ONSCREEN_NATIVE (onscreen);
   MetaRendererNative *renderer_native = onscreen_native->renderer_native;
@@ -981,6 +972,7 @@ update_secondary_gpu_state_post_swap_buffers (CoglOnscreen *onscreen,
   if (secondary_gpu_state)
     {
       MetaRendererNativeGpuData *renderer_gpu_data;
+      g_autoptr (MetaDrmBuffer) next_fb = NULL;
 
       renderer_gpu_data =
         meta_renderer_native_get_gpu_data (renderer_native,
@@ -988,23 +980,30 @@ update_secondary_gpu_state_post_swap_buffers (CoglOnscreen *onscreen,
       switch (renderer_gpu_data->secondary.copy_mode)
         {
         case META_SHARED_FRAMEBUFFER_COPY_MODE_ZERO:
-          if (import_shared_framebuffer (onscreen, secondary_gpu_state))
+          next_fb = import_shared_framebuffer (onscreen,
+                                               secondary_gpu_state,
+                                               primary_gpu_fb);
+          if (next_fb)
             break;
-
-          /* The fallback was prepared in pre_swap_buffers */
+          /* The fallback was prepared in pre_swap_buffers and is currently
+           * in secondary_gpu_fb.
+           */
           renderer_gpu_data->secondary.copy_mode =
             META_SHARED_FRAMEBUFFER_COPY_MODE_PRIMARY;
           G_GNUC_FALLTHROUGH;
         case META_SHARED_FRAMEBUFFER_COPY_MODE_PRIMARY:
-          /* Done before eglSwapBuffers. */
+          next_fb = g_object_ref (*secondary_gpu_fb);
           break;
         case META_SHARED_FRAMEBUFFER_COPY_MODE_SECONDARY_GPU:
-          copy_shared_framebuffer_gpu (onscreen,
-                                       secondary_gpu_state,
-                                       renderer_gpu_data,
-                                       egl_context_changed);
+          next_fb = copy_shared_framebuffer_gpu (onscreen,
+                                                 secondary_gpu_state,
+                                                 renderer_gpu_data,
+                                                 egl_context_changed,
+                                                 primary_gpu_fb);
           break;
         }
+
+      g_set_object (secondary_gpu_fb, next_fb);
     }
 }
 
@@ -1038,34 +1037,39 @@ 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);
-  MetaBackendNative *backend_native = META_BACKEND_NATIVE (backend);
-  MetaMonitorManager *monitor_manager =
-    meta_backend_get_monitor_manager (backend);
-  MetaKms *kms = meta_backend_native_get_kms (backend_native);
   MetaOnscreenNative *onscreen_native = META_ONSCREEN_NATIVE (onscreen);
   MetaGpuKms *render_gpu = onscreen_native->render_gpu;
   MetaDeviceFile *render_device_file;
   ClutterFrame *frame = user_data;
   CoglOnscreenClass *parent_class;
   gboolean egl_context_changed = FALSE;
-  MetaPowerSave power_save_mode;
   g_autoptr (GError) error = NULL;
   MetaDrmBufferFlags buffer_flags;
   MetaDrmBufferGbm *buffer_gbm;
-  MetaKmsCrtc *kms_crtc;
-  MetaKmsDevice *kms_device;
-  MetaKmsUpdateFlag flags;
-  g_autoptr (MetaKmsFeedback) kms_feedback = NULL;
-  const GError *feedback_error;
+  g_autoptr (MetaDrmBuffer) primary_gpu_fb = NULL;
+  g_autoptr (MetaDrmBuffer) secondary_gpu_fb = NULL;
+  size_t rectangles_size;
 
   COGL_TRACE_BEGIN_SCOPED (MetaRendererNativeSwapBuffers,
                            "Onscreen (swap-buffers)");
 
-  update_secondary_gpu_state_pre_swap_buffers (onscreen,
-                                               rectangles,
-                                               n_rectangles);
+  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,
+                                                 n_rectangles);
 
   parent_class = COGL_ONSCREEN_CLASS (meta_onscreen_native_parent_class);
   parent_class->swap_buffers_with_damage (onscreen,
@@ -1081,9 +1085,6 @@ 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);
-      g_clear_object (&onscreen_native->gbm.next_fb);
-
       buffer_flags = META_DRM_BUFFER_FLAG_NONE;
       if (!meta_renderer_native_use_modifiers (renderer_native))
         buffer_flags |= META_DRM_BUFFER_FLAG_DISABLE_MODIFIERS;
@@ -1101,7 +1102,12 @@ meta_onscreen_native_swap_buffers_with_damage (CoglOnscreen  *onscreen,
           return;
         }
 
-      onscreen_native->gbm.next_fb = META_DRM_BUFFER (buffer_gbm);
+      primary_gpu_fb = META_DRM_BUFFER (g_steal_pointer (&buffer_gbm));
+
+      g_object_set_data_full (G_OBJECT (primary_gpu_fb),
+                              "gbm_surface owner",
+                              g_object_ref (onscreen),
+                              (GDestroyNotify) g_object_unref);
 
       break;
     case META_RENDERER_NATIVE_MODE_SURFACELESS:
@@ -1113,7 +1119,46 @@ meta_onscreen_native_swap_buffers_with_damage (CoglOnscreen  *onscreen,
 #endif
     }
 
-  update_secondary_gpu_state_post_swap_buffers (onscreen, &egl_context_changed);
+  update_secondary_gpu_state_post_swap_buffers (onscreen,
+                                                &egl_context_changed,
+                                                primary_gpu_fb,
+                                                &secondary_gpu_fb);
+
+  switch (renderer_gpu_data->mode)
+    {
+    case META_RENDERER_NATIVE_MODE_GBM:
+      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);
+          hold_primary_gpu_fb_for_secondary_gpu_scanout (
+            onscreen_native->secondary_gpu_state,
+            primary_gpu_fb,
+            secondary_gpu_fb);
+        }
+      else
+        {
+          g_set_object (&onscreen_native->gbm.next_fb, primary_gpu_fb);
+        }
+      break;
+    case META_RENDERER_NATIVE_MODE_SURFACELESS:
+      break;
+#ifdef HAVE_EGL_DEVICE
+    case META_RENDERER_NATIVE_MODE_EGL_DEVICE:
+      break;
+#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
@@ -1124,23 +1169,71 @@ meta_onscreen_native_swap_buffers_with_damage (CoglOnscreen  *onscreen,
   if (egl_context_changed)
     _cogl_winsys_egl_ensure_current (cogl_display);
 
+  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;
+
+  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);
+  MetaMonitorManager *monitor_manager =
+    meta_backend_get_monitor_manager (backend);
+  MetaKms *kms = meta_backend_native_get_kms (backend_native);
+  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);
+  MetaKmsUpdateFlag flags;
+  g_autoptr (MetaKmsFeedback) kms_feedback = NULL;
+  const GError *feedback_error;
+  unsigned int frames_pending = cogl_onscreen_count_pending_frames (onscreen);
+
+  if (onscreen_native->swaps_pending == 0)
+    return;
+
+  g_assert (frames_pending >= onscreen_native->swaps_pending);
+
   power_save_mode = meta_monitor_manager_get_power_save_mode (monitor_manager);
   if (power_save_mode == META_POWER_SAVE_ON)
     {
+      unsigned int posts_pending;
+
+      posts_pending = frames_pending - onscreen_native->swaps_pending;
+      if (posts_pending > 0)
+        return;  /* wait for the next frame notification and then try again */
+
+      drop_stalled_swap (onscreen);
+      g_return_if_fail (onscreen_native->swaps_pending > 0);
+      onscreen_native->swaps_pending--;
+
       ensure_crtc_modes (onscreen);
       meta_onscreen_native_flip_crtc (onscreen,
                                       onscreen_native->view,
                                       onscreen_native->crtc,
                                       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;
     }
 
@@ -1158,9 +1251,6 @@ meta_onscreen_native_swap_buffers_with_damage (CoglOnscreen  *onscreen,
                       "Postponing primary plane composite update for CRTC %u (%s)",
                       meta_kms_crtc_get_id (kms_crtc),
                       meta_kms_device_get_path (kms_device));
-
-          clutter_frame_set_result (frame,
-                                    CLUTTER_FRAME_RESULT_PENDING_PRESENTED);
           return;
         }
       else if (meta_renderer_native_has_pending_mode_set (renderer_native))
@@ -1170,8 +1260,6 @@ meta_onscreen_native_swap_buffers_with_damage (CoglOnscreen  *onscreen,
 
           meta_renderer_native_notify_mode_sets_reset (renderer_native);
           meta_renderer_native_post_mode_set_updates (renderer_native);
-          clutter_frame_set_result (frame,
-                                    CLUTTER_FRAME_RESULT_PENDING_PRESENTED);
           return;
         }
       break;
@@ -1184,8 +1272,6 @@ meta_onscreen_native_swap_buffers_with_damage (CoglOnscreen  *onscreen,
         {
           meta_renderer_native_notify_mode_sets_reset (renderer_native);
           meta_renderer_native_post_mode_set_updates (renderer_native);
-          clutter_frame_set_result (frame,
-                                    CLUTTER_FRAME_RESULT_PENDING_PRESENTED);
           return;
         }
       break;
@@ -1198,18 +1284,16 @@ meta_onscreen_native_swap_buffers_with_damage (CoglOnscreen  *onscreen,
               meta_kms_device_get_path (kms_device));
 
   flags = META_KMS_UPDATE_FLAG_NONE;
-  kms_feedback = meta_kms_post_pending_update_sync (kms, kms_device, flags);
+  kms_feedback = meta_kms_post_pending_update_for_crtc_sync (kms,
+                                                             kms_crtc,
+                                                             flags);
+  g_return_if_fail (kms_feedback != NULL);
 
   switch (meta_kms_feedback_get_result (kms_feedback))
     {
     case META_KMS_FEEDBACK_PASSED:
-      clutter_frame_set_result (frame,
-                                CLUTTER_FRAME_RESULT_PENDING_PRESENTED);
       break;
     case META_KMS_FEEDBACK_FAILED:
-      clutter_frame_set_result (frame,
-                                CLUTTER_FRAME_RESULT_PENDING_PRESENTED);
-
       feedback_error = meta_kms_feedback_get_error (kms_feedback);
       if (!g_error_matches (feedback_error,
                             G_IO_ERROR,
@@ -1296,6 +1380,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);
 
@@ -1348,7 +1444,9 @@ meta_onscreen_native_direct_scanout (CoglOnscreen   *onscreen,
               meta_kms_device_get_path (kms_device));
 
   flags = META_KMS_UPDATE_FLAG_PRESERVE_ON_ERROR;
-  kms_feedback = meta_kms_post_pending_update_sync (kms, kms_device, flags);
+  kms_feedback = meta_kms_post_pending_update_for_crtc_sync (kms,
+                                                             kms_crtc,
+                                                             flags);
   switch (meta_kms_feedback_get_result (kms_feedback))
     {
     case META_KMS_FEEDBACK_PASSED:
@@ -1362,7 +1460,6 @@ meta_onscreen_native_direct_scanout (CoglOnscreen   *onscreen,
                            G_IO_ERROR, G_IO_ERROR_PERMISSION_DENIED))
         break;
 
-      g_clear_object (&onscreen_native->gbm.next_fb);
       g_propagate_error (error, g_error_copy (feedback_error));
       return FALSE;
     }
@@ -1398,7 +1495,10 @@ meta_onscreen_native_finish_frame (CoglOnscreen *onscreen,
   g_autoptr (MetaKmsFeedback) kms_feedback = NULL;
   const GError *error;
 
-  kms_update = meta_kms_get_pending_update (kms, kms_device);
+  if (cogl_onscreen_count_pending_frames (onscreen) > 0)
+    return;
+
+  kms_update = meta_kms_get_pending_update_for_crtc (kms, kms_crtc);
   if (!kms_update)
     {
       clutter_frame_set_result (frame, CLUTTER_FRAME_RESULT_IDLE);
@@ -1413,9 +1513,9 @@ meta_onscreen_native_finish_frame (CoglOnscreen *onscreen,
                                           g_object_unref);
 
   flags = META_KMS_UPDATE_FLAG_NONE;
-  kms_feedback = meta_kms_post_pending_update_sync (kms,
-                                                    kms_device,
-                                                    flags);
+  kms_feedback = meta_kms_post_pending_update_for_crtc_sync (kms,
+                                                             kms_crtc,
+                                                             flags);
   switch (meta_kms_feedback_get_result (kms_feedback))
     {
     case META_KMS_FEEDBACK_PASSED:
@@ -1437,6 +1537,17 @@ meta_onscreen_native_finish_frame (CoglOnscreen *onscreen,
     }
 }
 
+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
 should_surface_be_sharable (CoglOnscreen *onscreen)
 {
@@ -1985,6 +2096,21 @@ pick_secondary_gpu_framebuffer_format_for_cpu (CoglOnscreen *onscreen)
   return DRM_FORMAT_INVALID;
 }
 
+static void
+dumb_toggle_notify (gpointer  data,
+                    GObject  *object,
+                    gboolean  is_last_ref)
+{
+  MetaDrmBuffer **source_fb = data;
+
+  g_return_if_fail (source_fb != NULL);
+  if (is_last_ref && *source_fb)
+    {
+      g_return_if_fail (META_IS_DRM_BUFFER (*source_fb));
+      g_clear_object (source_fb);
+    }
+}
+
 static gboolean
 init_secondary_gpu_state_cpu_copy_mode (MetaRendererNative         *renderer_native,
                                         CoglOnscreen               *onscreen,
@@ -2041,6 +2167,12 @@ init_secondary_gpu_state_cpu_copy_mode (MetaRendererNative         *renderer_nat
         }
 
       secondary_gpu_state->cpu.dumb_fbs[i] = META_DRM_BUFFER_DUMB (dumb_buffer);
+      g_object_add_toggle_ref (G_OBJECT (dumb_buffer),
+                               dumb_toggle_notify,
+                               &secondary_gpu_state->cpu.source_fbs[i]);
+
+      /* It was incremented higher than we need by add_toggle_ref */
+      g_object_unref (dumb_buffer);
     }
 
   /*
@@ -2130,7 +2262,7 @@ meta_onscreen_native_new (MetaRendererNative *renderer_native,
   onscreen_native->renderer_native = renderer_native;
   onscreen_native->render_gpu = render_gpu;
   onscreen_native->output = output;
-  onscreen_native->crtc = crtc;
+  onscreen_native->crtc = g_object_ref (crtc);
 
   return onscreen_native;
 }
@@ -2151,7 +2283,6 @@ meta_onscreen_native_dispose (GObject *object)
     {
     case META_RENDERER_NATIVE_MODE_GBM:
       g_clear_object (&onscreen_native->gbm.next_fb);
-      free_current_bo (onscreen);
       break;
     case META_RENDERER_NATIVE_MODE_SURFACELESS:
       g_assert_not_reached ();
@@ -2179,9 +2310,12 @@ meta_onscreen_native_dispose (GObject *object)
 
   G_OBJECT_CLASS (meta_onscreen_native_parent_class)->dispose (object);
 
+  g_clear_object (&onscreen_native->crtc);
   g_clear_pointer (&onscreen_native->gbm.surface, gbm_surface_destroy);
   g_clear_pointer (&onscreen_native->secondary_gpu_state,
                    secondary_gpu_state_free);
+  g_clear_pointer (&onscreen_native->next_post.rectangles, g_free);
+  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 3a85ace..676c4c4 100644
--- a/src/backends/native/meta-onscreen-native.h
+++ b/src/backends/native/meta-onscreen-native.h
@@ -40,6 +40,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 41bf04d..58e6339 100644
--- a/src/backends/native/meta-renderer-native.c
+++ b/src/backends/native/meta-renderer-native.c
@@ -661,12 +661,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;
@@ -678,6 +684,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 =
@@ -1386,6 +1395,26 @@ meta_renderer_native_create_view (MetaRenderer       *renderer,
   return view;
 }
 
+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
 keep_current_onscreens_alive (MetaRenderer *renderer)
 {
@@ -1414,6 +1443,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);
   meta_kms_discard_pending_updates (kms);
 
