From: Daniel van Vugt <daniel.van.vugt@canonical.com>
Date: Thu, 31 Oct 2024 16:22:51 +0100
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.

Formatted for Debian (as of 48) with:
git remote add community-ubuntu git@ssh.gitlab.gnome.org:Community/Ubuntu/mutter.git
git fetch community-ubuntu
git merge --squash -e community-ubuntu/triple-buffering-v4-48
And then git commit but using all this as the header instead of what
git suggests. Also add Gbp-Pq: Topic debian

Bug: https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/3760
Forwarded: https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/1441
Applied-upstream: no, see also https://bugs.debian.org/1050020
---
 clutter/clutter/clutter-context.c          |   1 +
 clutter/clutter/clutter-frame-clock.c      | 338 ++++++++++++++++------
 clutter/clutter/clutter-main.h             |   1 +
 doc/clutter-frame-scheduling.md            |  18 +-
 src/backends/native/meta-frame-native.c    |  17 ++
 src/backends/native/meta-frame-native.h    |   7 +
 src/backends/native/meta-kms.c             |   9 +
 src/backends/native/meta-kms.h             |   2 +
 src/backends/native/meta-onscreen-native.c | 444 ++++++++++++++++++-----------
 src/backends/native/meta-onscreen-native.h |   2 +
 src/backends/native/meta-renderer-native.c |  45 ++-
 src/tests/native-kms-render.c              | 198 ++++++++-----
 12 files changed, 753 insertions(+), 329 deletions(-)

diff --git a/clutter/clutter/clutter-context.c b/clutter/clutter/clutter-context.c
index d30a70c..a8a0793 100644
--- a/clutter/clutter/clutter-context.c
+++ b/clutter/clutter/clutter-context.c
@@ -81,6 +81,7 @@ static const GDebugKey clutter_paint_debug_keys[] = {
   { "damage-region", CLUTTER_DEBUG_PAINT_DAMAGE_REGION },
   { "disable-dynamic-max-render-time", CLUTTER_DEBUG_DISABLE_DYNAMIC_MAX_RENDER_TIME },
   { "max-render-time", CLUTTER_DEBUG_PAINT_MAX_RENDER_TIME },
+  { "disable-triple-buffering", CLUTTER_DEBUG_DISABLE_TRIPLE_BUFFERING },
 };
 
 typedef struct _ClutterContextPrivate
diff --git a/clutter/clutter/clutter-frame-clock.c b/clutter/clutter/clutter-frame-clock.c
index fcb587c..defaaf3 100644
--- a/clutter/clutter/clutter-frame-clock.c
+++ b/clutter/clutter/clutter-frame-clock.c
@@ -76,8 +76,11 @@ typedef enum _ClutterFrameClockState
   CLUTTER_FRAME_CLOCK_STATE_SCHEDULED,
   CLUTTER_FRAME_CLOCK_STATE_SCHEDULED_NOW,
   CLUTTER_FRAME_CLOCK_STATE_SCHEDULED_LATER,
-  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_ONE_AND_SCHEDULED_NOW,
+  CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE_AND_SCHEDULED_LATER,
+  CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_TWO,
 } ClutterFrameClockState;
 
 typedef struct _Frame
@@ -113,9 +116,10 @@ struct _ClutterFrameClock
 
   int64_t next_update_time_us;
 
-  Frame frame_pool[2];
+  Frame frame_pool[3];
   Frame *prev_dispatch;
   Frame *next_presentation;
+  Frame *next_next_presentation;
   Frame *prev_presentation;
 
   gboolean is_next_presentation_time_valid;
@@ -349,10 +353,6 @@ static void
 maybe_update_longterm_max_duration_us (ClutterFrameClock *frame_clock,
                                        ClutterFrameInfo  *frame_info)
 {
-  /* Do not update long-term max if there has been no measurement */
-  if (!frame_clock->shortterm_max_update_duration_us)
-    return;
-
   if ((frame_info->presentation_time - frame_clock->longterm_promotion_us) <
       G_USEC_PER_SEC)
     return;
@@ -380,6 +380,11 @@ clutter_frame_clock_notify_presented (ClutterFrameClock *frame_clock,
                                       ClutterFrameInfo  *frame_info)
 {
   Frame *presented_frame;
+#ifdef CLUTTER_ENABLE_DEBUG
+  const char *debug_state =
+    frame_clock->state == CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_TWO ?
+    "Triple buffering" : "Double buffering";
+#endif
 
   COGL_TRACE_BEGIN_SCOPED (ClutterFrameClockNotifyPresented,
                            "Clutter::FrameClock::presented()");
@@ -390,6 +395,8 @@ clutter_frame_clock_notify_presented (ClutterFrameClock *frame_clock,
   clear_frame (&frame_clock->prev_presentation);
   presented_frame = frame_clock->prev_presentation =
     g_steal_pointer (&frame_clock->next_presentation);
+  frame_clock->next_presentation =
+    g_steal_pointer (&frame_clock->next_next_presentation);
 
   presented_frame->next_presentation_time_us =
     frame_clock->next_presentation_time_us;
@@ -475,22 +482,35 @@ clutter_frame_clock_notify_presented (ClutterFrameClock *frame_clock,
 
   presented_frame->got_measurements = FALSE;
 
-  if (frame_info->cpu_time_before_buffer_swap_us != 0 &&
-      frame_info->has_valid_gpu_rendering_duration)
+  if ((frame_info->cpu_time_before_buffer_swap_us != 0 &&
+       frame_info->has_valid_gpu_rendering_duration) ||
+      frame_clock->ever_got_measurements)
     {
       int64_t dispatch_to_swap_us, swap_to_rendering_done_us, swap_to_flip_us;
+      int64_t dispatch_time_us = presented_frame->dispatch_time_us;
+      int64_t flip_time_us = presented_frame->flip_time_us;
 
-      dispatch_to_swap_us =
-        frame_info->cpu_time_before_buffer_swap_us -
-        presented_frame->dispatch_time_us;
+      if (frame_info->cpu_time_before_buffer_swap_us == 0)
+        {
+          /* User thread cursor-only updates with no "swap": we do know
+           * the combined time from dispatch to flip at least.
+           */
+          dispatch_to_swap_us = 0;
+          swap_to_flip_us = flip_time_us - dispatch_time_us;
+        }
+      else
+        {
+          dispatch_to_swap_us = frame_info->cpu_time_before_buffer_swap_us -
+                                dispatch_time_us;
+          swap_to_flip_us = flip_time_us -
+                            frame_info->cpu_time_before_buffer_swap_us;
+        }
       swap_to_rendering_done_us =
         frame_info->gpu_rendering_duration_ns / 1000;
-      swap_to_flip_us =
-        presented_frame->flip_time_us -
-        frame_info->cpu_time_before_buffer_swap_us;
 
       CLUTTER_NOTE (FRAME_TIMINGS,
-                    "update2dispatch %ld µs, dispatch2swap %ld µs, swap2render %ld µs, swap2flip %ld µs",
+                    "%s: update2dispatch %ld µs, dispatch2swap %ld µs, swap2render %ld µs, swap2flip %ld µs",
+                    debug_state,
                     presented_frame->dispatch_lateness_us,
                     dispatch_to_swap_us,
                     swap_to_rendering_done_us,
@@ -501,7 +521,7 @@ clutter_frame_clock_notify_presented (ClutterFrameClock *frame_clock,
                MAX (swap_to_rendering_done_us, swap_to_flip_us) +
                frame_clock->deadline_evasion_us,
                frame_clock->shortterm_max_update_duration_us,
-               frame_clock->refresh_interval_us);
+               2 * frame_clock->refresh_interval_us);
 
       maybe_update_longterm_max_duration_us (frame_clock, frame_info);
 
@@ -510,7 +530,8 @@ clutter_frame_clock_notify_presented (ClutterFrameClock *frame_clock,
     }
   else
     {
-      CLUTTER_NOTE (FRAME_TIMINGS, "update2dispatch %ld µs",
+      CLUTTER_NOTE (FRAME_TIMINGS, "%s: update2dispatch %ld µs",
+                    debug_state,
                     presented_frame->dispatch_lateness_us);
     }
 
@@ -529,11 +550,26 @@ clutter_frame_clock_notify_presented (ClutterFrameClock *frame_clock,
     case CLUTTER_FRAME_CLOCK_STATE_SCHEDULED_LATER:
       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_ONE_AND_SCHEDULED_NOW:
+      frame_clock->state = CLUTTER_FRAME_CLOCK_STATE_SCHEDULED_NOW;
+      maybe_reschedule_update (frame_clock);
+      break;
+    case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE_AND_SCHEDULED_LATER:
+      frame_clock->state = CLUTTER_FRAME_CLOCK_STATE_SCHEDULED_LATER;
+      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;
     }
 }
 
@@ -543,7 +579,10 @@ clutter_frame_clock_notify_ready (ClutterFrameClock *frame_clock)
   COGL_TRACE_BEGIN_SCOPED (ClutterFrameClockNotifyReady, "Clutter::FrameClock::ready()");
   COGL_TRACE_DESCRIBE (ClutterFrameClockNotifyReady, frame_clock->output_name);
 
-  clear_frame (&frame_clock->next_presentation);
+  if (frame_clock->next_next_presentation)
+    clear_frame (&frame_clock->next_next_presentation);
+  else
+    clear_frame (&frame_clock->next_presentation);
 
   switch (frame_clock->state)
     {
@@ -554,26 +593,41 @@ clutter_frame_clock_notify_ready (ClutterFrameClock *frame_clock)
     case CLUTTER_FRAME_CLOCK_STATE_SCHEDULED_LATER:
       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_ONE_AND_SCHEDULED_NOW:
+      frame_clock->state = CLUTTER_FRAME_CLOCK_STATE_SCHEDULED_NOW;
+      maybe_reschedule_update (frame_clock);
+      break;
+    case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE_AND_SCHEDULED_LATER:
+      frame_clock->state = CLUTTER_FRAME_CLOCK_STATE_SCHEDULED_NOW;
+      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;
     }
 }
 
-static int64_t
-clutter_frame_clock_compute_max_render_time_us (ClutterFrameClock *frame_clock)
+static gboolean
+clutter_frame_clock_compute_max_render_time_us (ClutterFrameClock *frame_clock,
+                                                int64_t           *max_render_time_us)
 {
   int64_t refresh_interval_us;
-  int64_t max_render_time_us;
 
   refresh_interval_us = frame_clock->refresh_interval_us;
 
   if (!frame_clock->ever_got_measurements ||
       G_UNLIKELY (clutter_paint_debug_flags &
                   CLUTTER_DEBUG_DISABLE_DYNAMIC_MAX_RENDER_TIME))
-    return (int64_t) (refresh_interval_us * SYNC_DELAY_FALLBACK_FRACTION);
+    return FALSE;
 
   /* Max render time shows how early the frame clock needs to be dispatched
    * to make it to the predicted next presentation time. It is an estimate of
@@ -587,15 +641,15 @@ clutter_frame_clock_compute_max_render_time_us (ClutterFrameClock *frame_clock)
    * - The duration of vertical blank.
    * - A constant to account for variations in the above estimates.
    */
-  max_render_time_us =
+  *max_render_time_us =
     MAX (frame_clock->longterm_max_update_duration_us,
          frame_clock->shortterm_max_update_duration_us) +
     frame_clock->vblank_duration_us +
     clutter_max_render_time_constant_us;
 
-  max_render_time_us = CLAMP (max_render_time_us, 0, refresh_interval_us);
+  *max_render_time_us = CLAMP (*max_render_time_us, 0, 2 * refresh_interval_us);
 
-  return max_render_time_us;
+  return TRUE;
 }
 
 static void
@@ -611,13 +665,21 @@ calculate_next_update_time_us (ClutterFrameClock *frame_clock,
   int64_t min_render_time_allowed_us;
   int64_t max_render_time_allowed_us;
   int64_t next_presentation_time_us;
+  int64_t next_smooth_presentation_time_us = 0;
   int64_t next_update_time_us;
+  gboolean max_render_time_is_known;
 
   now_us = g_get_monotonic_time ();
 
   refresh_interval_us = frame_clock->refresh_interval_us;
 
-  if (!last_presentation || last_presentation->presentation_time_us == 0)
+  max_render_time_is_known =
+    clutter_frame_clock_compute_max_render_time_us (frame_clock,
+                                                    &max_render_time_allowed_us);
+
+  if (!last_presentation ||
+      !max_render_time_is_known ||
+      last_presentation->presentation_time_us == 0)
     {
       const Frame *last_dispatch = frame_clock->prev_dispatch;
 
@@ -633,8 +695,6 @@ calculate_next_update_time_us (ClutterFrameClock *frame_clock,
     }
 
   min_render_time_allowed_us = refresh_interval_us / 2;
-  max_render_time_allowed_us =
-    clutter_frame_clock_compute_max_render_time_us (frame_clock);
 
   if (min_render_time_allowed_us > max_render_time_allowed_us)
     min_render_time_allowed_us = max_render_time_allowed_us;
@@ -657,7 +717,31 @@ calculate_next_update_time_us (ClutterFrameClock *frame_clock,
    *
    */
   last_presentation_time_us = last_presentation->presentation_time_us;
-  next_presentation_time_us = last_presentation_time_us + refresh_interval_us;
+  switch (frame_clock->state)
+    {
+    case CLUTTER_FRAME_CLOCK_STATE_INIT:
+    case CLUTTER_FRAME_CLOCK_STATE_IDLE:
+    case CLUTTER_FRAME_CLOCK_STATE_SCHEDULED:
+    case CLUTTER_FRAME_CLOCK_STATE_SCHEDULED_NOW:
+    case CLUTTER_FRAME_CLOCK_STATE_SCHEDULED_LATER:
+      next_smooth_presentation_time_us = last_presentation_time_us +
+                                         refresh_interval_us;
+      break;
+    case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE:
+    case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE_AND_SCHEDULED:
+    case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE_AND_SCHEDULED_NOW:
+    case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE_AND_SCHEDULED_LATER:
+      next_smooth_presentation_time_us = last_presentation_time_us +
+                                         2 * refresh_interval_us;
+      break;
+    case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_TWO:
+      g_warn_if_reached ();  /* quad buffering would be a bug */
+      next_smooth_presentation_time_us = last_presentation_time_us +
+                                         3 * refresh_interval_us;
+      break;
+    }
+
+  next_presentation_time_us = next_smooth_presentation_time_us;
 
   /*
    * However, the last presentation could have happened more than a frame ago.
@@ -724,7 +808,7 @@ calculate_next_update_time_us (ClutterFrameClock *frame_clock,
     }
 
   if (last_presentation->presentation_flags & CLUTTER_FRAME_INFO_FLAG_VSYNC &&
-      next_presentation_time_us != last_presentation_time_us + refresh_interval_us)
+      next_presentation_time_us != next_smooth_presentation_time_us)
     {
       /* There was an idle period since the last presentation, so there seems
        * be no constantly updating actor. In this case it's best to start
@@ -768,7 +852,10 @@ calculate_next_variable_update_time_us (ClutterFrameClock *frame_clock,
 
   refresh_interval_us = frame_clock->refresh_interval_us;
 
-  if (!last_presentation || last_presentation->presentation_time_us == 0)
+  if (!last_presentation ||
+      last_presentation->presentation_time_us == 0 ||
+      !clutter_frame_clock_compute_max_render_time_us (frame_clock,
+                                                       &max_render_time_allowed_us))
     {
       const Frame *last_dispatch = frame_clock->prev_dispatch;
 
@@ -783,9 +870,6 @@ calculate_next_variable_update_time_us (ClutterFrameClock *frame_clock,
       return;
     }
 
-  max_render_time_allowed_us =
-    clutter_frame_clock_compute_max_render_time_us (frame_clock);
-
   last_presentation_time_us = last_presentation->presentation_time_us;
   next_presentation_time_us = last_presentation_time_us + refresh_interval_us;
 
@@ -861,8 +945,21 @@ clutter_frame_clock_inhibit (ClutterFrameClock *frame_clock)
           frame_clock->pending_reschedule_now = 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_AND_SCHEDULED_NOW:
+          frame_clock->pending_reschedule = TRUE;
+          frame_clock->pending_reschedule_now = TRUE;
+          frame_clock->state = CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE;
+          break;
+        case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE_AND_SCHEDULED_LATER:
+          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;
         }
 
@@ -881,6 +978,16 @@ clutter_frame_clock_uninhibit (ClutterFrameClock *frame_clock)
     maybe_reschedule_update (frame_clock);
 }
 
+static gboolean
+want_triple_buffering (ClutterFrameClock *frame_clock)
+{
+  if (G_UNLIKELY (clutter_paint_debug_flags &
+                  CLUTTER_DEBUG_DISABLE_TRIPLE_BUFFERING))
+    return FALSE;
+
+  return TRUE;
+}
+
 void
 clutter_frame_clock_schedule_update_now (ClutterFrameClock *frame_clock)
 {
@@ -899,11 +1006,25 @@ clutter_frame_clock_schedule_update_now (ClutterFrameClock *frame_clock)
     case CLUTTER_FRAME_CLOCK_STATE_IDLE:
     case CLUTTER_FRAME_CLOCK_STATE_SCHEDULED:
     case CLUTTER_FRAME_CLOCK_STATE_SCHEDULED_LATER:
+      frame_clock->state = CLUTTER_FRAME_CLOCK_STATE_SCHEDULED_NOW;
       break;
     case CLUTTER_FRAME_CLOCK_STATE_SCHEDULED_NOW:
+    case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE_AND_SCHEDULED_NOW:
       return;
-    case CLUTTER_FRAME_CLOCK_STATE_DISPATCHING:
-    case CLUTTER_FRAME_CLOCK_STATE_PENDING_PRESENTED:
+    case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE_AND_SCHEDULED:
+    case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE_AND_SCHEDULED_LATER:
+      frame_clock->state =
+        CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE_AND_SCHEDULED_NOW;
+      break;
+    case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE:
+      if (want_triple_buffering (frame_clock))
+        {
+          frame_clock->state =
+            CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE_AND_SCHEDULED_NOW;
+          break;
+        }
+      G_GNUC_FALLTHROUGH;
+    case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_TWO:
       frame_clock->pending_reschedule = TRUE;
       frame_clock->pending_reschedule_now = TRUE;
       return;
@@ -932,7 +1053,6 @@ clutter_frame_clock_schedule_update_now (ClutterFrameClock *frame_clock)
 
   frame_clock->next_update_time_us = next_update_time_us;
   g_source_set_ready_time (frame_clock->source, next_update_time_us);
-  frame_clock->state = CLUTTER_FRAME_CLOCK_STATE_SCHEDULED_NOW;
 }
 
 void
@@ -954,13 +1074,24 @@ clutter_frame_clock_schedule_update (ClutterFrameClock *frame_clock)
       frame_clock->state = CLUTTER_FRAME_CLOCK_STATE_SCHEDULED;
       return;
     case CLUTTER_FRAME_CLOCK_STATE_IDLE:
-    case CLUTTER_FRAME_CLOCK_STATE_SCHEDULED_LATER:
+      frame_clock->state = CLUTTER_FRAME_CLOCK_STATE_SCHEDULED;
       break;
     case CLUTTER_FRAME_CLOCK_STATE_SCHEDULED:
     case CLUTTER_FRAME_CLOCK_STATE_SCHEDULED_NOW:
+    case CLUTTER_FRAME_CLOCK_STATE_SCHEDULED_LATER:
+    case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE_AND_SCHEDULED:
+    case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE_AND_SCHEDULED_NOW:
+    case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE_AND_SCHEDULED_LATER:
       return;
-    case CLUTTER_FRAME_CLOCK_STATE_DISPATCHING:
-    case CLUTTER_FRAME_CLOCK_STATE_PENDING_PRESENTED:
+    case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE:
+      if (want_triple_buffering (frame_clock))
+        {
+          frame_clock->state =
+            CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE_AND_SCHEDULED;
+          break;
+        }
+      G_GNUC_FALLTHROUGH;
+    case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_TWO:
       frame_clock->pending_reschedule = TRUE;
       return;
     }
@@ -989,7 +1120,6 @@ clutter_frame_clock_schedule_update (ClutterFrameClock *frame_clock)
 
   frame_clock->next_update_time_us = next_update_time_us;
   g_source_set_ready_time (frame_clock->source, next_update_time_us);
-  frame_clock->state = CLUTTER_FRAME_CLOCK_STATE_SCHEDULED;
 }
 
 static void
@@ -1014,12 +1144,24 @@ clutter_frame_clock_schedule_update_later (ClutterFrameClock *frame_clock,
     case CLUTTER_FRAME_CLOCK_STATE_INIT:
     case CLUTTER_FRAME_CLOCK_STATE_IDLE:
     case CLUTTER_FRAME_CLOCK_STATE_SCHEDULED_LATER:
+      frame_clock->state = CLUTTER_FRAME_CLOCK_STATE_SCHEDULED_LATER;
       break;
     case CLUTTER_FRAME_CLOCK_STATE_SCHEDULED_NOW:
     case CLUTTER_FRAME_CLOCK_STATE_SCHEDULED:
+    case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE_AND_SCHEDULED:
+    case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE_AND_SCHEDULED_NOW:
       return;
-    case CLUTTER_FRAME_CLOCK_STATE_DISPATCHING:
-    case CLUTTER_FRAME_CLOCK_STATE_PENDING_PRESENTED:
+    case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE_AND_SCHEDULED_LATER:
+      break;
+    case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE:
+      if (want_triple_buffering (frame_clock))
+        {
+          frame_clock->state =
+            CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE_AND_SCHEDULED_LATER;
+          break;
+        }
+      G_GNUC_FALLTHROUGH;
+    case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_TWO:
       frame_clock->pending_reschedule = TRUE;
       frame_clock->pending_reschedule_now = TRUE;
       return;
@@ -1062,15 +1204,18 @@ clutter_frame_clock_schedule_update_later (ClutterFrameClock *frame_clock,
       ready_time_us = extrapolated_presentation_time_us - max_render_time_us;
       break;
     case CLUTTER_FRAME_CLOCK_MODE_VARIABLE:
-      max_render_time_us =
-        clutter_frame_clock_compute_max_render_time_us (frame_clock);
+      if (!clutter_frame_clock_compute_max_render_time_us (frame_clock,
+                                                           &max_render_time_us))
+        {
+          max_render_time_us = (int64_t) (frame_clock->refresh_interval_us *
+                                          SYNC_DELAY_FALLBACK_FRACTION);
+        }
       ready_time_us = target_us - max_render_time_us;
       break;
     }
 
   g_source_set_ready_time (frame_clock->source, ready_time_us);
   frame_clock->pending_reschedule = TRUE;
-  frame_clock->state = CLUTTER_FRAME_CLOCK_STATE_SCHEDULED_LATER;
 }
 
 static int
@@ -1112,6 +1257,8 @@ clutter_frame_clock_set_mode (ClutterFrameClock     *frame_clock,
     {
     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:
       break;
     case CLUTTER_FRAME_CLOCK_STATE_SCHEDULED:
     case CLUTTER_FRAME_CLOCK_STATE_SCHEDULED_LATER:
@@ -1123,8 +1270,15 @@ clutter_frame_clock_set_mode (ClutterFrameClock     *frame_clock,
       frame_clock->pending_reschedule_now = 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:
+    case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE_AND_SCHEDULED_LATER:
+      frame_clock->pending_reschedule = TRUE;
+      frame_clock->state = CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE;
+      break;
+    case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE_AND_SCHEDULED_NOW:
+      frame_clock->pending_reschedule = TRUE;
+      frame_clock->pending_reschedule_now = TRUE;
+      frame_clock->state = CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE;
       break;
     }
 
@@ -1170,9 +1324,16 @@ clutter_frame_clock_dispatch (ClutterFrameClock *frame_clock,
   this_dispatch = frame_clock->prev_dispatch =
     clutter_frame_clock_new_frame (frame_clock);
 
-  /* This will need changing for triple buffering */
-  g_warn_if_fail (frame_clock->next_presentation == NULL);
-  frame_clock->next_presentation = ref_frame (this_dispatch);
+  if (frame_clock->next_presentation == NULL)
+    {
+      frame_clock->next_presentation = ref_frame (this_dispatch);
+    }
+  else
+    {
+      g_warn_if_fail (frame_clock->next_next_presentation == NULL);
+      frame_clock->next_next_presentation =
+        ref_frame (this_dispatch);
+    }
 
   ideal_dispatch_time_us = frame_clock->next_update_time_us;
 
@@ -1182,7 +1343,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)
     this_dispatch->dispatch_lateness_us = 0;
   else
     this_dispatch->dispatch_lateness_us = lateness_us;
@@ -1206,7 +1367,25 @@ clutter_frame_clock_dispatch (ClutterFrameClock *frame_clock,
   this_dispatch->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:
+    case CLUTTER_FRAME_CLOCK_STATE_SCHEDULED_NOW:
+    case CLUTTER_FRAME_CLOCK_STATE_SCHEDULED_LATER:
+      frame_clock->state = CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE;
+      break;
+    case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE_AND_SCHEDULED:
+    case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE_AND_SCHEDULED_NOW:
+    case CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_ONE_AND_SCHEDULED_LATER:
+      frame_clock->state = CLUTTER_FRAME_CLOCK_STATE_DISPATCHED_TWO;
+      break;
+    }
 
   frame_count = frame_clock->frame_count++;
 
@@ -1237,27 +1416,13 @@ clutter_frame_clock_dispatch (ClutterFrameClock *frame_clock,
   result = iface->frame (frame_clock, frame, frame_clock->listener.user_data);
   COGL_TRACE_END (ClutterFrameClockFrame);
 
-  switch (frame_clock->state)
+  switch (result)
     {
-    case CLUTTER_FRAME_CLOCK_STATE_INIT:
-    case CLUTTER_FRAME_CLOCK_STATE_PENDING_PRESENTED:
-      g_warn_if_reached ();
+    case CLUTTER_FRAME_RESULT_PENDING_PRESENTED:
       break;
-    case CLUTTER_FRAME_CLOCK_STATE_IDLE:
-    case CLUTTER_FRAME_CLOCK_STATE_SCHEDULED:
-    case CLUTTER_FRAME_CLOCK_STATE_SCHEDULED_NOW:
-    case CLUTTER_FRAME_CLOCK_STATE_SCHEDULED_LATER:
-      break;
-    case CLUTTER_FRAME_CLOCK_STATE_DISPATCHING:
-      switch (result)
-        {
-        case CLUTTER_FRAME_RESULT_PENDING_PRESENTED:
-          frame_clock->state = CLUTTER_FRAME_CLOCK_STATE_PENDING_PRESENTED;
-          break;
-        case CLUTTER_FRAME_RESULT_IDLE:
-          clutter_frame_clock_notify_ready (frame_clock);
-          break;
-        }
+    case CLUTTER_FRAME_RESULT_IDLE:
+      /* The frame was aborted; nothing to paint/present */
+      clutter_frame_clock_notify_ready (frame_clock);
       break;
     }
 
@@ -1301,12 +1466,19 @@ GString *
 clutter_frame_clock_get_max_render_time_debug_info (ClutterFrameClock *frame_clock)
 {
   const Frame *last_presentation = frame_clock->prev_presentation;
+  int64_t max_render_time_us;
   int64_t max_update_duration_us;
   GString *string;
 
-  string = g_string_new (NULL);
-  g_string_append_printf (string, "Max render time: %ld µs",
-                          clutter_frame_clock_compute_max_render_time_us (frame_clock));
+  string = g_string_new ("Max render time: ");
+  if (!clutter_frame_clock_compute_max_render_time_us (frame_clock,
+                                                       &max_render_time_us))
+    {
+      g_string_append (string, "unknown");
+      return string;
+    }
+
+  g_string_append_printf (string, "%ld µs", max_render_time_us);
 
   if (last_presentation && last_presentation->got_measurements)
     g_string_append_printf (string, " =");
@@ -1475,8 +1647,6 @@ clutter_frame_clock_dispose (GObject *object)
 {
   ClutterFrameClock *frame_clock = CLUTTER_FRAME_CLOCK (object);
 
-  g_warn_if_fail (frame_clock->state != CLUTTER_FRAME_CLOCK_STATE_DISPATCHING);
-
   if (frame_clock->source)
     {
       g_signal_emit (frame_clock, signals[DESTROY], 0);
diff --git a/clutter/clutter/clutter-main.h b/clutter/clutter/clutter-main.h
index 343ab55..82602a2 100644
--- a/clutter/clutter/clutter-main.h
+++ b/clutter/clutter/clutter-main.h
@@ -77,6 +77,7 @@ typedef enum
   CLUTTER_DEBUG_PAINT_DAMAGE_REGION             = 1 << 8,
   CLUTTER_DEBUG_DISABLE_DYNAMIC_MAX_RENDER_TIME = 1 << 9,
   CLUTTER_DEBUG_PAINT_MAX_RENDER_TIME           = 1 << 10,
+  CLUTTER_DEBUG_DISABLE_TRIPLE_BUFFERING        = 1 << 11,
 } ClutterDrawDebugFlag;
 
 /**
diff --git a/doc/clutter-frame-scheduling.md b/doc/clutter-frame-scheduling.md
index f6ac3cb..64633b9 100644
--- a/doc/clutter-frame-scheduling.md
+++ b/doc/clutter-frame-scheduling.md
@@ -4,11 +4,15 @@
 
 ```mermaid
 stateDiagram
-    Init --> Scheduled/ScheduledNow : schedule update() -> now
-    Idle --> Scheduled/ScheduledNow : schedule update() -> given presentation time
-    Scheduled/ScheduledNow --> Dispatching : target time hit
-    Dispatching --> PendingPresented : queued page flip
-    Dispatching --> Idle : no queued page flip
-    PendingPresented --> Scheduled/ScheduledNow : page flipped, if recent schedule update
-    PendingPresented --> Idle : page flipped
+    INIT --> SCHEDULED* : first frame scheduled
+    IDLE --> SCHEDULED* : next frame scheduled
+    SCHEDULED* --> IDLE : frame clock inhibited or mode changed
+    SCHEDULED* --> SCHEDULED* : schedule upgraded to SCHEDULED_NOW
+    SCHEDULED* --> DISPATCHED_ONE : begin an update
+    DISPATCHED_ONE --> IDLE : frame was either presented or aborted with nothing to draw
+    DISPATCHED_ONE --> DISPATCHED_ONE_AND_SCHEDULED* : entering triple buffering
+    DISPATCHED_ONE_AND_SCHEDULED* --> SCHEDULED* : frame was either presented or aborted with nothing to draw
+    DISPATCHED_ONE_AND_SCHEDULED* --> DISPATCHED_ONE : frame clock inhibited or mode changed
+    DISPATCHED_ONE_AND_SCHEDULED* --> DISPATCHED_TWO : start a second concurrent frame
+    DISPATCHED_TWO --> DISPATCHED_ONE : leaving triple buffering
 ```
diff --git a/src/backends/native/meta-frame-native.c b/src/backends/native/meta-frame-native.c
index 70461be..527c4a1 100644
--- a/src/backends/native/meta-frame-native.c
+++ b/src/backends/native/meta-frame-native.c
@@ -31,6 +31,8 @@ struct _MetaFrameNative
   CoglScanout *scanout;
 
   MetaKmsUpdate *kms_update;
+
+  MtkRegion *damage;
 };
 
 static void
@@ -38,6 +40,7 @@ meta_frame_native_release (ClutterFrame *frame)
 {
   MetaFrameNative *frame_native = meta_frame_native_from_frame (frame);
 
+  g_clear_pointer (&frame_native->damage, mtk_region_unref);
   g_clear_object (&frame_native->buffer);
   g_clear_object (&frame_native->scanout);
 
@@ -108,3 +111,17 @@ meta_frame_native_get_scanout (MetaFrameNative *frame_native)
 {
   return frame_native->scanout;
 }
+
+void
+meta_frame_native_set_damage (MetaFrameNative *frame_native,
+                              const MtkRegion *damage)
+{
+  g_clear_pointer (&frame_native->damage, mtk_region_unref);
+  frame_native->damage = mtk_region_copy (damage);
+}
+
+MtkRegion *
+meta_frame_native_get_damage (MetaFrameNative *frame_native)
+{
+  return frame_native->damage;
+}
diff --git a/src/backends/native/meta-frame-native.h b/src/backends/native/meta-frame-native.h
index 3df4eff..32349d3 100644
--- a/src/backends/native/meta-frame-native.h
+++ b/src/backends/native/meta-frame-native.h
@@ -47,3 +47,10 @@ void meta_frame_native_set_scanout (MetaFrameNative *frame_native,
                                     CoglScanout     *scanout);
 
 CoglScanout * meta_frame_native_get_scanout (MetaFrameNative *frame_native);
+
+void
+meta_frame_native_set_damage (MetaFrameNative *frame_native,
+                              const MtkRegion *damage);
+
+MtkRegion *
+meta_frame_native_get_damage (MetaFrameNative *frame_native);
diff --git a/src/backends/native/meta-kms.c b/src/backends/native/meta-kms.c
index b1aac87..e42e059 100644
--- a/src/backends/native/meta-kms.c
+++ b/src/backends/native/meta-kms.c
@@ -66,6 +66,8 @@ struct _MetaKms
   int kernel_thread_inhibit_count;
 
   MetaKmsCursorManager *cursor_manager;
+
+  gboolean shutting_down;
 };
 
 G_DEFINE_TYPE (MetaKms, meta_kms, META_TYPE_THREAD)
@@ -352,6 +354,12 @@ meta_kms_create_device (MetaKms            *kms,
   return device;
 }
 
+gboolean
+meta_kms_is_shutting_down (MetaKms *kms)
+{
+  return kms->shutting_down;
+}
+
 static gpointer
 prepare_shutdown_in_impl (MetaThreadImpl  *thread_impl,
                           gpointer         user_data,
@@ -367,6 +375,7 @@ static void
 on_prepare_shutdown (MetaBackend *backend,
                      MetaKms     *kms)
 {
+  kms->shutting_down = TRUE;
   meta_kms_run_impl_task_sync (kms, prepare_shutdown_in_impl, NULL, NULL);
   meta_thread_flush_callbacks (META_THREAD (kms));
 
diff --git a/src/backends/native/meta-kms.h b/src/backends/native/meta-kms.h
index f5ec4c1..77fd626 100644
--- a/src/backends/native/meta-kms.h
+++ b/src/backends/native/meta-kms.h
@@ -61,6 +61,8 @@ MetaKmsDevice * meta_kms_create_device (MetaKms            *kms,
                                         MetaKmsDeviceFlag   flags,
                                         GError            **error);
 
+gboolean meta_kms_is_shutting_down (MetaKms *kms);
+
 MetaKms * meta_kms_new (MetaBackend   *backend,
                         MetaKmsFlags   flags,
                         GError       **error);
diff --git a/src/backends/native/meta-onscreen-native.c b/src/backends/native/meta-onscreen-native.c
index 5148905..29e2a84 100644
--- a/src/backends/native/meta-onscreen-native.c
+++ b/src/backends/native/meta-onscreen-native.c
@@ -78,7 +78,7 @@ typedef struct _MetaOnscreenNativeSecondaryGpuState
 
   struct {
     MetaDrmBufferDumb *current_dumb_fb;
-    MetaDrmBufferDumb *dumb_fbs[2];
+    MetaDrmBufferDumb *dumb_fbs[3];
   } cpu;
 
   gboolean noted_primary_gpu_copy_ok;
@@ -103,8 +103,11 @@ struct _MetaOnscreenNative
   MetaCrtc *crtc;
 
   MetaOnscreenNativeSecondaryGpuState *secondary_gpu_state;
+  gboolean secondary_gpu_used;
 
   ClutterFrame *presented_frame;
+  ClutterFrame *posted_frame;
+  ClutterFrame *superseded_frame;
   ClutterFrame *next_frame;
 
   struct {
@@ -119,6 +122,8 @@ struct _MetaOnscreenNative
   } egl;
 #endif
 
+  gboolean needs_flush;
+
   gboolean frame_sync_requested;
   gboolean frame_sync_enabled;
 
@@ -138,6 +143,13 @@ G_DEFINE_TYPE (MetaOnscreenNative, meta_onscreen_native,
 
 static GQuark blit_source_quark = 0;
 
+static void
+maybe_post_next_frame (CoglOnscreen *onscreen);
+
+static void
+post_nonprimary_plane_update (MetaOnscreenNative *onscreen_native,
+                              MetaKmsUpdate      *kms_update);
+
 static gboolean
 init_secondary_gpu_state (MetaRendererNative  *renderer_native,
                           CoglOnscreen        *onscreen,
@@ -148,20 +160,20 @@ meta_onscreen_native_swap_drm_fb (CoglOnscreen *onscreen)
 {
   MetaOnscreenNative *onscreen_native = META_ONSCREEN_NATIVE (onscreen);
 
-  if (!onscreen_native->next_frame)
+  if (!onscreen_native->posted_frame)
     return;
 
   g_clear_pointer (&onscreen_native->presented_frame, clutter_frame_unref);
   onscreen_native->presented_frame =
-    g_steal_pointer (&onscreen_native->next_frame);
+    g_steal_pointer (&onscreen_native->posted_frame);
 }
 
 static void
-meta_onscreen_native_clear_next_fb (CoglOnscreen *onscreen)
+meta_onscreen_native_clear_posted_fb (CoglOnscreen *onscreen)
 {
   MetaOnscreenNative *onscreen_native = META_ONSCREEN_NATIVE (onscreen);
 
-  g_clear_pointer (&onscreen_native->next_frame, clutter_frame_unref);
+  g_clear_pointer (&onscreen_native->posted_frame, clutter_frame_unref);
 }
 
 static void
@@ -199,7 +211,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_return_if_fail (info);
 
   _cogl_onscreen_notify_frame_sync (onscreen, info);
   _cogl_onscreen_notify_complete (onscreen, info);
@@ -241,6 +253,7 @@ notify_view_crtc_presented (MetaRendererView *view,
 
   meta_onscreen_native_notify_frame_complete (onscreen);
   meta_onscreen_native_swap_drm_fb (onscreen);
+  maybe_post_next_frame (onscreen);
 }
 
 static void
@@ -290,15 +303,13 @@ page_flip_feedback_ready (MetaKmsCrtc *kms_crtc,
   CoglFramebuffer *framebuffer =
     clutter_stage_view_get_onscreen (CLUTTER_STAGE_VIEW (view));
   CoglOnscreen *onscreen = COGL_ONSCREEN (framebuffer);
-  MetaOnscreenNative *onscreen_native = META_ONSCREEN_NATIVE (onscreen);
   CoglFrameInfo *frame_info;
 
   frame_info = cogl_onscreen_peek_head_frame_info (onscreen);
   frame_info->flags |= COGL_FRAME_INFO_FLAG_SYMBOLIC;
 
-  g_warn_if_fail (!onscreen_native->next_frame);
-
   meta_onscreen_native_notify_frame_complete (onscreen);
+  maybe_post_next_frame (onscreen);
 }
 
 static void
@@ -365,7 +376,8 @@ page_flip_feedback_discarded (MetaKmsCrtc  *kms_crtc,
     }
 
   meta_onscreen_native_notify_frame_complete (onscreen);
-  meta_onscreen_native_clear_next_fb (onscreen);
+  meta_onscreen_native_clear_posted_fb (onscreen);
+  maybe_post_next_frame (onscreen);
 }
 
 static const MetaKmsPageFlipListenerVtable page_flip_listener_vtable = {
@@ -426,18 +438,37 @@ 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
+clear_superseded_frame (CoglOnscreen *onscreen)
 {
   CoglFrameInfo *frame_info;
+  MetaOnscreenNative *onscreen_native = META_ONSCREEN_NATIVE (onscreen);
 
-  meta_onscreen_native_swap_drm_fb (onscreen);
+  if (onscreen_native->superseded_frame == NULL)
+    return;
+
+  g_clear_pointer (&onscreen_native->superseded_frame, clutter_frame_unref);
 
   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)
+{
+  clear_superseded_frame (onscreen);
+
+  /* If the monitor woke up in the 100ms between this callback being queued
+   * and dispatched, and the shell is fully idle (has nothing more to swap)
+   * then we just woke to an indefinitely black screen. The only saving grace
+   * here is that shells usually have multiple frames they want to display
+   * soon after wakeup. But let's not assume that's always the case. Fix it
+   * by displaying the last swap (which is never classified as "superseded").
+   */
+  maybe_post_next_frame (onscreen);
+}
+
 static void
 apply_transform (MetaCrtcKms            *crtc_kms,
                  MetaKmsPlaneAssignment *kms_plane_assignment,
@@ -531,8 +562,9 @@ assign_primary_plane (MetaCrtcKms            *crtc_kms,
   return plane_assignment;
 }
 
-static void
+static gboolean
 meta_onscreen_native_flip_crtc (CoglOnscreen           *onscreen,
+                                ClutterFrame           *frame,
                                 MetaRendererView       *view,
                                 MetaCrtc               *crtc,
                                 MetaKmsUpdate          *kms_update,
@@ -541,7 +573,6 @@ meta_onscreen_native_flip_crtc (CoglOnscreen           *onscreen,
 {
   MetaOnscreenNative *onscreen_native = META_ONSCREEN_NATIVE (onscreen);
   MetaRendererNative *renderer_native = onscreen_native->renderer_native;
-  ClutterFrame *frame = onscreen_native->next_frame;
   MetaFrameNative *frame_native;
   MetaGpuKms *render_gpu = onscreen_native->render_gpu;
   MetaCrtcKms *crtc_kms = META_CRTC_KMS (crtc);
@@ -557,8 +588,6 @@ meta_onscreen_native_flip_crtc (CoglOnscreen           *onscreen,
   COGL_TRACE_BEGIN_SCOPED (MetaOnscreenNativeFlipCrtcs,
                            "Meta::OnscreenNative::flip_crtc()");
 
-  g_return_if_fail (frame);
-
   gpu_kms = META_GPU_KMS (meta_crtc_get_gpu (crtc));
 
   g_assert (meta_gpu_kms_is_crtc_active (gpu_kms, crtc));
@@ -570,6 +599,9 @@ meta_onscreen_native_flip_crtc (CoglOnscreen           *onscreen,
     case META_RENDERER_NATIVE_MODE_GBM:
       frame_native = meta_frame_native_from_frame (frame);
       buffer = meta_frame_native_get_buffer (frame_native);
+      if (!buffer)
+        return FALSE;
+
       scanout = meta_frame_native_get_scanout (frame_native);
 
       if (scanout)
@@ -622,6 +654,7 @@ meta_onscreen_native_flip_crtc (CoglOnscreen           *onscreen,
                                           NULL,
                                           g_object_ref (view),
                                           g_object_unref);
+  return TRUE;
 }
 
 static void
@@ -1022,12 +1055,17 @@ static MetaDrmBufferDumb *
 secondary_gpu_get_next_dumb_buffer (MetaOnscreenNativeSecondaryGpuState *secondary_gpu_state)
 {
   MetaDrmBufferDumb *current_dumb_fb;
+  const int n_dumb_fbs = G_N_ELEMENTS (secondary_gpu_state->cpu.dumb_fbs);
+  int i;
 
   current_dumb_fb = secondary_gpu_state->cpu.current_dumb_fb;
-  if (current_dumb_fb == secondary_gpu_state->cpu.dumb_fbs[0])
-    return secondary_gpu_state->cpu.dumb_fbs[1];
-  else
-    return secondary_gpu_state->cpu.dumb_fbs[0];
+  for (i = 0; i < n_dumb_fbs; i++)
+    {
+      if (current_dumb_fb == secondary_gpu_state->cpu.dumb_fbs[i])
+        return secondary_gpu_state->cpu.dumb_fbs[(i + 1) % n_dumb_fbs];
+    }
+
+  return secondary_gpu_state->cpu.dumb_fbs[0];
 }
 
 static MetaDrmBuffer *
@@ -1372,16 +1410,47 @@ swap_buffer_result_feedback (const MetaKmsFeedback *kms_feedback,
     g_warning ("Page flip failed: %s", error->message);
 
   frame_info = cogl_onscreen_peek_head_frame_info (onscreen);
-  frame_info->flags |= COGL_FRAME_INFO_FLAG_SYMBOLIC;
 
-  meta_onscreen_native_notify_frame_complete (onscreen);
-  meta_onscreen_native_clear_next_fb (onscreen);
+  /* After resuming from suspend, clear_superseded_frame might have done this
+   * already and emptied the frame_info queue.
+   */
+  if (frame_info)
+    {
+      frame_info->flags |= COGL_FRAME_INFO_FLAG_SYMBOLIC;
+      meta_onscreen_native_notify_frame_complete (onscreen);
+    }
+
+  meta_onscreen_native_clear_posted_fb (onscreen);
+}
+
+static void
+assign_next_frame (MetaOnscreenNative *onscreen_native,
+                   ClutterFrame       *frame)
+{
+  CoglOnscreen *onscreen = COGL_ONSCREEN (onscreen_native);
+
+  if (onscreen_native->next_frame != NULL)
+    {
+      clear_superseded_frame (onscreen);
+      onscreen_native->superseded_frame =
+        g_steal_pointer (&onscreen_native->next_frame);
+    }
+
+  onscreen_native->next_frame = clutter_frame_ref (frame);
 }
 
 static const MetaKmsResultListenerVtable swap_buffer_result_listener_vtable = {
   .feedback = swap_buffer_result_feedback,
 };
 
+static void
+scanout_result_feedback (const MetaKmsFeedback *kms_feedback,
+                         gpointer               user_data);
+
+static const MetaKmsResultListenerVtable scanout_result_listener_vtable = {
+  .feedback = scanout_result_feedback,
+};
+
 static void
 meta_onscreen_native_swap_buffers_with_damage (CoglOnscreen    *onscreen,
                                                const MtkRegion *region,
@@ -1394,30 +1463,18 @@ meta_onscreen_native_swap_buffers_with_damage (CoglOnscreen    *onscreen,
   CoglRendererEGL *cogl_renderer_egl = cogl_renderer->winsys;
   MetaRendererNativeGpuData *renderer_gpu_data = cogl_renderer_egl->platform;
   MetaRendererNative *renderer_native = renderer_gpu_data->renderer_native;
-  MetaRenderer *renderer = META_RENDERER (renderer_native);
-  MetaBackend *backend = meta_renderer_get_backend (renderer);
-  MetaMonitorManager *monitor_manager =
-    meta_backend_get_monitor_manager (backend);
   MetaOnscreenNative *onscreen_native = META_ONSCREEN_NATIVE (onscreen);
-  MetaOnscreenNativeSecondaryGpuState *secondary_gpu_state;
   MetaGpuKms *render_gpu = onscreen_native->render_gpu;
   MetaDeviceFile *render_device_file;
   ClutterFrame *frame = user_data;
   MetaFrameNative *frame_native = meta_frame_native_from_frame (frame);
-  MetaKmsUpdate *kms_update;
   CoglOnscreenClass *parent_class;
-  gboolean secondary_gpu_used = FALSE;
-  MetaPowerSave power_save_mode;
   g_autoptr (GError) error = NULL;
   MetaDrmBufferFlags buffer_flags;
   MetaDrmBufferGbm *buffer_gbm;
   g_autoptr (MetaDrmBuffer) primary_gpu_fb = NULL;
   g_autoptr (MetaDrmBuffer) secondary_gpu_fb = NULL;
   g_autoptr (MetaDrmBuffer) buffer = NULL;
-  MetaKmsCrtc *kms_crtc;
-  MetaKmsDevice *kms_device;
-
-  COGL_TRACE_SCOPED_ANCHOR (MetaRendererNativePostKmsUpdate);
 
   COGL_TRACE_BEGIN_SCOPED (MetaRendererNativeSwapBuffers,
                            "Meta::OnscreenNative::swap_buffers_with_damage()");
@@ -1425,20 +1482,7 @@ meta_onscreen_native_swap_buffers_with_damage (CoglOnscreen    *onscreen,
   secondary_gpu_fb =
     update_secondary_gpu_state_pre_swap_buffers (onscreen, region);
 
-  secondary_gpu_state = onscreen_native->secondary_gpu_state;
-  if (secondary_gpu_state)
-    {
-      MetaRendererNativeGpuData *secondary_gpu_data;
-
-      secondary_gpu_data =
-        meta_renderer_native_get_gpu_data (renderer_native,
-                                           secondary_gpu_state->gpu_kms);
-      secondary_gpu_used =
-        secondary_gpu_data->secondary.copy_mode ==
-        META_SHARED_FRAMEBUFFER_COPY_MODE_SECONDARY_GPU;
-    }
-
-  if (!secondary_gpu_used)
+  if (!onscreen_native->secondary_gpu_used)
     cogl_onscreen_egl_maybe_create_timestamp_query (onscreen, frame_info);
 
   parent_class = COGL_ONSCREEN_CLASS (meta_onscreen_native_parent_class);
@@ -1500,42 +1544,113 @@ meta_onscreen_native_swap_buffers_with_damage (CoglOnscreen    *onscreen,
 #endif
     }
 
-  g_warn_if_fail (!onscreen_native->next_frame);
-  onscreen_native->next_frame = clutter_frame_ref (frame);
+  assign_next_frame (onscreen_native, frame);
+
+  clutter_frame_set_result (frame,
+                            CLUTTER_FRAME_RESULT_PENDING_PRESENTED);
+
+  meta_frame_native_set_damage (frame_native, region);
+  maybe_post_next_frame (onscreen);
+  return;
+
+swap_failed:
+  frame_info->flags |= COGL_FRAME_INFO_FLAG_SYMBOLIC;
+  meta_onscreen_native_notify_frame_complete (onscreen);
+  clutter_frame_set_result (frame, CLUTTER_FRAME_RESULT_IDLE);
+}
+
+static void
+maybe_post_next_frame (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);
+  MetaMonitorManager *monitor_manager =
+    meta_backend_get_monitor_manager (backend);
+  MetaOnscreenNative *onscreen_native = META_ONSCREEN_NATIVE (onscreen);
+  MetaPowerSave power_save_mode;
+  MetaCrtcKms *crtc_kms = META_CRTC_KMS (onscreen_native->crtc);
+  MetaKmsCrtc *kms_crtc = meta_crtc_kms_get_kms_crtc (crtc_kms);
+  MetaKmsDevice *kms_device = meta_kms_crtc_get_device (kms_crtc);
+  MetaKmsUpdate *kms_update;
+  g_autoptr (MetaKmsFeedback) kms_feedback = NULL;
+  g_autoptr (ClutterFrame) frame = NULL;
+  MetaFrameNative *frame_native;
+  MtkRegion *region;
+  const MetaKmsResultListenerVtable *listener;
+  MetaKmsAssignPlaneFlag flip_flags;
+  gboolean is_direct_scanout;
+  MetaBackendNative *backend_native = META_BACKEND_NATIVE (backend);
+  MetaKms *kms = meta_backend_native_get_kms (backend_native);
+
+  COGL_TRACE_SCOPED_ANCHOR (MetaRendererNativePostKmsUpdate);
+
+  if (onscreen_native->next_frame == NULL ||
+      onscreen_native->posted_frame != NULL ||
+      onscreen_native->view == NULL ||
+      meta_kms_is_shutting_down (kms))
+    return;
+
+  power_save_mode = meta_monitor_manager_get_power_save_mode (monitor_manager);
+  if (power_save_mode != META_POWER_SAVE_ON)
+    {
+      meta_renderer_native_queue_power_save_page_flip (renderer_native,
+                                                       onscreen);
+      return;
+    }
+
+  frame = g_steal_pointer (&onscreen_native->next_frame);
+  frame_native = meta_frame_native_from_frame (frame);
+  region = meta_frame_native_get_damage (frame_native);
+
+  clear_superseded_frame (onscreen);
 
   kms_crtc = meta_crtc_kms_get_kms_crtc (META_CRTC_KMS (onscreen_native->crtc));
   kms_device = meta_kms_crtc_get_device (kms_crtc);
+  kms_update = meta_frame_native_ensure_kms_update (frame_native,
+                                                    kms_device);
 
-  power_save_mode = meta_monitor_manager_get_power_save_mode (monitor_manager);
-  if (power_save_mode == META_POWER_SAVE_ON)
+  is_direct_scanout = meta_frame_native_get_scanout (frame_native) != NULL;
+  if (is_direct_scanout)
     {
-      kms_update = meta_frame_native_ensure_kms_update (frame_native,
-                                                        kms_device);
-      meta_kms_update_add_result_listener (kms_update,
-                                           &swap_buffer_result_listener_vtable,
-                                           NULL,
-                                           onscreen_native,
-                                           NULL);
-
-      ensure_crtc_modes (onscreen, kms_update);
-      meta_onscreen_native_flip_crtc (onscreen,
-                                      onscreen_native->view,
-                                      onscreen_native->crtc,
-                                      kms_update,
-                                      META_KMS_ASSIGN_PLANE_FLAG_NONE,
-                                      region);
+      listener = &scanout_result_listener_vtable;
+      flip_flags = META_KMS_ASSIGN_PLANE_FLAG_DISABLE_IMPLICIT_SYNC;
     }
   else
     {
-      meta_renderer_native_queue_power_save_page_flip (renderer_native,
-                                                       onscreen);
-      clutter_frame_set_result (frame,
-                                CLUTTER_FRAME_RESULT_PENDING_PRESENTED);
+      listener = &swap_buffer_result_listener_vtable;
+      flip_flags = META_KMS_ASSIGN_PLANE_FLAG_NONE;
+    }
+
+  meta_kms_update_add_result_listener (kms_update,
+                                       listener,
+                                       NULL,
+                                       onscreen_native,
+                                       NULL);
+
+  ensure_crtc_modes (onscreen, kms_update);
+  if (!meta_onscreen_native_flip_crtc (onscreen,
+                                       frame,
+                                       onscreen_native->view,
+                                       onscreen_native->crtc,
+                                       kms_update,
+                                       flip_flags,
+                                       region))
+    {
+      kms_update = meta_frame_native_steal_kms_update (frame_native);
+      post_nonprimary_plane_update (onscreen_native, kms_update);
       return;
     }
 
+  onscreen_native->posted_frame = clutter_frame_ref (frame);
+
   COGL_TRACE_BEGIN_ANCHORED (MetaRendererNativePostKmsUpdate,
-                             "Meta::OnscreenNative::swap_buffers_with_damage#post_pending_update()");
+                             "Meta::OnscreenNative::maybe_post_next_frame#post_pending_update()");
 
   switch (renderer_gpu_data->mode)
     {
@@ -1550,8 +1665,6 @@ meta_onscreen_native_swap_buffers_with_damage (CoglOnscreen    *onscreen,
           kms_update = meta_frame_native_steal_kms_update (frame_native);
           meta_renderer_native_queue_mode_set_update (renderer_native,
                                                       kms_update);
-          clutter_frame_set_result (frame,
-                                    CLUTTER_FRAME_RESULT_PENDING_PRESENTED);
           return;
         }
       else if (meta_renderer_native_has_pending_mode_set (renderer_native))
@@ -1565,8 +1678,6 @@ meta_onscreen_native_swap_buffers_with_damage (CoglOnscreen    *onscreen,
 
           meta_frame_native_steal_kms_update (frame_native);
           meta_renderer_native_post_mode_set_updates (renderer_native);
-          clutter_frame_set_result (frame,
-                                    CLUTTER_FRAME_RESULT_PENDING_PRESENTED);
           return;
         }
       break;
@@ -1582,8 +1693,6 @@ meta_onscreen_native_swap_buffers_with_damage (CoglOnscreen    *onscreen,
                                                       kms_update);
 
           meta_renderer_native_post_mode_set_updates (renderer_native);
-          clutter_frame_set_result (frame,
-                                    CLUTTER_FRAME_RESULT_PENDING_PRESENTED);
           return;
         }
       break;
@@ -1591,13 +1700,14 @@ meta_onscreen_native_swap_buffers_with_damage (CoglOnscreen    *onscreen,
     }
 
   meta_topic (META_DEBUG_KMS,
-              "Posting primary plane composite update for CRTC %u (%s)",
+              "Posting primary plane %s update for CRTC %u (%s)",
+              is_direct_scanout ? "direct scanout" : "composite",
               meta_kms_crtc_get_id (kms_crtc),
               meta_kms_device_get_path (kms_device));
 
   kms_update = meta_frame_native_steal_kms_update (frame_native);
 
-  if (!secondary_gpu_used)
+  if (!onscreen_native->secondary_gpu_used)
     {
       int sync_fd;
 
@@ -1607,13 +1717,6 @@ meta_onscreen_native_swap_buffers_with_damage (CoglOnscreen    *onscreen,
 
   meta_kms_device_post_update (kms_device, kms_update,
                                META_KMS_UPDATE_FLAG_NONE);
-  clutter_frame_set_result (frame, CLUTTER_FRAME_RESULT_PENDING_PRESENTED);
-  return;
-
-swap_failed:
-  frame_info->flags |= COGL_FRAME_INFO_FLAG_SYMBOLIC;
-  meta_onscreen_native_notify_frame_complete (onscreen);
-  clutter_frame_set_result (frame, CLUTTER_FRAME_RESULT_IDLE);
 }
 
 gboolean
@@ -1681,30 +1784,29 @@ scanout_result_feedback (const MetaKmsFeedback *kms_feedback,
                         G_IO_ERROR_PERMISSION_DENIED))
     {
       ClutterStageView *view = CLUTTER_STAGE_VIEW (onscreen_native->view);
-      ClutterFrame *next_frame = onscreen_native->next_frame;
-      MetaFrameNative *next_frame_native =
-        meta_frame_native_from_frame (next_frame);
+      ClutterFrame *posted_frame = onscreen_native->posted_frame;
+      MetaFrameNative *posted_frame_native =
+        meta_frame_native_from_frame (posted_frame);
       CoglScanout *scanout =
-        meta_frame_native_get_scanout (next_frame_native);
+        meta_frame_native_get_scanout (posted_frame_native);
 
       g_warning ("Direct scanout page flip failed: %s", error->message);
 
       cogl_scanout_notify_failed (scanout, onscreen);
-      clutter_stage_view_add_redraw_clip (view, NULL);
-      clutter_stage_view_schedule_update_now (view);
+      if (onscreen_native->next_frame == NULL)
+        {
+          clutter_stage_view_add_redraw_clip (view, NULL);
+          clutter_stage_view_schedule_update_now (view);
+        }
     }
 
   frame_info = cogl_onscreen_peek_head_frame_info (onscreen);
   frame_info->flags |= COGL_FRAME_INFO_FLAG_SYMBOLIC;
 
   meta_onscreen_native_notify_frame_complete (onscreen);
-  meta_onscreen_native_clear_next_fb (onscreen);
+  meta_onscreen_native_clear_posted_fb (onscreen);
 }
 
-static const MetaKmsResultListenerVtable scanout_result_listener_vtable = {
-  .feedback = scanout_result_feedback,
-};
-
 static gboolean
 meta_onscreen_native_direct_scanout (CoglOnscreen   *onscreen,
                                      CoglScanout    *scanout,
@@ -1720,43 +1822,15 @@ meta_onscreen_native_direct_scanout (CoglOnscreen   *onscreen,
   CoglRendererEGL *cogl_renderer_egl = cogl_renderer->winsys;
   MetaRendererNativeGpuData *renderer_gpu_data = cogl_renderer_egl->platform;
   MetaRendererNative *renderer_native = renderer_gpu_data->renderer_native;
-  MetaRenderer *renderer = META_RENDERER (renderer_native);
-  MetaBackend *backend = meta_renderer_get_backend (renderer);
-  MetaMonitorManager *monitor_manager =
-    meta_backend_get_monitor_manager (backend);
-  MetaPowerSave power_save_mode;
   ClutterFrame *frame = user_data;
   MetaFrameNative *frame_native = meta_frame_native_from_frame (frame);
-  MetaKmsCrtc *kms_crtc;
-  MetaKmsDevice *kms_device;
-  MetaKmsUpdate *kms_update;
-
-  power_save_mode = meta_monitor_manager_get_power_save_mode (monitor_manager);
-  if (power_save_mode != META_POWER_SAVE_ON)
-    {
-      g_set_error_literal (error,
-                           COGL_SCANOUT_ERROR,
-                           COGL_SCANOUT_ERROR_INHIBITED,
-                           "Direct scanout is inhibited during power saving mode");
-      return FALSE;
-    }
-
-  if (meta_renderer_native_has_pending_mode_set (renderer_native))
-    {
-      g_set_error_literal (error,
-                           COGL_SCANOUT_ERROR,
-                           COGL_SCANOUT_ERROR_INHIBITED,
-                           "Direct scanout is inhibited when a mode set is pending");
-      return FALSE;
-    }
 
   renderer_gpu_data = meta_renderer_native_get_gpu_data (renderer_native,
                                                          render_gpu);
 
   g_warn_if_fail (renderer_gpu_data->mode == META_RENDERER_NATIVE_MODE_GBM);
 
-  g_warn_if_fail (!onscreen_native->next_frame);
-  onscreen_native->next_frame = clutter_frame_ref (frame);
+  assign_next_frame (onscreen_native, frame);
 
   meta_frame_native_set_scanout (frame_native, scanout);
   meta_frame_native_set_buffer (frame_native,
@@ -1767,33 +1841,9 @@ meta_onscreen_native_direct_scanout (CoglOnscreen   *onscreen,
   if (cogl_context_has_feature (cogl_context, COGL_FEATURE_ID_TIMESTAMP_QUERY))
     frame_info->has_valid_gpu_rendering_duration = TRUE;
 
-  kms_crtc = meta_crtc_kms_get_kms_crtc (META_CRTC_KMS (onscreen_native->crtc));
-  kms_device = meta_kms_crtc_get_device (kms_crtc);
-  kms_update = meta_frame_native_ensure_kms_update (frame_native, kms_device);
-
-  meta_kms_update_add_result_listener (kms_update,
-                                       &scanout_result_listener_vtable,
-                                       NULL,
-                                       onscreen_native,
-                                       NULL);
-
-  meta_onscreen_native_flip_crtc (onscreen,
-                                  onscreen_native->view,
-                                  onscreen_native->crtc,
-                                  kms_update,
-                                  META_KMS_ASSIGN_PLANE_FLAG_DISABLE_IMPLICIT_SYNC,
-                                  NULL);
-
-  meta_topic (META_DEBUG_KMS,
-              "Posting direct scanout update for CRTC %u (%s)",
-              meta_kms_crtc_get_id (kms_crtc),
-              meta_kms_device_get_path (kms_device));
-
-  kms_update = meta_frame_native_steal_kms_update (frame_native);
-  meta_kms_device_post_update (kms_device, kms_update,
-                               META_KMS_UPDATE_FLAG_NONE);
   clutter_frame_set_result (frame, CLUTTER_FRAME_RESULT_PENDING_PRESENTED);
 
+  maybe_post_next_frame (onscreen);
   return TRUE;
 }
 
@@ -1995,21 +2045,74 @@ meta_onscreen_native_finish_frame (CoglOnscreen *onscreen,
   MetaFrameNative *frame_native = meta_frame_native_from_frame (frame);
   MetaKmsUpdate *kms_update;
 
-  kms_update = meta_frame_native_steal_kms_update (frame_native);
-  if (!kms_update)
+  onscreen_native->needs_flush |= meta_kms_device_handle_flush (kms_device,
+                                                                kms_crtc);
+
+  if (!meta_frame_native_has_kms_update (frame_native))
     {
-      if (meta_kms_device_handle_flush (kms_device, kms_crtc))
-        {
-          kms_update = meta_kms_update_new (kms_device);
-          meta_kms_update_set_flushing (kms_update, kms_crtc);
-        }
-      else
+      if (!onscreen_native->needs_flush ||
+          onscreen_native->posted_frame != NULL)
         {
           clutter_frame_set_result (frame, CLUTTER_FRAME_RESULT_IDLE);
           return;
         }
     }
 
+  if (onscreen_native->posted_frame != NULL &&
+      onscreen_native->next_frame == NULL)
+    {
+      g_return_if_fail (meta_frame_native_has_kms_update (frame_native));
+      assign_next_frame (onscreen_native, frame);
+      clutter_frame_set_result (frame, CLUTTER_FRAME_RESULT_PENDING_PRESENTED);
+      return;
+    }
+
+  kms_update = meta_frame_native_steal_kms_update (frame_native);
+
+  if (onscreen_native->posted_frame != NULL &&
+      onscreen_native->next_frame != NULL)
+    {
+      MetaFrameNative *next_frame_native;
+      MetaKmsUpdate *next_kms_update;
+
+      g_return_if_fail (kms_update);
+
+      next_frame_native =
+        meta_frame_native_from_frame (onscreen_native->next_frame);
+      next_kms_update =
+        meta_frame_native_ensure_kms_update (next_frame_native, kms_device);
+      meta_kms_update_merge_from (next_kms_update, kms_update);
+      meta_kms_update_free (kms_update);
+      clutter_frame_set_result (frame, CLUTTER_FRAME_RESULT_IDLE);
+      return;
+    }
+
+  if (!kms_update)
+    {
+      kms_update = meta_kms_update_new (kms_device);
+      g_warn_if_fail (onscreen_native->needs_flush);
+    }
+
+  if (onscreen_native->needs_flush)
+    {
+      meta_kms_update_set_flushing (kms_update, kms_crtc);
+      onscreen_native->needs_flush = FALSE;
+    }
+
+  post_nonprimary_plane_update (onscreen_native, kms_update);
+
+  clutter_frame_set_result (frame, CLUTTER_FRAME_RESULT_PENDING_PRESENTED);
+}
+
+static void
+post_nonprimary_plane_update (MetaOnscreenNative *onscreen_native,
+                              MetaKmsUpdate      *kms_update)
+{
+  MetaCrtc *crtc = onscreen_native->crtc;
+  MetaKmsCrtc *kms_crtc = meta_crtc_kms_get_kms_crtc (META_CRTC_KMS (crtc));
+  MetaKmsDevice *kms_device = meta_kms_crtc_get_device (kms_crtc);
+  g_autoptr (MetaKmsFeedback) kms_feedback = NULL;
+
   meta_kms_update_add_result_listener (kms_update,
                                        &finish_frame_result_listener_vtable,
                                        NULL,
@@ -2032,7 +2135,15 @@ meta_onscreen_native_finish_frame (CoglOnscreen *onscreen,
   meta_kms_update_set_flushing (kms_update, kms_crtc);
   meta_kms_device_post_update (kms_device, kms_update,
                                META_KMS_UPDATE_FLAG_NONE);
-  clutter_frame_set_result (frame, CLUTTER_FRAME_RESULT_PENDING_PRESENTED);
+}
+
+void
+meta_onscreen_native_discard_pending_swaps (CoglOnscreen *onscreen)
+{
+  MetaOnscreenNative *onscreen_native = META_ONSCREEN_NATIVE (onscreen);
+
+  g_clear_pointer (&onscreen_native->superseded_frame, clutter_frame_unref);
+  g_clear_pointer (&onscreen_native->next_frame, clutter_frame_unref);
 }
 
 static gboolean
@@ -2722,7 +2833,10 @@ init_secondary_gpu_state (MetaRendererNative  *renderer_native,
                                                   onscreen,
                                                   renderer_gpu_data,
                                                   &local_error))
-        return TRUE;
+        {
+          onscreen_native->secondary_gpu_used = TRUE;
+          return TRUE;
+        }
 
       g_warning ("Secondary GPU initialization failed (%s). "
                  "Falling back to GPU-less mode instead, so the "
@@ -2856,6 +2970,8 @@ meta_onscreen_native_dispose (GObject *object)
   meta_onscreen_native_detach (onscreen_native);
 
   g_clear_pointer (&onscreen_native->next_frame, clutter_frame_unref);
+  g_clear_pointer (&onscreen_native->superseded_frame, clutter_frame_unref);
+  g_clear_pointer (&onscreen_native->posted_frame, clutter_frame_unref);
   g_clear_pointer (&onscreen_native->presented_frame, clutter_frame_unref);
 
   renderer_gpu_data =
diff --git a/src/backends/native/meta-onscreen-native.h b/src/backends/native/meta-onscreen-native.h
index 0e11933..e30357d 100644
--- a/src/backends/native/meta-onscreen-native.h
+++ b/src/backends/native/meta-onscreen-native.h
@@ -48,6 +48,8 @@ void meta_onscreen_native_dummy_power_save_page_flip (CoglOnscreen *onscreen);
 gboolean meta_onscreen_native_is_buffer_scanout_compatible (CoglOnscreen *onscreen,
                                                             CoglScanout  *scanout);
 
+void meta_onscreen_native_discard_pending_swaps (CoglOnscreen *onscreen);
+
 void meta_onscreen_native_set_view (CoglOnscreen     *onscreen,
                                     MetaRendererView *view);
 
diff --git a/src/backends/native/meta-renderer-native.c b/src/backends/native/meta-renderer-native.c
index 6a421b0..59a843c 100644
--- a/src/backends/native/meta-renderer-native.c
+++ b/src/backends/native/meta-renderer-native.c
@@ -740,17 +740,24 @@ configure_disabled_crtcs (MetaKmsDevice      *kms_device,
     }
 }
 
-static void
+static gboolean
 dummy_power_save_page_flip_cb (gpointer user_data)
 {
   MetaRendererNative *renderer_native = user_data;
+  g_autolist (GObject) old_list = NULL;
+
+  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_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;
 }
 
 void
@@ -759,12 +766,15 @@ 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 =
-        g_timeout_add_once (timeout_ms,
-                            dummy_power_save_page_flip_cb,
-                            renderer_native);
+        g_timeout_add (timeout_ms,
+                       dummy_power_save_page_flip_cb,
+                       renderer_native);
     }
 
   renderer_native->power_save_page_flip_onscreens =
@@ -1565,6 +1575,26 @@ detach_onscreens (MetaRenderer *renderer)
     }
 }
 
+static void
+discard_pending_swaps (MetaRenderer *renderer)
+{
+  GList *views = meta_renderer_get_views (renderer);;
+  GList *l;
+
+  for (l = views; l; l = l->next)
+    {
+      ClutterStageView *stage_view = l->data;
+      CoglFramebuffer *fb = clutter_stage_view_get_onscreen (stage_view);
+      CoglOnscreen *onscreen;
+
+      if (!COGL_IS_ONSCREEN (fb))
+        continue;
+
+      onscreen = COGL_ONSCREEN (fb);
+      meta_onscreen_native_discard_pending_swaps (onscreen);
+    }
+}
+
 static void
 meta_renderer_native_rebuild_views (MetaRenderer *renderer)
 {
@@ -1575,6 +1605,7 @@ meta_renderer_native_rebuild_views (MetaRenderer *renderer)
   MetaRendererClass *parent_renderer_class =
     META_RENDERER_CLASS (meta_renderer_native_parent_class);
 
+  discard_pending_swaps (renderer);
   meta_kms_discard_pending_page_flips (kms);
   g_hash_table_remove_all (renderer_native->mode_set_updates);
 
diff --git a/src/tests/native-kms-render.c b/src/tests/native-kms-render.c
index aea2260..9ce5064 100644
--- a/src/tests/native-kms-render.c
+++ b/src/tests/native-kms-render.c
@@ -39,6 +39,8 @@
 #include "tests/meta-wayland-test-driver.h"
 #include "tests/meta-wayland-test-utils.h"
 
+#define N_FRAMES_PER_TEST 30
+
 typedef struct
 {
   int number_of_frames_left;
@@ -46,15 +48,21 @@ typedef struct
 
   struct {
     int n_paints;
-    uint32_t fb_id;
+    int n_presentations;
+    int n_direct_scanouts;
+    GList *fb_ids;
   } scanout;
 
   gboolean wait_for_scanout;
+  gboolean expect_double_buffering;
 
   struct {
-    gboolean scanout_sabotaged;
-    gboolean fallback_painted;
-    guint repaint_guard_id;
+    int last_frame_started;
+    int last_frame_presented;
+    int frame_sabotaged;
+    int first_scanout;
+    int fallbacks_painted;
+    gboolean first_scanout_presented;
     ClutterStageView *scanout_failed_view;
   } scanout_fallback;
 } KmsRenderingTest;
@@ -101,7 +109,7 @@ meta_test_kms_render_basic (void)
   gulong handler_id;
 
   test = (KmsRenderingTest) {
-    .number_of_frames_left = 10,
+    .number_of_frames_left = N_FRAMES_PER_TEST,
     .loop = g_main_loop_new (NULL, FALSE),
   };
   handler_id = g_signal_connect (stage, "after-update",
@@ -123,7 +131,6 @@ on_scanout_before_update (ClutterStage     *stage,
                           KmsRenderingTest *test)
 {
   test->scanout.n_paints = 0;
-  test->scanout.fb_id = 0;
 }
 
 static void
@@ -135,6 +142,7 @@ on_scanout_before_paint (ClutterStage     *stage,
   CoglScanout *scanout;
   CoglScanoutBuffer *scanout_buffer;
   MetaDrmBuffer *buffer;
+  uint32_t fb_id;
 
   scanout = clutter_stage_view_peek_scanout (stage_view);
   if (!scanout)
@@ -143,8 +151,13 @@ on_scanout_before_paint (ClutterStage     *stage,
   scanout_buffer = cogl_scanout_get_buffer (scanout);
   g_assert_true (META_IS_DRM_BUFFER (scanout_buffer));
   buffer = META_DRM_BUFFER (scanout_buffer);
-  test->scanout.fb_id = meta_drm_buffer_get_fb_id (buffer);
-  g_assert_cmpuint (test->scanout.fb_id, >, 0);
+  fb_id = meta_drm_buffer_get_fb_id (buffer);
+  g_assert_cmpuint (fb_id, >, 0);
+  test->scanout.fb_ids = g_list_append (test->scanout.fb_ids,
+                                        GUINT_TO_POINTER (fb_id));
+
+  /* Triple buffering, but no higher */
+  g_assert_cmpuint (g_list_length (test->scanout.fb_ids), <=, 2);
 }
 
 static void
@@ -173,12 +186,12 @@ on_scanout_presented (ClutterStage     *stage,
   MetaDeviceFile *device_file;
   GError *error = NULL;
   drmModeCrtc *drm_crtc;
+  uint32_t first_fb_id_expected;
 
-  if (test->wait_for_scanout && test->scanout.n_paints > 0)
+  if (test->wait_for_scanout && test->scanout.fb_ids == NULL)
     return;
 
-  if (test->wait_for_scanout && test->scanout.fb_id == 0)
-    return;
+  test->scanout.n_presentations++;
 
   device_pool = meta_backend_native_get_device_pool (backend_native);
 
@@ -197,15 +210,36 @@ on_scanout_presented (ClutterStage     *stage,
   drm_crtc = drmModeGetCrtc (meta_device_file_get_fd (device_file),
                              meta_kms_crtc_get_id (kms_crtc));
   g_assert_nonnull (drm_crtc);
-  if (test->scanout.fb_id == 0)
-    g_assert_cmpuint (drm_crtc->buffer_id, !=, test->scanout.fb_id);
+
+  /* Triple buffering remains in effect even when transitioning to
+   * direct scanout. So we expect the first presentation after
+   * wait_for_scanout will still be composited and won't match the head of
+   * fb_ids yet...
+   */
+  if (test->scanout.fb_ids &&
+      (test->expect_double_buffering || test->scanout.n_presentations > 1))
+    {
+      test->scanout.n_direct_scanouts++;
+      first_fb_id_expected = GPOINTER_TO_UINT (test->scanout.fb_ids->data);
+      test->scanout.fb_ids = g_list_delete_link (test->scanout.fb_ids,
+                                                 test->scanout.fb_ids);
+      g_assert_cmpuint (drm_crtc->buffer_id, ==, first_fb_id_expected);
+    }
   else
-    g_assert_cmpuint (drm_crtc->buffer_id, ==, test->scanout.fb_id);
+    {
+      first_fb_id_expected = 0;
+      g_assert_cmpuint (drm_crtc->buffer_id, !=, first_fb_id_expected);
+    }
+
   drmModeFreeCrtc (drm_crtc);
 
   meta_device_file_release (device_file);
 
-  g_main_loop_quit (test->loop);
+  test->number_of_frames_left--;
+  if (test->number_of_frames_left <= 0)
+    g_main_loop_quit (test->loop);
+  else
+    clutter_actor_queue_redraw (CLUTTER_ACTOR (stage));
 }
 
 typedef enum
@@ -244,7 +278,9 @@ meta_test_kms_render_client_scanout (void)
   g_assert_nonnull (wayland_test_client);
 
   test = (KmsRenderingTest) {
+    .number_of_frames_left = N_FRAMES_PER_TEST,
     .loop = g_main_loop_new (NULL, FALSE),
+    .scanout = {0},
     .wait_for_scanout = TRUE,
   };
 
@@ -270,7 +306,8 @@ meta_test_kms_render_client_scanout (void)
   clutter_actor_queue_redraw (CLUTTER_ACTOR (stage));
   g_main_loop_run (test.loop);
 
-  g_assert_cmpuint (test.scanout.fb_id, >, 0);
+  g_assert_cmpint (test.scanout.n_presentations, ==, N_FRAMES_PER_TEST);
+  g_assert_cmpint (test.scanout.n_direct_scanouts, ==, N_FRAMES_PER_TEST - 1);
 
   g_debug ("Unmake fullscreen");
   window = meta_find_window_from_title (test_context, "dma-buf-scanout-test");
@@ -292,10 +329,16 @@ meta_test_kms_render_client_scanout (void)
   g_assert_cmpint (buffer_rect.y, ==, 10);
 
   test.wait_for_scanout = FALSE;
+  test.expect_double_buffering = TRUE; /* because wait_for_sync_point */
+  test.number_of_frames_left = N_FRAMES_PER_TEST;
+  test.scanout.n_presentations = 0;
+  test.scanout.n_direct_scanouts = 0;
+
   clutter_actor_queue_redraw (CLUTTER_ACTOR (stage));
   g_main_loop_run (test.loop);
 
-  g_assert_cmpuint (test.scanout.fb_id, ==, 0);
+  g_assert_cmpint (test.scanout.n_presentations, ==, N_FRAMES_PER_TEST);
+  g_assert_cmpint (test.scanout.n_direct_scanouts, ==, 1);
 
   g_debug ("Moving back to 0, 0");
   meta_window_move_frame (window, TRUE, 0, 0);
@@ -307,10 +350,16 @@ meta_test_kms_render_client_scanout (void)
   g_assert_cmpint (buffer_rect.y, ==, 0);
 
   test.wait_for_scanout = TRUE;
+  test.expect_double_buffering = FALSE;
+  test.number_of_frames_left = N_FRAMES_PER_TEST;
+  test.scanout.n_presentations = 0;
+  test.scanout.n_direct_scanouts = 0;
+
   clutter_actor_queue_redraw (CLUTTER_ACTOR (stage));
   g_main_loop_run (test.loop);
 
-  g_assert_cmpuint (test.scanout.fb_id, >, 0);
+  g_assert_cmpint (test.scanout.n_presentations, ==, N_FRAMES_PER_TEST);
+  g_assert_cmpint (test.scanout.n_direct_scanouts, ==, N_FRAMES_PER_TEST - 1);
 
   g_signal_handler_disconnect (stage, before_update_handler_id);
   g_signal_handler_disconnect (stage, before_paint_handler_id);
@@ -322,30 +371,6 @@ meta_test_kms_render_client_scanout (void)
   g_main_loop_unref (test.loop);
 }
 
-static gboolean
-needs_repainted_guard (gpointer user_data)
-{
-  g_assert_not_reached ();
-  return G_SOURCE_REMOVE;
-}
-
-static void
-scanout_fallback_result_feedback (const MetaKmsFeedback *kms_feedback,
-                                  gpointer               user_data)
-{
-  KmsRenderingTest *test = user_data;
-
-  g_assert_cmpuint (test->scanout_fallback.repaint_guard_id, ==, 0);
-  g_assert_nonnull (test->scanout_fallback.scanout_failed_view);
-
-  test->scanout_fallback.repaint_guard_id =
-    g_idle_add_full (G_PRIORITY_LOW, needs_repainted_guard, test, NULL);
-}
-
-static const MetaKmsResultListenerVtable scanout_fallback_result_listener_vtable = {
-  .feedback = scanout_fallback_result_feedback,
-};
-
 static void
 on_scanout_fallback_before_paint (ClutterStage     *stage,
                                   ClutterStageView *stage_view,
@@ -356,15 +381,43 @@ on_scanout_fallback_before_paint (ClutterStage     *stage,
   MetaCrtc *crtc = meta_renderer_view_get_crtc (view);
   MetaKmsCrtc *kms_crtc = meta_crtc_kms_get_kms_crtc (META_CRTC_KMS (crtc));
   MetaKmsDevice *kms_device = meta_kms_crtc_get_device (kms_crtc);
-  MetaFrameNative *frame_native = meta_frame_native_from_frame (frame);
   CoglScanout *scanout;
-  MetaKmsUpdate *kms_update;
+  int this_frame;
+
+  /* We don't know exactly how many frames the test will take due to:
+   *  1. Client scanouts taking a while to get started.
+   *  2. Triple buffering being asynchronous so one can't infer which DRM
+   *     calls have completed from just the painting state.
+   *  3. Atomic commits now live in a separate thread!
+   *
+   * So ensure there's always a reason to start the next frame and
+   * the test never hangs;
+   */
+  clutter_actor_queue_redraw (CLUTTER_ACTOR (stage));
+
+  this_frame = ++test->scanout_fallback.last_frame_started;
 
   scanout = clutter_stage_view_peek_scanout (stage_view);
   if (!scanout)
     return;
 
-  g_assert_false (test->scanout_fallback.scanout_sabotaged);
+  if (!test->scanout_fallback.first_scanout)
+    {
+      test->scanout_fallback.first_scanout = this_frame;
+      return;
+    }
+
+  /* Keep the test simple: Only one frame is ever sabotaged and it is
+   * definitely a direct scanout. But we can't rely on the value of 'scanout'
+   * alone because that may be non-NULL even when the next commit is going
+   * to be composited (triple buffering). So wait until first_scanout_presented
+   * before doing the sabotage.
+   */
+  if (test->scanout_fallback.frame_sabotaged ||
+      !test->scanout_fallback.first_scanout_presented)
+    return;
+
+  test->scanout_fallback.frame_sabotaged = this_frame;
 
   if (is_atomic_mode_setting (kms_device))
     {
@@ -375,17 +428,6 @@ on_scanout_fallback_before_paint (ClutterStage     *stage,
       drm_mock_queue_error (DRM_MOCK_CALL_PAGE_FLIP, EINVAL);
       drm_mock_queue_error (DRM_MOCK_CALL_SET_CRTC, EINVAL);
     }
-
-  test->scanout_fallback.scanout_sabotaged = TRUE;
-
-  kms_update = meta_frame_native_ensure_kms_update (frame_native, kms_device);
-  meta_kms_update_add_result_listener (kms_update,
-                                       &scanout_fallback_result_listener_vtable,
-                                       NULL,
-                                       test,
-                                       NULL);
-
-  test->scanout_fallback.scanout_failed_view = stage_view;
 }
 
 static void
@@ -395,13 +437,14 @@ on_scanout_fallback_paint_view (ClutterStage     *stage,
                                 ClutterFrame     *frame,
                                 KmsRenderingTest *test)
 {
-  if (test->scanout_fallback.scanout_sabotaged)
-    {
-      g_assert_cmpuint (test->scanout_fallback.repaint_guard_id, !=, 0);
-      g_clear_handle_id (&test->scanout_fallback.repaint_guard_id,
-                         g_source_remove);
-      test->scanout_fallback.fallback_painted = TRUE;
-    }
+  /* With triple buffering, usable fallback paints may occur even before the
+   * failing commit they are needed to replace. So it would be too racy to
+   * check if the a notification of the failed commit has been emitted yet.
+   * Just make sure there has been at least one repaint after the sabotage AND
+   * that at the end of the test g_test_assert_expected_messages passes.
+   */
+  if (test->scanout_fallback.frame_sabotaged)
+    test->scanout_fallback.fallbacks_painted++;
 }
 
 static void
@@ -410,11 +453,21 @@ on_scanout_fallback_presented (ClutterStage     *stage,
                                ClutterFrameInfo *frame_info,
                                KmsRenderingTest *test)
 {
-  if (!test->scanout_fallback.scanout_sabotaged)
-    return;
+  int this_frame;
+
+  if (test->scanout_fallback.last_frame_started <= 0)
+    return;  /* Leftovers from previous tests. Ignore. */
+
+  this_frame = ++test->scanout_fallback.last_frame_presented;
+  if (this_frame >= test->scanout_fallback.first_scanout)
+    test->scanout_fallback.first_scanout_presented = TRUE;
 
-  g_assert_true (test->scanout_fallback.fallback_painted);
-  g_main_loop_quit (test->loop);
+  if (test->scanout_fallback.fallbacks_painted > 0)
+    g_main_loop_quit (test->loop);
+
+  test->number_of_frames_left--;
+  g_assert_cmpint (test->number_of_frames_left, >, 0);
+  clutter_actor_queue_redraw (CLUTTER_ACTOR (stage));
 }
 
 static void
@@ -443,6 +496,7 @@ meta_test_kms_render_client_scanout_fallback (void)
   g_assert_nonnull (wayland_test_client);
 
   test = (KmsRenderingTest) {
+    .number_of_frames_left = N_FRAMES_PER_TEST,
     .loop = g_main_loop_new (NULL, FALSE),
   };
 
@@ -464,6 +518,16 @@ meta_test_kms_render_client_scanout_fallback (void)
   g_main_loop_run (test.loop);
   g_main_loop_unref (test.loop);
 
+  g_test_message ("Test ending with:\n"
+                  "\tfallbacks_painted: %d\n"
+                  "\tlast_frame_started: %d\n"
+                  "\tlast_frame_presented: %d\n"
+                  "\tframe_sabotaged: %d",
+                  test.scanout_fallback.fallbacks_painted,
+                  test.scanout_fallback.last_frame_started,
+                  test.scanout_fallback.last_frame_presented,
+                  test.scanout_fallback.frame_sabotaged);
+
   g_test_assert_expected_messages ();
 
   g_signal_handler_disconnect (stage, before_paint_handler_id);
