diff --git a/gsk/gskoffload.c b/gsk/gskoffload.c
new file mode 100644
index 0000000000..89c44c2ccc
--- /dev/null
+++ b/gsk/gskoffload.c
@@ -0,0 +1,653 @@
+/* gskoffload.c
+ *
+ * Copyright 2023 Red Hat, Inc.
+ *
+ * This file is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU Lesser General Public License as published by the Free
+ * Software Foundation; either version 2.1 of the License, or (at your option)
+ * any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see .
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "config.h"
+
+#include "gskoffloadprivate.h"
+
+#include "gskrendernode.h"
+#include "gskrectprivate.h"
+#include "gskroundedrectprivate.h"
+#include "gsktransform.h"
+#include "gskdebugprivate.h"
+#include "gskrendernodeprivate.h"
+#include "gdksurfaceprivate.h"
+
+#include
+
+typedef struct
+{
+ GskRoundedRect rect;
+ guint is_rectilinear : 1;
+ guint is_fully_contained : 1;
+ guint is_empty : 1;
+ guint is_complex : 1;
+} Clip;
+
+struct _GskOffload
+{
+ GdkSurface *surface;
+ GskOffloadInfo *subsurfaces;
+ gsize n_subsurfaces;
+
+ GSList *transforms;
+ GSList *clips;
+
+ Clip *current_clip;
+
+ GskOffloadInfo *last_info;
+ gboolean can_raise;
+};
+
+static GdkTexture *
+find_texture_to_attach (const GskRenderNode *node)
+{
+ switch ((int)GSK_RENDER_NODE_TYPE (node))
+ {
+ case GSK_DEBUG_NODE:
+ return find_texture_to_attach (gsk_debug_node_get_child (node));
+
+ case GSK_CONTAINER_NODE:
+ if (gsk_container_node_get_n_children (node) == 1)
+ return find_texture_to_attach (gsk_container_node_get_child (node, 0));
+ break;
+
+ case GSK_TRANSFORM_NODE:
+ {
+ GskTransform *t = gsk_transform_node_get_transform (node);
+ if (gsk_transform_get_category (t) >= GSK_TRANSFORM_CATEGORY_2D_AFFINE)
+ return find_texture_to_attach (gsk_transform_node_get_child (node));
+ }
+ break;
+
+ case GSK_TEXTURE_NODE:
+ return gsk_texture_node_get_texture (node);
+
+ default:
+ break;
+ }
+
+ return NULL;
+}
+
+static void
+push_transform (GskOffload *self,
+ GskTransform *transform)
+{
+ if (self->transforms)
+ {
+ GskTransform *t = self->transforms->data;
+ t = gsk_transform_transform (gsk_transform_ref (t), transform);
+ self->transforms = g_slist_prepend (self->transforms, t);
+ }
+ else
+ self->transforms = g_slist_prepend (NULL, gsk_transform_ref (transform));
+}
+
+static void
+pop_transform (GskOffload *self)
+{
+ GSList *l = self->transforms;
+ GskTransform *t = l->data;
+
+ g_assert (self->transforms != NULL);
+
+ self->transforms = self->transforms->next;
+
+ g_slist_free_1 (l);
+ gsk_transform_unref (t);
+}
+
+static inline void
+transform_bounds (GskOffload *self,
+ const graphene_rect_t *bounds,
+ graphene_rect_t *rect)
+{
+ GskTransform *t = self->transforms ? self->transforms->data : NULL;
+ float sx, sy, dx, dy;
+
+ g_assert (gsk_transform_get_category (t) >= GSK_TRANSFORM_CATEGORY_2D_AFFINE);
+
+ gsk_transform_to_affine (t, &sx, &sy, &dx, &dy);
+
+ rect->origin.x = bounds->origin.x * sx + dx;
+ rect->origin.y = bounds->origin.y * sy + dy;
+ rect->size.width = bounds->size.width * sx;
+ rect->size.height = bounds->size.height * sy;
+}
+
+static inline void
+transform_rounded_rect (GskOffload *self,
+ const GskRoundedRect *rect,
+ GskRoundedRect *out_rect)
+{
+ GskTransform *t = self->transforms ? self->transforms->data : NULL;
+ float sx, sy, dx, dy;
+
+ g_assert (gsk_transform_get_category (t) >= GSK_TRANSFORM_CATEGORY_2D_AFFINE);
+
+ gsk_transform_to_affine (t, &sx, &sy, &dx, &dy);
+ gsk_rounded_rect_scale_affine (out_rect, rect, sx, sy, dx, dy);
+}
+
+static void
+push_rect_clip (GskOffload *self,
+ const GskRoundedRect *rect)
+{
+ Clip *clip = g_new0 (Clip, 1);
+ clip->rect = *rect;
+ clip->is_rectilinear = gsk_rounded_rect_is_rectilinear (rect);
+ clip->is_empty = rect->bounds.size.width == 0 && rect->bounds.size.height == 0;
+
+ self->clips = g_slist_prepend (self->clips, clip);
+ self->current_clip = self->clips->data;
+}
+
+static void
+push_empty_clip (GskOffload *self)
+{
+ push_rect_clip (self, &GSK_ROUNDED_RECT_INIT (0, 0, 0, 0));
+}
+
+static void
+push_contained_clip (GskOffload *self)
+{
+ Clip *current_clip = self->clips->data;
+ Clip *clip = g_new0 (Clip, 1);
+
+ clip->rect = current_clip->rect;
+ clip->is_rectilinear = TRUE;
+ clip->is_fully_contained = TRUE;
+
+ self->clips = g_slist_prepend (self->clips, clip);
+ self->current_clip = self->clips->data;
+}
+
+static void
+push_complex_clip (GskOffload *self)
+{
+ Clip *current_clip = self->clips->data;
+ Clip *clip = g_new0 (Clip, 1);
+
+ clip->rect = current_clip->rect;
+ clip->is_complex = TRUE;
+
+ self->clips = g_slist_prepend (self->clips, clip);
+ self->current_clip = self->clips->data;
+}
+
+static void
+pop_clip (GskOffload *self)
+{
+ GSList *l = self->clips;
+ Clip *clip = l->data;
+
+ g_assert (self->clips != NULL);
+
+ self->clips = self->clips->next;
+ if (self->clips)
+ self->current_clip = self->clips->data;
+
+ g_slist_free_1 (l);
+ g_free (clip);
+}
+
+static inline void
+rounded_rect_get_inner (const GskRoundedRect *rect,
+ graphene_rect_t *inner)
+{
+ float left = MAX (rect->corner[GSK_CORNER_TOP_LEFT].width, rect->corner[GSK_CORNER_BOTTOM_LEFT].width);
+ float right = MAX (rect->corner[GSK_CORNER_TOP_RIGHT].width, rect->corner[GSK_CORNER_BOTTOM_RIGHT].width);
+ float top = MAX (rect->corner[GSK_CORNER_TOP_LEFT].height, rect->corner[GSK_CORNER_TOP_RIGHT].height);
+ float bottom = MAX (rect->corner[GSK_CORNER_BOTTOM_LEFT].height, rect->corner[GSK_CORNER_BOTTOM_RIGHT].height);
+
+ inner->origin.x = rect->bounds.origin.x + left;
+ inner->size.width = rect->bounds.size.width - (left + right);
+
+ inner->origin.y = rect->bounds.origin.y + top;
+ inner->size.height = rect->bounds.size.height - (top + bottom);
+}
+
+static inline gboolean
+interval_contains (float p1, float w1,
+ float p2, float w2)
+{
+ if (p2 < p1)
+ return FALSE;
+
+ if (p2 + w2 > p1 + w1)
+ return FALSE;
+
+ return TRUE;
+}
+
+static gboolean
+update_clip (GskOffload *self,
+ const graphene_rect_t *bounds)
+{
+ graphene_rect_t transformed_bounds;
+ gboolean no_clip = FALSE;
+ gboolean rect_clip = FALSE;
+
+ if (self->current_clip->is_fully_contained ||
+ self->current_clip->is_empty ||
+ self->current_clip->is_complex)
+ return FALSE;
+
+ transform_bounds (self, bounds, &transformed_bounds);
+
+ if (!gsk_rect_intersects (&self->current_clip->rect.bounds, &transformed_bounds))
+ {
+ push_empty_clip (self);
+ return TRUE;
+ }
+
+ if (self->current_clip->is_rectilinear)
+ {
+ if (gsk_rect_contains_rect (&self->current_clip->rect.bounds, &transformed_bounds))
+ no_clip = TRUE;
+ else
+ rect_clip = TRUE;
+ }
+ else if (gsk_rounded_rect_contains_rect (&self->current_clip->rect, &transformed_bounds))
+ {
+ no_clip = TRUE;
+ }
+ else
+ {
+ graphene_rect_t inner;
+
+ rounded_rect_get_inner (&self->current_clip->rect, &inner);
+
+ if (interval_contains (inner.origin.x, inner.size.width,
+ transformed_bounds.origin.x, transformed_bounds.size.width) ||
+ interval_contains (inner.origin.y, inner.size.height,
+ transformed_bounds.origin.y, transformed_bounds.size.height))
+ rect_clip = TRUE;
+ }
+
+ if (no_clip)
+ {
+ /* This node is completely contained inside the clip.
+ * Record this fact on the clip stack, so we don't do
+ * more work for child nodes.
+ */
+ push_contained_clip (self);
+ return TRUE;
+ }
+ else if (rect_clip && !self->current_clip->is_rectilinear)
+ {
+ graphene_rect_t rect;
+
+ /* The clip gets simpler for this node */
+
+ graphene_rect_intersection (&self->current_clip->rect.bounds, &transformed_bounds, &rect);
+ push_rect_clip (self, &GSK_ROUNDED_RECT_INIT_FROM_RECT (rect));
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static GskOffloadInfo *
+find_subsurface_info (GskOffload *self,
+ GdkSubsurface *subsurface)
+{
+ for (gsize i = 0; i < self->n_subsurfaces; i++)
+ {
+ GskOffloadInfo *info = &self->subsurfaces[i];
+ if (info->subsurface == subsurface)
+ return info;
+ }
+
+ return NULL;
+}
+
+static void
+visit_node (GskOffload *self,
+ GskRenderNode *node)
+{
+ gboolean has_clip;
+
+ if (self->last_info && self->can_raise)
+ {
+ graphene_rect_t transformed_bounds;
+
+ transform_bounds (self, &node->bounds, &transformed_bounds);
+ if (gsk_rect_intersects (&transformed_bounds, &self->last_info->rect))
+ {
+ GskRenderNodeType type = GSK_RENDER_NODE_TYPE (node);
+
+ if (type != GSK_CONTAINER_NODE &&
+ type != GSK_TRANSFORM_NODE &&
+ type != GSK_CLIP_NODE &&
+ type != GSK_ROUNDED_CLIP_NODE &&
+ type != GSK_DEBUG_NODE)
+ {
+ GDK_DISPLAY_DEBUG (gdk_surface_get_display (self->surface), OFFLOAD,
+ "Can't raise subsurface %p because a %s overlaps",
+ self->last_info->subsurface,
+ g_type_name_from_instance ((GTypeInstance *) node));
+ self->can_raise = FALSE;
+ }
+ }
+ }
+
+ has_clip = update_clip (self, &node->bounds);
+
+ switch (GSK_RENDER_NODE_TYPE (node))
+ {
+ case GSK_BORDER_NODE:
+ case GSK_COLOR_NODE:
+ case GSK_CONIC_GRADIENT_NODE:
+ case GSK_LINEAR_GRADIENT_NODE:
+ case GSK_REPEATING_LINEAR_GRADIENT_NODE:
+ case GSK_RADIAL_GRADIENT_NODE:
+ case GSK_REPEATING_RADIAL_GRADIENT_NODE:
+ case GSK_TEXT_NODE:
+ case GSK_TEXTURE_NODE:
+ case GSK_TEXTURE_SCALE_NODE:
+ case GSK_CAIRO_NODE:
+ case GSK_INSET_SHADOW_NODE:
+ case GSK_OUTSET_SHADOW_NODE:
+ case GSK_GL_SHADER_NODE:
+ case GSK_BLEND_NODE:
+ case GSK_BLUR_NODE:
+ case GSK_COLOR_MATRIX_NODE:
+ case GSK_OPACITY_NODE:
+ case GSK_CROSS_FADE_NODE:
+ case GSK_SHADOW_NODE:
+ case GSK_REPEAT_NODE:
+ case GSK_MASK_NODE:
+ case GSK_FILL_NODE:
+ case GSK_STROKE_NODE:
+ break;
+
+ case GSK_CLIP_NODE:
+ {
+ const graphene_rect_t *clip = gsk_clip_node_get_clip (node);
+ graphene_rect_t transformed_clip;
+ GskRoundedRect intersection;
+
+ transform_bounds (self, clip, &transformed_clip);
+
+ if (self->current_clip->is_rectilinear)
+ {
+ memset (&intersection.corner, 0, sizeof intersection.corner);
+ graphene_rect_intersection (&transformed_clip,
+ &self->current_clip->rect.bounds,
+ &intersection.bounds);
+
+ push_rect_clip (self, &intersection);
+ visit_node (self, gsk_clip_node_get_child (node));
+ pop_clip (self);
+ }
+ else
+ {
+ GskRoundedRectIntersection result;
+
+ result = gsk_rounded_rect_intersect_with_rect (&self->current_clip->rect,
+ &transformed_clip,
+ &intersection);
+
+ if (result == GSK_INTERSECTION_EMPTY)
+ push_empty_clip (self);
+ else if (result == GSK_INTERSECTION_NONEMPTY)
+ push_rect_clip (self, &intersection);
+ else
+ push_complex_clip (self);
+ visit_node (self, gsk_clip_node_get_child (node));
+ pop_clip (self);
+ }
+ }
+ break;
+
+ case GSK_ROUNDED_CLIP_NODE:
+ {
+ const GskRoundedRect *clip = gsk_rounded_clip_node_get_clip (node);
+ GskRoundedRect transformed_clip;
+
+ transform_rounded_rect (self, clip, &transformed_clip);
+
+ if (self->current_clip->is_rectilinear)
+ {
+ GskRoundedRect intersection;
+ GskRoundedRectIntersection result;
+
+ result = gsk_rounded_rect_intersect_with_rect (&transformed_clip,
+ &self->current_clip->rect.bounds,
+ &intersection);
+
+ if (result == GSK_INTERSECTION_EMPTY)
+ push_empty_clip (self);
+ else if (result == GSK_INTERSECTION_NONEMPTY)
+ push_rect_clip (self, &intersection);
+ else
+ goto complex_clip;
+ visit_node (self, gsk_rounded_clip_node_get_child (node));
+ pop_clip (self);
+ }
+ else
+ {
+complex_clip:
+ if (gsk_rounded_rect_contains_rect (&self->current_clip->rect, &transformed_clip.bounds))
+ push_rect_clip (self, &transformed_clip);
+ else
+ push_complex_clip (self);
+ visit_node (self, gsk_rounded_clip_node_get_child (node));
+ pop_clip (self);
+ }
+ }
+ break;
+
+ case GSK_TRANSFORM_NODE:
+ {
+ GskTransform *transform = gsk_transform_node_get_transform (node);
+ const GskTransformCategory category = gsk_transform_get_category (transform);
+
+ switch (category)
+ {
+ case GSK_TRANSFORM_CATEGORY_IDENTITY:
+ visit_node (self, gsk_transform_node_get_child (node));
+ break;
+
+ case GSK_TRANSFORM_CATEGORY_2D_TRANSLATE:
+ case GSK_TRANSFORM_CATEGORY_2D_AFFINE:
+ push_transform (self, transform);
+ visit_node (self, gsk_transform_node_get_child (node));
+ pop_transform (self);
+ break;
+
+ case GSK_TRANSFORM_CATEGORY_2D:
+ case GSK_TRANSFORM_CATEGORY_3D:
+ case GSK_TRANSFORM_CATEGORY_ANY:
+ case GSK_TRANSFORM_CATEGORY_UNKNOWN:
+ break;
+
+ default:
+ g_assert_not_reached ();
+ }
+ }
+ break;
+
+ case GSK_CONTAINER_NODE:
+ for (gsize i = 0; i < gsk_container_node_get_n_children (node); i++)
+ visit_node (self, gsk_container_node_get_child (node, i));
+ break;
+
+ case GSK_DEBUG_NODE:
+ visit_node (self, gsk_debug_node_get_child (node));
+ break;
+
+ case GSK_SUBSURFACE_NODE:
+ {
+ GdkSubsurface *subsurface = gsk_subsurface_node_get_subsurface (node);
+
+ GskOffloadInfo *info = find_subsurface_info (self, subsurface);
+
+ if (info)
+ {
+ info->texture = find_texture_to_attach (gsk_subsurface_node_get_child (node));
+ info->can_offload = self->current_clip->is_fully_contained &&
+ info->texture != NULL;
+
+ if (info->can_offload)
+ {
+ transform_bounds (self, &node->bounds, &info->rect);
+ info->place_above = self->last_info ? self->last_info->subsurface : NULL;
+ self->last_info = info;
+ self->can_raise = TRUE;
+ }
+ else
+ {
+ GDK_DISPLAY_DEBUG (gdk_surface_get_display (self->surface), OFFLOAD,
+ "Can't offload subsurface %p: %s",
+ subsurface,
+ info->texture == NULL ? "no texture" : "clipped");
+ }
+ }
+ }
+ break;
+
+ case GSK_NOT_A_RENDER_NODE:
+ default:
+ g_assert_not_reached ();
+ break;
+ }
+
+ if (has_clip)
+ pop_clip (self);
+}
+
+GskOffload *
+gsk_offload_new (GdkSurface *surface,
+ GskRenderNode *root)
+{
+ GdkDisplay *display = gdk_surface_get_display (surface);
+ GskOffload *self;
+
+ self = g_new0 (GskOffload, 1);
+
+ self->surface = surface;
+ self->transforms = NULL;
+ self->clips = NULL;
+
+ self->last_info = NULL;
+
+ self->n_subsurfaces = gdk_surface_get_n_subsurfaces (self->surface);
+ self->subsurfaces = g_new0 (GskOffloadInfo, self->n_subsurfaces);
+
+ for (gsize i = 0; i < self->n_subsurfaces; i++)
+ {
+ GskOffloadInfo *info = &self->subsurfaces[i];
+ info->subsurface = gdk_surface_get_subsurface (self->surface, i);
+ info->was_offloaded = gdk_subsurface_get_texture (info->subsurface) != NULL;
+ /* Stack them all below, initially */
+ gdk_subsurface_place_below (info->subsurface, NULL);
+ }
+
+ if (self->n_subsurfaces > 0)
+ {
+ push_rect_clip (self, &GSK_ROUNDED_RECT_INIT (0, 0,
+ gdk_surface_get_width (surface),
+ gdk_surface_get_height (surface)));
+
+ visit_node (self, root);
+
+ pop_clip (self);
+ }
+
+ for (gsize i = 0; i < self->n_subsurfaces; i++)
+ {
+ GskOffloadInfo *info = &self->subsurfaces[i];
+
+ if (info->can_offload)
+ {
+ GDK_DISPLAY_DEBUG (display, OFFLOAD, "Attaching %dx%d texture to subsurface %p at %g %g %g %g",
+ gdk_texture_get_width (info->texture),
+ gdk_texture_get_height (info->texture),
+ info->subsurface,
+ info->rect.origin.x, info->rect.origin.y,
+ info->rect.size.width, info->rect.size.height);
+
+ info->is_offloaded = gdk_subsurface_attach (info->subsurface,
+ info->texture,
+ &info->rect);
+
+ if (info->place_above)
+ gdk_subsurface_place_above (info->subsurface, info->place_above);
+ }
+ else
+ {
+ info->is_offloaded = FALSE;
+ if (info->was_offloaded)
+ {
+ GDK_DISPLAY_DEBUG (display, OFFLOAD, "Hiding subsurface %p", info->subsurface);
+ gdk_subsurface_detach (info->subsurface);
+ }
+ }
+ }
+
+ if (self->can_raise && self->last_info)
+ {
+ GDK_DISPLAY_DEBUG (display, OFFLOAD, "Raising subsurface %p", self->last_info->subsurface);
+ gdk_subsurface_place_above (self->last_info->subsurface, NULL);
+ }
+
+ return self;
+}
+
+void
+gsk_offload_free (GskOffload *self)
+{
+ g_free (self->subsurfaces);
+ g_free (self);
+}
+
+GskOffloadInfo *
+gsk_offload_get_subsurface_info (GskOffload *self,
+ GdkSubsurface *subsurface)
+{
+ return find_subsurface_info (self, subsurface);
+}
+
+gboolean
+gsk_offload_subsurface_was_offloaded (GskOffload *self,
+ GdkSubsurface *subsurface)
+{
+ GskOffloadInfo *info = find_subsurface_info (self, subsurface);
+
+ if (!info)
+ return FALSE;
+
+ return info->was_offloaded;
+}
+
+gboolean
+gsk_offload_subsurface_is_offloaded (GskOffload *self,
+ GdkSubsurface *subsurface)
+{
+ GskOffloadInfo *info = find_subsurface_info (self, subsurface);
+
+ if (!info)
+ return FALSE;
+
+ return info->is_offloaded;
+}
diff --git a/gsk/gskoffloadprivate.h b/gsk/gskoffloadprivate.h
new file mode 100644
index 0000000000..495c965bc5
--- /dev/null
+++ b/gsk/gskoffloadprivate.h
@@ -0,0 +1,52 @@
+/* gskoffloadprivate.h
+ *
+ * Copyright 2023 Red Hat, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see .
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#include
+#include "gdk/gdksubsurfaceprivate.h"
+#include "gskrendernode.h"
+
+typedef struct _GskOffload GskOffload;
+
+typedef struct
+{
+ GdkSubsurface *subsurface;
+ GdkTexture *texture;
+ GdkSubsurface *place_above;
+ graphene_rect_t rect;
+ guint was_offloaded : 1;
+ guint can_offload : 1;
+ guint is_offloaded : 1;
+} GskOffloadInfo;
+
+GskOffload * gsk_offload_new (GdkSurface *surface,
+ GskRenderNode *root);
+void gsk_offload_free (GskOffload *self);
+
+GskOffloadInfo * gsk_offload_get_subsurface_info (GskOffload *self,
+ GdkSubsurface *subsurface);
+
+gboolean gsk_offload_subsurface_was_offloaded (GskOffload *self,
+ GdkSubsurface *subsurface);
+
+gboolean gsk_offload_subsurface_is_offloaded (GskOffload *self,
+ GdkSubsurface *subsurface);
+
diff --git a/gsk/meson.build b/gsk/meson.build
index 23ebe82ce0..8efbd7d7ad 100644
--- a/gsk/meson.build
+++ b/gsk/meson.build
@@ -27,6 +27,7 @@ gsk_public_sources = files([
'gskcairorenderer.c',
'gskdiff.c',
'gskglshader.c',
+ 'gskoffload.c',
'gskpath.c',
'gskpathbuilder.c',
'gskpathmeasure.c',