#include "config.h"

#include "gskgpunodeprocessorprivate.h"

#include "gskgpuborderopprivate.h"
#include "gskgpuboxshadowopprivate.h"
#include "gskgpublendmodeopprivate.h"
#include "gskgpublendopprivate.h"
#include "gskgpublitopprivate.h"
#include "gskgpubluropprivate.h"
#include "gskgpucacheprivate.h"
#include "gskgpucachedglyphprivate.h"
#include "gskgpucachedfillprivate.h"
#include "gskgpucachedstrokeprivate.h"
#include "gskgpuclearopprivate.h"
#include "gskgpuclipprivate.h"
#include "gskgpucolorizeopprivate.h"
#include "gskgpucolormatrixopprivate.h"
#include "gskgpucomponenttransferopprivate.h"
#include "gskgpucoloropprivate.h"
#include "gskgpuconicgradientopprivate.h"
#include "gskgpuconvertbuiltinopprivate.h"
#include "gskgpuconvertcicpopprivate.h"
#include "gskgpuconvertopprivate.h"
#include "gskgpucrossfadeopprivate.h"
#include "gskgpudeviceprivate.h"
#include "gskgpuframeprivate.h"
#include "gskgpuglobalsopprivate.h"
#include "gskgpuimageprivate.h"
#include "gskgpulineargradientopprivate.h"
#include "gskgpumaskopprivate.h"
#include "gskgpumipmapopprivate.h"
#include "gskgpuradialgradientopprivate.h"
#include "gskgpurenderpassopprivate.h"
#include "gskgpuroundedcoloropprivate.h"
#include "gskgpuscissoropprivate.h"
#include "gskgputextureopprivate.h"
#include "gskgpuuploadopprivate.h"
#include "gskgpuutilsprivate.h"

#include "gskcairoblurprivate.h"
#include "gskdebugprivate.h"
#include "gskpath.h"
#include "gskrectprivate.h"
#include "gskrendernodeprivate.h"
#include "gskroundedrectprivate.h"
#include "gsktransformprivate.h"
#include "gskprivate.h"

#include "gdk/gdkcolorstateprivate.h"
#include "gdk/gdkcairoprivate.h"
#include "gdk/gdkmemorytextureprivate.h"
#include "gdk/gdkrgbaprivate.h"
#include "gdk/gdksubsurfaceprivate.h"
#include "gdk/gdktextureprivate.h"

/* the epsilon we allow pixels to be off due to rounding errors.
 * Chosen rather randomly.
 */
#define EPSILON 0.001

/* the amount of pixels for us to potentially save to warrant
 * carving out a rectangle for an extra render pass
 */
#define MIN_PIXELS_FOR_OCCLUSION_PASS 1000 * 100

/* the amount of the whole image for us to potentially save to warrant
 * carving out a rectangle for an extra render pass
 */
#define MIN_PERCENTAGE_FOR_OCCLUSION_PASS 10

/* A note about coordinate systems
 *
 * The rendering code keeps track of multiple coordinate systems to optimize rendering as
 * much as possible and in the coordinate system it makes most sense in.
 * Sometimes there are cases where GL requires a certain coordinate system, too.
 *
 * 1. the node coordinate system
 * This is the coordinate system of the rendernode. It is basically not used outside of
 * looking at the node and basically never hits the GPU (it does for paths). We immediately
 * convert it to:
 *
 * 2. the basic coordinate system
 * convert on CPU: NodeProcessor.offset
 * convert on GPU: ---
 * This is the coordinate system we emit vertex state in, the clip is tracked here.
 * The main benefit is that most transform nodes only change the offset, so we can avoid
 * updating any state in this coordinate system when that happens.
 *
 * 3. the scaled coordinate system
 * converts on CPU: NodeProcessor.scale
 * converts on GPU: GSK_GLOBAL_SCALE
 * This includes the current scale of the transform. It is usually equal to the scale factor
 * of the window we are rendering to (which is bad because devs without hidpi screens can
 * forget this and then everyone else will see bugs). We make decisions about pixel sizes in
 * this coordinate system, like picking glyphs from the glyph cache or the sizes of offscreens
 * for offscreen rendering.
 *
 * 4. the device coordinate system
 * converts on CPU: NodeProcessor.modelview
 * converts on GPU: ---
 * The scissor rect is tracked in this coordinate system. It represents the actual device pixels.
 * A bunch of optimizations (like glScissor() and glClear()) can be done here, so in the case
 * that modelview == NULL and we end up with integer coordinates (because pixels), we try to go
 * here.
 * This coordinate system does not exist on shaders as they rarely reason about pixels, and if
 * they need to, they can ask the fragment shader via gl_FragCoord.
 *
 * 5. the GL coordinate system
 * converts on CPU: NodeProcessor.projection
 * converts on GPU: GSK_GLOBAL_MVP (from scaled coordinate system)
 * This coordinate system is what GL (or Vulkan) expect coordinates to appear in, and is usually
 * (-1, -1) => (1, 1), but may be flipped etc depending on the render target. The CPU essentially
 * never uses it, other than to allow the vertex shaders to emit its vertices.
 */


typedef struct _GskGpuNodeProcessor GskGpuNodeProcessor;

typedef enum {
  /* The returned image will be sampled outside the bounds, so it is
   * important that it returns the right values.
   * In particular, opaque textures must ensure they return transparency
   * and images must not be contained in an atlas.
   */
  GSK_GPU_AS_IMAGE_SAMPLED_OUT_OF_BOUNDS = (1 << 0),
  /* The returned image needs to be the exact size of the given clip
   * rect, for example because it will be repeated.
   * In detail: out_bounds must equal clip_bounds
   */
  GSK_GPU_AS_IMAGE_EXACT_SIZE = (1 << 1),
} GskGpuAsImageFlags;

typedef enum {
  GSK_GPU_GLOBAL_MATRIX  = (1 << 0),
  GSK_GPU_GLOBAL_SCALE   = (1 << 1),
  GSK_GPU_GLOBAL_CLIP    = (1 << 2),
  GSK_GPU_GLOBAL_SCISSOR = (1 << 3),
  GSK_GPU_GLOBAL_BLEND   = (1 << 4),
} GskGpuGlobals;

struct _GskGpuNodeProcessor
{
  GskGpuFrame                   *frame;
  GdkColorState                 *ccs;
  cairo_rectangle_int_t          scissor;
  GskGpuBlend                    blend;
  graphene_point_t               offset;
  graphene_matrix_t              projection;
  graphene_vec2_t                scale;
  GskTransform                  *modelview;
  GskGpuClip                     clip;
  float                          opacity;

  GskGpuGlobals                  pending_globals;
};

typedef struct _GskGpuFirstNodeInfo GskGpuFirstNodeInfo;

struct _GskGpuFirstNodeInfo
{
  GskGpuImage *target;
  cairo_rectangle_int_t extents;
  GskRenderPassType pass_type;
  gsize min_occlusion_pixels;
  float background_color[4];

  guint whole_area : 1;
  guint has_background : 1;
  guint has_started_rendering : 1;
};

static void             gsk_gpu_node_processor_add_node                 (GskGpuNodeProcessor            *self,
                                                                         GskRenderNode                  *node);
static gboolean         gsk_gpu_node_processor_add_first_node           (GskGpuNodeProcessor            *self,
                                                                         GskGpuFirstNodeInfo            *info,
                                                                         GskRenderNode                  *node);
static GskGpuImage *    gsk_gpu_get_node_as_image                       (GskGpuFrame                    *frame,
                                                                         GskGpuAsImageFlags              flags,
                                                                         GdkColorState                  *ccs,
                                                                         const graphene_rect_t          *clip_bounds,
                                                                         const graphene_vec2_t          *scale,
                                                                         GskRenderNode                  *node,
                                                                         graphene_rect_t                *out_bounds);

static void
gsk_gpu_node_processor_finish (GskGpuNodeProcessor *self)
{
  g_clear_pointer (&self->modelview, gsk_transform_unref);
}

static void
gsk_gpu_node_processor_init (GskGpuNodeProcessor         *self,
                             GskGpuFrame                 *frame,
                             GskGpuImage                 *target,
                             GdkColorState               *ccs,
                             const cairo_rectangle_int_t *clip,
                             const graphene_rect_t       *viewport)
{
  gsize width, height;

  width = gsk_gpu_image_get_width (target);
  height = gsk_gpu_image_get_height (target);

  self->frame = frame;
  self->ccs = ccs;

  self->scissor = *clip;
  self->blend = GSK_GPU_BLEND_OVER;
  if (clip->x == 0 && clip->y == 0 && clip->width == width && clip->height == height)
    {
      gsk_gpu_clip_init_empty (&self->clip, &GRAPHENE_RECT_INIT (0, 0, viewport->size.width, viewport->size.height));
    }
  else
    {
      float scale_x = viewport->size.width / width;
      float scale_y = viewport->size.height / height;
      gsk_gpu_clip_init_empty (&self->clip,
                               &GRAPHENE_RECT_INIT (
                                   scale_x * clip->x,
                                   scale_y * clip->y,
                                   scale_x * clip->width,
                                   scale_y * clip->height
                               ));
    }

  self->modelview = NULL;
  gsk_gpu_image_get_projection_matrix (target, &self->projection);
  graphene_vec2_init (&self->scale,
                      width / viewport->size.width,
                      height / viewport->size.height);
  self->offset = GRAPHENE_POINT_INIT (-viewport->origin.x,
                                      -viewport->origin.y);
  self->opacity = 1.0;
  self->pending_globals = GSK_GPU_GLOBAL_MATRIX | GSK_GPU_GLOBAL_SCALE | GSK_GPU_GLOBAL_CLIP | GSK_GPU_GLOBAL_SCISSOR | GSK_GPU_GLOBAL_BLEND;
}

static void
gsk_gpu_node_processor_emit_globals_op (GskGpuNodeProcessor *self)
{
  graphene_matrix_t mvp;

  if (self->modelview)
    {
      gsk_transform_to_matrix (self->modelview, &mvp);
      graphene_matrix_multiply (&mvp, &self->projection, &mvp);
    }
  else
    graphene_matrix_init_from_matrix (&mvp, &self->projection);

  gsk_gpu_globals_op (self->frame,
                      &self->scale,
                      &mvp,
                      &self->clip.rect);

  self->pending_globals &= ~(GSK_GPU_GLOBAL_MATRIX | GSK_GPU_GLOBAL_SCALE | GSK_GPU_GLOBAL_CLIP);
}

static void
gsk_gpu_node_processor_emit_scissor_op (GskGpuNodeProcessor *self)
{
  gsk_gpu_scissor_op (self->frame,
                      &self->scissor);
  self->pending_globals &= ~GSK_GPU_GLOBAL_SCISSOR;
}

static void
gsk_gpu_node_processor_emit_blend_op (GskGpuNodeProcessor *self)
{
  gsk_gpu_blend_op (self->frame, self->blend);
  self->pending_globals &= ~GSK_GPU_GLOBAL_BLEND;
}

static void
gsk_gpu_node_processor_sync_globals (GskGpuNodeProcessor *self,
                                     GskGpuGlobals        ignored)
{
  GskGpuGlobals required;

  required = self->pending_globals & ~ignored;

  if (required & (GSK_GPU_GLOBAL_MATRIX | GSK_GPU_GLOBAL_SCALE | GSK_GPU_GLOBAL_CLIP))
    gsk_gpu_node_processor_emit_globals_op (self);
  if (required & GSK_GPU_GLOBAL_SCISSOR)
    gsk_gpu_node_processor_emit_scissor_op (self);
  if (required & GSK_GPU_GLOBAL_BLEND)
    gsk_gpu_node_processor_emit_blend_op (self);
}

static inline GskGpuColorStates
gsk_gpu_node_processor_color_states_explicit (GskGpuNodeProcessor *self,
                                              GdkColorState       *alt,
                                              gboolean             alt_premultiplied)
{
  return gsk_gpu_color_states_create (self->ccs,
                                      TRUE,
                                      alt,
                                      alt_premultiplied);
}

static GskGpuImage *
gsk_gpu_node_processor_init_draw (GskGpuNodeProcessor   *self,
                                  GskGpuFrame           *frame,
                                  GdkColorState         *ccs,
                                  GdkMemoryDepth         depth,
                                  const graphene_vec2_t *scale,
                                  const graphene_rect_t *viewport)
{
  GskGpuImage *image;
  cairo_rectangle_int_t area;

  area.x = 0;
  area.y = 0;
  area.width = MAX (1, ceilf (graphene_vec2_get_x (scale) * viewport->size.width - EPSILON));
  area.height = MAX (1, ceilf (graphene_vec2_get_y (scale) * viewport->size.height - EPSILON));

  image = gsk_gpu_device_create_offscreen_image (gsk_gpu_frame_get_device (frame),
                                                 FALSE,
                                                 gdk_memory_depth_get_format (depth),
                                                 gdk_memory_depth_is_srgb (depth),
                                                 area.width, area.height);
  if (image == NULL)
    return NULL;

  gsk_gpu_node_processor_init (self,
                               frame,
                               image,
                               ccs,
                               &area,
                               viewport);

  gsk_gpu_render_pass_begin_op (frame,
                                image,
                                &area,
                                GSK_GPU_LOAD_OP_CLEAR,
                                GSK_VEC4_TRANSPARENT,
                                GSK_RENDER_PASS_OFFSCREEN);

  return image;
}

static void
gsk_gpu_node_processor_finish_draw (GskGpuNodeProcessor *self,
                                    GskGpuImage         *image)
{
  gsk_gpu_render_pass_end_op (self->frame,
                              image,
                              GSK_RENDER_PASS_OFFSCREEN);

  gsk_gpu_node_processor_finish (self);
}

static void
extract_scale_from_transform (GskTransform *transform,
                              float        *out_scale_x,
                              float        *out_scale_y)
{
  switch (gsk_transform_get_fine_category (transform))
    {
    default:
      g_assert_not_reached ();
    case GSK_FINE_TRANSFORM_CATEGORY_IDENTITY:
    case GSK_FINE_TRANSFORM_CATEGORY_2D_TRANSLATE:
      *out_scale_x = 1.0f;
      *out_scale_y = 1.0f;
      return;

    case GSK_FINE_TRANSFORM_CATEGORY_2D_AFFINE:
      {
        float scale_x, scale_y, dx, dy;
        gsk_transform_to_affine (transform, &scale_x, &scale_y, &dx, &dy);
        *out_scale_x = scale_x;
        *out_scale_y = scale_y;
      }
      return;

    case GSK_FINE_TRANSFORM_CATEGORY_2D_NEGATIVE_AFFINE:
    case GSK_FINE_TRANSFORM_CATEGORY_2D_DIHEDRAL:
    case GSK_FINE_TRANSFORM_CATEGORY_2D:
      {
        float skew_x, skew_y, scale_x, scale_y, angle, dx, dy;
        gsk_transform_to_2d_components (transform,
                                        &skew_x, &skew_y,
                                        &scale_x, &scale_y,
                                        &angle,
                                        &dx, &dy);
        *out_scale_x = fabs (scale_x);
        *out_scale_y = fabs (scale_y);
      }
      return;

    case GSK_FINE_TRANSFORM_CATEGORY_UNKNOWN:
    case GSK_FINE_TRANSFORM_CATEGORY_ANY:
    case GSK_FINE_TRANSFORM_CATEGORY_3D:
      {
        graphene_quaternion_t rotation;
        graphene_matrix_t matrix;
        graphene_vec4_t perspective;
        graphene_vec3_t translation;
        graphene_vec3_t matrix_scale;
        graphene_vec3_t shear;

        gsk_transform_to_matrix (transform, &matrix);
        graphene_matrix_decompose (&matrix,
                                   &translation,
                                   &matrix_scale,
                                   &rotation,
                                   &shear,
                                   &perspective);

        *out_scale_x = fabs (graphene_vec3_get_x (&matrix_scale));
        *out_scale_y = fabs (graphene_vec3_get_y (&matrix_scale));
      }
      return;
    }
}

static gboolean
gsk_gpu_node_processor_rect_clip_to_device (GskGpuNodeProcessor   *self,
                                            const graphene_rect_t *src,
                                            graphene_rect_t       *dest)
{
  graphene_rect_t transformed;
  float scale_x = graphene_vec2_get_x (&self->scale);
  float scale_y = graphene_vec2_get_y (&self->scale);

  switch (gsk_transform_get_fine_category (self->modelview))
    {
    case GSK_FINE_TRANSFORM_CATEGORY_UNKNOWN:
    case GSK_FINE_TRANSFORM_CATEGORY_ANY:
    case GSK_FINE_TRANSFORM_CATEGORY_3D:
    case GSK_FINE_TRANSFORM_CATEGORY_2D:
      return FALSE;

    case GSK_FINE_TRANSFORM_CATEGORY_2D_DIHEDRAL:
    case GSK_FINE_TRANSFORM_CATEGORY_2D_NEGATIVE_AFFINE:
    case GSK_FINE_TRANSFORM_CATEGORY_2D_AFFINE:
    case GSK_FINE_TRANSFORM_CATEGORY_2D_TRANSLATE:
      gsk_transform_transform_bounds (self->modelview, src, &transformed);
      src = &transformed;
      break;

    case GSK_FINE_TRANSFORM_CATEGORY_IDENTITY:
    default:
      break;
    }

  dest->origin.x = src->origin.x * scale_x;
  dest->origin.y = src->origin.y * scale_y;
  dest->size.width = src->size.width * scale_x;
  dest->size.height = src->size.height * scale_y;

  return TRUE;
}

static gboolean
gsk_gpu_node_processor_rect_device_to_clip (GskGpuNodeProcessor   *self,
                                            const graphene_rect_t *src,
                                            graphene_rect_t       *dest)
{
  graphene_rect_t transformed;
  float scale_x = graphene_vec2_get_x (&self->scale);
  float scale_y = graphene_vec2_get_y (&self->scale);

  switch (gsk_transform_get_fine_category (self->modelview))
    {
    case GSK_FINE_TRANSFORM_CATEGORY_UNKNOWN:
    case GSK_FINE_TRANSFORM_CATEGORY_ANY:
    case GSK_FINE_TRANSFORM_CATEGORY_3D:
    case GSK_FINE_TRANSFORM_CATEGORY_2D:
      return FALSE;

    case GSK_FINE_TRANSFORM_CATEGORY_2D_DIHEDRAL:
    case GSK_FINE_TRANSFORM_CATEGORY_2D_NEGATIVE_AFFINE:
    case GSK_FINE_TRANSFORM_CATEGORY_2D_AFFINE:
    case GSK_FINE_TRANSFORM_CATEGORY_2D_TRANSLATE:
      {
        GskTransform *inverse = gsk_transform_invert (gsk_transform_ref (self->modelview));
        g_return_val_if_fail (inverse != NULL, FALSE);
        gsk_transform_transform_bounds (inverse, src, &transformed);
        gsk_transform_unref (inverse);
        src = &transformed;
      }
      break;

    case GSK_FINE_TRANSFORM_CATEGORY_IDENTITY:
    default:
      break;
    }

  dest->origin.x = src->origin.x / scale_x;
  dest->origin.y = src->origin.y / scale_y;
  dest->size.width = src->size.width / scale_x;
  dest->size.height = src->size.height / scale_y;

  return TRUE;
}

static gboolean
gsk_gpu_node_processor_rect_to_device_shrink (GskGpuNodeProcessor   *self,
                                              const graphene_rect_t *rect,
                                              cairo_rectangle_int_t *int_rect)
{
  graphene_rect_t tmp;

  gsk_rect_init_offset (&tmp, rect, &self->offset);

  if (!gsk_gpu_node_processor_rect_clip_to_device (self, &tmp, &tmp))
    return FALSE;

  gsk_rect_to_cairo_shrink (&tmp, int_rect);

  return int_rect->width > 0 && int_rect->height > 0;
}

static gboolean
gsk_gpu_node_processor_rect_is_integer (GskGpuNodeProcessor   *self,
                                        const graphene_rect_t *rect,
                                        cairo_rectangle_int_t *int_rect)
{
  graphene_rect_t tmp;

  if (!gsk_gpu_node_processor_rect_clip_to_device (self, rect, &tmp))
    return FALSE;

  gsk_rect_to_cairo_shrink (&tmp, int_rect);

  return int_rect->x == tmp.origin.x
      && int_rect->y == tmp.origin.y
      && int_rect->width == tmp.size.width
      && int_rect->height == tmp.size.height;
}

static void
gsk_gpu_node_processor_get_clip_bounds (GskGpuNodeProcessor *self,
                                        graphene_rect_t     *out_bounds)
{
  graphene_rect_t scissor;

  if (gsk_gpu_node_processor_rect_device_to_clip (self,
                                                  &GSK_RECT_INIT_CAIRO (&self->scissor),
                                                  &scissor))
    {
      if (!gsk_rect_intersection (&scissor, &self->clip.rect.bounds, out_bounds))
        {
          g_warning ("Clipping is broken, everything is clipped, but we didn't early-exit.");
          *out_bounds = self->clip.rect.bounds;
        }
    }
  else
    {
      *out_bounds = self->clip.rect.bounds;
    }

  out_bounds->origin.x -= self->offset.x;
  out_bounds->origin.y -= self->offset.y;
}
 
static gboolean G_GNUC_WARN_UNUSED_RESULT
gsk_gpu_node_processor_clip_node_bounds (GskGpuNodeProcessor *self,
                                         GskRenderNode       *node,
                                         graphene_rect_t     *out_bounds)
{
  graphene_rect_t tmp;

  gsk_gpu_node_processor_get_clip_bounds (self, &tmp);
  
  if (!gsk_rect_intersection (&tmp, &node->bounds, out_bounds))
    return FALSE;

  return TRUE;
}

static gboolean G_GNUC_WARN_UNUSED_RESULT
gsk_gpu_node_processor_clip_node_bounds_and_snap_to_grid (GskGpuNodeProcessor *self,
                                                          GskRenderNode *node,
                                                          graphene_rect_t *out_bounds)
{
  graphene_rect_t tmp;

  gsk_gpu_node_processor_get_clip_bounds (self, &tmp);

  if (!gsk_rect_intersection (&tmp, &node->bounds, out_bounds))
    return FALSE;

  if (!gsk_rect_snap_to_grid (out_bounds, &self->scale, &self->offset, out_bounds))
    return FALSE;

  return TRUE;
}

static void
gsk_gpu_node_processor_image_op (GskGpuNodeProcessor   *self,
                                 GskGpuImage           *image,
                                 GdkColorState         *image_color_state,
                                 GskGpuSampler          sampler,
                                 const graphene_rect_t *rect,
                                 const graphene_rect_t *tex_rect)
{
  g_assert (self->pending_globals == 0);

  if (GDK_IS_BUILTIN_COLOR_STATE (image_color_state))
    {
      gsk_gpu_convert_from_builtin_op (self->frame,
                                       gsk_gpu_clip_get_shader_clip (&self->clip, &self->offset, rect),
                                       self->ccs,
                                       image_color_state,
                                       self->opacity,
                                       &self->offset,
                                       &(GskGpuShaderImage) {
                                         image,
                                         sampler,
                                         rect,
                                         tex_rect
                                       });
    }
  else if (!GDK_IS_DEFAULT_COLOR_STATE (image_color_state))
    {
      const GdkCicp *cicp = gdk_color_state_get_cicp (image_color_state);

      g_assert (cicp != NULL);

      gsk_gpu_convert_from_cicp_op (self->frame,
                                    gsk_gpu_clip_get_shader_clip (&self->clip, &self->offset, rect),
                                    cicp,
                                    gsk_gpu_color_states_create_cicp (self->ccs, TRUE, TRUE),
                                    self->opacity,
                                    &self->offset,
                                    &(GskGpuShaderImage) {
                                      image,
                                      sampler,
                                      rect,
                                      tex_rect
                                    });
    }
  else if (gsk_gpu_image_get_shader_op (image) != GDK_SHADER_DEFAULT ||
           self->opacity < 1.0 ||
           !gdk_color_state_equal (image_color_state, self->ccs))
    {
      gsk_gpu_convert_op (self->frame,
                          gsk_gpu_clip_get_shader_clip (&self->clip, &self->offset, rect),
                          gsk_gpu_node_processor_color_states_explicit (self,
                                                                        image_color_state,
                                                                        TRUE),
                          self->opacity,
                          &self->offset,
                          &(GskGpuShaderImage) {
                              image,
                              sampler,
                              rect,
                              tex_rect
                          });
    }
  else
    {
      gsk_gpu_texture_op (self->frame,
                          gsk_gpu_clip_get_shader_clip (&self->clip, &self->offset, rect),
                          &self->offset,
                          &(GskGpuShaderImage) {
                              image,
                              sampler,
                              rect,
                              tex_rect
                          });
    }
}

static GskGpuImage *
gsk_gpu_node_processor_create_offscreen (GskGpuFrame           *frame,
                                         GdkColorState         *ccs,
                                         const graphene_vec2_t *scale,
                                         const graphene_rect_t *viewport,
                                         GskRenderNode         *node)
{
  GskGpuImage *image;
  cairo_rectangle_int_t area;
  GdkMemoryDepth depth;

  area.x = 0;
  area.y = 0;
  area.width = MAX (1, ceilf (graphene_vec2_get_x (scale) * viewport->size.width - EPSILON));
  area.height = MAX (1, ceilf (graphene_vec2_get_y (scale) * viewport->size.height - EPSILON));

  depth = gdk_memory_depth_merge (gdk_color_state_get_depth (ccs),
                                  gsk_render_node_get_preferred_depth (node));

  image = gsk_gpu_device_create_offscreen_image (gsk_gpu_frame_get_device (frame),
                                                 FALSE,
                                                 gdk_memory_depth_get_format (depth),
                                                 gdk_memory_depth_is_srgb (depth),
                                                 area.width, area.height);
  if (image == NULL)
    return NULL;

  gsk_gpu_node_processor_process (frame,
                                  image,
                                  ccs,
                                  cairo_region_create_rectangle (&area),
                                  node,
                                  viewport,
                                  GSK_RENDER_PASS_OFFSCREEN);

  return image;
}

static GskGpuImage *
gsk_gpu_get_node_as_image_via_offscreen (GskGpuFrame           *frame,
                                         GskGpuAsImageFlags     flags,
                                         GdkColorState         *ccs,
                                         const graphene_rect_t *clip_bounds,
                                         const graphene_vec2_t *scale,
                                         GskRenderNode         *node,
                                         graphene_rect_t       *out_bounds)
{
  GskGpuImage *result;

  GSK_DEBUG (FALLBACK, "Offscreening node '%s'", g_type_name_from_instance ((GTypeInstance *) node));
  result = gsk_gpu_node_processor_create_offscreen (frame,
                                                    ccs,
                                                    scale,
                                                    clip_bounds,
                                                    node);

  *out_bounds = *clip_bounds;
  return result;
}

/*
 * gsk_gpu_node_copy_image:
 * @frame: The frame the image will be copied in
 * @ccs: color state the copy will be in
 * @image: (transfer full): The image to copy
 * @prepare_mipmap: If the copied image should reserve space for
 *   mipmaps
 *
 * Generates a copy of @image, but makes the copy premultiplied and potentially
 * reserves space for mipmaps.
 *
 * Returns: (transfer full): The copy of the image.
 **/
static GskGpuImage *
gsk_gpu_copy_image (GskGpuFrame   *frame,
                    GdkColorState *ccs,
                    GskGpuImage   *image,
                    GdkColorState *image_cs,
                    gboolean       prepare_mipmap)
{
  GskGpuImage *copy;
  gsize width, height;
  GskGpuImageFlags flags;
  GdkMemoryDepth depth;

  width = gsk_gpu_image_get_width (image);
  height = gsk_gpu_image_get_height (image);
  flags = gsk_gpu_image_get_flags (image);
  depth = gdk_memory_format_get_depth (gsk_gpu_image_get_format (image),
                                       gsk_gpu_image_get_conversion (image) == GSK_GPU_CONVERSION_SRGB);
  depth = gdk_memory_depth_merge (depth, gdk_color_state_get_depth (ccs));

  copy = gsk_gpu_device_create_offscreen_image (gsk_gpu_frame_get_device (frame),
                                                prepare_mipmap,
                                                gdk_memory_depth_get_format (depth),
                                                gdk_memory_depth_is_srgb (depth),
                                                width, height);

  if (gsk_gpu_frame_should_optimize (frame, GSK_GPU_OPTIMIZE_BLIT) &&
      (flags & (GSK_GPU_IMAGE_BLIT | GSK_GPU_IMAGE_FILTERABLE)) == (GSK_GPU_IMAGE_FILTERABLE | GSK_GPU_IMAGE_BLIT) &&
      gsk_gpu_image_get_shader_op (image) == GDK_SHADER_DEFAULT &&
      gdk_color_state_equal (ccs, image_cs))
    {
      gsk_gpu_blit_op (frame,
                       image,
                       copy,
                       &(cairo_rectangle_int_t) { 0, 0, width, height },
                       &(cairo_rectangle_int_t) { 0, 0, width, height },
                       GSK_GPU_BLIT_NEAREST);
    }
  else
    {
      GskGpuNodeProcessor other;
      graphene_rect_t rect = GRAPHENE_RECT_INIT (0, 0, width, height);

      gsk_gpu_node_processor_init (&other,
                                   frame,
                                   copy,
                                   ccs,
                                   &(cairo_rectangle_int_t) { 0, 0, width, height },
                                   &rect);

      gsk_gpu_render_pass_begin_op (other.frame,
                                    copy,
                                    &(cairo_rectangle_int_t) { 0, 0, width, height },
                                    GSK_GPU_LOAD_OP_DONT_CARE,
                                    NULL,
                                    GSK_RENDER_PASS_OFFSCREEN);

      other.blend = GSK_GPU_BLEND_NONE;
      other.pending_globals |= GSK_GPU_GLOBAL_BLEND;
      gsk_gpu_node_processor_sync_globals (&other, 0);

      gsk_gpu_node_processor_image_op (&other,
                                       image,
                                       image_cs,
                                       GSK_GPU_SAMPLER_DEFAULT,
                                       &rect,
                                       &rect);

      gsk_gpu_render_pass_end_op (other.frame,
                                  copy,
                                  GSK_RENDER_PASS_OFFSCREEN);

      gsk_gpu_node_processor_finish (&other);
    }

  g_object_unref (image);

  return copy;
}

/*
 * gsk_gpu_node_processor_get_node_as_image:
 * @self: a node processor
 * @flags: flags for the image
 * @clip_bounds: (nullable): clip rectangle to use or NULL to use
 *   the current clip
 * @node: the node to turn into an image
 * @out_bounds: bounds of the the image in node space
 *
 * Generates an image for the given node. The image is restricted to the
 * region in the clip bounds.
 *
 * The resulting image is guaranteed to be premultiplied.
 *
 * Returns: (nullable): The node as an image or %NULL if the node is fully
 *     clipped
 **/
static GskGpuImage *
gsk_gpu_node_processor_get_node_as_image (GskGpuNodeProcessor   *self,
                                          GskGpuAsImageFlags     flags,
                                          const graphene_rect_t *clip_bounds,
                                          GskRenderNode         *node,
                                          graphene_rect_t       *out_bounds)
{
  graphene_rect_t clip;

  if (flags & GSK_GPU_AS_IMAGE_EXACT_SIZE)
    {
      if (clip_bounds == NULL)
        clip = node->bounds;
      else
        clip = *clip_bounds;
    }
  else
    {
      if (clip_bounds == NULL)
        {
          if (!gsk_gpu_node_processor_clip_node_bounds (self, node, &clip))
            return NULL;
        }
      else
        {
          if (!gsk_rect_intersection (clip_bounds, &node->bounds, &clip))
            return NULL;
        }
      if (!gsk_rect_snap_to_grid (&clip, &self->scale, &self->offset, &clip))
        return NULL;
    }

  return gsk_gpu_get_node_as_image (self->frame,
                                    flags,
                                    self->ccs,
                                    &clip,
                                    &self->scale,
                                    node,
                                    out_bounds);
}

static void
gsk_gpu_node_processor_blur_op (GskGpuNodeProcessor       *self,
                                const graphene_rect_t     *rect,
                                const graphene_point_t    *shadow_offset,
                                float                      blur_radius,
                                const GdkColor            *shadow_color,
                                GskGpuImage               *source_image,
                                GdkMemoryDepth             source_depth,
                                const graphene_rect_t     *source_rect)
{
  GskGpuNodeProcessor other;
  GskGpuImage *intermediate;
  graphene_vec2_t direction;
  graphene_rect_t clip_rect, intermediate_rect;
  graphene_point_t real_offset;
  float clip_radius;

  clip_radius = gsk_cairo_blur_compute_pixels (blur_radius / 2.0);

  /* FIXME: Handle clip radius growing the clip too much */
  gsk_gpu_node_processor_get_clip_bounds (self, &clip_rect);
  clip_rect.origin.x -= shadow_offset->x;
  clip_rect.origin.y -= shadow_offset->y;
  graphene_rect_inset (&clip_rect, 0.f, -clip_radius);
  if (!gsk_rect_intersection (rect, &clip_rect, &intermediate_rect))
    return;

  if (!gsk_rect_snap_to_grid (&intermediate_rect, &self->scale, &self->offset, &intermediate_rect))
    return;

  intermediate = gsk_gpu_node_processor_init_draw (&other,
                                                   self->frame,
                                                   self->ccs,
                                                   source_depth,
                                                   &self->scale,
                                                   &intermediate_rect);
  g_return_if_fail (intermediate != NULL);

  gsk_gpu_node_processor_sync_globals (&other, 0);

  graphene_vec2_init (&direction, blur_radius, 0.0f);
  gsk_gpu_blur_op (other.frame,
                   gsk_gpu_clip_get_shader_clip (&other.clip, &other.offset, &intermediate_rect),
                   other.ccs,
                   1,
                   &other.offset,
                   &(GskGpuShaderImage) {
                       source_image,
                       GSK_GPU_SAMPLER_TRANSPARENT,
                       &intermediate_rect,
                       source_rect
                   },
                   &direction);

  gsk_gpu_node_processor_finish_draw (&other, intermediate);

  real_offset = GRAPHENE_POINT_INIT (self->offset.x + shadow_offset->x,
                                     self->offset.y + shadow_offset->y);
  graphene_vec2_init (&direction, 0.0f, blur_radius);
  if (shadow_color)
    {
      gsk_gpu_blur_shadow_op (self->frame,
                              gsk_gpu_clip_get_shader_clip (&self->clip, &real_offset, rect),
                              self->ccs,
                              1,
                              &real_offset,
                              &(GskGpuShaderImage) {
                                  intermediate,
                                  GSK_GPU_SAMPLER_TRANSPARENT,
                                  rect,
                                  &intermediate_rect,
                              },
                              &direction,
                              shadow_color);
    }
  else
    {
      gsk_gpu_blur_op (self->frame,
                       gsk_gpu_clip_get_shader_clip (&self->clip, &real_offset, rect),
                       self->ccs,
                       1,
                       &real_offset,
                       &(GskGpuShaderImage) {
                           intermediate,
                           GSK_GPU_SAMPLER_TRANSPARENT,
                           rect,
                           &intermediate_rect,
                       },
                       &direction);
    }

  g_object_unref (intermediate);
}

static void
gsk_gpu_node_processor_add_cairo_node (GskGpuNodeProcessor *self,
                                       GskRenderNode       *node)
{
  GskGpuImage *image;
  graphene_rect_t clipped_bounds;

  if (!gsk_gpu_node_processor_clip_node_bounds_and_snap_to_grid (self, node, &clipped_bounds))
    return;

  gsk_gpu_node_processor_sync_globals (self, 0);

  image = gsk_gpu_upload_cairo_op (self->frame,
                                   &self->scale,
                                   &clipped_bounds,
                                   (GskGpuCairoFunc) gsk_render_node_draw_fallback,
                                   gsk_render_node_ref (node),
                                   (GDestroyNotify) gsk_render_node_unref);

  gsk_gpu_node_processor_image_op (self,
                                   image,
                                   GDK_COLOR_STATE_SRGB,
                                   GSK_GPU_SAMPLER_DEFAULT,
                                   &node->bounds,
                                   &clipped_bounds);
}

static void
gsk_gpu_node_processor_add_without_opacity (GskGpuNodeProcessor *self,
                                            GskRenderNode       *node)
{
  GskGpuImage *image;
  graphene_rect_t tex_rect;

  gsk_gpu_node_processor_sync_globals (self, 0);

  image = gsk_gpu_node_processor_get_node_as_image (self,
                                                    0,
                                                    NULL,
                                                    node,
                                                    &tex_rect);
  if (image == NULL)
    return;

  gsk_gpu_node_processor_image_op (self,
                                   image,
                                   self->ccs,
                                   GSK_GPU_SAMPLER_DEFAULT,
                                   &node->bounds,
                                   &tex_rect);

  g_object_unref (image);
}

static void
gsk_gpu_node_processor_add_node_clipped (GskGpuNodeProcessor   *self,
                                         GskRenderNode         *node,
                                         const graphene_rect_t *clip_bounds)
{
  GskGpuClip old_clip;
  graphene_rect_t clip;
  cairo_rectangle_int_t scissor;

  if (gsk_rect_contains_rect (clip_bounds, &node->bounds))
    {
      gsk_gpu_node_processor_add_node (self, node);
      return;
    }

  gsk_rect_init_offset (&clip, clip_bounds, &self->offset);

  gsk_gpu_clip_init_copy (&old_clip, &self->clip);

  /* Check if we can use scissoring for the clip */
  if (gsk_gpu_node_processor_rect_is_integer (self, &clip, &scissor))
    {
      cairo_rectangle_int_t old_scissor;

      if (!gdk_rectangle_intersect (&scissor, &self->scissor, &scissor))
        return;

      old_scissor = self->scissor;

      if (gsk_gpu_clip_intersect_rect (&self->clip, &old_clip, &clip))
        {
          if (self->clip.type == GSK_GPU_CLIP_ALL_CLIPPED)
            {
              gsk_gpu_clip_init_copy (&self->clip, &old_clip);
              return;
            }
          else if ((self->clip.type == GSK_GPU_CLIP_RECT || self->clip.type == GSK_GPU_CLIP_CONTAINED) &&
                   gsk_rect_contains_rect (&self->clip.rect.bounds, &clip))
            {
              self->clip.type = GSK_GPU_CLIP_NONE;
            }

          self->scissor = scissor;
          self->pending_globals |= GSK_GPU_GLOBAL_SCISSOR | GSK_GPU_GLOBAL_CLIP;

          gsk_gpu_node_processor_add_node (self, node);

          gsk_gpu_clip_init_copy (&self->clip, &old_clip);
          self->scissor = old_scissor;
          self->pending_globals |= GSK_GPU_GLOBAL_SCISSOR | GSK_GPU_GLOBAL_CLIP;
        }
      else
        {
          self->scissor = scissor;
          self->pending_globals |= GSK_GPU_GLOBAL_SCISSOR;

          gsk_gpu_clip_init_copy (&self->clip, &old_clip);

          gsk_gpu_node_processor_add_node (self, node);

          self->scissor = old_scissor;
          self->pending_globals |= GSK_GPU_GLOBAL_SCISSOR;
        }
    }
  else
    {
      graphene_rect_t scissored_clip;

      if (gsk_gpu_node_processor_rect_device_to_clip (self,
                                                      &GSK_RECT_INIT_CAIRO (&self->scissor),
                                                      &scissored_clip))
        {
          if (!gsk_rect_intersection (&scissored_clip, &clip, &clip))
            {
              gsk_gpu_clip_init_copy (&self->clip, &old_clip);
              return;
            }
        }

      if (!gsk_gpu_clip_intersect_rect (&self->clip, &old_clip, &clip))
        {
          GskGpuImage *image;
          graphene_rect_t bounds, tex_rect;

          gsk_gpu_clip_init_copy (&self->clip, &old_clip);
          gsk_gpu_node_processor_sync_globals (self, 0);

          if (gsk_gpu_node_processor_clip_node_bounds (self, node, &bounds) &&
              gsk_rect_intersection (&bounds, clip_bounds, &bounds))
            image = gsk_gpu_node_processor_get_node_as_image (self,
                                                              0,
                                                              &bounds,
                                                              node,
                                                              &tex_rect);
          else
            image = NULL;
          if (image)
            {
              gsk_gpu_node_processor_image_op (self,
                                               image,
                                               self->ccs,
                                               GSK_GPU_SAMPLER_DEFAULT,
                                               &bounds,
                                               &tex_rect);
              g_object_unref (image);
            }
          return;
        }

      if (self->clip.type == GSK_GPU_CLIP_ALL_CLIPPED)
        {
          gsk_gpu_clip_init_copy (&self->clip, &old_clip);
          return;
        }

      self->pending_globals |= GSK_GPU_GLOBAL_CLIP;

      gsk_gpu_node_processor_add_node (self, node);

      gsk_gpu_clip_init_copy (&self->clip, &old_clip);
      self->pending_globals |= GSK_GPU_GLOBAL_CLIP;
    }
}

static void
gsk_gpu_node_processor_add_clip_node (GskGpuNodeProcessor *self,
                                      GskRenderNode       *node)
{
  gsk_gpu_node_processor_add_node_clipped (self,
                                           gsk_clip_node_get_child (node),
                                           gsk_clip_node_get_clip (node));
}

/*
 * gsk_gpu_first_node_begin_rendering:
 * @self: The node processor
 * @info: The first node info
 * @clear_color: (nullable): The color to clear to
 *
 * Begins rendering the given scissor rect and if given clears the background
 * to the given clear color.
 **/
static void
gsk_gpu_first_node_begin_rendering (GskGpuNodeProcessor *self,
                                    GskGpuFirstNodeInfo *info,
                                    float                clear_color[4])
{
  if (info->has_started_rendering)
    {
      if (clear_color &&
          (!info->has_background ||
           (info->has_background &&
            (clear_color[0] != info->background_color[0] ||
             clear_color[1] != info->background_color[1] ||
             clear_color[2] != info->background_color[2] ||
             clear_color[3] != info->background_color[3]))))
        {
          gsk_gpu_clear_op (self->frame, &self->scissor, clear_color);
        }
    }
  else
    {
      GskGpuLoadOp load_op;

      if (!info->whole_area)
        {
          info->has_background = FALSE;
          load_op = GSK_GPU_LOAD_OP_LOAD;
        }
      else if (clear_color)
        {
          info->background_color[0] = clear_color[0];
          info->background_color[1] = clear_color[1];
          info->background_color[2] = clear_color[2];
          info->background_color[3] = clear_color[3];
          info->has_background = TRUE;
          load_op = GSK_GPU_LOAD_OP_CLEAR;
        }
      else
        {
          info->has_background = FALSE;
          load_op = GSK_GPU_LOAD_OP_DONT_CARE;
        }

      info->has_started_rendering = TRUE;
      gsk_gpu_render_pass_begin_op (self->frame,
                                    info->target,
                                    &info->extents,
                                    load_op,
                                    clear_color,
                                    info->pass_type);

      if (!info->whole_area && clear_color)
        gsk_gpu_clear_op (self->frame, &self->scissor, clear_color);
    }
}

/*
 * gsk_gpu_node_processor_clip_first_node:
 * @self: the nodeprocessor
 * @info: the first pass info
 * @opaque: an opaque rectangle to clip to
 *
 * Shrinks the clip during a first node determination to only cover
 * the passed in opaque rect - or rather its intersection with the
 * previous clip.
 *
 * This can fail if the resulting scissor rect would be smaller than
 * min_occlusion_pixels and not warrant an occlusion pass.
 *
 * Adjusts scissor rect and clip, when not starting a first node,
 * you need to revert them.
 *
 * Returns: TRUE if the adjustment was successful.
 **/
static gboolean
gsk_gpu_node_processor_clip_first_node (GskGpuNodeProcessor   *self,
                                        GskGpuFirstNodeInfo   *info,
                                        const graphene_rect_t *opaque)
{
  cairo_rectangle_int_t device_clip;
  graphene_rect_t rect;

  if (!gsk_gpu_node_processor_rect_to_device_shrink (self, opaque, &device_clip) ||
      !gdk_rectangle_intersect (&device_clip, &self->scissor, &device_clip) ||
      device_clip.width * device_clip.height < info->min_occlusion_pixels)
    return FALSE;

  self->scissor = device_clip;

  gsk_gpu_node_processor_rect_device_to_clip (self,
                                              &GSK_RECT_INIT_CAIRO (&device_clip),
                                              &rect);
  gsk_gpu_clip_init_empty (&self->clip, &rect);

  return TRUE;
}

static gboolean
gsk_gpu_node_processor_add_first_node_clipped (GskGpuNodeProcessor   *self,
                                               GskGpuFirstNodeInfo   *info,
                                               const graphene_rect_t *clip,
                                               GskRenderNode         *node)
{
  GskGpuClip old_clip;
  cairo_rectangle_int_t old_scissor;

  old_scissor = self->scissor;
  gsk_gpu_clip_init_copy (&old_clip, &self->clip);

  if (!gsk_gpu_node_processor_clip_first_node (self, info, clip))
    return FALSE;

  if (gsk_gpu_node_processor_add_first_node (self,
                                             info,
                                             node))
    {
      /* don't revert clip here, the add_first_node() adjusted it to a correct value */
      return TRUE;
    }

  self->scissor = old_scissor;
  gsk_gpu_clip_init_copy (&old_clip, &self->clip);

  return FALSE;
}

static gboolean
gsk_gpu_node_processor_add_first_clip_node (GskGpuNodeProcessor *self,
                                            GskGpuFirstNodeInfo *info,
                                            GskRenderNode       *node)
{
  return gsk_gpu_node_processor_add_first_node_clipped (self,
                                                        info,
                                                        &node->bounds,
                                                        gsk_clip_node_get_child (node));
}

static void
gsk_gpu_node_processor_add_rounded_clip_node_with_mask (GskGpuNodeProcessor *self,
                                                        GskRenderNode       *node)
{
  GskGpuNodeProcessor other;
  graphene_rect_t clip_bounds, child_rect;
  GskGpuImage *child_image, *mask_image;
  GdkColor white;

  if (!gsk_gpu_node_processor_clip_node_bounds_and_snap_to_grid (self, node, &clip_bounds))
    return;

  child_image = gsk_gpu_node_processor_get_node_as_image (self,
                                                          0,
                                                          &clip_bounds,
                                                          gsk_rounded_clip_node_get_child (node),
                                                          &child_rect);
  if (child_image == NULL)
    return;

  mask_image = gsk_gpu_node_processor_init_draw (&other,
                                                 self->frame,
                                                 self->ccs,
                                                 gdk_memory_depth_merge (gdk_color_state_get_depth (self->ccs),
                                                                         gsk_render_node_get_preferred_depth (node)),
                                                 &self->scale,
                                                 &clip_bounds);

  g_return_if_fail (mask_image != NULL);

  gdk_color_init (&white, self->ccs, ((float[]){ 1, 1, 1, 1 }));
  gsk_gpu_node_processor_sync_globals (&other, 0);
  gsk_gpu_rounded_color_op (other.frame,
                            gsk_gpu_clip_get_shader_clip (&other.clip, &other.offset, &node->bounds),
                            self->ccs,
                            1,
                            &other.offset,
                            gsk_rounded_clip_node_get_clip (node),
                            &white);
  gsk_gpu_node_processor_finish_draw (&other, mask_image);

  gsk_gpu_node_processor_sync_globals (self, 0);
  gsk_gpu_mask_op (self->frame,
                   gsk_gpu_clip_get_shader_clip (&self->clip, &self->offset, &clip_bounds),
                   &clip_bounds,
                   &self->offset,
                   self->opacity,
                   GSK_MASK_MODE_ALPHA,
                   &(GskGpuShaderImage) {
                       child_image,
                       GSK_GPU_SAMPLER_DEFAULT,
                       NULL,
                       &child_rect,
                   },
                   &(GskGpuShaderImage) {
                       mask_image,
                       GSK_GPU_SAMPLER_DEFAULT,
                       NULL,
                       &clip_bounds,
                   });

  g_object_unref (child_image);
  g_object_unref (mask_image);
  gdk_color_finish (&white);
}

static void
gsk_gpu_node_processor_add_rounded_clip_node (GskGpuNodeProcessor *self,
                                              GskRenderNode       *node)
{
  GskGpuClip old_clip;
  GskRoundedRect clip;
  const GskRoundedRect *original_clip;
  GskRenderNode *child;
  graphene_rect_t scissor;

  child = gsk_rounded_clip_node_get_child (node);
  original_clip = gsk_rounded_clip_node_get_clip (node);

  /* Common case for entries etc: rounded solid color background.
   * And we have a shader for that */
  if (gsk_render_node_get_node_type (child) == GSK_COLOR_NODE &&
      gsk_rect_contains_rect (&child->bounds, &original_clip->bounds))
    {
      gsk_gpu_node_processor_sync_globals (self, 0);

      gsk_gpu_rounded_color_op (self->frame,
                                gsk_gpu_clip_get_shader_clip (&self->clip, &self->offset, &original_clip->bounds),
                                self->ccs,
                                self->opacity,
                                &self->offset,
                                original_clip,
                                gsk_color_node_get_gdk_color (child));
      return;
    }

  gsk_gpu_clip_init_copy (&old_clip, &self->clip);

  clip = *original_clip;
  gsk_rounded_rect_offset (&clip, self->offset.x, self->offset.y);

  if (!gsk_gpu_clip_intersect_rounded_rect (&self->clip, &old_clip, &clip))
    {
      gsk_gpu_clip_init_copy (&self->clip, &old_clip);
      gsk_gpu_node_processor_add_rounded_clip_node_with_mask (self, node);
      return;
    }

  if (gsk_gpu_node_processor_rect_device_to_clip (self,
                                                  &GSK_RECT_INIT_CAIRO (&self->scissor),
                                                  &scissor))
    {
      GskGpuClip scissored_clip;
      if (gsk_gpu_clip_intersect_rect (&scissored_clip, &self->clip, &scissor))
        gsk_gpu_clip_init_copy (&self->clip, &scissored_clip);
    }

  if (self->clip.type == GSK_GPU_CLIP_ALL_CLIPPED)
    {
      gsk_gpu_clip_init_copy (&self->clip, &old_clip);
      return;
    }

  self->pending_globals |= GSK_GPU_GLOBAL_CLIP;

  gsk_gpu_node_processor_add_node (self, gsk_rounded_clip_node_get_child (node));

  gsk_gpu_clip_init_copy (&self->clip, &old_clip);
  self->pending_globals |= GSK_GPU_GLOBAL_CLIP;
}

static gboolean
gsk_gpu_node_processor_add_first_rounded_clip_node (GskGpuNodeProcessor *self,
                                                    GskGpuFirstNodeInfo *info,
                                                    GskRenderNode       *node)
{
  graphene_rect_t cover, clip;

  gsk_gpu_node_processor_get_clip_bounds (self, &clip);
  gsk_rounded_rect_get_largest_cover (gsk_rounded_clip_node_get_clip (node),
                                      &clip,
                                      &cover);

  return gsk_gpu_node_processor_add_first_node_clipped (self,
                                                        info,
                                                        &cover,
                                                        gsk_rounded_clip_node_get_child (node));
}

static void
gsk_gpu_node_processor_add_transform_node (GskGpuNodeProcessor *self,
                                           GskRenderNode       *node)
{
  GskRenderNode *child;
  GskTransform *transform;
  graphene_point_t old_offset;
  graphene_vec2_t old_scale;
  GskTransform *old_modelview;
  GskGpuClip old_clip;

  child = gsk_transform_node_get_child (node);
  transform = gsk_transform_node_get_transform (node);

  switch (gsk_transform_get_fine_category (transform))
    {
    case GSK_FINE_TRANSFORM_CATEGORY_IDENTITY:
    case GSK_FINE_TRANSFORM_CATEGORY_2D_TRANSLATE:
      {
        float dx, dy;
        gsk_transform_to_translate (transform, &dx, &dy);
        old_offset = self->offset;
        self->offset.x += dx;
        self->offset.y += dy;
        gsk_gpu_node_processor_add_node (self, child);
        self->offset = old_offset;
      }
      return;

    case GSK_FINE_TRANSFORM_CATEGORY_2D_AFFINE:
      {
        float dx, dy, scale_x, scale_y;

        gsk_gpu_clip_init_copy (&old_clip, &self->clip);
        old_offset = self->offset;
        old_scale = self->scale;
        old_modelview = self->modelview;

        gsk_transform_to_affine (transform, &scale_x, &scale_y, &dx, &dy);
        gsk_gpu_clip_scale (&self->clip, &old_clip, GDK_DIHEDRAL_NORMAL, scale_x, scale_y);
        self->offset.x = (self->offset.x + dx) / scale_x;
        self->offset.y = (self->offset.y + dy) / scale_y;
        graphene_vec2_init (&self->scale, scale_x, scale_y);
        graphene_vec2_multiply (&self->scale, &old_scale, &self->scale);
      }
      break;

    case GSK_FINE_TRANSFORM_CATEGORY_2D_NEGATIVE_AFFINE:
    case GSK_FINE_TRANSFORM_CATEGORY_2D_DIHEDRAL:
      {
        GdkDihedral dihedral, inverted;
        float xx, xy, yx, yy, dx, dy, scale_x, scale_y, old_scale_x, old_scale_y;

        gsk_gpu_clip_init_copy (&old_clip, &self->clip);
        old_offset = self->offset;
        old_scale = self->scale;
        old_modelview = gsk_transform_ref (self->modelview);

        gsk_transform_to_dihedral (transform, &dihedral, &scale_x, &scale_y, &dx, &dy);
        inverted = gdk_dihedral_invert (dihedral);
        gdk_dihedral_get_mat2 (inverted, &xx, &xy, &yx, &yy);
        gsk_gpu_clip_scale (&self->clip, &old_clip, inverted, scale_x, scale_y);
        self->offset.x = (self->offset.x + dx) / scale_x;
        self->offset.y = (self->offset.y + dy) / scale_y;
        self->offset = GRAPHENE_POINT_INIT (xx * self->offset.x + xy * self->offset.y,
                                            yx * self->offset.x + yy * self->offset.y);
        old_scale_x = graphene_vec2_get_x (&old_scale);
        old_scale_y = graphene_vec2_get_y (&old_scale);
        graphene_vec2_init (&self->scale,
                            fabs (scale_x * (old_scale_x * xx + old_scale_y * yx)),
                            fabs (scale_y * (old_scale_x * xy + old_scale_y * yy)));
        self->modelview = gsk_transform_dihedral (self->modelview, dihedral);
      }
      break;

    case GSK_FINE_TRANSFORM_CATEGORY_2D:
    case GSK_FINE_TRANSFORM_CATEGORY_UNKNOWN:
    case GSK_FINE_TRANSFORM_CATEGORY_ANY:
    case GSK_FINE_TRANSFORM_CATEGORY_3D:
      {
        GskTransform *clip_transform;
        float scale_x, scale_y, old_pixels, new_pixels;
        graphene_rect_t scissor;

        clip_transform = gsk_transform_transform (gsk_transform_translate (NULL, &self->offset), transform);
        gsk_gpu_clip_init_copy (&old_clip, &self->clip);

        if (gsk_gpu_clip_contains_rect (&self->clip, &self->offset, &node->bounds))
          {
            gsk_gpu_clip_init_contained (&self->clip, &child->bounds);
          }
        else if (old_clip.type == GSK_GPU_CLIP_NONE)
          {
            GskTransform *inverse;
            graphene_rect_t new_bounds;
            inverse = gsk_transform_invert (gsk_transform_ref (clip_transform));
            gsk_transform_transform_bounds (inverse, &old_clip.rect.bounds, &new_bounds);
            gsk_transform_unref (inverse);
            gsk_gpu_clip_init_empty (&self->clip, &new_bounds);
          }
        else if (!gsk_gpu_clip_transform (&self->clip, &old_clip, clip_transform, &child->bounds))
          {
            GskGpuImage *image;
            graphene_rect_t tex_rect;
            gsk_transform_unref (clip_transform);
            /* This cannot loop because the next time we'll hit the branch above */
            gsk_gpu_node_processor_sync_globals (self, 0);
            image = gsk_gpu_node_processor_get_node_as_image (self,
                                                              0,
                                                              NULL,
                                                              node,
                                                              &tex_rect);
            if (image != NULL)
              {
                gsk_gpu_node_processor_image_op (self,
                                                 image,
                                                 self->ccs,
                                                 GSK_GPU_SAMPLER_DEFAULT,
                                                 &node->bounds,
                                                 &tex_rect);
                g_object_unref (image);
              }
            return;
          }

        old_offset = self->offset;
        old_scale = self->scale;
        old_modelview = gsk_transform_ref (self->modelview);

        self->modelview = gsk_transform_scale (self->modelview,
                                               graphene_vec2_get_x (&self->scale),
                                               graphene_vec2_get_y (&self->scale));
        self->modelview = gsk_transform_transform (self->modelview, clip_transform);
        gsk_transform_unref (clip_transform);

        extract_scale_from_transform (self->modelview, &scale_x, &scale_y);

        old_pixels = MAX (graphene_vec2_get_x (&old_scale) * old_clip.rect.bounds.size.width,
                          graphene_vec2_get_y (&old_scale) * old_clip.rect.bounds.size.height);
        new_pixels = MAX (scale_x * self->clip.rect.bounds.size.width,
                          scale_y * self->clip.rect.bounds.size.height);

        /* Check that our offscreen doesn't get too big.  1.5 ~ sqrt(2) */
        if (new_pixels > 1.5 * old_pixels)
          {
            float forced_downscale = 2 * old_pixels / new_pixels;
            scale_x *= forced_downscale;
            scale_y *= forced_downscale;
          }

        self->modelview = gsk_transform_scale (self->modelview, 1 / scale_x, 1 / scale_y);
        graphene_vec2_init (&self->scale, scale_x, scale_y);
        self->offset = *graphene_point_zero ();

        if (gsk_gpu_node_processor_rect_device_to_clip (self,
                                                        &GSK_RECT_INIT_CAIRO (&self->scissor),
                                                        &scissor))
          {
            GskGpuClip scissored_clip;
            if (gsk_gpu_clip_intersect_rect (&scissored_clip, &self->clip, &scissor))
              gsk_gpu_clip_init_copy (&self->clip, &scissored_clip);

            if (self->clip.type == GSK_GPU_CLIP_ALL_CLIPPED)
              {
                self->offset = old_offset;
                self->scale = old_scale;
                gsk_gpu_clip_init_copy (&self->clip, &old_clip);
              }
          }

      }
      break;

    default:
      g_assert_not_reached ();
      break;
    }

  self->pending_globals |= GSK_GPU_GLOBAL_SCALE | GSK_GPU_GLOBAL_CLIP;
  if (self->modelview != old_modelview)
    self->pending_globals |= GSK_GPU_GLOBAL_MATRIX;

  gsk_gpu_node_processor_add_node (self, child);

  self->offset = old_offset;
  self->scale = old_scale;
  gsk_gpu_clip_init_copy (&self->clip, &old_clip);
  self->pending_globals |= GSK_GPU_GLOBAL_SCALE | GSK_GPU_GLOBAL_CLIP;
  if (self->modelview != old_modelview)
    {
      self->pending_globals |= GSK_GPU_GLOBAL_MATRIX;
      gsk_transform_unref (self->modelview);
      self->modelview = old_modelview;
    }
}

static gboolean
gsk_gpu_node_processor_add_first_transform_node (GskGpuNodeProcessor *self,
                                                 GskGpuFirstNodeInfo *info,
                                                 GskRenderNode       *node)
{
  GskTransform *transform;
  float dx, dy, scale_x, scale_y;
  GskGpuClip old_clip;
  graphene_point_t old_offset;
  graphene_vec2_t old_scale;
  gboolean result;

  transform = gsk_transform_node_get_transform (node);

  switch (gsk_transform_get_fine_category (transform))
    {
    case GSK_FINE_TRANSFORM_CATEGORY_IDENTITY:
    case GSK_FINE_TRANSFORM_CATEGORY_2D_TRANSLATE:
      gsk_transform_to_translate (transform, &dx, &dy);
      old_offset = self->offset;
      self->offset.x += dx;
      self->offset.y += dy;
      result = gsk_gpu_node_processor_add_first_node (self,
                                                      info,
                                                      gsk_transform_node_get_child (node));
      self->offset = old_offset;
      return result;

    case GSK_FINE_TRANSFORM_CATEGORY_2D_AFFINE:
      gsk_transform_to_affine (transform, &scale_x, &scale_y, &dx, &dy);

      gsk_gpu_clip_init_copy (&old_clip, &self->clip);
      old_offset = self->offset;
      old_scale = self->scale;

      gsk_gpu_clip_scale (&self->clip, &old_clip, GDK_DIHEDRAL_NORMAL, scale_x, scale_y);
      self->offset.x = (self->offset.x + dx) / scale_x;
      self->offset.y = (self->offset.y + dy) / scale_y;
      graphene_vec2_init (&self->scale, fabs (scale_x), fabs (scale_y));
      graphene_vec2_multiply (&self->scale, &old_scale, &self->scale);

      self->pending_globals |= GSK_GPU_GLOBAL_SCALE | GSK_GPU_GLOBAL_CLIP;

      result = gsk_gpu_node_processor_add_first_node (self,
                                                      info,
                                                      gsk_transform_node_get_child (node));

      self->offset = old_offset;
      self->scale = old_scale;
      gsk_gpu_clip_init_copy (&self->clip, &old_clip);

      self->pending_globals |= GSK_GPU_GLOBAL_SCALE | GSK_GPU_GLOBAL_CLIP;

      return result;

    case GSK_FINE_TRANSFORM_CATEGORY_2D_NEGATIVE_AFFINE:
    case GSK_FINE_TRANSFORM_CATEGORY_2D_DIHEDRAL:
      {
        GdkDihedral dihedral, inverted;
        float xx, xy, yx, yy, old_scale_x, old_scale_y;
        GskTransform *old_modelview;

        gsk_gpu_clip_init_copy (&old_clip, &self->clip);
        old_offset = self->offset;
        old_scale = self->scale;
        old_modelview = gsk_transform_ref (self->modelview);

        gsk_transform_to_dihedral (transform, &dihedral, &scale_x, &scale_y, &dx, &dy);
        inverted = gdk_dihedral_invert (dihedral);
        gdk_dihedral_get_mat2 (inverted, &xx, &xy, &yx, &yy);
        gsk_gpu_clip_scale (&self->clip, &old_clip, inverted, scale_x, scale_y);
        self->offset.x = (self->offset.x + dx) / scale_x;
        self->offset.y = (self->offset.y + dy) / scale_y;
        self->offset = GRAPHENE_POINT_INIT (xx * self->offset.x + xy * self->offset.y,
                                            yx * self->offset.x + yy * self->offset.y);
        old_scale_x = graphene_vec2_get_x (&old_scale);
        old_scale_y = graphene_vec2_get_y (&old_scale);
        graphene_vec2_init (&self->scale,
                            fabs (scale_x * (old_scale_x * xx + old_scale_y * yx)),
                            fabs (scale_y * (old_scale_x * xy + old_scale_y * yy)));
        self->modelview = gsk_transform_dihedral (self->modelview, dihedral);

        self->pending_globals |= GSK_GPU_GLOBAL_MATRIX | GSK_GPU_GLOBAL_SCALE | GSK_GPU_GLOBAL_CLIP;

        result = gsk_gpu_node_processor_add_first_node (self,
                                                        info,
                                                        gsk_transform_node_get_child (node));

        self->offset = old_offset;
        self->scale = old_scale;
        gsk_gpu_clip_init_copy (&self->clip, &old_clip);
        gsk_transform_unref (self->modelview);
        self->modelview = old_modelview;

        self->pending_globals |= GSK_GPU_GLOBAL_MATRIX | GSK_GPU_GLOBAL_SCALE | GSK_GPU_GLOBAL_CLIP;

        return result;
      }

    case GSK_FINE_TRANSFORM_CATEGORY_2D:
    case GSK_FINE_TRANSFORM_CATEGORY_UNKNOWN:
    case GSK_FINE_TRANSFORM_CATEGORY_ANY:
    case GSK_FINE_TRANSFORM_CATEGORY_3D:
      return FALSE;

    default:
      g_assert_not_reached ();
      return FALSE;
    }
}

static void
gsk_gpu_node_processor_add_opacity_node (GskGpuNodeProcessor *self,
                                         GskRenderNode       *node)
{
  float old_opacity = self->opacity;

  self->opacity *= gsk_opacity_node_get_opacity (node);

  gsk_gpu_node_processor_add_node (self, gsk_opacity_node_get_child (node));

  self->opacity = old_opacity;
}

static void
gsk_gpu_node_processor_add_color_node (GskGpuNodeProcessor *self,
                                       GskRenderNode       *node)
{
  cairo_rectangle_int_t int_clipped;
  graphene_rect_t rect, clipped;
  float clear_color[4];

  gsk_rect_init_offset (&rect, &node->bounds, &self->offset);
  gsk_rect_intersection (&self->clip.rect.bounds, &rect, &clipped);

  if (gsk_gpu_frame_should_optimize (self->frame, GSK_GPU_OPTIMIZE_CLEAR) &&
      gdk_color_is_opaque (gsk_color_node_get_gdk_color (node)) &&
      self->opacity >= 1.0 &&
      node->bounds.size.width * node->bounds.size.height > 100 * 100 && /* not worth the effort for small images */
      gsk_gpu_node_processor_rect_is_integer (self, &clipped, &int_clipped))
    {
      /* now handle all the clip */
      if (!gdk_rectangle_intersect (&int_clipped, &self->scissor, &int_clipped))
        return;

      /* we have handled the bounds, now do the corners */
      if (self->clip.type == GSK_GPU_CLIP_ROUNDED)
        {
          graphene_rect_t cover;
          GskGpuShaderClip shader_clip;
          float scale_x, scale_y;

          if (self->modelview)
            {
              /* Yuck, rounded clip and modelview. I give up. */
              gsk_gpu_color_op (self->frame,
                                gsk_gpu_clip_get_shader_clip (&self->clip, &self->offset, &node->bounds),
                                self->ccs,
                                self->opacity,
                                &self->offset,
                                &node->bounds,
                                gsk_color_node_get_gdk_color (node));
              return;
            }

          scale_x = graphene_vec2_get_x (&self->scale);
          scale_y = graphene_vec2_get_y (&self->scale);
          clipped = GRAPHENE_RECT_INIT (int_clipped.x / scale_x, int_clipped.y / scale_y,
                                        int_clipped.width / scale_x, int_clipped.height / scale_y);
          shader_clip = gsk_gpu_clip_get_shader_clip (&self->clip, graphene_point_zero(), &clipped);
          if (shader_clip != GSK_GPU_SHADER_CLIP_NONE)
            {
              gsk_rounded_rect_get_largest_cover (&self->clip.rect, &clipped, &cover);
              int_clipped.x = ceilf (cover.origin.x * scale_x);
              int_clipped.y = ceilf (cover.origin.y * scale_y);
              int_clipped.width = floorf ((cover.origin.x + cover.size.width) * scale_x) - int_clipped.x;
              int_clipped.height = floorf ((cover.origin.y + cover.size.height) * scale_y) - int_clipped.y;
              if (int_clipped.width == 0 || int_clipped.height == 0)
                {
                  gsk_gpu_color_op (self->frame,
                                    shader_clip,
                                    self->ccs,
                                    self->opacity,
                                    graphene_point_zero (),
                                    &clipped,
                                    gsk_color_node_get_gdk_color (node));
                  return;
                }
              cover = GRAPHENE_RECT_INIT (int_clipped.x / scale_x, int_clipped.y / scale_y,
                                          int_clipped.width / scale_x, int_clipped.height / scale_y);
              if (clipped.origin.x != cover.origin.x)
                gsk_gpu_color_op (self->frame,
                                  shader_clip,
                                  self->ccs,
                                  self->opacity,
                                  graphene_point_zero (),
                                  &GRAPHENE_RECT_INIT (clipped.origin.x, clipped.origin.y, cover.origin.x - clipped.origin.x, clipped.size.height),
                                  gsk_color_node_get_gdk_color (node));
              if (clipped.origin.y != cover.origin.y)
                gsk_gpu_color_op (self->frame,
                                  shader_clip,
                                  self->ccs,
                                  self->opacity,
                                  graphene_point_zero (),
                                  &GRAPHENE_RECT_INIT (clipped.origin.x, clipped.origin.y, clipped.size.width, cover.origin.y - clipped.origin.y),
                                  gsk_color_node_get_gdk_color (node));
              if (clipped.origin.x + clipped.size.width != cover.origin.x + cover.size.width)
                gsk_gpu_color_op (self->frame,
                                  shader_clip,
                                  self->ccs,
                                  self->opacity,
                                  graphene_point_zero (),
                                  &GRAPHENE_RECT_INIT (cover.origin.x + cover.size.width,
                                                       clipped.origin.y,
                                                       clipped.origin.x + clipped.size.width - cover.origin.x - cover.size.width,
                                                       clipped.size.height),
                                  gsk_color_node_get_gdk_color (node));
              if (clipped.origin.y + clipped.size.height != cover.origin.y + cover.size.height)
                gsk_gpu_color_op (self->frame,
                                  shader_clip,
                                  self->ccs,
                                  self->opacity,
                                  graphene_point_zero (),
                                  &GRAPHENE_RECT_INIT (clipped.origin.x,
                                                       cover.origin.y + cover.size.height,
                                                       clipped.size.width,
                                                       clipped.origin.y + clipped.size.height - cover.origin.y - cover.size.height),
                                  gsk_color_node_get_gdk_color (node));
            }
        }

      gdk_color_to_float (gsk_color_node_get_gdk_color (node), self->ccs, clear_color);
      gsk_gpu_clear_op (self->frame, &int_clipped, clear_color);
      return;
    }

  gsk_gpu_color_op (self->frame,
                    gsk_gpu_clip_get_shader_clip (&self->clip, &self->offset, &node->bounds),
                    self->ccs,
                    self->opacity,
                    &self->offset,
                    &node->bounds,
                    gsk_color_node_get_gdk_color (node));
}

static gboolean
gsk_gpu_node_processor_add_first_color_node (GskGpuNodeProcessor *self,
                                             GskGpuFirstNodeInfo *info,
                                             GskRenderNode       *node)
{
  float clear_color[4];

  if (!node->fully_opaque)
    return FALSE;

  if (!gsk_gpu_node_processor_clip_first_node (self, info, &node->bounds))
    return FALSE;

  gdk_color_to_float (gsk_color_node_get_gdk_color (node), self->ccs, clear_color);
  gsk_gpu_first_node_begin_rendering (self, info, clear_color);

  return TRUE;
}

static void
gsk_gpu_node_processor_add_border_node (GskGpuNodeProcessor *self,
                                        GskRenderNode       *node)
{
  gsk_gpu_border_op (self->frame,
                     gsk_gpu_clip_get_shader_clip (&self->clip, &self->offset, &node->bounds),
                     self->ccs,
                     self->opacity,
                     &self->offset,
                     gsk_border_node_get_outline (node),
                     graphene_point_zero (),
                     gsk_border_node_get_widths (node),
                     gsk_border_node_get_gdk_colors (node));
}

static gboolean
texture_node_should_mipmap (GskRenderNode         *node,
                            GskGpuFrame           *frame,
                            const graphene_vec2_t *scale)
{
  GdkTexture *texture;

  texture = gsk_texture_node_get_texture (node);

  if (!gsk_gpu_frame_should_optimize (frame, GSK_GPU_OPTIMIZE_MIPMAP))
    return FALSE;

  return gdk_texture_get_width (texture) > 2 * node->bounds.size.width * graphene_vec2_get_x (scale) ||
         gdk_texture_get_height (texture) > 2 * node->bounds.size.height * graphene_vec2_get_y (scale);
}

static GskGpuImage *
gsk_gpu_lookup_texture (GskGpuFrame    *frame,
                        GdkColorState  *ccs,
                        GdkTexture     *texture,
                        gboolean        try_mipmap,
                        GdkColorState **out_image_cs)
{
  GskGpuCache *cache;
  GdkColorState *image_cs;
  GskGpuImage *image;

  cache = gsk_gpu_device_get_cache (gsk_gpu_frame_get_device (frame));

  image = gsk_gpu_cache_lookup_texture_image (cache, texture, ccs);
  if (image)
    {
      *out_image_cs = ccs;
      return image;
    }

  image = gsk_gpu_cache_lookup_texture_image (cache, texture, NULL);
  if (image == NULL)
    image = gsk_gpu_frame_upload_texture (frame, try_mipmap, texture);

  /* Happens ie for oversized textures */
  if (image == NULL)
    return NULL;

  image_cs = gsk_gpu_color_state_apply_conversion (gdk_texture_get_color_state (texture),
                                                   gsk_gpu_image_get_conversion (image));
  g_assert (image_cs);

  *out_image_cs = image_cs;
  return image;
}

static GskGpuSampler
gsk_gpu_sampler_for_scaling_filter (GskScalingFilter scaling_filter)
{
  switch (scaling_filter)
    {
      case GSK_SCALING_FILTER_LINEAR:
        return GSK_GPU_SAMPLER_DEFAULT;

      case GSK_SCALING_FILTER_NEAREST:
        return GSK_GPU_SAMPLER_NEAREST;

      case GSK_SCALING_FILTER_TRILINEAR:
        return GSK_GPU_SAMPLER_MIPMAP_DEFAULT;

      default:
        g_assert_not_reached ();
        return GSK_GPU_SAMPLER_DEFAULT;
    }
}

/* must be set up with BLEND_ADD to avoid seams */
static void
gsk_gpu_node_processor_draw_texture_tiles (GskGpuNodeProcessor    *self,
                                           const graphene_rect_t  *texture_bounds,
                                           GdkTexture             *texture,
                                           GskScalingFilter        scaling_filter)
{
  GskGpuCache *cache;
  GskGpuDevice *device;
  GskGpuImage *tile;
  GdkColorState *tile_cs;
  GskGpuSampler sampler;
  gboolean need_mipmap;
  GdkMemoryTexture *memtex;
  GdkTexture *subtex;
  float scale_factor, scaled_tile_width, scaled_tile_height;
  gsize tile_size, width, height, n_width, n_height, x, y;
  graphene_rect_t clip_bounds;
  guint lod_level;

  device = gsk_gpu_frame_get_device (self->frame);
  cache = gsk_gpu_device_get_cache (device);
  sampler = gsk_gpu_sampler_for_scaling_filter (scaling_filter);
  need_mipmap = scaling_filter == GSK_SCALING_FILTER_TRILINEAR;
  gsk_gpu_node_processor_get_clip_bounds (self, &clip_bounds);
  width = gdk_texture_get_width (texture);
  height = gdk_texture_get_height (texture);
  tile_size = gsk_gpu_device_get_tile_size (device);
  scale_factor = MIN (width / MAX (tile_size, texture_bounds->size.width),
                      height / MAX (tile_size, texture_bounds->size.height));
  if (scale_factor <= 1.0)
    lod_level = 0;
  else
    lod_level = floor (log2f (scale_factor));
  tile_size <<= lod_level;
  n_width = (width + tile_size - 1) / tile_size;
  n_height = (height + tile_size - 1) / tile_size;
  scaled_tile_width = texture_bounds->size.width * tile_size / width;
  scaled_tile_height = texture_bounds->size.height * tile_size / height;

  memtex = NULL;
  for (y = 0; y < n_height; y++)
    {
      for (x = 0; x < n_width; x++)
        {
          graphene_rect_t tile_rect = GRAPHENE_RECT_INIT (texture_bounds->origin.x + scaled_tile_width * x,
                                                          texture_bounds->origin.y + scaled_tile_height * y,
                                                          scaled_tile_width,
                                                          scaled_tile_height);
          if (!gsk_rect_intersection (&tile_rect, texture_bounds, &tile_rect) ||
              !gsk_rect_intersects (&clip_bounds, &tile_rect))
            continue;

          tile = gsk_gpu_cache_lookup_tile (cache, texture, lod_level, scaling_filter, y * n_width + x, &tile_cs);

          if (tile == NULL)
            {
              if (memtex == NULL)
                memtex = gdk_memory_texture_from_texture (texture);
              subtex = gdk_memory_texture_new_subtexture (memtex,
                                                          x * tile_size,
                                                          y * tile_size,
                                                          MIN (tile_size, width - x * tile_size),
                                                          MIN (tile_size, height - y * tile_size));
              tile = gsk_gpu_upload_texture_op_try (self->frame, need_mipmap, lod_level, scaling_filter, subtex);
              g_object_unref (subtex);
              if (tile == NULL)
                {
                  g_warning ("failed to create %zux%zu tile for %zux%zu texture. Out of memory?",
                             tile_size, tile_size, width, height);
                  goto out;
                }

              tile_cs = gdk_texture_get_color_state (texture);
              if (gsk_gpu_image_get_conversion (tile) == GSK_GPU_CONVERSION_SRGB)
                {
                  tile_cs = gdk_color_state_get_no_srgb_tf (tile_cs);
                  g_assert (tile_cs);
                }

              gsk_gpu_cache_cache_tile (cache, texture, lod_level, scaling_filter, y * n_width + x, tile, tile_cs);
            }

          if (need_mipmap &&
              (gsk_gpu_image_get_shader_op (tile) != GDK_SHADER_DEFAULT ||
               ((gsk_gpu_image_get_flags (tile) & GSK_GPU_IMAGE_CAN_MIPMAP)) != GSK_GPU_IMAGE_CAN_MIPMAP))
            {
              tile = gsk_gpu_copy_image (self->frame, self->ccs, tile, tile_cs, TRUE);
              tile_cs = self->ccs;
              gsk_gpu_cache_cache_tile (cache, texture, lod_level, scaling_filter, y * n_width + x, tile, tile_cs);
            }
          if (need_mipmap && !(gsk_gpu_image_get_flags (tile) & GSK_GPU_IMAGE_MIPMAP))
            gsk_gpu_mipmap_op (self->frame, tile);

          gsk_gpu_node_processor_image_op (self,
                                           tile,
                                           tile_cs,
                                           sampler,
                                           &tile_rect,
                                           &tile_rect);

          g_object_unref (tile);
        }
    }

out:
  g_clear_object (&memtex);
}

static GskGpuImage *
gsk_gpu_get_texture_tiles_as_image (GskGpuFrame            *frame,
                                    GdkColorState          *ccs,
                                    const graphene_rect_t  *clip_bounds,
                                    const graphene_vec2_t  *scale,
                                    const graphene_rect_t  *texture_bounds,
                                    GdkTexture             *texture,
                                    GskScalingFilter        scaling_filter)
{
  GskGpuNodeProcessor self;
  GskGpuImage *image;

  image = gsk_gpu_node_processor_init_draw (&self,
                                            frame,
                                            ccs,
                                            gdk_texture_get_depth (texture),
                                            scale,
                                            clip_bounds);
  if (image == NULL)
    return NULL;

  self.blend = GSK_GPU_BLEND_ADD;
  self.pending_globals |= GSK_GPU_GLOBAL_BLEND;
  gsk_gpu_node_processor_sync_globals (&self, 0);

  gsk_gpu_node_processor_draw_texture_tiles (&self,
                                             texture_bounds,
                                             texture,
                                             scaling_filter);

  gsk_gpu_node_processor_finish_draw (&self, image);

  return image;
}

static void
gsk_gpu_node_processor_add_texture_node (GskGpuNodeProcessor *self,
                                         GskRenderNode       *node)
{
  GdkColorState *image_cs;
  GskGpuImage *image;
  GdkTexture *texture;
  gboolean should_mipmap;

  texture = gsk_texture_node_get_texture (node);
  should_mipmap = texture_node_should_mipmap (node, self->frame, &self->scale);

  image = gsk_gpu_lookup_texture (self->frame, self->ccs, texture, should_mipmap, &image_cs);

  if (image == NULL)
    {
      graphene_rect_t clip, rounded_clip;

      if (!gsk_gpu_node_processor_clip_node_bounds (self, node, &clip))
        return;

      if (!gsk_rect_snap_to_grid (&clip, &self->scale, &self->offset, &rounded_clip))
        return;

      image = gsk_gpu_get_texture_tiles_as_image (self->frame,
                                                  self->ccs,
                                                  &rounded_clip,
                                                  &self->scale,
                                                  &node->bounds,
                                                  texture,
                                                  should_mipmap ? GSK_SCALING_FILTER_TRILINEAR : GSK_SCALING_FILTER_LINEAR);
      gsk_gpu_node_processor_image_op (self,
                                       image,
                                       self->ccs,
                                       GSK_GPU_SAMPLER_DEFAULT,
                                       &clip,
                                       &rounded_clip);
      g_object_unref (image);
      return;
    }

  if (should_mipmap)
    {
      if ((gsk_gpu_image_get_flags (image) & GSK_GPU_IMAGE_CAN_MIPMAP) != GSK_GPU_IMAGE_CAN_MIPMAP ||
          gsk_gpu_image_get_shader_op (image) != GDK_SHADER_DEFAULT ||
          !gdk_color_state_equal (image_cs, self->ccs))
        {
          image = gsk_gpu_copy_image (self->frame, self->ccs, image, image_cs, TRUE);
          gdk_color_state_unref (image_cs);
          image_cs = gdk_color_state_ref (self->ccs);
          gsk_gpu_cache_cache_texture_image (gsk_gpu_device_get_cache (gsk_gpu_frame_get_device (self->frame)),
                                             texture,
                                             image,
                                             image_cs);
        }

      if (!(gsk_gpu_image_get_flags (image) & GSK_GPU_IMAGE_MIPMAP))
        gsk_gpu_mipmap_op (self->frame, image);

      gsk_gpu_node_processor_image_op (self,
                                       image,
                                       image_cs,
                                       GSK_GPU_SAMPLER_MIPMAP_DEFAULT,
                                       &node->bounds,
                                       &node->bounds);
    }
  else
    {
      gsk_gpu_node_processor_image_op (self,
                                       image,
                                       image_cs,
                                       GSK_GPU_SAMPLER_DEFAULT,
                                       &node->bounds,
                                       &node->bounds);
    }

  gdk_color_state_unref (image_cs);
  g_object_unref (image);
}

static gboolean
gsk_gpu_node_processor_add_first_node_no_blend (GskGpuNodeProcessor *self,
                                                GskGpuFirstNodeInfo *info,
                                                GskRenderNode       *node)
{
  if (!node->fully_opaque)
    return FALSE;

  if (!gsk_gpu_node_processor_clip_first_node (self, info, &node->bounds))
    return FALSE;

  gsk_gpu_first_node_begin_rendering (self, info, NULL);

  self->blend = GSK_GPU_BLEND_NONE;
  self->pending_globals |= GSK_GPU_GLOBAL_BLEND;

  gsk_gpu_node_processor_add_node (self, node);

  self->blend = GSK_GPU_BLEND_OVER;
  self->pending_globals |= GSK_GPU_GLOBAL_BLEND;

  return TRUE;
}

static GskGpuImage *
gsk_gpu_get_texture_node_as_image (GskGpuFrame           *frame,
                                   GskGpuAsImageFlags     flags,
                                   GdkColorState         *ccs,
                                   const graphene_rect_t *clip_bounds,
                                   const graphene_vec2_t *scale,
                                   GskRenderNode         *node,
                                   graphene_rect_t       *out_bounds)
{
  GdkTexture *texture = gsk_texture_node_get_texture (node);
  GdkColorState *image_cs;
  GskGpuImage *image;
  gboolean should_mipmap;

  if ((flags & GSK_GPU_AS_IMAGE_EXACT_SIZE) &&
      !gsk_rect_equal (clip_bounds, &node->bounds))
    return gsk_gpu_get_node_as_image_via_offscreen (frame, flags, ccs, clip_bounds, scale, node, out_bounds);

  should_mipmap = texture_node_should_mipmap (node, frame, scale);
  image = gsk_gpu_lookup_texture (frame, ccs, texture, FALSE, &image_cs);

  if (image == NULL)
    {
      image = gsk_gpu_get_texture_tiles_as_image (frame,
                                                  ccs,
                                                  clip_bounds,
                                                  scale,
                                                  &node->bounds,
                                                  gsk_texture_node_get_texture (node),
                                                  should_mipmap ? GSK_SCALING_FILTER_TRILINEAR : GSK_SCALING_FILTER_LINEAR);
      *out_bounds = *clip_bounds;
      return image;
    }

  if (should_mipmap)
    {
      gdk_color_state_unref (image_cs);
      g_object_unref (image);
      return gsk_gpu_get_node_as_image_via_offscreen (frame, flags, ccs, clip_bounds, scale, node, out_bounds);
    }

  if (!gdk_color_state_equal (ccs, image_cs) ||
      gsk_gpu_image_get_shader_op (image) != GDK_SHADER_DEFAULT ||
      ((flags & GSK_GPU_AS_IMAGE_SAMPLED_OUT_OF_BOUNDS) &&
       gdk_memory_format_alpha (gsk_gpu_image_get_format (image)) == GDK_MEMORY_ALPHA_OPAQUE))
    {
      image = gsk_gpu_copy_image (frame, ccs, image, image_cs, FALSE);
      gdk_color_state_unref (image_cs);
      image_cs = gdk_color_state_ref (ccs);
      gsk_gpu_cache_cache_texture_image (gsk_gpu_device_get_cache (gsk_gpu_frame_get_device (frame)),
                                         texture,
                                         image,
                                         ccs);
    }

  gdk_color_state_unref (image_cs);
  *out_bounds = node->bounds;
  return image;
}

static void
gsk_gpu_node_processor_add_texture_scale_node (GskGpuNodeProcessor *self,
                                               GskRenderNode       *node)
{
  GskGpuImage *image;
  GdkTexture *texture;
  GdkColorState *image_cs;
  GskScalingFilter scaling_filter;
  gboolean need_mipmap, need_offscreen;

  texture = gsk_texture_scale_node_get_texture (node);
  scaling_filter = gsk_texture_scale_node_get_filter (node);
  need_mipmap = scaling_filter == GSK_SCALING_FILTER_TRILINEAR;
  image = gsk_gpu_lookup_texture (self->frame, self->ccs, texture, need_mipmap, &image_cs);

  need_offscreen = image == NULL ||
                   self->modelview != NULL ||
                   !graphene_vec2_equal (&self->scale, graphene_vec2_one ());

  if (need_offscreen)
    {
      GskGpuImage *offscreen;
      graphene_rect_t clip_bounds;

      gsk_gpu_node_processor_get_clip_bounds (self, &clip_bounds);
      /* first round to pixel boundaries, so we make sure the full pixels are covered */
      if (!gsk_rect_snap_to_grid (&clip_bounds, &self->scale, &self->offset, &clip_bounds))
        {
          if (image)
            {
              gdk_color_state_unref (image_cs);
              g_object_unref (image);
            }
          return;
        }
      /* then expand by half a pixel so that pixels needed for eventual linear
       * filtering are available */
      graphene_rect_inset (&clip_bounds, -0.5, -0.5);
      /* finally, round to full pixels */
      gsk_rect_round_larger (&clip_bounds);
      /* now intersect with actual node bounds */
      if (!gsk_rect_intersection (&clip_bounds, &node->bounds, &clip_bounds))
        {
          if (image)
            {
              gdk_color_state_unref (image_cs);
              g_object_unref (image);
            }
          return;
        }
      clip_bounds.size.width = ceilf (clip_bounds.size.width);
      clip_bounds.size.height = ceilf (clip_bounds.size.height);
      if (image == NULL)
        {
          offscreen = gsk_gpu_get_texture_tiles_as_image (self->frame,
                                                          self->ccs,
                                                          &clip_bounds,
                                                          graphene_vec2_one (),
                                                          &node->bounds,
                                                          texture,
                                                          scaling_filter);
        }
      else
        {
          offscreen = gsk_gpu_node_processor_create_offscreen (self->frame,
                                                               self->ccs,
                                                               graphene_vec2_one (),
                                                               &clip_bounds,
                                                               node);
          gdk_color_state_unref (image_cs);
          g_object_unref (image);
        }
      gsk_gpu_texture_op (self->frame,
                          gsk_gpu_clip_get_shader_clip (&self->clip, &self->offset, &node->bounds),
                          &self->offset,
                          &(GskGpuShaderImage) {
                              offscreen,
                              GSK_GPU_SAMPLER_DEFAULT,
                              &node->bounds,
                              &clip_bounds
                          });
      g_object_unref (offscreen);
      return;
    }

  if (gsk_gpu_image_get_shader_op (image) != GDK_SHADER_DEFAULT ||
      (need_mipmap && !(gsk_gpu_image_get_flags (image) & GSK_GPU_IMAGE_CAN_MIPMAP)) ||
      !gdk_color_state_equal (image_cs, self->ccs))
    {
      image = gsk_gpu_copy_image (self->frame, self->ccs, image, image_cs, need_mipmap);
      gdk_color_state_unref (image_cs);
      image_cs = gdk_color_state_ref (self->ccs);
      gsk_gpu_cache_cache_texture_image (gsk_gpu_device_get_cache (gsk_gpu_frame_get_device (self->frame)),
                                         texture,
                                         image,
                                         image_cs);
    }

  if (need_mipmap && !(gsk_gpu_image_get_flags (image) & GSK_GPU_IMAGE_MIPMAP))
    gsk_gpu_mipmap_op (self->frame, image);

  gsk_gpu_texture_op (self->frame,
                      gsk_gpu_clip_get_shader_clip (&self->clip, &self->offset, &node->bounds),
                      &self->offset,
                      &(GskGpuShaderImage) {
                          image,
                          gsk_gpu_sampler_for_scaling_filter (scaling_filter),
                          &node->bounds,
                          &node->bounds,
                      });

  gdk_color_state_unref (image_cs);
  g_object_unref (image);
}

static GskGpuImage *
gsk_gpu_get_cairo_node_as_image (GskGpuFrame           *frame,
                                 GskGpuAsImageFlags     flags,
                                 GdkColorState         *ccs,
                                 const graphene_rect_t *clip_bounds,
                                 const graphene_vec2_t *scale,
                                 GskRenderNode         *node,
                                 graphene_rect_t       *out_bounds)
{
  GskGpuImage *result;

  if (!gdk_color_state_equal (ccs, GDK_COLOR_STATE_SRGB))
    return gsk_gpu_get_node_as_image_via_offscreen (frame, flags, ccs, clip_bounds, scale, node, out_bounds);

  result = gsk_gpu_upload_cairo_op (frame,
                                    scale,
                                    clip_bounds,
                                    (GskGpuCairoFunc) gsk_render_node_draw_fallback,
                                    gsk_render_node_ref (node),
                                    (GDestroyNotify) gsk_render_node_unref);

  g_object_ref (result);

  *out_bounds = *clip_bounds;
  return result;
}

static void
gsk_gpu_node_processor_add_inset_shadow_node (GskGpuNodeProcessor *self,
                                              GskRenderNode       *node)
{
  float spread, blur_radius;
  const GdkColor *color;

  color = gsk_inset_shadow_node_get_gdk_color (node);
  spread = gsk_inset_shadow_node_get_spread (node);
  blur_radius = gsk_inset_shadow_node_get_blur_radius (node);

  if (blur_radius < 0.01)
    {
      GdkColor colors[4];

      for (int i = 0; i < 4; i++)
        gdk_color_init_copy (&colors[i], color);

      gsk_gpu_border_op (self->frame,
                         gsk_gpu_clip_get_shader_clip (&self->clip, &self->offset, &node->bounds),
                         self->ccs,
                         self->opacity,
                         &self->offset,
                         gsk_inset_shadow_node_get_outline (node),
                         gsk_inset_shadow_node_get_offset (node),
                         (float[4]) { spread, spread, spread, spread },
                         colors);

      for (int i = 0; i < 4; i++)
        gdk_color_finish (&colors[i]);
    }
  else
    {
      gsk_gpu_box_shadow_op (self->frame,
                             gsk_gpu_clip_get_shader_clip (&self->clip, &self->offset, &node->bounds),
                             self->ccs,
                             self->opacity,
                             &self->offset,
                             TRUE,
                             &node->bounds,
                             gsk_inset_shadow_node_get_outline (node),
                             gsk_inset_shadow_node_get_offset (node),
                             spread,
                             blur_radius,
                             color);
    }
}

static void
gsk_gpu_node_processor_add_outset_shadow_node (GskGpuNodeProcessor *self,
                                               GskRenderNode       *node)
{
  float spread, blur_radius;
  const GdkColor *color;
  const graphene_point_t *offset;

  color = gsk_outset_shadow_node_get_gdk_color (node);
  spread = gsk_outset_shadow_node_get_spread (node);
  blur_radius = gsk_outset_shadow_node_get_blur_radius (node);
  offset = gsk_outset_shadow_node_get_offset (node);

  if (blur_radius < 0.01)
    {
      GskRoundedRect outline;
      GdkColor colors[4];

      gsk_rounded_rect_init_copy (&outline, gsk_outset_shadow_node_get_outline (node));
      gsk_rounded_rect_shrink (&outline, -spread, -spread, -spread, -spread);
      gsk_rect_init_offset (&outline.bounds, &outline.bounds, offset);

      for (int i = 0; i < 4; i++)
        gdk_color_init_copy (&colors[i], color);

      gsk_gpu_border_op (self->frame,
                         gsk_gpu_clip_get_shader_clip (&self->clip, &self->offset, &node->bounds),
                         self->ccs,
                         self->opacity,
                         &self->offset,
                         &outline,
                         &GRAPHENE_POINT_INIT (- offset->x, - offset->y),
                         (float[4]) { spread, spread, spread, spread },
                         colors);

      for (int i = 0; i < 4; i++)
        gdk_color_finish (&colors[i]);
    }
  else
    {
      gsk_gpu_box_shadow_op (self->frame,
                             gsk_gpu_clip_get_shader_clip (&self->clip, &self->offset, &node->bounds),
                             self->ccs,
                             self->opacity,
                             &self->offset,
                             FALSE,
                             &node->bounds,
                             gsk_outset_shadow_node_get_outline (node),
                             offset,
                             spread,
                             blur_radius,
                             color);
    }
}

typedef void (* GradientOpFunc) (GskGpuNodeProcessor   *self,
                                 GdkColorState         *target,
                                 GskRenderNode         *node,
                                 const GskGradientStop *stops,
                                 gsize                  n_stops);

static void
gsk_gpu_node_processor_add_gradient_node (GskGpuNodeProcessor   *self,
                                          GskRenderNode         *node,
                                          GdkColorState         *ics,
                                          const GskGradientStop *stops,
                                          gsize                  n_stops,
                                          GradientOpFunc         func)
{
  GskGradientStop real_stops[7];
  GskGpuNodeProcessor other;
  graphene_rect_t bounds;
  gsize i, j;
  GskGpuImage *image;

  if (n_stops < 8 && GDK_IS_DEFAULT_COLOR_STATE (ics))
    {
      func (self, self->ccs, node, stops, n_stops);
      return;
    }

  if (!gsk_gpu_node_processor_clip_node_bounds_and_snap_to_grid (self, node, &bounds))
    return;

  image = gsk_gpu_node_processor_init_draw (&other,
                                            self->frame,
                                            ics,
                                            gdk_memory_depth_merge (gdk_color_state_get_depth (self->ccs),
                                                                    gsk_render_node_get_preferred_depth (node)),
                                            &self->scale,
                                            &bounds);

  g_return_if_fail (image != NULL);

  other.blend = GSK_GPU_BLEND_ADD;
  other.pending_globals |= GSK_GPU_GLOBAL_BLEND;
  gsk_gpu_node_processor_sync_globals (&other, 0);

  for (i = 0; i < n_stops; /* happens inside the loop */)
    {
      if (i == 0)
        {
          real_stops[0].offset = stops[i].offset;
          real_stops[0].transition_hint = stops[i].transition_hint;
          gdk_color_init_copy (&real_stops[i].color, &stops[i].color);
          i++;
        }
      else
        {
          real_stops[0].offset = stops[i - 1].offset;
          real_stops[0].transition_hint = stops[i - 1].transition_hint;
          gdk_color_init_copy (&real_stops[0].color, &stops[i - 1].color);
          real_stops[0].color.alpha *= 0;
        }
      for (j = 1; j < 6 && i < n_stops; j++)
        {
          real_stops[j].offset = stops[i].offset;
          real_stops[j].transition_hint = stops[i].transition_hint;
          gdk_color_init_copy (&real_stops[j].color, &stops[i].color);
          i++;
        }
      if (i == n_stops - 1)
        {
          g_assert (j == 6);
          real_stops[j].offset = stops[i].offset;
          real_stops[j].transition_hint = stops[i].transition_hint;
          gdk_color_init_copy (&real_stops[j].color, &stops[i].color);
          j++;
          i++;
        }
      else if (i < n_stops)
        {
          real_stops[j].offset = stops[i].offset;
          real_stops[j].transition_hint = stops[i].transition_hint;
          gdk_color_init_copy (&real_stops[j].color, &stops[i].color);
          real_stops[j].color.alpha *= 0;
          j++;
        }

      func (&other, NULL, node, real_stops, j);
    }

  gsk_gpu_node_processor_finish_draw (&other, image);

  gsk_gpu_node_processor_image_op (self,
                                   image,
                                   ics,
                                   GSK_GPU_SAMPLER_DEFAULT,
                                   &node->bounds,
                                   &bounds);

  g_object_unref (image);
}

static void
gsk_gpu_node_processor_linear_gradient_op (GskGpuNodeProcessor   *self,
                                           GdkColorState         *target,
                                           GskRenderNode         *node,
                                           const GskGradientStop *stops,
                                           gsize                  n_stops)
{
  gsk_gpu_linear_gradient_op (self->frame,
                              gsk_gpu_clip_get_shader_clip (&self->clip, &self->offset, &node->bounds),
                              target,
                              self->opacity,
                              &self->offset,
                              gsk_gradient_node_get_interpolation (node),
                              gsk_gradient_node_get_hue_interpolation (node),
                              GSK_RENDER_NODE_TYPE (node) == GSK_REPEATING_LINEAR_GRADIENT_NODE,
                              &node->bounds,
                              gsk_linear_gradient_node_get_start (node),
                              gsk_linear_gradient_node_get_end (node),
                              stops,
                              n_stops);
}

static void
gsk_gpu_node_processor_add_linear_gradient_node (GskGpuNodeProcessor *self,
                                                 GskRenderNode       *node)
{
  gsk_gpu_node_processor_add_gradient_node (self,
                                            node,
                                            gsk_gradient_node_get_interpolation (node),
                                            gsk_gradient_node_get_stops (node),
                                            gsk_gradient_node_get_n_stops (node),
                                            gsk_gpu_node_processor_linear_gradient_op);
}

static void
gsk_gpu_node_processor_radial_gradient_op (GskGpuNodeProcessor   *self,
                                           GdkColorState         *target,
                                           GskRenderNode         *node,
                                           const GskGradientStop *stops,
                                           gsize                  n_stops)
{
  gsk_gpu_radial_gradient_op (self->frame,
                              gsk_gpu_clip_get_shader_clip (&self->clip, &self->offset, &node->bounds),
                              target,
                              self->opacity,
                              &self->offset,
                              gsk_gradient_node_get_interpolation (node),
                              gsk_gradient_node_get_hue_interpolation (node),
                              GSK_RENDER_NODE_TYPE (node) == GSK_REPEATING_RADIAL_GRADIENT_NODE,
                              &node->bounds,
                              gsk_radial_gradient_node_get_center (node),
                              &GRAPHENE_POINT_INIT (
                                  gsk_radial_gradient_node_get_hradius (node),
                                  gsk_radial_gradient_node_get_vradius (node)
                              ),
                              gsk_radial_gradient_node_get_start (node),
                              gsk_radial_gradient_node_get_end (node),
                              stops,
                              n_stops);
}

static void
gsk_gpu_node_processor_add_radial_gradient_node (GskGpuNodeProcessor *self,
                                                 GskRenderNode       *node)
{
  gsk_gpu_node_processor_add_gradient_node (self,
                                            node,
                                            gsk_gradient_node_get_interpolation (node),
                                            gsk_gradient_node_get_stops (node),
                                            gsk_gradient_node_get_n_stops (node),
                                            gsk_gpu_node_processor_radial_gradient_op);
}

static void
gsk_gpu_node_processor_conic_gradient_op (GskGpuNodeProcessor   *self,
                                          GdkColorState         *target,
                                          GskRenderNode         *node,
                                          const GskGradientStop *stops,
                                          gsize                  n_stops)
{
  gsk_gpu_conic_gradient_op (self->frame,
                             gsk_gpu_clip_get_shader_clip (&self->clip, &self->offset, &node->bounds),
                             target,
                             self->opacity,
                             &self->offset,
                             gsk_gradient_node_get_interpolation (node),
                             gsk_gradient_node_get_hue_interpolation (node),
                             &node->bounds,
                             gsk_conic_gradient_node_get_center (node),
                             gsk_conic_gradient_node_get_angle (node),
                             stops,
                             n_stops);
}

static void
gsk_gpu_node_processor_add_conic_gradient_node (GskGpuNodeProcessor *self,
                                                GskRenderNode       *node)
{
  gsk_gpu_node_processor_add_gradient_node (self,
                                            node,
                                            gsk_gradient_node_get_interpolation (node),
                                            gsk_gradient_node_get_stops (node),
                                            gsk_gradient_node_get_n_stops (node),
                                            gsk_gpu_node_processor_conic_gradient_op);
}

static void
gsk_gpu_node_processor_add_blur_node (GskGpuNodeProcessor *self,
                                      GskRenderNode       *node)
{
  GskRenderNode *child;
  GskGpuImage *image;
  graphene_rect_t tex_rect, clip_rect;
  float blur_radius, clip_radius;

  child = gsk_blur_node_get_child (node);
  blur_radius = gsk_blur_node_get_radius (node);
  if (blur_radius <= 0.f)
    {
      gsk_gpu_node_processor_add_node (self, child);
      return;
    }

  clip_radius = gsk_cairo_blur_compute_pixels (blur_radius / 2.0);
  gsk_gpu_node_processor_get_clip_bounds (self, &clip_rect);
  graphene_rect_inset (&clip_rect, -clip_radius, -clip_radius);
  image = gsk_gpu_node_processor_get_node_as_image (self,
                                                    GSK_GPU_AS_IMAGE_SAMPLED_OUT_OF_BOUNDS,
                                                    &clip_rect,
                                                    child,
                                                    &tex_rect);
  if (image == NULL)
    return;

  gsk_gpu_node_processor_blur_op (self,
                                  &node->bounds,
                                  graphene_point_zero (),
                                  blur_radius,
                                  NULL,
                                  image,
                                  gdk_memory_format_get_depth (gsk_gpu_image_get_format (image),
                                                               gsk_gpu_image_get_conversion (image) == GSK_GPU_CONVERSION_SRGB),
                                  &tex_rect);

  g_object_unref (image);
}

static void
gsk_gpu_node_processor_add_shadow_node (GskGpuNodeProcessor *self,
                                        GskRenderNode       *node)
{
  GskGpuImage *image;
  graphene_rect_t clip_bounds, tex_rect;
  GskRenderNode *child;
  gsize i, n_shadows;

  n_shadows = gsk_shadow_node_get_n_shadows (node);
  child = gsk_shadow_node_get_child (node);
  /* enlarge clip for shadow offsets */
  gsk_gpu_node_processor_get_clip_bounds (self, &clip_bounds);
  clip_bounds = GRAPHENE_RECT_INIT (clip_bounds.origin.x - node->bounds.size.width + child->bounds.size.width - node->bounds.origin.x + child->bounds.origin.x,
                                    clip_bounds.origin.y - node->bounds.size.height + child->bounds.size.height - node->bounds.origin.y + child->bounds.origin.y,
                                    clip_bounds.size.width + node->bounds.size.width - child->bounds.size.width,
                                    clip_bounds.size.height + node->bounds.size.height - child->bounds.size.height);

  image = gsk_gpu_node_processor_get_node_as_image (self,
                                                    GSK_GPU_AS_IMAGE_SAMPLED_OUT_OF_BOUNDS,
                                                    &clip_bounds, 
                                                    child,
                                                    &tex_rect);
  if (image == NULL)
    return;

  for (i = 0; i < n_shadows; i++)
    {
      const GskShadowEntry *shadow = gsk_shadow_node_get_shadow_entry (node, i);

      if (shadow->radius == 0)
        {
          graphene_point_t shadow_offset = GRAPHENE_POINT_INIT (self->offset.x + shadow->offset.x,
                                                                self->offset.y + shadow->offset.y);

          gsk_gpu_colorize_op (self->frame,
                               gsk_gpu_clip_get_shader_clip (&self->clip, &shadow_offset, &child->bounds),
                               self->ccs,
                               1,
                               &shadow_offset,
                               &(GskGpuShaderImage) {
                                   image,
                                   GSK_GPU_SAMPLER_TRANSPARENT,
                                   &tex_rect,
                                   &tex_rect,
                               },
                               &shadow->color);
        }
      else
        {
          graphene_rect_t bounds;
          float clip_radius = gsk_cairo_blur_compute_pixels (0.5 * shadow->radius);
          graphene_rect_inset_r (&child->bounds, - clip_radius, - clip_radius, &bounds);
          gsk_gpu_node_processor_blur_op (self,
                                          &bounds,
                                          &shadow->offset,
                                          shadow->radius,
                                          &shadow->color,
                                          image,
                                          gdk_memory_format_get_depth (gsk_gpu_image_get_format (image),
                                                                       gsk_gpu_image_get_conversion (image) == GSK_GPU_CONVERSION_SRGB),
                                          &tex_rect);
        }
    }

  gsk_gpu_texture_op (self->frame,
                      gsk_gpu_clip_get_shader_clip (&self->clip, &self->offset, &child->bounds),
                      &self->offset,
                      &(GskGpuShaderImage) {
                          image,
                          GSK_GPU_SAMPLER_DEFAULT,
                          &tex_rect,
                          &tex_rect,
                      });

  g_object_unref (image);
}

static void
gsk_gpu_node_processor_add_gl_shader_node (GskGpuNodeProcessor *self,
                                           GskRenderNode       *node)
{
  gsk_gpu_color_op (self->frame,
                    gsk_gpu_clip_get_shader_clip (&self->clip, &self->offset, &node->bounds),
                    self->ccs,
                    self->opacity,
                    &self->offset,
                    &node->bounds,
                    &GDK_COLOR_SRGB (1, 105/255., 180/255., 1));
}

static void
gsk_gpu_node_processor_add_blend_node (GskGpuNodeProcessor *self,
                                       GskRenderNode       *node)
{
  GskRenderNode *bottom_child, *top_child;
  graphene_rect_t bottom_rect, top_rect;
  GskGpuImage *bottom_image, *top_image;

  bottom_child = gsk_blend_node_get_bottom_child (node);
  top_child = gsk_blend_node_get_top_child (node);

  bottom_image = gsk_gpu_node_processor_get_node_as_image (self,
                                                           0,
                                                           NULL,
                                                           bottom_child,
                                                           &bottom_rect);
  top_image = gsk_gpu_node_processor_get_node_as_image (self,
                                                        0,
                                                        NULL,
                                                        top_child,
                                                        &top_rect);

  if (bottom_image == NULL)
    {
      if (top_image == NULL)
        return;

      bottom_image = g_object_ref (top_image);
      bottom_rect = *graphene_rect_zero ();
    }
  else if (top_image == NULL)
    {
      top_image = g_object_ref (bottom_image);
      top_rect = *graphene_rect_zero ();
    }

  gsk_gpu_blend_mode_op (self->frame,
                         gsk_gpu_clip_get_shader_clip (&self->clip, &self->offset, &node->bounds),
                         &node->bounds,
                         &self->offset,
                         self->opacity,
                         gsk_blend_node_get_blend_mode (node),
                         &(GskGpuShaderImage) {
                             bottom_image,
                             GSK_GPU_SAMPLER_DEFAULT,
                             NULL,
                             &bottom_rect
                         },
                         &(GskGpuShaderImage) {
                             top_image,
                             GSK_GPU_SAMPLER_DEFAULT,
                             NULL,
                             &top_rect
                         });

  g_object_unref (top_image);
  g_object_unref (bottom_image);
}

static void
gsk_gpu_node_processor_add_cross_fade_node (GskGpuNodeProcessor *self,
                                            GskRenderNode       *node)
{
  GskRenderNode *start_child, *end_child;
  graphene_rect_t start_rect, end_rect;
  GskGpuImage *start_image, *end_image;
  float progress, old_opacity;

  start_child = gsk_cross_fade_node_get_start_child (node);
  end_child = gsk_cross_fade_node_get_end_child (node);
  progress = gsk_cross_fade_node_get_progress (node);

  if (progress <= 0.0)
    {
      gsk_gpu_node_processor_add_node (self, start_child);
      return;
    }
  if (progress >= 1.0)
    {
      gsk_gpu_node_processor_add_node (self, end_child);
      return;
    }

  start_image = gsk_gpu_node_processor_get_node_as_image (self,
                                                          0,
                                                          NULL,
                                                          start_child,
                                                          &start_rect);
  end_image = gsk_gpu_node_processor_get_node_as_image (self,
                                                        0,
                                                        NULL,
                                                        end_child,
                                                        &end_rect);

  if (start_image == NULL)
    {
      if (end_image == NULL)
        return;

      old_opacity = self->opacity;
      self->opacity *= progress;
      gsk_gpu_node_processor_image_op (self,
                                       end_image,
                                       self->ccs,
                                       GSK_GPU_SAMPLER_DEFAULT,
                                       &end_child->bounds,
                                       &end_rect);
      g_object_unref (end_image);
      self->opacity = old_opacity;
      return;
    }
  else if (end_image == NULL)
    {
      old_opacity = self->opacity;
      self->opacity *= (1 - progress);
      gsk_gpu_node_processor_image_op (self,
                                       start_image,
                                       self->ccs,
                                       GSK_GPU_SAMPLER_DEFAULT,
                                       &start_child->bounds,
                                       &start_rect);
      g_object_unref (start_image);
      self->opacity = old_opacity;
      return;
    }

  gsk_gpu_cross_fade_op (self->frame,
                         gsk_gpu_clip_get_shader_clip (&self->clip, &self->offset, &node->bounds),
                         &node->bounds,
                         &self->offset,
                         self->opacity,
                         progress,
                         &(GskGpuShaderImage) {
                             start_image,
                             GSK_GPU_SAMPLER_DEFAULT,
                             NULL,
                             &start_rect
                         },
                         &(GskGpuShaderImage) {
                             end_image,
                             GSK_GPU_SAMPLER_DEFAULT,
                             NULL,
                             &end_rect
                         });

  g_object_unref (end_image);
  g_object_unref (start_image);
}

static void
gsk_gpu_node_processor_add_mask_node (GskGpuNodeProcessor *self,
                                      GskRenderNode       *node)
{
  GskRenderNode *source_child, *mask_child;
  GskGpuImage *mask_image;
  graphene_rect_t bounds, mask_rect;
  GskMaskMode mask_mode;

  source_child = gsk_mask_node_get_source (node);
  mask_child = gsk_mask_node_get_mask (node);
  mask_mode = gsk_mask_node_get_mask_mode (node);

  if (!gsk_gpu_node_processor_clip_node_bounds_and_snap_to_grid (self, node, &bounds))
    return;

  mask_image = gsk_gpu_node_processor_get_node_as_image (self,
                                                         0,
                                                         &bounds,
                                                         mask_child,
                                                         &mask_rect);
  if (mask_image == NULL)
    {
      if (mask_mode == GSK_MASK_MODE_INVERTED_ALPHA)
        gsk_gpu_node_processor_add_node (self, source_child);
      return;
    }

  if (gsk_render_node_get_node_type (source_child) == GSK_COLOR_NODE &&
      mask_mode == GSK_MASK_MODE_ALPHA)
    {
      gsk_gpu_colorize_op (self->frame,
                           gsk_gpu_clip_get_shader_clip (&self->clip, &self->offset, &bounds),
                           self->ccs,
                           self->opacity,
                           &self->offset,
                           &(GskGpuShaderImage) {
                               mask_image,
                               GSK_GPU_SAMPLER_DEFAULT,
                               &bounds,
                               &mask_rect,
                           },
                           gsk_color_node_get_gdk_color (source_child));
    }
  else
    {
      GskGpuImage *source_image;
      graphene_rect_t source_rect;

      source_image = gsk_gpu_node_processor_get_node_as_image (self,
                                                               0,
                                                               &bounds,
                                                               source_child,
                                                               &source_rect);
      if (source_image == NULL)
        {
          g_object_unref (mask_image);
          return;
        }

      gsk_gpu_mask_op (self->frame,
                       gsk_gpu_clip_get_shader_clip (&self->clip, &self->offset, &bounds),
                       &bounds,
                       &self->offset,
                       self->opacity,
                       mask_mode,
                       &(GskGpuShaderImage) {
                           source_image,
                           GSK_GPU_SAMPLER_DEFAULT,
                           NULL,
                           &source_rect,
                       },
                       &(GskGpuShaderImage) {
                           mask_image,
                           GSK_GPU_SAMPLER_DEFAULT,
                           NULL,
                           &mask_rect,
                       });

      g_object_unref (source_image);
    }

  g_object_unref (mask_image);
}

static void
gsk_gpu_node_processor_add_glyph_node (GskGpuNodeProcessor *self,
                                       GskRenderNode       *node)
{
  GskGpuCache *cache;
  const PangoGlyphInfo *glyphs;
  PangoFont *font;
  graphene_point_t offset;
  guint num_glyphs;
  float scale;
  float align_scale_x, align_scale_y;
  unsigned int flags_mask;
  const float pango_scale = PANGO_SCALE;
  cairo_hint_style_t hint_style;
  const GdkColor *color;
  GdkColorState *alt;
  GskGpuColorStates color_states;
  GdkColor color2;
  GskGpuShaderClip node_clip;

  if (self->opacity < 1.0 &&
      gsk_text_node_has_color_glyphs (node))
    {
      gsk_gpu_node_processor_add_without_opacity (self, node);
      return;
    }

  cache = gsk_gpu_device_get_cache (gsk_gpu_frame_get_device (self->frame));

  glyphs = gsk_text_node_get_glyphs (node, &num_glyphs);
  font = gsk_text_node_get_font (node);
  offset = *gsk_text_node_get_offset (node);
  hint_style = gsk_text_node_get_font_hint_style (node);
  color = gsk_text_node_get_gdk_color (node);

  alt = gsk_gpu_color_states_find (self->ccs, color);
  color_states = gsk_gpu_color_states_create (self->ccs, TRUE, alt, FALSE);
  gdk_color_convert (&color2, alt, color);

  node_clip = gsk_gpu_clip_get_shader_clip (&self->clip, &self->offset, &node->bounds),

  offset.x += self->offset.x;
  offset.y += self->offset.y;

  scale = MAX (graphene_vec2_get_x (&self->scale), graphene_vec2_get_y (&self->scale));

  if (hint_style != CAIRO_HINT_STYLE_NONE)
    {
      align_scale_x = scale * 4;
      align_scale_y = scale;
      flags_mask = 3;
    }
  else
    {
      align_scale_x = align_scale_y = scale * 4;
      flags_mask = 15;
    }

  for (guint i = 0; i < num_glyphs; i++)
    {
      GskGpuImage *image;
      graphene_rect_t glyph_bounds, glyph_tex_rect;
      graphene_point_t glyph_offset, glyph_origin;
      GskGpuGlyphLookupFlags flags;
      GskGpuShaderClip glyph_clip;

      glyph_origin = GRAPHENE_POINT_INIT (offset.x + glyphs[i].geometry.x_offset / pango_scale,
                                          offset.y + glyphs[i].geometry.y_offset / pango_scale);

      glyph_origin.x = floorf (glyph_origin.x * align_scale_x + 0.5f);
      glyph_origin.y = floorf (glyph_origin.y * align_scale_y + 0.5f);
      flags = (((int) glyph_origin.x & 3) | (((int) glyph_origin.y & 3) << 2)) & flags_mask;
      glyph_origin.x /= align_scale_x;
      glyph_origin.y /= align_scale_y;

      image = gsk_gpu_cached_glyph_lookup (cache,
                                           self->frame,
                                           font,
                                           glyphs[i].glyph,
                                           flags,
                                           scale,
                                           &glyph_bounds,
                                           &glyph_offset);

      glyph_tex_rect = GRAPHENE_RECT_INIT (-glyph_bounds.origin.x / scale,
                                           -glyph_bounds.origin.y / scale,
                                           gsk_gpu_image_get_width (image) / scale,
                                           gsk_gpu_image_get_height (image) / scale);
      glyph_bounds = GRAPHENE_RECT_INIT (0,
                                         0,
                                         glyph_bounds.size.width / scale,
                                         glyph_bounds.size.height / scale);
      glyph_origin = GRAPHENE_POINT_INIT (glyph_origin.x - glyph_offset.x / scale,
                                          glyph_origin.y - glyph_offset.y / scale);

      if (node_clip == GSK_GPU_SHADER_CLIP_NONE)
        glyph_clip = GSK_GPU_SHADER_CLIP_NONE;
      else
        glyph_clip = gsk_gpu_clip_get_shader_clip (&self->clip, &glyph_origin, &glyph_bounds);

      if (glyphs[i].attr.is_color)
        gsk_gpu_texture_op (self->frame,
                            glyph_clip,
                            &glyph_origin,
                            &(GskGpuShaderImage) {
                                image,
                                GSK_GPU_SAMPLER_DEFAULT,
                                &glyph_bounds,
                                &glyph_tex_rect
                            });
      else
        gsk_gpu_colorize_op2 (self->frame,
                              glyph_clip,
                              color_states,
                              self->opacity,
                              &glyph_origin,
                              &(GskGpuShaderImage) {
                                  image,
                                  GSK_GPU_SAMPLER_DEFAULT,
                                  &glyph_bounds,
                                  &glyph_tex_rect
                              },
                              &color2);

      offset.x += glyphs[i].geometry.width / pango_scale;
    }

  gdk_color_finish (&color2);
}

static void
gsk_gpu_node_processor_add_color_matrix_node (GskGpuNodeProcessor *self,
                                              GskRenderNode       *node)
{
  GskGpuImage *image;
  GskRenderNode *child;
  graphene_matrix_t opacity_matrix;
  const graphene_matrix_t *color_matrix;
  graphene_rect_t tex_rect;

  child = gsk_color_matrix_node_get_child (node);

  color_matrix = gsk_color_matrix_node_get_color_matrix (node);
  if (self->opacity < 1.0f)
    {
      graphene_matrix_init_from_float (&opacity_matrix,
                                       (float[16]) {
                                           1.0f, 0.0f, 0.0f, 0.0f,
                                           0.0f, 1.0f, 0.0f, 0.0f,
                                           0.0f, 0.0f, 1.0f, 0.0f,
                                           0.0f, 0.0f, 0.0f, self->opacity
                                      });
      graphene_matrix_multiply (&opacity_matrix, color_matrix, &opacity_matrix);
      color_matrix = &opacity_matrix;
    }

  image = gsk_gpu_node_processor_get_node_as_image (self,
                                                    0,
                                                    NULL,
                                                    child,
                                                    &tex_rect);
  if (image == NULL)
    return;

  gsk_gpu_color_matrix_op (self->frame,
                           gsk_gpu_clip_get_shader_clip (&self->clip, &self->offset, &node->bounds),
                           gsk_gpu_node_processor_color_states_explicit (self, self->ccs, FALSE),
                           &self->offset,
                           &(GskGpuShaderImage) {
                               image,
                               GSK_GPU_SAMPLER_DEFAULT,
                               &node->bounds,
                               &tex_rect,
                           },
                           color_matrix,
                           gsk_color_matrix_node_get_color_offset (node));

  g_object_unref (image);
}

static void
gsk_gpu_node_processor_add_component_transfer_node (GskGpuNodeProcessor *self,
                                                    GskRenderNode       *node)
{
  GskGpuImage *image;
  GskRenderNode *child;
  graphene_rect_t tex_rect;

  child = gsk_component_transfer_node_get_child (node);

  image = gsk_gpu_node_processor_get_node_as_image (self,
                                                    0,
                                                    NULL,
                                                    child,
                                                    &tex_rect);
  if (image == NULL)
    return;

  gsk_gpu_component_transfer_op (self->frame,
                                 gsk_gpu_clip_get_shader_clip (&self->clip, &self->offset, &node->bounds),
                                 gsk_gpu_node_processor_color_states_explicit (self, self->ccs, FALSE),
                                 &self->offset,
                                 &(GskGpuShaderImage) {
                                   image,
                                   GSK_GPU_SAMPLER_DEFAULT,
                                   &node->bounds,
                                   &tex_rect,
                                 },
                                 gsk_component_transfer_node_get_transfer (node, 0),
                                 gsk_component_transfer_node_get_transfer (node, 1),
                                 gsk_component_transfer_node_get_transfer (node, 2),
                                 gsk_component_transfer_node_get_transfer (node, 3));

  g_object_unref (image);
}

static void
gsk_gpu_node_processor_repeat_tile (GskGpuNodeProcessor    *self,
                                    const graphene_rect_t  *rect,
                                    float                   x,
                                    float                   y,
                                    GskRenderNode          *child,
                                    const graphene_rect_t  *child_bounds)
{
  GskGpuImage *image;
  graphene_rect_t clipped_child_bounds, offset_rect;

  gsk_rect_init_offset (&offset_rect,
                        rect,
                        &GRAPHENE_POINT_INIT (- x * child_bounds->size.width,
                                              - y * child_bounds->size.height));
  if (!gsk_rect_intersection (&offset_rect, child_bounds, &clipped_child_bounds))
    {
      /* rounding error hits again */
      return;
    }

  image = gsk_gpu_node_processor_get_node_as_image (self,
                                                    GSK_GPU_AS_IMAGE_EXACT_SIZE,
                                                    &clipped_child_bounds,
                                                    child,
                                                    &clipped_child_bounds);
  g_return_if_fail (image);

  gsk_gpu_texture_op (self->frame,
                      gsk_gpu_clip_get_shader_clip (&self->clip, &self->offset, rect),
                      &self->offset,
                      &(GskGpuShaderImage) {
                          image,
                          GSK_GPU_SAMPLER_REPEAT,
                          rect,
                          &GRAPHENE_RECT_INIT (
                              clipped_child_bounds.origin.x + x * child_bounds->size.width,
                              clipped_child_bounds.origin.y + y * child_bounds->size.height,
                              clipped_child_bounds.size.width,
                              clipped_child_bounds.size.height
                          )
                      });

  g_object_unref (image);
}

static void
gsk_gpu_node_processor_add_repeat_node (GskGpuNodeProcessor *self,
                                        GskRenderNode       *node)
{
  GskRenderNode *child;
  const graphene_rect_t *child_bounds;
  graphene_rect_t bounds;
  float tile_left, tile_right, tile_top, tile_bottom;
  gboolean avoid_offscreen;

  child = gsk_repeat_node_get_child (node);
  child_bounds = gsk_repeat_node_get_child_bounds (node);
  if (gsk_rect_is_empty (child_bounds))
    return;

  gsk_gpu_node_processor_get_clip_bounds (self, &bounds);
  if (!gsk_rect_intersection (&bounds, &node->bounds, &bounds))
    return;

  tile_left = (bounds.origin.x - child_bounds->origin.x) / child_bounds->size.width;
  tile_right = (bounds.origin.x + bounds.size.width - child_bounds->origin.x) / child_bounds->size.width;
  tile_top = (bounds.origin.y - child_bounds->origin.y) / child_bounds->size.height;
  tile_bottom = (bounds.origin.y + bounds.size.height - child_bounds->origin.y) / child_bounds->size.height;
  avoid_offscreen = !gsk_gpu_frame_should_optimize (self->frame, GSK_GPU_OPTIMIZE_REPEAT);

  /* the 1st check tests that a tile fully fits into the bounds,
   * the 2nd check is to catch the case where it fits exactly */
  if (!avoid_offscreen &&
      ceilf (tile_left) < floorf (tile_right) &&
      bounds.size.width > child_bounds->size.width)
    {
      if (ceilf (tile_top) < floorf (tile_bottom) &&
          bounds.size.height > child_bounds->size.height)
        {
          /* tile in both directions */
          gsk_gpu_node_processor_repeat_tile (self,
                                              &bounds,
                                              ceilf (tile_left),
                                              ceilf (tile_top),
                                              child,
                                              child_bounds);
        }
      else
        {
          /* tile horizontally, repeat vertically */
          float y;
          for (y = floorf (tile_top); y < ceilf (tile_bottom); y++)
            {
              float start_y = MAX (bounds.origin.y,
                                   child_bounds->origin.y + y * child_bounds->size.height);
              float end_y = MIN (bounds.origin.y + bounds.size.height,
                                 child_bounds->origin.y + (y + 1) * child_bounds->size.height);
              gsk_gpu_node_processor_repeat_tile (self,
                                                  &GRAPHENE_RECT_INIT (
                                                    bounds.origin.x,
                                                    start_y,
                                                    bounds.size.width,
                                                    end_y - start_y
                                                  ),
                                                  ceilf (tile_left),
                                                  y,
                                                  child,
                                                  child_bounds);
            }
        }
    }
  else if (!avoid_offscreen &&
           ceilf (tile_top) < floorf (tile_bottom) &&
           bounds.size.height > child_bounds->size.height)
    {
      /* repeat horizontally, tile vertically */
      float x;
      for (x = floorf (tile_left); x < ceilf (tile_right); x++)
        {
          float start_x = MAX (bounds.origin.x,
                               child_bounds->origin.x + x * child_bounds->size.width);
          float end_x = MIN (bounds.origin.x + bounds.size.width,
                             child_bounds->origin.x + (x + 1) * child_bounds->size.width);
          gsk_gpu_node_processor_repeat_tile (self,
                                              &GRAPHENE_RECT_INIT (
                                                start_x,
                                                bounds.origin.y,
                                                end_x - start_x,
                                                bounds.size.height
                                              ),
                                              x,
                                              ceilf (tile_top),
                                              child,
                                              child_bounds);
        }
    }
  else
    {
      /* repeat in both directions */
      graphene_point_t old_offset, offset;
      graphene_rect_t clip_bounds;
      float x, y;

      old_offset = self->offset;

      for (x = floorf (tile_left); x < ceilf (tile_right); x++)
        {
          offset.x = x * child_bounds->size.width;
          for (y = floorf (tile_top); y < ceilf (tile_bottom); y++)
            {
              offset.y = y * child_bounds->size.height;
              self->offset = GRAPHENE_POINT_INIT (old_offset.x + offset.x, old_offset.y + offset.y);
              clip_bounds = GRAPHENE_RECT_INIT (bounds.origin.x - offset.x,
                                                bounds.origin.y - offset.y,
                                                bounds.size.width,
                                                bounds.size.height);
              if (!gsk_rect_intersection (&clip_bounds, child_bounds, &clip_bounds))
                continue;
              gsk_gpu_node_processor_add_node_clipped (self,
                                                       child,
                                                       &clip_bounds);
            }
        }

      self->offset = old_offset;
    }
}

static void
gsk_gpu_node_processor_add_fill_node (GskGpuNodeProcessor *self,
                                      GskRenderNode       *node)
{
  graphene_rect_t clip_bounds;
  GskGpuImage *mask_image;
  GskRenderNode *child;
  graphene_rect_t tex_rect;
  GskGpuCache *cache;

  if (!gsk_gpu_node_processor_clip_node_bounds_and_snap_to_grid (self, node, &clip_bounds))
    return;

  child = gsk_fill_node_get_child (node);

  cache = gsk_gpu_device_get_cache (gsk_gpu_frame_get_device (self->frame));

  mask_image = gsk_gpu_cached_fill_lookup (cache,
                                           self->frame,
                                           &self->scale,
                                           &clip_bounds,
                                           gsk_fill_node_get_path (node),
                                           gsk_fill_node_get_fill_rule (node),
                                           &tex_rect);
  if (mask_image == NULL)
    return;

  if (GSK_RENDER_NODE_TYPE (child) == GSK_COLOR_NODE)
    {
      gsk_gpu_colorize_op (self->frame,
                           gsk_gpu_clip_get_shader_clip (&self->clip, &self->offset, &clip_bounds),
                           self->ccs,
                           self->opacity,
                           &self->offset,
                           &(GskGpuShaderImage) {
                             mask_image,
                             GSK_GPU_SAMPLER_DEFAULT,
                             &clip_bounds,
                             &tex_rect,
                           },
                           gsk_color_node_get_gdk_color (child));
    }
  else
    {
      GskGpuImage *source_image;
      graphene_rect_t source_rect;

      source_image = gsk_gpu_node_processor_get_node_as_image (self,
                                                               0,
                                                               &clip_bounds,
                                                               child,
                                                               &source_rect);

      if (source_image != NULL)
        {
          gsk_gpu_mask_op (self->frame,
                           gsk_gpu_clip_get_shader_clip (&self->clip, &self->offset, &clip_bounds),
                           &clip_bounds,
                           &self->offset,
                           self->opacity,
                           GSK_MASK_MODE_ALPHA,
                           &(GskGpuShaderImage) {
                             source_image,
                             GSK_GPU_SAMPLER_DEFAULT,
                             NULL,
                             &source_rect,
                           },
                           &(GskGpuShaderImage) {
                             mask_image,
                             GSK_GPU_SAMPLER_DEFAULT,
                             &clip_bounds,
                             &tex_rect,
                           });

          g_object_unref (source_image);
        }
    }

  g_object_unref (mask_image);
}

static void
gsk_gpu_node_processor_add_stroke_node (GskGpuNodeProcessor *self,
                                        GskRenderNode       *node)
{
  graphene_rect_t clip_bounds;
  GskGpuImage *mask_image;
  GskRenderNode *child;
  graphene_rect_t tex_rect;
  GskGpuCache *cache;

  if (!gsk_gpu_node_processor_clip_node_bounds_and_snap_to_grid (self, node, &clip_bounds))
    return;

  child = gsk_stroke_node_get_child (node);

  cache = gsk_gpu_device_get_cache (gsk_gpu_frame_get_device (self->frame));

  mask_image = gsk_gpu_cached_stroke_lookup (cache,
                                             self->frame,
                                             &self->scale,
                                             &clip_bounds,
                                             gsk_stroke_node_get_path (node),
                                             gsk_stroke_node_get_stroke (node),
                                             &tex_rect);
  if (mask_image == NULL)
    return;

  if (GSK_RENDER_NODE_TYPE (child) == GSK_COLOR_NODE)
    {
      gsk_gpu_colorize_op (self->frame,
                           gsk_gpu_clip_get_shader_clip (&self->clip, &self->offset, &clip_bounds),
                           self->ccs,
                           self->opacity,
                           &self->offset,
                           &(GskGpuShaderImage) {
                             mask_image,
                             GSK_GPU_SAMPLER_DEFAULT,
                             &clip_bounds,
                             &tex_rect,
                           },
                           gsk_color_node_get_gdk_color (child));
    }
  else
    {
      GskGpuImage *source_image;
      graphene_rect_t source_rect;

      source_image = gsk_gpu_node_processor_get_node_as_image (self,
                                                               0,
                                                               &clip_bounds,
                                                               child,
                                                               &source_rect);
      if (source_image != NULL)
        {
          gsk_gpu_mask_op (self->frame,
                           gsk_gpu_clip_get_shader_clip (&self->clip, &self->offset, &clip_bounds),
                           &clip_bounds,
                           &self->offset,
                           self->opacity,
                           GSK_MASK_MODE_ALPHA,
                           &(GskGpuShaderImage) {
                             source_image,
                             GSK_GPU_SAMPLER_DEFAULT,
                             NULL,
                             &source_rect,
                           },
                           &(GskGpuShaderImage) {
                             mask_image,
                             GSK_GPU_SAMPLER_DEFAULT,
                             &clip_bounds,
                             &tex_rect,
                           });

          g_object_unref (source_image);
        }
    }

  g_object_unref (mask_image);
}

static void
gsk_gpu_node_processor_add_subsurface_node (GskGpuNodeProcessor *self,
                                            GskRenderNode       *node)
{
  GdkSubsurface *subsurface;

  subsurface = gsk_subsurface_node_get_subsurface (node);
  if (subsurface == NULL ||
      gdk_subsurface_get_texture (subsurface) == NULL ||
      gdk_subsurface_get_parent (subsurface) != gdk_draw_context_get_surface (gsk_gpu_frame_get_context (self->frame)))
    {
      gsk_gpu_node_processor_add_node (self, gsk_subsurface_node_get_child (node));
      return;
    }

  if (!gdk_subsurface_is_above_parent (subsurface))
    {
      cairo_rectangle_int_t int_clipped;
      graphene_rect_t rect, clipped;

      gsk_rect_init_offset (&rect, &node->bounds, &self->offset);
      gsk_rect_intersection (&self->clip.rect.bounds, &rect, &clipped);

      if (gsk_gpu_frame_should_optimize (self->frame, GSK_GPU_OPTIMIZE_CLEAR) &&
          node->bounds.size.width * node->bounds.size.height > 100 * 100 && /* not worth the effort for small images */
          (self->clip.type != GSK_GPU_CLIP_ROUNDED ||
           gsk_gpu_clip_contains_rect (&self->clip, &GRAPHENE_POINT_INIT(0,0), &clipped)) &&
          gsk_gpu_node_processor_rect_is_integer (self, &clipped, &int_clipped))
        {
          if (gdk_rectangle_intersect (&int_clipped, &self->scissor, &int_clipped))
            {
              float color[4] = { 0, 0, 0, 0 };
              gsk_gpu_clear_op (self->frame, &int_clipped, color);
            }
        }
      else
        {
          self->blend = GSK_GPU_BLEND_CLEAR;
          self->pending_globals |= GSK_GPU_GLOBAL_BLEND;
          gsk_gpu_node_processor_sync_globals (self, 0);
          GdkColor white;

          gdk_color_init (&white, self->ccs, ((float[]) { 1, 1, 1, 1 }));
          gsk_gpu_color_op (self->frame,
                            gsk_gpu_clip_get_shader_clip (&self->clip, &self->offset, &node->bounds),
                            self->ccs,
                            1,
                            &self->offset,
                            &node->bounds,
                            &white);
          gdk_color_finish (&white);

          self->blend = GSK_GPU_BLEND_OVER;
          self->pending_globals |= GSK_GPU_GLOBAL_BLEND;
        }
    }
}

static gboolean
gsk_gpu_node_processor_add_first_subsurface_node (GskGpuNodeProcessor *self,
                                                  GskGpuFirstNodeInfo *info,
                                                  GskRenderNode       *node)
{
  GdkSubsurface *subsurface;

  subsurface = gsk_subsurface_node_get_subsurface (node);
  if (subsurface == NULL ||
      gdk_subsurface_get_texture (subsurface) == NULL ||
      gdk_subsurface_get_parent (subsurface) != gdk_draw_context_get_surface (gsk_gpu_frame_get_context (self->frame)))
    {
      return gsk_gpu_node_processor_add_first_node (self,
                                                    info,
                                                    gsk_subsurface_node_get_child (node));
    }

  if (gdk_subsurface_is_above_parent (subsurface))
    return FALSE;

  if (!gsk_gpu_node_processor_clip_first_node (self, info, &node->bounds))
    return FALSE;

  gsk_gpu_first_node_begin_rendering (self, info, GSK_VEC4_TRANSPARENT);

  return TRUE;
}

static GskGpuImage *
gsk_gpu_get_subsurface_node_as_image (GskGpuFrame           *frame,
                                      GskGpuAsImageFlags     flags,
                                      GdkColorState         *ccs,
                                      const graphene_rect_t *clip_bounds,
                                      const graphene_vec2_t *scale,
                                      GskRenderNode         *node,
                                      graphene_rect_t       *out_bounds)
{
#ifndef G_DISABLE_ASSERT
  GdkSubsurface *subsurface;

  subsurface = gsk_subsurface_node_get_subsurface (node);
  g_assert (subsurface == NULL ||
            gdk_subsurface_get_texture (subsurface) == NULL ||
            gdk_subsurface_get_parent (subsurface) != gdk_draw_context_get_surface (gsk_gpu_frame_get_context (frame)));
#endif

  return gsk_gpu_get_node_as_image (frame,
                                    flags,
                                    ccs,
                                    clip_bounds,
                                    scale,
                                    gsk_subsurface_node_get_child (node),
                                    out_bounds);
}

static void
gsk_gpu_node_processor_add_container_node (GskGpuNodeProcessor *self,
                                           GskRenderNode       *node)
{
  GskRenderNode **children;
  guint i, n_children;

  if (self->opacity < 1.0 && !gsk_container_node_is_disjoint (node))
    {
      gsk_gpu_node_processor_add_without_opacity (self, node);
      return;
    }

  children = gsk_container_node_get_children (node, &n_children);

  if (node->fully_opaque && !gsk_container_node_is_disjoint (node) && n_children > 0)
    {
      graphene_rect_t opaque;

      /* Try to find a child that fully covers the container node */
      for (i = n_children - 1; i > 0; i--)
        {
          if (gsk_render_node_get_opaque_rect (children[i], &opaque) &&
              gsk_rect_equal (&opaque, &node->bounds))
            break;
        }
    }
  else
    i = 0;

  for (; i < n_children; i++)
    gsk_gpu_node_processor_add_node (self, children[i]);
}

static gboolean
gsk_gpu_node_processor_add_first_container_node (GskGpuNodeProcessor *self,
                                                 GskGpuFirstNodeInfo *info,
                                                 GskRenderNode       *node)
{
  GskRenderNode **children;
  graphene_rect_t opaque;
  int i;
  guint n;

  children = gsk_container_node_get_children (node, &n);
  if (n == 0)
    return FALSE;

  if (!gsk_render_node_get_opaque_rect (node, &opaque) ||
      !gsk_gpu_node_processor_clip_first_node (self, info, &opaque))
    return FALSE;

  for (i = n; i-- > 0; )
    {
      if (gsk_gpu_node_processor_add_first_node (self, info, children[i]))
        break;
    }

  if (i < 0)
    gsk_gpu_first_node_begin_rendering (self, info, GSK_VEC4_TRANSPARENT);

  for (i++; i < n; i++)
    gsk_gpu_node_processor_add_node (self, children[i]);

  return TRUE;
}

static void
gsk_gpu_node_processor_add_debug_node (GskGpuNodeProcessor *self,
                                       GskRenderNode       *node)
{
  gsk_gpu_node_processor_add_node (self, gsk_debug_node_get_child (node));
}

static gboolean
gsk_gpu_node_processor_add_first_debug_node (GskGpuNodeProcessor *self,
                                             GskGpuFirstNodeInfo *info,
                                             GskRenderNode       *node)
{
  return gsk_gpu_node_processor_add_first_node (self,
                                                info,
                                                gsk_debug_node_get_child (node));
}

static GskGpuImage *
gsk_gpu_get_debug_node_as_image (GskGpuFrame           *frame,
                                 GskGpuAsImageFlags     flags,
                                 GdkColorState         *ccs,
                                 const graphene_rect_t *clip_bounds,
                                 const graphene_vec2_t *scale,
                                 GskRenderNode         *node,
                                 graphene_rect_t       *out_bounds)
{
  return gsk_gpu_get_node_as_image (frame,
                                    flags,
                                    ccs,
                                    clip_bounds,
                                    scale,
                                    gsk_debug_node_get_child (node),
                                    out_bounds);
}

typedef enum {
  GSK_GPU_HANDLE_OPACITY = (1 << 0)
} GskGpuNodeFeatures;

static const struct
{
  GskGpuGlobals ignored_globals;
  GskGpuNodeFeatures features;
  void                  (* process_node)                        (GskGpuNodeProcessor    *self,
                                                                 GskRenderNode          *node);
  gboolean              (* process_first_node)                  (GskGpuNodeProcessor    *self,
                                                                 GskGpuFirstNodeInfo    *info,
                                                                 GskRenderNode          *node);
  GskGpuImage *         (* get_node_as_image)                   (GskGpuFrame            *self,
                                                                 GskGpuAsImageFlags      flags,
                                                                 GdkColorState          *ccs,
                                                                 const graphene_rect_t  *clip_bounds,
                                                                 const graphene_vec2_t  *scale,
                                                                 GskRenderNode          *node,
                                                                 graphene_rect_t        *out_bounds);
} nodes_vtable[] = {
  [GSK_NOT_A_RENDER_NODE] = {
    0,
    0,
    NULL,
    NULL,
    NULL,
  },
  [GSK_CONTAINER_NODE] = {
    GSK_GPU_GLOBAL_MATRIX | GSK_GPU_GLOBAL_SCALE | GSK_GPU_GLOBAL_CLIP | GSK_GPU_GLOBAL_SCISSOR,
    GSK_GPU_HANDLE_OPACITY,
    gsk_gpu_node_processor_add_container_node,
    gsk_gpu_node_processor_add_first_container_node,
    NULL,
  },
  [GSK_CAIRO_NODE] = {
    0,
    GSK_GPU_HANDLE_OPACITY,
    gsk_gpu_node_processor_add_cairo_node,
    gsk_gpu_node_processor_add_first_node_no_blend,
    gsk_gpu_get_cairo_node_as_image,
  },
  [GSK_COLOR_NODE] = {
    0,
    GSK_GPU_HANDLE_OPACITY,
    gsk_gpu_node_processor_add_color_node,
    gsk_gpu_node_processor_add_first_color_node,
    NULL,
  },
  [GSK_LINEAR_GRADIENT_NODE] = {
    0,
    GSK_GPU_HANDLE_OPACITY,
    gsk_gpu_node_processor_add_linear_gradient_node,
    gsk_gpu_node_processor_add_first_node_no_blend,
    NULL,
  },
  [GSK_REPEATING_LINEAR_GRADIENT_NODE] = {
    0,
    GSK_GPU_HANDLE_OPACITY,
    gsk_gpu_node_processor_add_linear_gradient_node,
    gsk_gpu_node_processor_add_first_node_no_blend,
    NULL,
  },
  [GSK_RADIAL_GRADIENT_NODE] = {
    0,
    GSK_GPU_HANDLE_OPACITY,
    gsk_gpu_node_processor_add_radial_gradient_node,
    gsk_gpu_node_processor_add_first_node_no_blend,
    NULL,
  },
  [GSK_REPEATING_RADIAL_GRADIENT_NODE] = {
    0,
    GSK_GPU_HANDLE_OPACITY,
    gsk_gpu_node_processor_add_radial_gradient_node,
    gsk_gpu_node_processor_add_first_node_no_blend,
    NULL,
  },
  [GSK_CONIC_GRADIENT_NODE] = {
    0,
    GSK_GPU_HANDLE_OPACITY,
    gsk_gpu_node_processor_add_conic_gradient_node,
    gsk_gpu_node_processor_add_first_node_no_blend,
    NULL,
  },
  [GSK_BORDER_NODE] = {
    0,
    GSK_GPU_HANDLE_OPACITY,
    gsk_gpu_node_processor_add_border_node,
    NULL,
    NULL,
  },
  [GSK_TEXTURE_NODE] = {
    0,
    GSK_GPU_HANDLE_OPACITY,
    gsk_gpu_node_processor_add_texture_node,
    gsk_gpu_node_processor_add_first_node_no_blend,
    gsk_gpu_get_texture_node_as_image,
  },
  [GSK_INSET_SHADOW_NODE] = {
    0,
    GSK_GPU_HANDLE_OPACITY,
    gsk_gpu_node_processor_add_inset_shadow_node,
    NULL,
    NULL,
  },
  [GSK_OUTSET_SHADOW_NODE] = {
    0,
    GSK_GPU_HANDLE_OPACITY,
    gsk_gpu_node_processor_add_outset_shadow_node,
    NULL,
    NULL,
  },
  [GSK_TRANSFORM_NODE] = {
    GSK_GPU_GLOBAL_MATRIX | GSK_GPU_GLOBAL_SCALE | GSK_GPU_GLOBAL_CLIP | GSK_GPU_GLOBAL_SCISSOR | GSK_GPU_GLOBAL_BLEND,
    GSK_GPU_HANDLE_OPACITY,
    gsk_gpu_node_processor_add_transform_node,
    gsk_gpu_node_processor_add_first_transform_node,
    NULL,
  },
  [GSK_OPACITY_NODE] = {
    GSK_GPU_GLOBAL_MATRIX | GSK_GPU_GLOBAL_SCALE | GSK_GPU_GLOBAL_CLIP | GSK_GPU_GLOBAL_SCISSOR,
    GSK_GPU_HANDLE_OPACITY,
    gsk_gpu_node_processor_add_opacity_node,
    NULL,
    NULL,
  },
  [GSK_COLOR_MATRIX_NODE] = {
    0,
    GSK_GPU_HANDLE_OPACITY,
    gsk_gpu_node_processor_add_color_matrix_node,
    NULL,
    NULL,
  },
  [GSK_REPEAT_NODE] = {
    0,
    GSK_GPU_HANDLE_OPACITY,
    gsk_gpu_node_processor_add_repeat_node,
    NULL,
    NULL,
  },
  [GSK_CLIP_NODE] = {
    GSK_GPU_GLOBAL_MATRIX | GSK_GPU_GLOBAL_SCALE | GSK_GPU_GLOBAL_CLIP | GSK_GPU_GLOBAL_SCISSOR | GSK_GPU_GLOBAL_BLEND,
    GSK_GPU_HANDLE_OPACITY,
    gsk_gpu_node_processor_add_clip_node,
    gsk_gpu_node_processor_add_first_clip_node,
    NULL,
  },
  [GSK_ROUNDED_CLIP_NODE] = {
    GSK_GPU_GLOBAL_MATRIX | GSK_GPU_GLOBAL_SCALE | GSK_GPU_GLOBAL_CLIP | GSK_GPU_GLOBAL_SCISSOR | GSK_GPU_GLOBAL_BLEND,
    GSK_GPU_HANDLE_OPACITY,
    gsk_gpu_node_processor_add_rounded_clip_node,
    gsk_gpu_node_processor_add_first_rounded_clip_node,
    NULL,
  },
  [GSK_SHADOW_NODE] = {
    0,
    0,
    gsk_gpu_node_processor_add_shadow_node,
    NULL,
    NULL,
  },
  [GSK_BLEND_NODE] = {
    0,
    GSK_GPU_HANDLE_OPACITY,
    gsk_gpu_node_processor_add_blend_node,
    NULL,
    NULL,
  },
  [GSK_CROSS_FADE_NODE] = {
    0,
    GSK_GPU_HANDLE_OPACITY,
    gsk_gpu_node_processor_add_cross_fade_node,
    NULL,
    NULL,
  },
  [GSK_TEXT_NODE] = {
    0,
    GSK_GPU_HANDLE_OPACITY,
    gsk_gpu_node_processor_add_glyph_node,
    NULL,
    NULL,
  },
  [GSK_BLUR_NODE] = {
    0,
    0,
    gsk_gpu_node_processor_add_blur_node,
    NULL,
    NULL,
  },
  [GSK_DEBUG_NODE] = {
    GSK_GPU_GLOBAL_MATRIX | GSK_GPU_GLOBAL_SCALE | GSK_GPU_GLOBAL_CLIP | GSK_GPU_GLOBAL_SCISSOR | GSK_GPU_GLOBAL_BLEND,
    GSK_GPU_HANDLE_OPACITY,
    gsk_gpu_node_processor_add_debug_node,
    gsk_gpu_node_processor_add_first_debug_node,
    gsk_gpu_get_debug_node_as_image,
  },
  [GSK_GL_SHADER_NODE] = {
    0,
    GSK_GPU_HANDLE_OPACITY,
    gsk_gpu_node_processor_add_gl_shader_node,
    NULL,
    NULL,
  },
  [GSK_TEXTURE_SCALE_NODE] = {
    0,
    0,
    gsk_gpu_node_processor_add_texture_scale_node,
    gsk_gpu_node_processor_add_first_node_no_blend,
    NULL,
  },
  [GSK_MASK_NODE] = {
    0,
    GSK_GPU_HANDLE_OPACITY,
    gsk_gpu_node_processor_add_mask_node,
    NULL,
    NULL,
  },
  [GSK_FILL_NODE] = {
    0,
    GSK_GPU_HANDLE_OPACITY,
    gsk_gpu_node_processor_add_fill_node,
    NULL,
    NULL,
  },
  [GSK_STROKE_NODE] = {
    0,
    GSK_GPU_HANDLE_OPACITY,
    gsk_gpu_node_processor_add_stroke_node,
    NULL,
    NULL,
  },
  [GSK_SUBSURFACE_NODE] = {
    GSK_GPU_GLOBAL_MATRIX | GSK_GPU_GLOBAL_SCALE | GSK_GPU_GLOBAL_CLIP | GSK_GPU_GLOBAL_SCISSOR | GSK_GPU_GLOBAL_BLEND,
    GSK_GPU_HANDLE_OPACITY,
    gsk_gpu_node_processor_add_subsurface_node,
    gsk_gpu_node_processor_add_first_subsurface_node,
    gsk_gpu_get_subsurface_node_as_image,
  },
  [GSK_COMPONENT_TRANSFER_NODE] = {
    0,
    0,
    gsk_gpu_node_processor_add_component_transfer_node,
    NULL,
    NULL,
  },
};

static void
gsk_gpu_node_processor_add_node (GskGpuNodeProcessor *self,
                                 GskRenderNode       *node)
{
  GskRenderNodeType node_type;

  /* This catches the corner cases of empty nodes, so after this check
   * there's quaranteed to be at least 1 pixel that needs to be drawn
   */
  if (node->bounds.size.width == 0 || node->bounds.size.height == 0)
    return;

  if (!gsk_gpu_clip_may_intersect_rect (&self->clip, &self->offset, &node->bounds))
    return;

  node_type = gsk_render_node_get_node_type (node);
  if (node_type >= G_N_ELEMENTS (nodes_vtable))
    {
      g_critical ("unknown node type %u for %s", node_type, g_type_name_from_instance ((GTypeInstance *) node));
      return;
    }

  if (self->opacity < 1.0 && (nodes_vtable[node_type].features & GSK_GPU_HANDLE_OPACITY) == 0)
    {
      gsk_gpu_node_processor_add_without_opacity (self, node);
      return;
    }

  gsk_gpu_node_processor_sync_globals (self, nodes_vtable[node_type].ignored_globals);
  g_assert ((self->pending_globals & ~nodes_vtable[node_type].ignored_globals) == 0);

  if (nodes_vtable[node_type].process_node)
    {
      nodes_vtable[node_type].process_node (self, node);
    }
  else
    {
      g_warning_once ("Unimplemented node '%s'",
                      g_type_name_from_instance ((GTypeInstance *) node));
      /* Maybe it's implemented in the Cairo renderer? */
      gsk_gpu_node_processor_add_cairo_node (self, node);
    }
}

static gboolean
gsk_gpu_node_processor_add_first_node (GskGpuNodeProcessor *self,
                                       GskGpuFirstNodeInfo *info,
                                       GskRenderNode       *node)
{
  GskRenderNodeType node_type;
  graphene_rect_t opaque;

  /* This catches the corner cases of empty nodes, so after this check
   * there's quaranteed to be at least 1 pixel that needs to be drawn
   */
  if (node->bounds.size.width == 0 || node->bounds.size.height == 0 ||
      !gsk_render_node_get_opaque_rect (node, &opaque))
    return FALSE;

  if (!gsk_gpu_clip_may_intersect_rect (&self->clip, &self->offset, &node->bounds))
    return FALSE;

  node_type = gsk_render_node_get_node_type (node);
  if (node_type >= G_N_ELEMENTS (nodes_vtable))
    {
      g_critical ("unknown node type %u for %s", node_type, g_type_name_from_instance ((GTypeInstance *) node));
      return FALSE;
    }

  if (nodes_vtable[node_type].process_first_node)
    return nodes_vtable[node_type].process_first_node (self, info, node);

  /* fallback starts here */
  if (!gsk_gpu_node_processor_clip_first_node (self, info, &opaque))
    return FALSE;

  gsk_gpu_first_node_begin_rendering (self, info, GSK_VEC4_TRANSPARENT);
  gsk_gpu_node_processor_add_node (self, node);

  return TRUE;
}

/*
 * gsk_gpu_get_node_as_image:
 * @frame: frame to render in
 * @flags: flags for the image
 * @ccs: the color state to composite the image in
 * @clip_bounds: region of node that must be included in image
 * @scale: scale factor to use for the image
 * @node: the node to render
 * @out_bounds: the actual bounds of the result
 *
 * Get the part of the node indicated by the clip bounds as an image.
 *
 * The resulting image will be in the given colorstate and premultiplied.
 *
 * It is perfectly valid for this function to return an image covering
 * a larger or smaller rectangle than the given clip bounds.
 * It can be smaller if the node is actually smaller than the clip
 * bounds and it's not necessary to create such a large offscreen, and
 * it can be larger if only part of a node is drawn but a cached image
 * for the full node (usually a texture node) already exists.
 *
 * The rectangle that is actually covered by the image is returned in
 * out_bounds.
 *
 * Returns: the image or %NULL if there was nothing to render
 **/
static GskGpuImage *
gsk_gpu_get_node_as_image (GskGpuFrame           *frame,
                           GskGpuAsImageFlags     flags,
                           GdkColorState         *ccs,
                           const graphene_rect_t *clip_bounds,
                           const graphene_vec2_t *scale,
                           GskRenderNode         *node,
                           graphene_rect_t       *out_bounds)
{
  GskRenderNodeType node_type;

  node_type = gsk_render_node_get_node_type (node);
  if (node_type >= G_N_ELEMENTS (nodes_vtable))
    {
      g_critical ("unknown node type %u for %s", node_type, g_type_name_from_instance ((GTypeInstance *) node));
      return NULL;
    }

  if (gsk_gpu_frame_should_optimize (frame, GSK_GPU_OPTIMIZE_TO_IMAGE) &&
      nodes_vtable[node_type].get_node_as_image)
    {
      return nodes_vtable[node_type].get_node_as_image (frame, flags, ccs, clip_bounds, scale, node, out_bounds);
    }
  else
    {
      GSK_DEBUG (FALLBACK, "Unsupported node '%s'",
                 g_type_name_from_instance ((GTypeInstance *) node));
      return gsk_gpu_get_node_as_image_via_offscreen (frame, flags, ccs, clip_bounds, scale, node, out_bounds);
    }
}

static void
gsk_gpu_node_processor_set_scissor (GskGpuNodeProcessor         *self,
                                    const cairo_rectangle_int_t *scissor)
{
  graphene_rect_t clip_rect;

  self->scissor = *scissor;

  if (!gsk_gpu_node_processor_rect_device_to_clip (self,
                                                   &GSK_RECT_INIT_CAIRO (scissor),
                                                   &clip_rect))
    {
      /* If the transform is screwed up in the places where we call this function,
       * something has seriously gone wrong.
       */
      g_assert_not_reached ();
    }
  
  gsk_gpu_clip_init_empty (&self->clip, &clip_rect);

  self->pending_globals |= GSK_GPU_GLOBAL_CLIP | GSK_GPU_GLOBAL_SCISSOR;
}

static void
gsk_gpu_node_processor_render (GskGpuNodeProcessor   *self,
                               GskGpuImage           *target,
                               cairo_region_t        *clip,
                               GskRenderNode         *node,
                               GskRenderPassType      pass_type)
{
  GskGpuFirstNodeInfo info = {
    .target = target,
    .pass_type = pass_type,
  };
  int i, n, best, best_size;
  cairo_rectangle_int_t rect;
  gboolean do_culling;

  cairo_region_get_extents (clip, &info.extents);
  info.whole_area = cairo_region_contains_rectangle (clip, &info.extents) == CAIRO_REGION_OVERLAP_IN;
  info.min_occlusion_pixels = gsk_gpu_image_get_width (target) * gsk_gpu_image_get_height (target) *
                              MIN_PERCENTAGE_FOR_OCCLUSION_PASS / 100;
  info.min_occlusion_pixels = MAX (info.min_occlusion_pixels, MIN_PIXELS_FOR_OCCLUSION_PASS);

  do_culling = gsk_gpu_frame_should_optimize (self->frame, GSK_GPU_OPTIMIZE_OCCLUSION_CULLING);

  while (do_culling &&
         (n = cairo_region_num_rectangles (clip)) > 0)
    {
      best = -1;
      best_size = 0;
      for (i = 0; i < n; i++)
        {
          cairo_region_get_rectangle (clip, i, &rect);
          if (rect.width * rect.height > best_size)
            {
              best = i;
              best_size = rect.width * rect.height;
            }
        }

      cairo_region_get_rectangle (clip, best, &rect);

      if (best_size < MIN_PIXELS_FOR_OCCLUSION_PASS)
        break;

      gsk_gpu_node_processor_set_scissor (self, &rect);

      if (!gsk_gpu_node_processor_add_first_node (self,
                                                  &info,
                                                  node))
        {
          gsk_gpu_first_node_begin_rendering (self, &info, GSK_VEC4_TRANSPARENT);
          gsk_gpu_node_processor_add_node (self, node);
          do_culling = FALSE;
        }
      else if (GSK_DEBUG_CHECK (OCCLUSION))
        {
          gsk_gpu_node_processor_sync_globals (self, 0);
          gsk_gpu_color_op (self->frame,
                            GSK_GPU_SHADER_CLIP_NONE,
                            self->ccs,
                            1.0,
                            &self->offset,
                            &GRAPHENE_RECT_INIT(0, 0, 10000, 10000),
                            &GDK_COLOR_SRGB (1.0, 1.0, 1.0, 0.6));
        }

      cairo_region_subtract_rectangle (clip, &self->scissor);
    }

  for (i = 0; i < cairo_region_num_rectangles (clip); i++) 
    {
      cairo_region_get_rectangle (clip, i, &rect);

      gsk_gpu_node_processor_set_scissor (self, &rect);

      /* only run pass if it's covering the whole rectangle */
      info.min_occlusion_pixels = rect.width * rect.height;

      if (!gsk_gpu_node_processor_add_first_node (self,
                                                  &info,
                                                  node))
        {
          gsk_gpu_first_node_begin_rendering (self, &info, GSK_VEC4_TRANSPARENT);
          gsk_gpu_node_processor_add_node (self, node);
        }
    }

  if (info.has_started_rendering)
    {
      gsk_gpu_render_pass_end_op (self->frame,
                                  target,
                                  pass_type);
    }
}

static void
gsk_gpu_node_processor_convert_to (GskGpuNodeProcessor   *self,
                                   GdkShaderOp            target_shader_op,
                                   GskGpuImage           *image,
                                   GdkColorState         *image_color_state,
                                   const graphene_rect_t *rect,
                                   const graphene_rect_t *tex_rect)
{
  gboolean target_premultiplied;

  switch (target_shader_op)
    {
    case GDK_SHADER_DEFAULT:
      target_premultiplied = TRUE;
      break;
    case GDK_SHADER_STRAIGHT:
      target_premultiplied = FALSE;
      break;
    case GDK_SHADER_2_PLANES:
    case GDK_SHADER_3_PLANES:
    case GDK_SHADER_3_PLANES_10BIT_LSB:
    case GDK_SHADER_3_PLANES_12BIT_LSB:
    default:
      g_return_if_reached ();
    }

  gsk_gpu_node_processor_sync_globals (self, 0);

  if (GDK_IS_BUILTIN_COLOR_STATE (self->ccs))
    {
      gsk_gpu_convert_to_builtin_op (self->frame,
                                     gsk_gpu_clip_get_shader_clip (&self->clip, &self->offset, rect),
                                     self->ccs,
                                     target_premultiplied,
                                     image_color_state,
                                     self->opacity,
                                     &self->offset,
                                     &(GskGpuShaderImage) {
                                         image,
                                         GSK_GPU_SAMPLER_DEFAULT,
                                         rect,
                                         tex_rect
                                     });
    }
  else if (!GDK_IS_DEFAULT_COLOR_STATE (self->ccs))
    {
      const GdkCicp *cicp = gdk_color_state_get_cicp (self->ccs);

      g_assert (cicp != NULL);

      gsk_gpu_convert_to_cicp_op (self->frame,
                                  gsk_gpu_clip_get_shader_clip (&self->clip, &self->offset, rect),
                                  cicp,
                                  gsk_gpu_color_states_create_cicp (image_color_state, TRUE, target_premultiplied),
                                  self->opacity,
                                  &self->offset,
                                  &(GskGpuShaderImage) {
                                      image,
                                      GSK_GPU_SAMPLER_DEFAULT,
                                      rect,
                                      tex_rect
                                  });
    }
  else
    {
      gsk_gpu_convert_op (self->frame,
                          gsk_gpu_clip_get_shader_clip (&self->clip, &self->offset, rect),
                          gsk_gpu_color_states_create (self->ccs, target_premultiplied,
                                                       image_color_state, TRUE),
                          self->opacity,
                          &self->offset,
                          &(GskGpuShaderImage) {
                              image,
                              GSK_GPU_SAMPLER_DEFAULT,
                              rect,
                              tex_rect
                          });
    }
}

void
gsk_gpu_node_processor_process (GskGpuFrame           *frame,
                                GskGpuImage           *target,
                                GdkColorState         *target_color_state,
                                cairo_region_t        *clip,
                                GskRenderNode         *node,
                                const graphene_rect_t *viewport,
                                GskRenderPassType      pass_type)
{
  GskGpuNodeProcessor self;
  GdkColorState *ccs;
  GskGpuImage *image;
  graphene_rect_t clip_bounds, tex_rect;
  cairo_rectangle_int_t extents;
  int i;

  ccs = gdk_color_state_get_rendering_color_state (target_color_state);

  cairo_region_get_extents (clip, &extents);

  gsk_gpu_node_processor_init (&self,
                               frame,
                               target,
                               target_color_state,
                               &extents,
                               viewport);

  if (gdk_color_state_equal (ccs, target_color_state))
    {
      gsk_gpu_node_processor_render (&self, target, clip, node, pass_type);
    }
  else
    {
      for (i = 0; i < cairo_region_num_rectangles (clip); i++)
        {
          cairo_rectangle_int_t rect;

          cairo_region_get_rectangle (clip, i, &rect);
          gsk_gpu_node_processor_set_scissor (&self, &rect);

          /* Can't use gsk_gpu_node_processor_get_node_as_image () because of colorspaces */
          if (!gsk_gpu_node_processor_clip_node_bounds_and_snap_to_grid (&self, node, &clip_bounds))
            continue;

          image = gsk_gpu_get_node_as_image (self.frame,
                                             0,
                                             ccs,
                                             &clip_bounds,
                                             &self.scale,
                                             node,
                                             &tex_rect);
          if (image == NULL)
            continue;

          gsk_gpu_render_pass_begin_op (frame,
                                        target,
                                        &rect,
                                        GSK_GPU_LOAD_OP_DONT_CARE,
                                        NULL,
                                        pass_type);

          self.blend = GSK_GPU_BLEND_NONE;
          self.pending_globals |= GSK_GPU_GLOBAL_BLEND;

          gsk_gpu_node_processor_convert_to (&self,
                                             gsk_gpu_image_get_shader_op (target),
                                             image,
                                             ccs,
                                             &clip_bounds,
                                             &tex_rect);

          gsk_gpu_render_pass_end_op (frame,
                                      target,
                                      pass_type);

          g_object_unref (image);
        }
    }

  gsk_gpu_node_processor_finish (&self);

  cairo_region_destroy (clip);
}

GskGpuImage *
gsk_gpu_node_processor_convert_image (GskGpuFrame     *frame,
                                      GdkMemoryFormat  target_format,
                                      GdkColorState   *target_color_state,
                                      GskGpuImage     *image,
                                      GdkColorState   *image_color_state)
{
  GskGpuNodeProcessor self;
  GskGpuImage *target, *intermediate = NULL;
  gsize width, height;
  gboolean target_shader_op, image_shader_op;

  width = gsk_gpu_image_get_width (image);
  height = gsk_gpu_image_get_height (image);

  target = gsk_gpu_device_create_offscreen_image (gsk_gpu_frame_get_device (frame),
                                                  FALSE,
                                                  target_format,
                                                  gsk_gpu_image_get_conversion (image) == GSK_GPU_CONVERSION_SRGB,
                                                  width,
                                                  height);
  if (target == NULL)
    return NULL;

  target_shader_op = gsk_gpu_image_get_shader_op (target);
  image_shader_op = gsk_gpu_image_get_shader_op (image);

  /* We need to go via an intermediate colorstate */
  if (!(GDK_IS_DEFAULT_COLOR_STATE (image_color_state) && image_shader_op == GDK_SHADER_DEFAULT) &&
      !(GDK_IS_DEFAULT_COLOR_STATE (target_color_state) && target_shader_op == GDK_SHADER_DEFAULT))
    {
      GdkColorState *ccs = gdk_color_state_get_rendering_color_state (image_color_state);

      intermediate = gsk_gpu_copy_image (frame, ccs, image, image_color_state, FALSE);
      if (intermediate == NULL)
        return NULL;
      image = intermediate;
      image_color_state = ccs;
      image_shader_op = GDK_SHADER_DEFAULT;
    }

  gsk_gpu_node_processor_init (&self,
                               frame,
                               target,
                               target_color_state,
                               &(cairo_rectangle_int_t) { 0, 0, width, height },
                               &GRAPHENE_RECT_INIT (0, 0, width, height));
  gsk_gpu_render_pass_begin_op (frame,
                                target,
                                &(cairo_rectangle_int_t) { 0, 0, width, height },
                                GSK_GPU_LOAD_OP_DONT_CARE,
                                NULL,
                                GSK_RENDER_PASS_OFFSCREEN);

  self.blend = GSK_GPU_BLEND_NONE;
  self.pending_globals |= GSK_GPU_GLOBAL_BLEND;
  gsk_gpu_node_processor_sync_globals (&self, 0);

  if (GDK_IS_DEFAULT_COLOR_STATE (target_color_state) && target_shader_op == GDK_SHADER_DEFAULT)
    {
      gsk_gpu_node_processor_image_op (&self,
                                       image,
                                       image_color_state,
                                       GSK_GPU_SAMPLER_DEFAULT,
                                       &GRAPHENE_RECT_INIT (0, 0, width, height),
                                       &GRAPHENE_RECT_INIT (0, 0, width, height));
    }
  else
    {
      gsk_gpu_node_processor_convert_to (&self,
                                         target_shader_op,
                                         image,
                                         image_color_state,
                                         &GRAPHENE_RECT_INIT (0, 0, width, height),
                                         &GRAPHENE_RECT_INIT (0, 0, width, height));
    }

  gsk_gpu_render_pass_end_op (self.frame,
                              target,
                              GSK_RENDER_PASS_OFFSCREEN);
  gsk_gpu_node_processor_finish (&self);

  g_clear_object (&intermediate);

  return target;
}
