/* 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 <http://www.gnu.org/licenses/>.
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 */

#include "config.h"

#include "gskoffloadprivate.h"

#include "gskclipnode.h"
#include "gskcolornodeprivate.h"
#include "gskcontainernodeprivate.h"
#include "gskdebugnode.h"
#include "gskdebugprivate.h"
#include "gskisolationnode.h"
#include "gskopacitynode.h"
#include "gskrendernode.h"
#include "gskrectprivate.h"
#include "gskrendernodeprivate.h"
#include "gskroundedclipnode.h"
#include "gskroundedrectprivate.h"
#include "gsksubsurfacenode.h"
#include "gsktexturenode.h"
#include "gsktransformnode.h"
#include "gsktransformprivate.h"

#include "gdkrgbaprivate.h"
#include "gdksurfaceprivate.h"

#include <graphene.h>

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;
  const char *offload_impossible;

  GskOffloadInfo *last_info;
};

static GdkDihedral
find_texture_transform (GskTransform *transform)
{
  GdkDihedral dihedral;
  float sx, sy, dx, dy;

  g_assert (gsk_transform_get_fine_category (transform) >= GSK_FINE_TRANSFORM_CATEGORY_2D_DIHEDRAL);

  gsk_transform_to_dihedral (transform, &dihedral, &sx, &sy, &dx, &dy);

  return dihedral;
}

static gboolean
color_is_black (const GdkColor *color)
{
  return color->red < 255.f / 65535.f &&
         color->green < 255.f / 65535.f &&
         color->blue < 255.f / 65535.f &&
         color->alpha > 65280.f / 65535.f;
}

static GdkTexture *
find_texture_to_attach (GskOffload          *self,
                        const GskRenderNode *subsurface_node,
                        graphene_rect_t     *out_texture_rect,
                        graphene_rect_t     *out_source_rect,
                        gboolean            *has_background,
                        GdkDihedral         *out_texture_transform)
{
  GdkSubsurface *subsurface;
  const GskRenderNode *node;
  gboolean has_clip = FALSE;
  graphene_rect_t clip;
  GskTransform *transform = NULL;
  GdkTexture *ret = NULL;

  *has_background = FALSE;
  *out_texture_transform = GDK_DIHEDRAL_NORMAL;

  subsurface = gsk_subsurface_node_get_subsurface (subsurface_node);
  node = subsurface_node;

  for (;;)
    {
      switch ((int) GSK_RENDER_NODE_TYPE (node))
        {
        case GSK_DEBUG_NODE:
          node = gsk_debug_node_get_child (node);
          break;

        case GSK_SUBSURFACE_NODE:
          node = gsk_subsurface_node_get_child (node);
          break;

        case GSK_OPACITY_NODE:
          if (gsk_opacity_node_get_opacity (node) < 1.0)
            goto out;
          node = gsk_opacity_node_get_child (node);
          break;

        case GSK_ISOLATION_NODE:
          node = gsk_isolation_node_get_child (node);
          break;

        case GSK_CONTAINER_NODE:
          if (gsk_container_node_get_n_children (node) == 1)
            {
              node = gsk_container_node_get_child (node, 0);
              break;
            }
          else if (gsk_container_node_get_n_children (node) == 2)
            {
              GskRenderNode *child = gsk_container_node_get_child (node, 0);
              graphene_rect_t bounds;

              gsk_transform_transform_bounds (transform, &child->bounds, &bounds);
              if (GSK_RENDER_NODE_TYPE (child) == GSK_COLOR_NODE &&
                  gsk_rect_equal (&bounds, &subsurface_node->bounds) &&
                  color_is_black (gsk_color_node_get_gdk_color (child)))
                {
                  *has_background = TRUE;
                  node = gsk_container_node_get_child (node, 1);
                  break;
                }
            }

          GDK_DISPLAY_DEBUG (gdk_surface_get_display (self->surface), OFFLOAD,
                             "[%p] 🗙 Too much content, container with %d children",
                             subsurface, gsk_container_node_get_n_children (node));
          goto out;

        case GSK_TRANSFORM_NODE:
          {
            GskTransform *t = gsk_transform_node_get_transform (node);

            if (gsk_transform_get_fine_category (t) < GSK_FINE_TRANSFORM_CATEGORY_2D_DIHEDRAL)
              {
                char *s = gsk_transform_to_string (t);
                GDK_DISPLAY_DEBUG (gdk_surface_get_display (self->surface), OFFLOAD,
                                   "[%p] 🗙 Transform %s is not dihedral",
                                   subsurface, s);
                g_free (s);
                goto out;
              }

            if (has_clip)
              {
                GskTransform *inv = gsk_transform_invert (gsk_transform_ref (t));
                gsk_transform_transform_bounds (inv, &clip, &clip);
                gsk_transform_unref (inv);
              }

            transform = gsk_transform_transform (transform, gsk_transform_ref (t));

            node = gsk_transform_node_get_child (node);
          }
          break;

        case GSK_CLIP_NODE:
          {
            const graphene_rect_t *c = gsk_clip_node_get_clip (node);

            if (has_clip)
              {
                if (!gsk_rect_intersection (c, &clip, &clip))
                  {
                    GDK_DISPLAY_DEBUG (gdk_surface_get_display (self->surface), OFFLOAD,
                                       "[%p] 🗙 Empty clip", subsurface);
                    goto out;
                  }
              }
            else
              {
                gsk_transform_transform_bounds (transform, &node->bounds, out_texture_rect);
                clip = *c;
                has_clip = TRUE;
              }

            node = gsk_clip_node_get_child (node);
          }
          break;

        case GSK_TEXTURE_NODE:
          {
            GdkTexture *texture = gsk_texture_node_get_texture (node);
            int width, height;

            if (GDK_IS_MEMORY_TEXTURE (texture) &&
                !GDK_DISPLAY_DEBUG_CHECK (gdk_surface_get_display (self->surface),
                                          FORCE_OFFLOAD))
              {
                GDK_DISPLAY_DEBUG (gdk_surface_get_display (self->surface), OFFLOAD,
                                   "[%p] 🗙 Memory texture", subsurface);
                goto out;
              }

            if (gsk_transform_get_fine_category (transform) < GSK_FINE_TRANSFORM_CATEGORY_2D_DIHEDRAL)
              {
                char *s = gsk_transform_to_string (transform);
                GDK_DISPLAY_DEBUG (gdk_surface_get_display (self->surface), OFFLOAD,
                                   "[%p] 🗙 Transform %s is not dihedral",
                                   subsurface, s);
                g_free (s);
                goto out;
              }

            width = gdk_texture_get_width (texture);
            height = gdk_texture_get_height (texture);

            *out_texture_transform = find_texture_transform (transform);

            if (gdk_dihedral_swaps_xy (*out_texture_transform))
              {
                int tmp = width;
                width = height;
                height = tmp;
              }

            if (has_clip)
              {
                float dx = node->bounds.origin.x;
                float dy = node->bounds.origin.y;
                float sx = width / node->bounds.size.width;
                float sy = height / node->bounds.size.height;

                if (!gsk_rect_intersection (&node->bounds, &clip, &clip))
                  {
                    GDK_DISPLAY_DEBUG (gdk_surface_get_display (self->surface), OFFLOAD,
                                       "[%p] 🗙 Empty clip", subsurface);
                    goto out;
                  }

                out_source_rect->origin.x = (clip.origin.x - dx) * sx;
                out_source_rect->origin.y = (clip.origin.y - dy) * sy;
                out_source_rect->size.width = clip.size.width * sx;
                out_source_rect->size.height = clip.size.height * sy;
              }
            else
              {
                gsk_transform_transform_bounds (transform, &node->bounds, out_texture_rect);
                out_source_rect->origin.x = 0;
                out_source_rect->origin.y = 0;
                out_source_rect->size.width = width;
                out_source_rect->size.height = height;
              }

            ret = texture;
            goto out;
          }

        default:
          GDK_DISPLAY_DEBUG (gdk_surface_get_display (self->surface), OFFLOAD,
                             "[%p] 🗙 Only textures supported (found %s)",
                             subsurface, g_type_name_from_instance ((GTypeInstance *) node));
          goto out;
        }
    }

out:
  g_clear_pointer (&transform, gsk_transform_unref);

  return ret;
}

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;

  gsk_transform_transform_bounds (t, bounds, rect);
}

static inline gboolean
transform_rounded_rect (GskOffload       *self,
                        const GskRoundedRect *rect,
                        GskRoundedRect       *out_rect)
{
  GskTransform *t = self->transforms ? self->transforms->data : NULL;
  GdkDihedral dihedral;
  float sx, sy, dx, dy;
  GskRoundedRect tmp;

  if (gsk_transform_get_fine_category (t) < GSK_FINE_TRANSFORM_CATEGORY_2D_DIHEDRAL)
    return FALSE;

  gsk_transform_to_dihedral (t, &dihedral, &sx, &sy, &dx, &dy);
  gsk_rounded_rect_dihedral (&tmp, rect, dihedral);
  gsk_rounded_rect_scale_affine (out_rect, &tmp, sx, sy, dx, dy);

  return TRUE;
}

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 *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;

  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 */

      if (gsk_rect_intersection (&self->current_clip->rect.bounds, transformed_bounds, &rect))
        push_rect_clip (self, &GSK_ROUNDED_RECT_INIT_FROM_RECT (rect));
      else
        push_empty_clip (self);
      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);

static void
visit_children (GskOffload    *self,
                GskRenderNode *node)
{
  GskRenderNode **children;
  gsize i, n_children;

  children = gsk_render_node_get_children (node, &n_children);

  for (i = 0; i < n_children; i++)
    {
      visit_node (self, children[i]);
    }
}

static void
visit_node (GskOffload    *self,
            GskRenderNode *node)
{
  gboolean has_clip;
  graphene_rect_t transformed_bounds;

  transform_bounds (self, &node->bounds, &transformed_bounds);

  for (gsize i = 0; i < self->n_subsurfaces; i++)
    {
      GskOffloadInfo *info = &self->subsurfaces[i];

      if (info->can_offload && info->can_raise)
        {
          if (gsk_rect_intersects (&transformed_bounds, &info->texture_rect) ||
              gsk_rect_intersects (&transformed_bounds, &info->background_rect))
            {
              GskRenderNodeType type = GSK_RENDER_NODE_TYPE (node);

              if (!gsk_render_node_contains_subsurface_node (node) ||
                  (type != GSK_CONTAINER_NODE &&
                   type != GSK_TRANSFORM_NODE &&
                   type != GSK_CLIP_NODE &&
                   type != GSK_ROUNDED_CLIP_NODE &&
                   type != GSK_DEBUG_NODE))
                {
                  if (gsk_render_node_clears_background (node))
                    {
                      GDK_DISPLAY_DEBUG (gdk_surface_get_display (self->surface), OFFLOAD,
                                         "[%p]   Disabling because a %s clears the background",
                                         info->subsurface,
                                         g_type_name_from_instance ((GTypeInstance *) node));
                      info->can_offload = FALSE;
                    }
                  else
                    {
                      GDK_DISPLAY_DEBUG (gdk_surface_get_display (self->surface), OFFLOAD,
                                         "[%p]   Lowering because a %s overlaps",
                                         info->subsurface,
                                         g_type_name_from_instance ((GTypeInstance *) node));
                      info->can_raise = FALSE;
                    }
                }
            }
        }
    }

  if (!gsk_render_node_contains_subsurface_node (node))
    return;

  has_clip = update_clip (self, &transformed_bounds);

  switch (GSK_RENDER_NODE_TYPE (node))
    {
    case GSK_BORDER_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_COLOR_NODE:
    case GSK_INSET_SHADOW_NODE:
    case GSK_OUTSET_SHADOW_NODE:
    case GSK_PASTE_NODE:
    case GSK_TURBULENCE_NODE:
      /* no children */
      break;

    case GSK_CONTAINER_NODE:
      {
        GskRenderNode **children;
        gsize i, okay, n_children;
        const char *was_impossible = self->offload_impossible;

        children = gsk_render_node_get_children (node, &n_children);

        if (!was_impossible &&
            (gsk_render_node_clears_background (node) ||
             gsk_render_node_get_copy_mode (node) != GSK_COPY_NONE))
          {
            for (okay = n_children; okay > 0; okay--)
              {
                if (gsk_render_node_clears_background (children[okay - 1]))
                  {
                    self->offload_impossible = "Composite operations";
                    break;
                  }

                if (gsk_render_node_get_copy_mode (children[okay - 1]) != GSK_COPY_NONE)
                  {
                    self->offload_impossible = "Copied contents";
                    okay--;
                    break;
                  }
              }
          }
        else
          okay = 0;

        for (i = 0; i < n_children ; i++)
          {
            if (i == okay)
              self->offload_impossible = was_impossible;

            visit_node (self, children[i]);
          }
      }
      break;

    case GSK_DEBUG_NODE:
    case GSK_COPY_NODE:
    case GSK_ISOLATION_NODE:
      /* keep going */
      visit_children (self, node);
      break;

    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:
    case GSK_COMPONENT_TRANSFER_NODE:
    case GSK_COMPOSITE_NODE:
    case GSK_DISPLACEMENT_NODE:
    case GSK_ARITHMETIC_NODE:
      /* cannot offload */
      {
        const char *was_impossible = self->offload_impossible;
        self->offload_impossible = g_type_name_from_instance ((GTypeInstance *) node);
        visit_children (self, node);
        self->offload_impossible = was_impossible;
      }
      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);
            if (gsk_rect_intersection (&transformed_clip,
                                       &self->current_clip->rect.bounds,
                                       &intersection.bounds))
              push_rect_clip (self, &intersection);
            else
              push_empty_clip (self);
            visit_node (self, gsk_clip_node_get_child (node));
            pop_clip (self);
          }
        else
          {
            GskRoundedRectIntersection intersect;

            intersect = gsk_rounded_rect_intersect_with_rect (&self->current_clip->rect,
                                                              &transformed_clip,
                                                              &intersection);

            if (intersect == GSK_INTERSECTION_EMPTY)
              push_empty_clip (self);
            else if (intersect == 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;

        if (!transform_rounded_rect (self, clip, &transformed_clip))
          {
            const char *was_impossible = self->offload_impossible;
            self->offload_impossible = "Non-dihedral transform";
            visit_node (self, gsk_rounded_clip_node_get_child (node));
            self->offload_impossible = was_impossible;
          }
        else if (self->current_clip->is_rectilinear)
          {
            GskRoundedRect intersection;
            GskRoundedRectIntersection intersect;

            intersect = gsk_rounded_rect_intersect_with_rect (&transformed_clip,
                                                              &self->current_clip->rect.bounds,
                                                              &intersection);

            if (intersect == GSK_INTERSECTION_EMPTY)
              push_empty_clip (self);
            else if (intersect == 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:
      push_transform (self, gsk_transform_node_get_transform (node));
      visit_node (self, gsk_transform_node_get_child (node));
      pop_transform (self);
      break;

    case GSK_SUBSURFACE_NODE:
      {
        GdkSubsurface *subsurface = gsk_subsurface_node_get_subsurface (node);
        GskTransform *transform;

        GskOffloadInfo *info = find_subsurface_info (self, subsurface);

        transform = self->transforms ? (GskTransform *) self->transforms->data : NULL;

        if (info == NULL)
          {
            GDK_DISPLAY_DEBUG (gdk_surface_get_display (self->surface), OFFLOAD,
                               "[%p] 🗙 Unknown subsurface",
                               subsurface);
          }
        else
          {
            info->n_nodes++;

            if (info->n_nodes > 1)
              {
                /* only print this message once, not once per node */
                if (info->n_nodes == 2)
                  GDK_DISPLAY_DEBUG (gdk_surface_get_display (self->surface), OFFLOAD,
                                     "[%p] 🗙 Multiple nodes referring to subsurface",
                                     subsurface);
                if (info->can_offload)
                  {
                    info->can_offload = FALSE;
                    info->can_raise = FALSE;
                  }
              }
            else if (self->offload_impossible)
              {
                GDK_DISPLAY_DEBUG (gdk_surface_get_display (self->surface), OFFLOAD,
                                   "[%p] 🗙 %s",
                                   subsurface, self->offload_impossible);
              }
            else if (!self->current_clip->is_fully_contained)
              {
                GDK_DISPLAY_DEBUG (gdk_surface_get_display (self->surface), OFFLOAD,
                                   "[%p] 🗙 Clipped",
                                   subsurface);
              }
            else if (gsk_transform_get_fine_category (transform) < GSK_FINE_TRANSFORM_CATEGORY_2D_DIHEDRAL)
              {
                GDK_DISPLAY_DEBUG (gdk_surface_get_display (self->surface), OFFLOAD,
                                   "[%p] 🗙 Non-dihedral transform",
                                   subsurface);
              }
            else
              {
                gboolean has_background;
                float sx, sy, dx, dy;
                GdkDihedral context_transform;
                GdkDihedral inner_transform;

                gsk_transform_to_dihedral (transform, &context_transform, &sx, &sy, &dx, &dy);

                info->texture = find_texture_to_attach (self, node, &info->texture_rect, &info->source_rect, &has_background, &inner_transform);
                if (info->texture)
                  {
                    info->transform = gdk_dihedral_combine (context_transform, inner_transform);
                    info->can_offload = TRUE;
                    info->can_raise = TRUE;
                    transform_bounds (self, &info->texture_rect, &info->texture_rect);
                    info->has_background = has_background;
                    transform_bounds (self, &node->bounds, &info->background_rect);
                    if (self->last_info && self->last_info->subsurface != info->subsurface)
                      info->place_above = self->last_info->subsurface;
                    else
                      info->place_above = NULL;
                    self->last_info = info;
                  }
              }
          }
      }
      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,
                 cairo_region_t *diff)
{
  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];
      graphene_rect_t rect;

      info->subsurface = gdk_surface_get_subsurface (self->surface, i);
      info->was_offloaded = gdk_subsurface_get_texture (info->subsurface) != NULL;
      info->was_above = gdk_subsurface_is_above_parent (info->subsurface);
      info->had_background = gdk_subsurface_get_background_rect (info->subsurface, &rect);
    }

  if (self->n_subsurfaces > 0 && gsk_render_node_contains_subsurface_node (root))
    {
      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];
      graphene_rect_t old_bounds;
      graphene_rect_t bounds;

      gdk_subsurface_get_bounds (info->subsurface, &old_bounds);

      if (info->can_offload)
        {
          if (info->can_raise)
            info->is_offloaded = gdk_subsurface_attach (info->subsurface,
                                                        info->texture,
                                                        &info->source_rect,
                                                        &info->texture_rect,
                                                        info->transform,
                                                        info->has_background ? &info->background_rect : NULL,
                                                        TRUE, NULL);
          else
            info->is_offloaded = gdk_subsurface_attach (info->subsurface,
                                                        info->texture,
                                                        &info->source_rect,
                                                        &info->texture_rect,
                                                        info->transform,
                                                        info->has_background ? &info->background_rect : NULL,
                                                        info->place_above != NULL,
                                                        info->place_above);
        }
      else
        {
          info->is_offloaded = FALSE;
          if (info->was_offloaded)
            gdk_subsurface_detach (info->subsurface);
        }

      info->is_above = info->is_offloaded && gdk_subsurface_is_above_parent (info->subsurface);

      gdk_subsurface_get_bounds (info->subsurface, &bounds);

      if (info->is_offloaded != info->was_offloaded ||
          info->is_above != info->was_above ||
          (info->is_offloaded && !gsk_rect_equal (&bounds, &old_bounds)))
        {
          /* We changed things, need to invalidate everything */
          cairo_rectangle_int_t rect;

          if (info->is_offloaded)
            {
              gsk_rect_to_cairo_grow (&bounds, &rect);
              cairo_region_union_rectangle (diff, &rect);
            }
          if (info->was_offloaded)
            {
              gsk_rect_to_cairo_grow (&old_bounds, &rect);
              cairo_region_union_rectangle (diff, &rect);
            }
        }

    }

  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);
}
