macos: fix cairo renderer with double buffering
authorChristian Hergert <christian@hergert.me>
Mon, 28 Feb 2022 09:42:48 +0000 (01:42 -0800)
committerChristian Hergert <chergert@redhat.com>
Wed, 16 Mar 2022 19:24:11 +0000 (12:24 -0700)
If we are double buffering surfaces with IOSurface then we need to copy
the area that was damaged in the previous frame to the back buffer. This
can be done with IOSurface but we need to hold the read-only lock so that
we don't cause the underlying IOSurface contents to be invalidated.

Additionally, since this is only used in the context of rendering to a
GdkMacosSurface, we know the life-time of the cairo_surface_t and can
simply lock/unlock the IOSurface buffer from begin_frame/end_frame to have
the buffer flushing semantics we want.

To ensure that we don't over damage, we store the damage in begin_frame
(and copy it) and then subtract it from the next frames damage to determine
the smallest amount we need to copy (taking scale factor into account).

We don't care to modify the damage region to swapBuffers because they
already have the right contents and could potentially fall into another
tile anyway and we'd like to avoid damaging that.

Fixes #4735

gdk/macos/gdkmacosbuffer.c
gdk/macos/gdkmacoscairocontext.c

index 46a050446126e8a96bdebfbe4c745ba1c14c84a4..eb8a719dbcf4bab359082c19fe117e3747fd6fb8 100644 (file)
@@ -281,7 +281,7 @@ _gdk_macos_buffer_set_damage (GdkMacosBuffer *self,
     return;
 
   g_clear_pointer (&self->damage, cairo_region_destroy);
-  self->damage = cairo_region_reference (damage);
+  self->damage = cairo_region_copy (damage);
 }
 
 gpointer
index 041f1193e68c39629b40b059c764871b7dc04e8f..31eecd5df682ab2bd94c48af28007709463677a8 100644 (file)
@@ -42,19 +42,6 @@ struct _GdkMacosCairoContextClass
 
 G_DEFINE_TYPE (GdkMacosCairoContext, _gdk_macos_cairo_context, GDK_TYPE_CAIRO_CONTEXT)
 
-static const cairo_user_data_key_t buffer_key;
-
-static void
-unlock_buffer (gpointer data)
-{
-  GdkMacosBuffer *buffer = data;
-
-  g_assert (GDK_IS_MACOS_BUFFER (buffer));
-
-  _gdk_macos_buffer_unlock (buffer);
-  g_clear_object (&buffer);
-}
-
 static cairo_t *
 _gdk_macos_cairo_context_cairo_create (GdkCairoContext *cairo_context)
 {
@@ -106,12 +93,9 @@ _gdk_macos_cairo_context_cairo_create (GdkCairoContext *cairo_context)
                                                        stride);
   cairo_surface_set_device_scale (image_surface, scale, scale);
 
-  /* Lock the buffer so we can modify it safely */
-  _gdk_macos_buffer_lock (buffer);
-  cairo_surface_set_user_data (image_surface,
-                               &buffer_key,
-                               g_object_ref (buffer),
-                               unlock_buffer);
+  /* The buffer should already be locked at this point, and will
+   * be unlocked as part of end_frame.
+   */
 
   if (!(cr = cairo_create (image_surface)))
     goto failure;
@@ -158,6 +142,52 @@ failure:
   return cr;
 }
 
+static void
+copy_surface_data (GdkMacosBuffer       *from,
+                   GdkMacosBuffer       *to,
+                   const cairo_region_t *region,
+                   int                   scale)
+{
+  const guint8 *from_base;
+  guint8 *to_base;
+  guint from_stride;
+  guint to_stride;
+  guint n_rects;
+
+  g_assert (GDK_IS_MACOS_BUFFER (from));
+  g_assert (GDK_IS_MACOS_BUFFER (to));
+  g_assert (region != NULL);
+  g_assert (!cairo_region_is_empty (region));
+
+  from_base = _gdk_macos_buffer_get_data (from);
+  from_stride = _gdk_macos_buffer_get_stride (from);
+
+  to_base = _gdk_macos_buffer_get_data (to);
+  to_stride = _gdk_macos_buffer_get_stride (to);
+
+  n_rects = cairo_region_num_rectangles (region);
+
+  for (guint i = 0; i < n_rects; i++)
+    {
+      cairo_rectangle_int_t rect;
+      int y2;
+
+      cairo_region_get_rectangle (region, i, &rect);
+
+      rect.y *= scale;
+      rect.height *= scale;
+      rect.x *= scale;
+      rect.width *= scale;
+
+      y2 = rect.y + rect.height;
+
+      for (int y = rect.y; y < y2; y++)
+        memcpy (&to_base[y * to_stride + rect.x * 4],
+                &from_base[y * from_stride + rect.x * 4],
+                rect.width * 4);
+    }
+}
+
 static void
 _gdk_macos_cairo_context_begin_frame (GdkDrawContext *draw_context,
                                       gboolean        prefers_high_depth,
@@ -165,34 +195,68 @@ _gdk_macos_cairo_context_begin_frame (GdkDrawContext *draw_context,
 {
   GdkMacosCairoContext *self = (GdkMacosCairoContext *)draw_context;
   GdkMacosBuffer *buffer;
-  GdkSurface *surface;
+  GdkMacosSurface *surface;
 
   g_assert (GDK_IS_MACOS_CAIRO_CONTEXT (self));
 
   [CATransaction begin];
   [CATransaction setDisableActions:YES];
 
-  surface = gdk_draw_context_get_surface (draw_context);
-  buffer = _gdk_macos_surface_get_buffer (GDK_MACOS_SURFACE (surface));
+  surface = GDK_MACOS_SURFACE (gdk_draw_context_get_surface (draw_context));
+  buffer = _gdk_macos_surface_get_buffer (surface);
 
   _gdk_macos_buffer_set_damage (buffer, region);
   _gdk_macos_buffer_set_flipped (buffer, FALSE);
+
+  _gdk_macos_buffer_lock (buffer);
+
+  /* If there is damage that was on the previous frame that is not on
+   * this frame, we need to copy that rendered region over to the back
+   * buffer so that when swapping buffers, we still have that content.
+   * This is done with a read-only lock on the IOSurface to avoid
+   * invalidating the buffer contents.
+   */
+  if (surface->front != NULL)
+    {
+      const cairo_region_t *previous = _gdk_macos_buffer_get_damage (surface->front);
+
+      if (previous != NULL)
+        {
+          cairo_region_t *copy;
+
+          copy = cairo_region_copy (previous);
+          cairo_region_subtract (copy, region);
+
+          if (!cairo_region_is_empty (copy))
+            {
+              int scale = gdk_surface_get_scale_factor (GDK_SURFACE (surface));
+
+              _gdk_macos_buffer_read_lock (surface->front);
+              copy_surface_data (surface->front, buffer, copy, scale);
+              _gdk_macos_buffer_read_unlock (surface->front);
+            }
+
+          cairo_region_destroy (copy);
+        }
+    }
 }
 
 static void
 _gdk_macos_cairo_context_end_frame (GdkDrawContext *draw_context,
                                     cairo_region_t *painted)
 {
+  GdkMacosCairoContext *self = (GdkMacosCairoContext *)draw_context;
+  GdkMacosSurface *surface;
   GdkMacosBuffer *buffer;
-  GdkSurface *surface;
 
-  g_assert (GDK_IS_MACOS_CAIRO_CONTEXT (draw_context));
+  g_assert (GDK_IS_MACOS_CAIRO_CONTEXT (self));
 
-  surface = gdk_draw_context_get_surface (draw_context);
-  buffer = _gdk_macos_surface_get_buffer (GDK_MACOS_SURFACE (surface));
+  surface = GDK_MACOS_SURFACE (gdk_draw_context_get_surface (draw_context));
+  buffer = _gdk_macos_surface_get_buffer (surface);
+
+  _gdk_macos_buffer_unlock (buffer);
 
-  _gdk_macos_surface_swap_buffers (GDK_MACOS_SURFACE (surface), painted);
-  _gdk_macos_buffer_set_damage (buffer, NULL);
+  _gdk_macos_surface_swap_buffers (surface, painted);
 
   [CATransaction commit];
 }