#include "config.h"

#include "gskgpunodeprocessorprivate.h"

#include "gskgpuarithmeticopprivate.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 "gskgpucachedtileprivate.h"
#include "gskgpuclearopprivate.h"
#include "gskgpucolorizeopprivate.h"
#include "gskgpucolormatrixopprivate.h"
#include "gskgpucomponenttransferopprivate.h"
#include "gskgpucompositeopprivate.h"
#include "gskgpucoloropprivate.h"
#include "gskgpuconicgradientopprivate.h"
#include "gskgpuconvertbuiltinopprivate.h"
#include "gskgpuconvertcicpopprivate.h"
#include "gskgpuconvertopprivate.h"
#include "gskgpucrossfadeopprivate.h"
#include "gskgpudisplacementopprivate.h"
#include "gskgpudeviceprivate.h"
#include "gskgpuframeprivate.h"
#include "gskgpuglobalsopprivate.h"
#include "gskgpuimageprivate.h"
#include "gskgpulineargradientopprivate.h"
#include "gskgpumaskopprivate.h"
#include "gskgpumipmapopprivate.h"
#include "gskgpuocclusionprivate.h"
#include "gskgpuradialgradientopprivate.h"
#include "gskgpurenderpassprivate.h"
#include "gskgpuroundedcoloropprivate.h"
#include "gskgpuscissoropprivate.h"
#include "gskgputextureopprivate.h"
#include "gskgputurbulenceopprivate.h"
#include "gskgpuuploadopprivate.h"
#include "gskgpuutilsprivate.h"

#include "gskarithmeticnodeprivate.h"
#include "gskblendnodeprivate.h"
#include "gskblurnode.h"
#include "gskbordernodeprivate.h"
#include "gskcairoblurprivate.h"
#include "gskclipnode.h"
#include "gskcolormatrixnodeprivate.h"
#include "gskcolornodeprivate.h"
#include "gskcomponenttransfernodeprivate.h"
#include "gskcomponenttransferprivate.h"
#include "gskcompositenode.h"
#include "gskconicgradientnodeprivate.h"
#include "gskcontainernodeprivate.h"
#include "gskcrossfadenode.h"
#include "gskdebugprivate.h"
#include "gskdebugnode.h"
#include "gskdisplacementnodeprivate.h"
#include "gskfillnode.h"
#include "gskinsetshadownodeprivate.h"
#include "gskisolationnode.h"
#include "gsklineargradientnodeprivate.h"
#include "gskmasknode.h"
#include "gskopacitynode.h"
#include "gskoutsetshadownodeprivate.h"
#include "gskpath.h"
#include "gskradialgradientnodeprivate.h"
#include "gskrectprivate.h"
#include "gskrendernodeprivate.h"
#include "gskrepeatnodeprivate.h"
#include "gskroundedclipnode.h"
#include "gskroundedrectprivate.h"
#include "gskshadownodeprivate.h"
#include "gskstrokenode.h"
#include "gsksubsurfacenode.h"
#include "gsktextnodeprivate.h"
#include "gsktexturenode.h"
#include "gsktexturescalenode.h"
#include "gsktransformnode.h"
#include "gskturbulencenodeprivate.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 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;

static void             gsk_gpu_node_processor_add_node_untracked       (GskGpuRenderPass            *self,
                                                                         GskRenderNode                  *node);
static GskGpuImage *    gsk_gpu_get_node_as_image                       (GskGpuFrame                    *frame,
                                                                         GskGpuAsImageFlags              flags,
                                                                         GdkColorState                  *ccs,
                                                                         const graphene_rect_t          *clip_bounds,
                                                                         const graphene_size_t          *scale,
                                                                         GskRenderNode                  *node,
                                                                         graphene_rect_t                *out_bounds);

static GskGpuImage *
create_offscreen_image (GskGpuFrame     *frame,
                        gboolean         with_mipmap,
                        GdkMemoryFormat  format,
                        gboolean         is_srgb,
                        gsize            width,
                        gsize            height)
{
  GskGpuImage *result;
  GskDebugProfile *profile;

  result = gsk_gpu_device_create_offscreen_image (gsk_gpu_frame_get_device (frame),
                                                  with_mipmap,
                                                  format,
                                                  is_srgb,
                                                  width,
                                                  height);
  if (result == NULL)
    return NULL;

  profile = gsk_gpu_frame_get_profile (frame);
  if (profile)
    {
      profile->self.n_offscreens++;
      profile->self.offscreen_pixels += width * height;
    }

  return result;
}
     
static GskGpuRenderPass *
gsk_gpu_node_processor_new_draw (GskGpuFrame            *frame,
                                 GdkColorState          *ccs,
                                 GdkMemoryDepth          depth,
                                 const graphene_size_t  *scale,
                                 const graphene_rect_t  *viewport,
                                 GskGpuImage           **out_image)
{
  GskGpuRenderPass *self;
  GskGpuImage *image;
  cairo_rectangle_int_t area;

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

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

  self = gsk_gpu_render_pass_new (frame,
                                  image,
                                  ccs,
                                  GSK_RENDER_PASS_OFFSCREEN,
                                  GSK_GPU_LOAD_OP_CLEAR,
                                  GSK_VEC4_TRANSPARENT,
                                  &area,
                                  viewport);

  *out_image = image;

  return self;
}

static gboolean G_GNUC_WARN_UNUSED_RESULT
gsk_gpu_node_processor_clip_bounds (GskGpuRenderPass      *self,
                                    const graphene_rect_t *bounds,
                                    GskRectSnap            snap,
                                    graphene_rect_t       *out_bounds)
{
  graphene_rect_t clip, snapped;

  if (!gsk_gpu_render_pass_get_clip_bounds (self, &clip))
    return FALSE;
  
  if (!gsk_gpu_render_pass_snap_rect (self, bounds, snap, &snapped))
    return FALSE;

  if (!gsk_rect_intersection (&clip, &snapped, out_bounds))
    return FALSE;

  return TRUE;
}

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

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

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

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

  return TRUE;
}

static GdkColorState *
gsk_gpu_get_acs_for_builtin (GdkColorState *builtin)
{
  switch (GDK_BUILTIN_COLOR_STATE_ID (builtin))
    {
    case GDK_BUILTIN_COLOR_STATE_ID_OKLAB:
    case GDK_BUILTIN_COLOR_STATE_ID_OKLCH:
      return GDK_COLOR_STATE_SRGB_LINEAR;

    case GDK_BUILTIN_COLOR_STATE_N_IDS:
    default:
      g_assert_not_reached ();
      return NULL;
    }
}

static GdkColorState *
gsk_gpu_get_acs_for_cicp (GdkColorState *cicp,
                          GdkColorState *ccs)
{
  switch (GDK_DEFAULT_COLOR_STATE_ID (ccs))
    {
      case GDK_COLOR_STATE_ID_SRGB:
      case GDK_COLOR_STATE_ID_SRGB_LINEAR:
        return GDK_COLOR_STATE_SRGB_LINEAR;

      case GDK_COLOR_STATE_ID_REC2100_PQ:
      case GDK_COLOR_STATE_ID_REC2100_LINEAR:
        return GDK_COLOR_STATE_REC2100_LINEAR;

    case GDK_COLOR_STATE_N_IDS:
    default:
      g_assert_not_reached ();
      return NULL;
    }
}

static void
gsk_gpu_node_processor_image_op (GskGpuRenderPass   *self,
                                 GskGpuImage           *image,
                                 GdkColorState         *image_color_state,
                                 GskGpuSampler          sampler,
                                 const graphene_rect_t *rect,
                                 const graphene_rect_t *tex_rect)
{
  GskGpuImage *copy = NULL;

  if (GDK_IS_BUILTIN_COLOR_STATE (image_color_state))
    {
      gsk_gpu_convert_builtin_op (self,
                                  self->ccs,
                                  gsk_gpu_get_acs_for_builtin (image_color_state),
                                  rect,
                                  image,
                                  sampler,
                                  GDK_BUILTIN_COLOR_STATE_ID (image_color_state),
                                  FALSE,
                                  FALSE,
                                  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_cicp_op (self,
                               self->ccs,
                               gsk_gpu_get_acs_for_cicp (image_color_state, self->ccs),
                               rect,
                               image,
                               sampler,
                               TRUE,
                               FALSE,
                               tex_rect,
                               cicp->color_primaries,
                               cicp->transfer_function,
                               cicp->matrix_coefficients,
                               cicp->range == GDK_CICP_RANGE_NARROW ? 0 : 1);
    }
  else if (gsk_gpu_image_get_shader_op (image) != GDK_SHADER_DEFAULT ||
           gsk_gpu_render_pass_has_opacity (self) ||
           !gdk_color_state_equal (image_color_state, self->ccs))
    {
      gsk_gpu_convert_op (self,
                          self->ccs,
                          TRUE,
                          image_color_state,
                          rect,
                          image,
                          sampler,
                          tex_rect);
    }
  else
    {
      gsk_gpu_texture_op (self,
                          self->ccs,
                          rect,
                          image,
                          sampler,
                          tex_rect);
    }

  g_clear_object (&copy);
}

static GskGpuImage *
gsk_gpu_node_processor_create_offscreen (GskGpuFrame           *frame,
                                         GdkColorState         *ccs,
                                         const graphene_size_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 (scale->width * viewport->size.width - EPSILON));
  area.height = MAX (1, ceilf (scale->height * viewport->size.height - EPSILON));

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

  image = create_offscreen_image (frame,
                                  FALSE,
                                  gdk_memory_depth_get_format (depth),
                                  FALSE,
                                  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 void
gsk_gpu_node_processor_add_node (GskGpuRenderPass *self,
                                 GskRenderNode       *node,
                                 gsize                pos)
{
  gsk_gpu_frame_start_node (self->frame, node, pos);

  gsk_gpu_node_processor_add_node_untracked (self, node);

  gsk_gpu_frame_end_node (self->frame);
}

static GskGpuImage *
gsk_gpu_get_node_as_image_via_offscreen (GskGpuFrame           *frame,
                                         GskGpuAsImageFlags     flags,
                                         GdkColorState         *ccs,
                                         const graphene_rect_t *clip_bounds,
                                         const graphene_size_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));
  depth = gdk_memory_depth_merge (depth, gdk_color_state_get_depth (ccs));

  copy = create_offscreen_image (frame,
                                 prepare_mipmap,
                                 gdk_memory_depth_get_format (depth),
                                 FALSE,
                                 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
    {
      GskGpuRenderPass *other;
      graphene_rect_t rect = GRAPHENE_RECT_INIT (0, 0, width, height);
      GskGpuRenderPassBlendStorage storage;

      other = gsk_gpu_render_pass_new (frame,
                                       copy,
                                       ccs,
                                       GSK_RENDER_PASS_OFFSCREEN,
                                       GSK_GPU_LOAD_OP_DONT_CARE,
                                       NULL,
                                       &(cairo_rectangle_int_t) { 0, 0, width, height },
                                       &rect);

      gsk_gpu_render_pass_push_blend (other, GSK_GPU_BLEND_NONE, &storage);

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

      gsk_gpu_render_pass_pop_blend (other, &storage);
      gsk_gpu_render_pass_free (other);
    }

  g_object_unref (image);

  return copy;
}

static GskGpuImage *
gsk_gpu_node_processor_get_node_as_image_untracked (GskGpuRenderPass   *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_bounds (self, &node->bounds, GSK_RECT_SNAP_NONE, &clip))
            return NULL;
        }
      else
        {
          if (!gsk_rect_intersection (clip_bounds, &node->bounds, &clip))
            return NULL;
        }
      if (!gsk_rect_snap_to_grid_grow (&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);
}

/*
 * 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
 * @pos: position of the node in the parent for tracking purposes or
 *   -1 to not do tracking
 * @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 (GskGpuRenderPass   *self,
                                          GskGpuAsImageFlags     flags,
                                          const graphene_rect_t *clip_bounds,
                                          GskRenderNode         *node,
                                          gsize                  pos,
                                          graphene_rect_t       *out_bounds)
{
  GskGpuImage *result;

  gsk_gpu_frame_start_node (self->frame, node, pos);

  result = gsk_gpu_node_processor_get_node_as_image_untracked (self,
                                                               flags,
                                                               clip_bounds,
                                                               node,
                                                               out_bounds);

  gsk_gpu_frame_end_node (self->frame);

  return result;
}

static void
gsk_gpu_node_processor_blur_op (GskGpuRenderPass       *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)
{
  GskGpuRenderPass *other;
  GskGpuImage *intermediate;
  graphene_size_t direction;
  graphene_rect_t clip_rect, intermediate_rect;
  float clip_radius;
  GskGpuRenderPassTranslateStorage storage;

  clip_radius = gsk_cairo_blur_compute_pixels (blur_radius / 2.0);

  /* FIXME: Handle clip radius growing the clip too much */
  if (!gsk_gpu_render_pass_get_clip_bounds (self, &clip_rect))
    return;
  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_grow (&intermediate_rect, &self->scale, &self->offset, &intermediate_rect))
    return;

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

  direction = GRAPHENE_SIZE_INIT (blur_radius, 0.0f);
  gsk_gpu_blur_op (other,
                   other->ccs,
                   other->ccs,
                   &intermediate_rect,
                   source_image,
                   GSK_GPU_SAMPLER_TRANSPARENT,
                   FALSE,
                   &intermediate_rect,
                   &(GdkColor) { .color_state = other->ccs, .values = { 1, 1, 1, 1 } }, /* doesn't matter */
                   source_rect,
                   &direction);

  gsk_gpu_render_pass_free (other);

  gsk_gpu_render_pass_push_translate (self, shadow_offset, &storage);
  direction = GRAPHENE_SIZE_INIT (0.0f, blur_radius);
  if (shadow_color)
    {
      gsk_gpu_blur_op (self,
                       self->ccs,
                       gsk_gpu_color_states_find (self->ccs, shadow_color),
                       rect,
                       intermediate,
                       GSK_GPU_SAMPLER_TRANSPARENT,
                       TRUE,
                       rect,
                       shadow_color,
                       &intermediate_rect,
                       &direction);
    }
  else
    {
      gsk_gpu_blur_op (self,
                       self->ccs,
                       self->ccs,
                       rect,
                       intermediate,
                       GSK_GPU_SAMPLER_TRANSPARENT,
                       FALSE,
                       rect,
                       &(GdkColor) { .color_state = self->ccs, .values = { 1, 1, 1, 1 } }, /* doesn't matter */
                       &intermediate_rect,
                       &direction);
    }
  gsk_gpu_render_pass_pop_translate (self, &storage);

  g_object_unref (intermediate);
}

static void
gsk_gpu_node_processor_add_cairo_node (GskGpuRenderPass *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;

  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_with_offscreen (GskGpuRenderPass *self,
                                           GskRenderNode       *node)
{
  GskGpuImage *image;
  graphene_rect_t tex_rect;

  image = gsk_gpu_node_processor_get_node_as_image_untracked (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 (GskGpuRenderPass   *self,
                                         GskRenderNode         *node,
                                         gsize                  pos,
                                         const graphene_rect_t *clip_bounds,
                                         GskRectSnap            snap)
{
  GskGpuRenderPassClipStorage storage;
  graphene_rect_t snapped_clip;

  if (!gsk_gpu_render_pass_snap_rect (self, clip_bounds, snap, &snapped_clip))
    return;

  gsk_gpu_render_pass_push_clip_rect (self, &snapped_clip, &storage);

  if (!gsk_gpu_render_pass_is_all_clipped (self))
    gsk_gpu_node_processor_add_node (self, node, pos);

  gsk_gpu_render_pass_pop_clip_rect (self, &storage);
}

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

static void
gsk_gpu_node_processor_add_rounded_clip_node (GskGpuRenderPass *self,
                                              GskRenderNode       *node)
{
  GskGpuRenderPassClipStorage storage;
  GskRoundedRect clip;
  GskRenderNode *child;

  child = gsk_rounded_clip_node_get_child (node);
  clip = *gsk_rounded_clip_node_get_clip (node);
  if (!gsk_gpu_render_pass_snap_rect (self,
                                      &clip.bounds,
                                      gsk_rounded_clip_node_get_snap (node),
                                      &clip.bounds))
    return;

  /* 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)
    {
      graphene_rect_t child_bounds;

      if (!gsk_gpu_render_pass_snap_rect (self,
                                          &child->bounds,
                                          gsk_color_node_get_snap (node),
                                          &child_bounds))
        return;

      if (gsk_rect_contains_rect (&child_bounds, &clip.bounds))
        {
          const GdkColor *color;

          color = gsk_color_node_get_gdk_color (child);

          gsk_gpu_rounded_color_op (self,
                                    self->ccs,
                                    gsk_gpu_color_states_find (self->ccs, color),
                                    &clip.bounds,
                                    &clip,
                                    color);
          return;
        }
    }

  gsk_gpu_render_pass_push_clip_rounded (self, &clip, &storage);

  if (!gsk_gpu_render_pass_is_all_clipped (self))
    gsk_gpu_node_processor_add_node (self, child, 0);

  gsk_gpu_render_pass_pop_clip_rounded (self, &storage);
}

static void
gsk_gpu_node_processor_add_transform_node (GskGpuRenderPass *self,
                                           GskRenderNode       *node)
{
  GskRenderNode *child;
  GskTransform *transform;

  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:
      {
        GskGpuRenderPassTranslateStorage storage;
        float dx, dy;

        gsk_transform_to_translate (transform, &dx, &dy);
        gsk_gpu_render_pass_push_translate (self, &GRAPHENE_POINT_INIT (dx, dy), &storage);
        gsk_gpu_node_processor_add_node (self, child, 0);
        gsk_gpu_render_pass_pop_translate (self, &storage);
      }
      break;

    case GSK_FINE_TRANSFORM_CATEGORY_2D_AFFINE:
    case GSK_FINE_TRANSFORM_CATEGORY_2D_NEGATIVE_AFFINE:
    case GSK_FINE_TRANSFORM_CATEGORY_2D_DIHEDRAL:
    case GSK_FINE_TRANSFORM_CATEGORY_2D:
    case GSK_FINE_TRANSFORM_CATEGORY_3D:
    case GSK_FINE_TRANSFORM_CATEGORY_ANY:
    case GSK_FINE_TRANSFORM_CATEGORY_UNKNOWN:
      {
        GskGpuRenderPassTransformStorage storage;

        gsk_gpu_render_pass_push_transform (self,
                                            transform,
                                            &node->bounds,
                                            &child->bounds,
                                            &storage);
        if (!gsk_gpu_render_pass_is_all_clipped (self))
          gsk_gpu_node_processor_add_node (self, child, 0);
        gsk_gpu_render_pass_pop_transform (self, &storage);
      }
      break;

    default:
      g_assert_not_reached ();
      break;
    }
}

static void
gsk_gpu_node_processor_add_opacity_node (GskGpuRenderPass *self,
                                         GskRenderNode       *node)
{
  GskGpuRenderPassOpacityStorage storage;
  GskRenderNode *child;

  gsk_gpu_render_pass_push_opacity (self,
                                    gsk_opacity_node_get_opacity (node),
                                    &storage);

  child = gsk_opacity_node_get_child (node);

  gsk_gpu_frame_start_node (self->frame, child, 0);

  if (gsk_render_node_clears_background (child))
    gsk_gpu_node_processor_add_with_offscreen (self, child);
  else
    gsk_gpu_node_processor_add_node_untracked (self, child);

  gsk_gpu_frame_end_node (self->frame);

  gsk_gpu_render_pass_pop_opacity (self, &storage);
}

static void
gsk_gpu_node_processor_add_color_node (GskGpuRenderPass *self,
                                       GskRenderNode       *node)
{
  cairo_rectangle_int_t device;
  graphene_rect_t bounds, cover;
  const GdkColor *color;

  color = gsk_color_node_get_gdk_color (node);
  if (!gsk_gpu_node_processor_clip_bounds (self,
                                           &node->bounds,
                                           gsk_color_node_get_snap (node),
                                           &bounds))
    return;

  if (gsk_gpu_frame_should_optimize (self->frame, GSK_GPU_OPTIMIZE_CLEAR) &&
      !self->modelview && 
      !gsk_gpu_render_pass_has_opacity (self) &&
      self->clip_mask == NULL &&
      gdk_color_is_opaque (color) &&
      gsk_gpu_clip_get_largest_cover (&self->clip, &self->offset, &bounds, &cover) &&
      gsk_gpu_render_pass_user_to_device_shrink (self, &cover, &device) &&
      gdk_rectangle_intersect (&device, &self->scissor, &device) &&
      device.width * device.height > 100 * 100 && /* not worth the effort for small images */
      gsk_gpu_render_pass_device_to_user (self, &device, &cover))
    {
      float clear_color[4];

      if (bounds.origin.x != cover.origin.x)
        gsk_gpu_color_op (self,
                          self->ccs,
                          gsk_gpu_color_states_find (self->ccs, color),
                          &GRAPHENE_RECT_INIT (bounds.origin.x,
                                               bounds.origin.y,
                                               cover.origin.x - bounds.origin.x,
                                               bounds.size.height),
                          color);
      if (bounds.origin.y != cover.origin.y)
        gsk_gpu_color_op (self,
                          self->ccs,
                          gsk_gpu_color_states_find (self->ccs, color),
                          &GRAPHENE_RECT_INIT (bounds.origin.x,
                                               bounds.origin.y,
                                               bounds.size.width,
                                               cover.origin.y - bounds.origin.y),
                          color);
      if (bounds.origin.x + bounds.size.width != cover.origin.x + cover.size.width)
        gsk_gpu_color_op (self,
                          self->ccs,
                          gsk_gpu_color_states_find (self->ccs, color),
                          &GRAPHENE_RECT_INIT (cover.origin.x + cover.size.width,
                                               bounds.origin.y,
                                               bounds.origin.x + bounds.size.width - cover.origin.x - cover.size.width,
                                               bounds.size.height),
                          color);
      if (bounds.origin.y + bounds.size.height != cover.origin.y + cover.size.height)
        gsk_gpu_color_op (self,
                          self->ccs,
                          gsk_gpu_color_states_find (self->ccs, color),
                          &GRAPHENE_RECT_INIT (bounds.origin.x,
                                               cover.origin.y + cover.size.height,
                                               bounds.size.width,
                                               bounds.origin.y + bounds.size.height - cover.origin.y - cover.size.height),
                          color);

      gdk_color_to_float (color, self->ccs, clear_color);
      gsk_gpu_clear_op (self->frame, &device, clear_color);
    }
  else
    {
      gsk_gpu_color_op (self,
                        self->ccs,
                        gsk_gpu_color_states_find (self->ccs, color),
                        &bounds,
                        color);
    }
}

static void
gsk_gpu_node_processor_add_border_node (GskGpuRenderPass *self,
                                        GskRenderNode       *node)
{
  GdkColorState *acs;
  const GdkColor *colors;
  graphene_vec4_t widths;
  GskRoundedRect outside;
  GskRectSnap border_snap;

  outside = *gsk_border_node_get_outline (node);
  if (!gsk_gpu_render_pass_snap_rect (self,
                                      &outside.bounds,
                                      gsk_border_node_get_snap (node),
                                      &outside.bounds))
    return;
  colors = gsk_border_node_get_gdk_colors (node);
  acs = gsk_gpu_color_states_find (self->ccs, &colors[0]);
  border_snap = gsk_border_node_get_border_snap (node);
  if (border_snap == GSK_RECT_SNAP_NONE)
    {
      graphene_vec4_init_from_float (&widths, gsk_border_node_get_widths (node));
    }
  else
    {
      float snapped_widths[4];
      const float *w;
      GskRoundedRect inside;
      G_GNUC_UNUSED gboolean ignored;
  
      inside = outside;
      w = gsk_border_node_get_widths (node);
      gsk_rounded_rect_shrink (&inside, w[0], w[1], w[2], w[3]);
      ignored = gsk_gpu_render_pass_snap_rect (self,
                                               &inside.bounds,
                                               border_snap,
                                               &inside.bounds);
      snapped_widths[0] = inside.bounds.origin.y - outside.bounds.origin.y;
      snapped_widths[1] = outside.bounds.origin.x + outside.bounds.size.width -
                          inside.bounds.origin.x - inside.bounds.size.width;
      snapped_widths[1] = outside.bounds.origin.y + outside.bounds.size.height -
                          inside.bounds.origin.y - inside.bounds.size.height;
      snapped_widths[3] = inside.bounds.origin.x - outside.bounds.origin.x;
      graphene_vec4_init_from_float (&widths, snapped_widths);
    }

  gsk_gpu_border_op (self,
                     self->ccs,
                     acs,
                     &node->bounds,
                     &outside,
                     &colors[0],
                     &colors[1],
                     &colors[2],
                     &colors[3],
                     &widths,
                     &GRAPHENE_SIZE_INIT (0, 0));
}

static gboolean
texture_node_should_mipmap (GskRenderNode         *node,
                            GskGpuFrame           *frame,
                            const graphene_size_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 * scale->width ||
         gdk_texture_get_height (texture) > 2 * node->bounds.size.height * scale->height;
}

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 (GskGpuRenderPass    *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;
  if (!gsk_gpu_render_pass_get_clip_bounds (self, &clip_bounds))
    return;
  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_size_t  *scale,
                                    const graphene_rect_t  *texture_bounds,
                                    GdkTexture             *texture,
                                    GskScalingFilter        scaling_filter)
{
  GskGpuRenderPass *self;
  GskGpuImage *image;
  GskGpuRenderPassBlendStorage storage;

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

  gsk_gpu_render_pass_push_blend (self, GSK_GPU_BLEND_ADD, &storage);

  gsk_gpu_node_processor_draw_texture_tiles (self,
                                             texture_bounds,
                                             texture,
                                             scaling_filter);

  gsk_gpu_render_pass_pop_blend (self, &storage);
  gsk_gpu_render_pass_free (self);

  return image;
}

static void
gsk_gpu_node_processor_add_texture_node (GskGpuRenderPass *self,
                                         GskRenderNode       *node)
{
  GdkColorState *image_cs;
  GskGpuImage *image;
  GdkTexture *texture;
  gboolean should_mipmap;
  GskGpuSampler sampler;
  graphene_rect_t bounds;

  texture = gsk_texture_node_get_texture (node);
  should_mipmap = texture_node_should_mipmap (node, self->frame, &self->scale);
  if (!gsk_gpu_render_pass_snap_rect (self,
                                      &node->bounds,
                                      gsk_texture_node_get_snap (node),
                                      &bounds))
    return;

  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_bounds (self,
                                               &bounds,
                                               /* already snapped */
                                               GSK_RECT_SNAP_NONE,
                                               &clip))
        return;

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

      image = gsk_gpu_get_texture_tiles_as_image (self->frame,
                                                  self->ccs,
                                                  &rounded_clip,
                                                  &self->scale,
                                                  &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)
    sampler = GSK_GPU_SAMPLER_MIPMAP_DEFAULT;
  else
    sampler = GSK_GPU_SAMPLER_DEFAULT;

  if (!gsk_gpu_image_supports_sampler (image, sampler) ||
      (should_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, should_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 (should_mipmap && !(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,
                                   sampler,
                                   &bounds,
                                   &bounds);

  gdk_color_state_unref (image_cs);
  g_object_unref (image);
}

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

  if (!gsk_rect_snap_to_grid (&node->bounds,
                              gsk_texture_node_get_snap (node),
                              scale,
                              &clip_bounds->origin,
                              &bounds))
    return NULL;

  if ((flags & GSK_GPU_AS_IMAGE_EXACT_SIZE) &&
      !gsk_rect_equal (clip_bounds, &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,
                                                  &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 = bounds;
  return image;
}

static void
gsk_gpu_node_processor_add_texture_scale_node (GskGpuRenderPass *self,
                                               GskRenderNode       *node)
{
  GskGpuImage *image;
  GdkTexture *texture;
  GdkColorState *image_cs;
  GskScalingFilter scaling_filter;
  GskGpuSampler sampler;
  graphene_rect_t bounds;
  gboolean need_mipmap, need_offscreen;

  texture = gsk_texture_scale_node_get_texture (node);
  scaling_filter = gsk_texture_scale_node_get_filter (node);
  sampler = gsk_gpu_sampler_for_scaling_filter (scaling_filter),
  need_mipmap = scaling_filter == GSK_SCALING_FILTER_TRILINEAR;
  image = gsk_gpu_lookup_texture (self->frame, self->ccs, texture, need_mipmap, &image_cs);
  if (!gsk_gpu_render_pass_snap_rect (self,
                                      &node->bounds,
                                      gsk_texture_scale_node_get_snap (node),
                                      &bounds))
    return;

  need_offscreen = image == NULL ||
                   self->modelview != NULL ||
                   self->scale.width != 1 ||
                   self->scale.height != 1;

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

      if (!gsk_gpu_render_pass_get_clip_bounds (self, &clip_bounds))
        return;

      /* first round to pixel boundaries, so we make sure the full pixels are covered */
      if (!gsk_rect_snap_to_grid_grow (&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 */
      if (!gsk_rect_snap (&clip_bounds, GSK_RECT_SNAP_GROW, &clip_bounds))
        {
          g_assert_not_reached ();
        }
      /* now intersect with actual node bounds */
      if (!gsk_rect_intersection (&clip_bounds, &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_SIZE_INIT (1, 1),
                                                          &bounds,
                                                          texture,
                                                          scaling_filter);
        }
      else
        {
          offscreen = gsk_gpu_node_processor_create_offscreen (self->frame,
                                                               self->ccs,
                                                               &GRAPHENE_SIZE_INIT (1, 1),
                                                               &clip_bounds,
                                                               node);
          gdk_color_state_unref (image_cs);
          g_object_unref (image);
        }
      gsk_gpu_node_processor_image_op (self,
                                       offscreen,
                                       self->ccs,
                                       GSK_GPU_SAMPLER_DEFAULT,
                                       &bounds,
                                       &clip_bounds);

      g_object_unref (offscreen);
      return;
    }

  if (!gsk_gpu_image_supports_sampler (image, sampler) ||
      (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_node_processor_image_op (self,
                                   image,
                                   image_cs,
                                   sampler,
                                   &bounds,
                                   &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_size_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 (GskGpuRenderPass *self,
                                              GskRenderNode       *node)
{
  float spread, blur_radius;
  const GdkColor *color;
  const graphene_point_t *offset;
  GskRoundedRect outline;

  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);
  offset = gsk_inset_shadow_node_get_offset (node);
  outline = *gsk_inset_shadow_node_get_outline (node);
  if (!gsk_gpu_render_pass_snap_rect (self,
                                      &outline.bounds,
                                      gsk_inset_shadow_node_get_snap (node),
                                      &outline.bounds))
    return;

  if (blur_radius < 0.01)
    {
      graphene_vec4_t widths;

      graphene_vec4_init (&widths, spread, spread, spread, spread);

      gsk_gpu_border_op (self,
                         self->ccs,
                         gsk_gpu_color_states_find (self->ccs, color),
                         &outline.bounds,
                         &outline,
                         color,
                         color,
                         color,
                         color,
                         &widths,
                         &GRAPHENE_SIZE_INIT (offset->x, offset->y));
    }
  else
    {
      gsk_gpu_box_shadow_op (self,
                             self->ccs,
                             gsk_gpu_color_states_find (self->ccs, color),
                             &outline.bounds,
                             TRUE,
                             &outline,
                             &GRAPHENE_SIZE_INIT (offset->x, offset->y),
                             spread,
                             blur_radius,
                             color);
    }
}

static void
gsk_gpu_node_processor_add_outset_shadow_node (GskGpuRenderPass *self,
                                               GskRenderNode       *node)
{
  float spread, blur_radius;
  const GdkColor *color;
  const graphene_point_t *offset;
  GskRoundedRect outline;
  GskRectSnap snap;
  graphene_rect_t bounds;

  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);
  snap = gsk_outset_shadow_node_get_snap (node);
  outline = *gsk_inset_shadow_node_get_outline (node);
  bounds = node->bounds;
  if (!gsk_gpu_render_pass_snap_rect (self,
                                      &outline.bounds,
                                      snap,
                                      &outline.bounds))
    return;
  if (snap != GSK_RECT_SNAP_NONE)
    {
      float extents[4];
      gsk_outset_shadow_node_get_extents (node, extents);
      bounds.origin.x -= extents[GSK_SIDE_LEFT];
      bounds.origin.y -= extents[GSK_SIDE_TOP];
      bounds.size.width += extents[GSK_SIDE_LEFT] + extents[GSK_SIDE_RIGHT];
      bounds.size.height += extents[GSK_SIDE_TOP] + extents[GSK_SIDE_BOTTOM];
    }

  if (blur_radius < 0.01)
    {
      graphene_vec4_t widths;

      graphene_vec4_init (&widths, spread, spread, spread, spread);
      gsk_rounded_rect_shrink (&outline, -spread, -spread, -spread, -spread);
      gsk_rect_init_offset (&outline.bounds, &outline.bounds, offset);

      gsk_gpu_border_op (self,
                         self->ccs,
                         gsk_gpu_color_states_find (self->ccs, color),
                         &bounds,
                         &outline,
                         color,
                         color,
                         color,
                         color,
                         &widths,
                         &GRAPHENE_SIZE_INIT (- offset->x, - offset->y));
    }
  else
    {
      gsk_gpu_box_shadow_op (self,
                             self->ccs,
                             gsk_gpu_color_states_find (self->ccs, color),
                             &bounds,
                             FALSE,
                             &outline,
                             &GRAPHENE_SIZE_INIT (offset->x, offset->y),
                             spread,
                             blur_radius,
                             color);
    }
}

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

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

  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;

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

  g_return_if_fail (other != NULL);

  gsk_gpu_render_pass_push_blend (other, GSK_GPU_BLEND_ADD, &storage);

  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_render_pass_pop_blend (other, &storage);
  gsk_gpu_render_pass_free (other);

  gsk_gpu_node_processor_image_op (self,
                                   image,
                                   ics,
                                   GSK_GPU_SAMPLER_DEFAULT,
                                   &bounds,
                                   &bounds);

  g_object_unref (image);
}

static void
gsk_gpu_node_processor_linear_gradient_op (GskGpuRenderPass   *self,
                                           GdkColorState         *target,
                                           GskRenderNode         *node,
                                           const GskGradientStop *stops,
                                           gsize                  n_stops)
{
  const GskGradient *gradient;
  graphene_rect_t bounds;
  GdkColor colors[7];
  graphene_vec4_t offsets[2];
  graphene_vec4_t hints[2];

  gradient = gsk_gradient_node_get_gradient (node);
  if (!gsk_gpu_render_pass_snap_rect (self,
                                      &node->bounds,
                                      gsk_linear_gradient_node_get_snap (node),
                                      &bounds))
    return;

  gsk_gpu_color_stops_to_shader (stops,
                                 n_stops,
                                 gsk_gradient_get_interpolation (gradient),
                                 gsk_gradient_get_hue_interpolation (gradient),
                                 colors,
                                 offsets,
                                 hints);

  gsk_gpu_linear_gradient_op (self,
                              target,
                              gsk_gradient_get_interpolation (gradient),
                              &bounds,
                              gsk_gpu_frame_should_optimize (self->frame, GSK_GPU_OPTIMIZE_GRADIENTS),
                              gsk_gradient_get_premultiplied (gradient),
                              gsk_gradient_get_repeat (gradient),
                              gsk_linear_gradient_node_get_start (node),
                              gsk_linear_gradient_node_get_end (node),
                              &colors[0],
                              &colors[1],
                              &colors[2],
                              &colors[3],
                              &colors[4],
                              &colors[5],
                              &colors[6],
                              &offsets[0],
                              &offsets[1],
                              &hints[0],
                              &hints[1]);
}

static void
gsk_gpu_node_processor_add_linear_gradient_node (GskGpuRenderPass *self,
                                                 GskRenderNode       *node)
{
  const GskGradient *gradient = gsk_gradient_node_get_gradient (node);

  if (gsk_linear_gradient_node_is_zero_length (node))
    {
      graphene_rect_t bounds;

      if (!gsk_gpu_node_processor_clip_bounds (self,
                                               &node->bounds,
                                               gsk_linear_gradient_node_get_snap (node),
                                               &bounds))
        return;

      switch (gsk_gradient_get_repeat (gradient))
        {
        case GSK_REPEAT_NONE:
          return;

        case GSK_REPEAT_PAD:
          /* average first and last color stop */
          {
            GdkColor color, start, end;
            GdkColorState *interpolation = gsk_gradient_get_interpolation (gradient);
            gdk_color_convert (&start,
                               interpolation,
                               gsk_gradient_get_stop_color (gradient, 0));
            gdk_color_convert (&end,
                               interpolation,
                               gsk_gradient_get_stop_color (gradient, gsk_gradient_get_n_stops (gradient) - 1));
            gdk_color_init (&color,
                            interpolation,
                            (float[4]) { 0.5 * (start.values[0] + end.values[0]),
                                         0.5 * (start.values[1] + end.values[1]),
                                         0.5 * (start.values[2] + end.values[2]),
                                         0.5 * (start.values[3] + end.values[3]) });
            gsk_gpu_color_op (self,
                              self->ccs,
                              gsk_gpu_color_states_find (self->ccs, &color),
                              &bounds,
                              &color);
          }
          break;

        case GSK_REPEAT_REPEAT:
        case GSK_REPEAT_REFLECT:
          {
            GdkColor color;
            gsk_gradient_get_average_color (gradient, &color);
            gsk_gpu_color_op (self,
                              self->ccs,
                              gsk_gpu_color_states_find (self->ccs, &color),
                              &bounds,
                              &color);
            gdk_color_finish (&color);
            return;
          }

        default:
          g_assert_not_reached ();
          return;
        }
    }

  gsk_gpu_node_processor_add_gradient_node (self,
                                            node,
                                            gsk_gradient_get_interpolation (gradient),
                                            gsk_gradient_get_stops (gradient),
                                            gsk_gradient_get_n_stops (gradient),
                                            gsk_gpu_node_processor_linear_gradient_op);
}

static void
gsk_gpu_node_processor_radial_gradient_op (GskGpuRenderPass   *self,
                                           GdkColorState         *target,
                                           GskRenderNode         *node,
                                           const GskGradientStop *stops,
                                           gsize                  n_stops)
{
  const graphene_point_t *start_center, *end_center;
  float start_radius, end_radius, aspect_ratio;
  const GskGradient *gradient;
  graphene_rect_t bounds;
  GdkColor colors[7];
  graphene_vec4_t offsets[2];
  graphene_vec4_t hints[2];

  if (!gsk_gpu_render_pass_snap_rect (self,
                                      &node->bounds,
                                      gsk_radial_gradient_node_get_snap (node),
                                      &bounds))
    return;

  gradient = gsk_gradient_node_get_gradient (node);
  start_center = gsk_radial_gradient_node_get_start_center (node);
  start_radius = gsk_radial_gradient_node_get_start_radius (node);
  end_center = gsk_radial_gradient_node_get_end_center (node);
  end_radius = gsk_radial_gradient_node_get_end_radius (node);
  aspect_ratio = gsk_radial_gradient_node_get_aspect_ratio (node);

  gsk_gpu_color_stops_to_shader (stops,
                                 n_stops,
                                 gsk_gradient_get_interpolation (gradient),
                                 gsk_gradient_get_hue_interpolation (gradient),
                                 colors,
                                 offsets,
                                 hints);

  gsk_gpu_radial_gradient_op (self,
                              target,
                              gsk_gradient_get_interpolation (gradient),
                              &bounds,
                              gsk_gpu_frame_should_optimize (self->frame, GSK_GPU_OPTIMIZE_GRADIENTS),
                              graphene_point_equal (start_center, end_center),
                              gsk_gradient_get_premultiplied (gradient),
                              gsk_gradient_get_repeat (gradient),
                              &colors[0],
                              &colors[1],
                              &colors[2],
                              &colors[3],
                              &colors[4],
                              &colors[5],
                              &colors[6],
                              &offsets[0],
                              &offsets[1],
                              &hints[0],
                              &hints[1],
                              start_center,
                              &GRAPHENE_SIZE_INIT (
                                  start_radius,
                                  start_radius / aspect_ratio
                              ),
                              end_center,
                              &GRAPHENE_SIZE_INIT (
                                  end_radius,
                                  end_radius / aspect_ratio
                              ));
}

static void
gsk_gpu_node_processor_add_radial_gradient_node (GskGpuRenderPass *self,
                                                 GskRenderNode       *node)
{
  const GskGradient *gradient = gsk_gradient_node_get_gradient (node);

  if (gsk_radial_gradient_node_is_zero_length (node))
    {
      graphene_rect_t bounds;

      if (!gsk_gpu_node_processor_clip_bounds (self,
                                               &node->bounds,
                                               gsk_radial_gradient_node_get_snap (node),
                                               &bounds))
        return;

      switch (gsk_gradient_get_repeat (gradient))
        {
        case GSK_REPEAT_NONE:
          return;

        case GSK_REPEAT_PAD:
          /* The default rendering does the right thing */
          break;

        case GSK_REPEAT_REPEAT:
        case GSK_REPEAT_REFLECT:
          {
            GdkColor color;
            gsk_gradient_get_average_color (gradient, &color);
            gsk_gpu_color_op (self,
                              self->ccs,
                              gsk_gpu_color_states_find (self->ccs, &color),
                              &bounds,
                              &color);
            gdk_color_finish (&color);
            return;
          }

        default:
          g_assert_not_reached ();
          return;
        }
    }

  gsk_gpu_node_processor_add_gradient_node (self,
                                            node,
                                            gsk_gradient_get_interpolation (gradient),
                                            gsk_gradient_get_stops (gradient),
                                            gsk_gradient_get_n_stops (gradient),
                                            gsk_gpu_node_processor_radial_gradient_op);
}

static void
gsk_gpu_node_processor_conic_gradient_op (GskGpuRenderPass   *self,
                                          GdkColorState         *target,
                                          GskRenderNode         *node,
                                          const GskGradientStop *stops,
                                          gsize                  n_stops)
{
  const GskGradient *gradient;
  graphene_rect_t bounds;
  GdkColor colors[7];
  graphene_vec4_t offsets[2];
  graphene_vec4_t hints[2];

  gradient = gsk_gradient_node_get_gradient (node);
  if (!gsk_gpu_render_pass_snap_rect (self,
                                      &node->bounds,
                                      gsk_conic_gradient_node_get_snap (node),
                                      &bounds))
    return;

  gsk_gpu_color_stops_to_shader (stops,
                                 n_stops,
                                 gsk_gradient_get_interpolation (gradient),
                                 gsk_gradient_get_hue_interpolation (gradient),
                                 colors,
                                 offsets,
                                 hints);

  gsk_gpu_conic_gradient_op (self,
                             target,
                             gsk_gradient_get_interpolation (gradient),
                             &bounds,
                             gsk_gpu_frame_should_optimize (self->frame, GSK_GPU_OPTIMIZE_GRADIENTS),
                             gsk_gradient_get_premultiplied (gradient),
                             gsk_conic_gradient_node_get_center (node),
                             gsk_conic_gradient_node_get_angle (node),
                             &colors[0],
                             &colors[1],
                             &colors[2],
                             &colors[3],
                             &colors[4],
                             &colors[5],
                             &colors[6],
                             &offsets[0],
                             &offsets[1],
                             &hints[0],
                             &hints[1]);
}

static void
gsk_gpu_node_processor_add_conic_gradient_node (GskGpuRenderPass *self,
                                                GskRenderNode       *node)
{
  const GskGradient *gradient = gsk_gradient_node_get_gradient (node);
  gsk_gpu_node_processor_add_gradient_node (self,
                                            node,
                                            gsk_gradient_get_interpolation (gradient),
                                            gsk_gradient_get_stops (gradient),
                                            gsk_gradient_get_n_stops (gradient),
                                            gsk_gpu_node_processor_conic_gradient_op);
}

static void
gsk_gpu_node_processor_add_blur_node (GskGpuRenderPass *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, 0);
      return;
    }

  clip_radius = gsk_cairo_blur_compute_pixels (blur_radius / 2.0);
  if (!gsk_gpu_render_pass_get_clip_bounds (self, &clip_rect))
    return;
  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,
                                                    0,
                                                    &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)),
                                  &tex_rect);

  g_object_unref (image);
}

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

  if (gsk_gpu_render_pass_has_opacity (self))
    {
      gsk_gpu_node_processor_add_with_offscreen (self, node);
      return;
    }

  n_shadows = gsk_shadow_node_get_n_shadows (node);
  child = gsk_shadow_node_get_child (node);
  /* enlarge clip for shadow offsets */
  if (!gsk_gpu_render_pass_get_clip_bounds (self, &clip_bounds))
    return;
  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,
                                                    0,
                                                    &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)
        {
          GskGpuRenderPassTranslateStorage storage;

          gsk_gpu_render_pass_push_translate (self, &shadow->offset, &storage);
          gsk_gpu_colorize_op (self,
                               self->ccs,
                               gsk_gpu_color_states_find (self->ccs, &shadow->color),
                               &tex_rect,
                               image,
                               GSK_GPU_SAMPLER_TRANSPARENT,
                               &tex_rect,
                               &shadow->color);
          gsk_gpu_render_pass_pop_translate (self, &storage);
        }
      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)),
                                          &tex_rect);
        }
    }

  gsk_gpu_texture_op (self,
                      self->ccs,
                      &tex_rect,
                      image,
                      GSK_GPU_SAMPLER_DEFAULT,
                      &tex_rect);

  g_object_unref (image);
}

static void
gsk_gpu_node_processor_add_gl_shader_node (GskGpuRenderPass *self,
                                           GskRenderNode       *node)
{
  gsk_gpu_color_op (self,
                    self->ccs,
                    GDK_COLOR_STATE_SRGB,
                    &node->bounds,
                    &GDK_COLOR_SRGB (1, 105/255., 180/255., 1));
}

static void
gsk_gpu_node_processor_add_blend_node (GskGpuRenderPass *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,
                                                           0,
                                                           &bottom_rect);
  top_image = gsk_gpu_node_processor_get_node_as_image (self,
                                                        0,
                                                        NULL,
                                                        top_child,
                                                        1,
                                                        &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,
                         self->ccs,
                         gsk_blend_node_get_color_state (node),
                         &node->bounds,
                         bottom_image,
                         GSK_GPU_SAMPLER_DEFAULT,
                         top_image,
                         GSK_GPU_SAMPLER_DEFAULT,
                         gsk_blend_node_get_blend_mode (node),
                         &bottom_rect,
                         &top_rect);

  g_object_unref (top_image);
  g_object_unref (bottom_image);
}

static void
gsk_gpu_node_processor_add_arithmetic_node (GskGpuRenderPass *self,
                                            GskRenderNode       *node)
{
  const float *k;
  GskRenderNode *first_child, *second_child;
  graphene_rect_t bounds, first_rect, second_rect;
  GskGpuImage *first_image, *second_image;

  k = gsk_arithmetic_node_get_factors (node);
  if (!gsk_gpu_node_processor_clip_bounds (self,
                                           &node->bounds,
                                           gsk_arithmetic_node_get_snap (node),
                                           &bounds))
    return;


  first_child = gsk_arithmetic_node_get_first_child (node);
  second_child = gsk_arithmetic_node_get_second_child (node);

  first_image = gsk_gpu_node_processor_get_node_as_image (self,
                                                          0,
                                                          NULL,
                                                          first_child,
                                                          0,
                                                          &first_rect);
  second_image = gsk_gpu_node_processor_get_node_as_image (self,
                                                           0,
                                                           NULL,
                                                           second_child,
                                                           1,
                                                           &second_rect);

  if (first_image == NULL)
    {
      if (second_image == NULL)
        return;

      first_image = g_object_ref (second_image);
      first_rect = *graphene_rect_zero ();
    }
  else if (second_image == NULL)
    {
      second_image = g_object_ref (first_image);
      second_rect = *graphene_rect_zero ();
    }

  gsk_gpu_arithmetic_op (self,
                         self->ccs,
                         gsk_arithmetic_node_get_color_state (node),
                         &bounds,
                         first_image,
                         GSK_GPU_SAMPLER_DEFAULT,
                         second_image,
                         GSK_GPU_SAMPLER_DEFAULT,
                         &first_rect,
                         &second_rect,
                         k[0], k[1], k[2], k[3]);

  g_object_unref (first_image);
  g_object_unref (second_image);
}

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

  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, 0);
      return;
    }
  if (progress >= 1.0)
    {
      gsk_gpu_node_processor_add_node (self, end_child, 1);
      return;
    }

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

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

      gsk_gpu_render_pass_push_opacity (self,
                                        progress,
                                        &storage);
      gsk_gpu_node_processor_image_op (self,
                                       end_image,
                                       self->ccs,
                                       GSK_GPU_SAMPLER_DEFAULT,
                                       &end_child->bounds,
                                       &end_rect);
      gsk_gpu_render_pass_pop_opacity (self,
                                       &storage);

      g_object_unref (end_image);
      return;
    }
  else if (end_image == NULL)
    {
      gsk_gpu_render_pass_push_opacity (self,
                                        1 - progress,
                                        &storage);
      gsk_gpu_node_processor_image_op (self,
                                       start_image,
                                       self->ccs,
                                       GSK_GPU_SAMPLER_DEFAULT,
                                       &start_child->bounds,
                                       &start_rect);
      gsk_gpu_render_pass_pop_opacity (self,
                                       &storage);

      g_object_unref (start_image);
      return;
    }

  gsk_gpu_cross_fade_op (self,
                         self->ccs,
                         &node->bounds,
                         start_image,
                         GSK_GPU_SAMPLER_DEFAULT,
                         end_image,
                         GSK_GPU_SAMPLER_DEFAULT,
                         &start_rect,
                         &end_rect,
                         progress);

  g_object_unref (end_image);
  g_object_unref (start_image);
}

static void
gsk_gpu_node_processor_add_displacement_node (GskGpuRenderPass *self,
                                              GskRenderNode       *node)
{
  graphene_rect_t bounds, child_bounds, displacement_rect, child_rect;
  GskRenderNode *displacement_child, *child;
  GskGpuImage *displacement_image, *child_image;
  const graphene_size_t *max;
  const GdkColorChannel *channels;
  const graphene_point_t *offset;

  if (!gsk_gpu_node_processor_clip_bounds (self,
                                           &node->bounds,
                                           gsk_displacement_node_get_snap (node),
                                           &bounds))
    return;

  displacement_child = gsk_displacement_node_get_displacement (node);
  child = gsk_displacement_node_get_child (node);
  max = gsk_displacement_node_get_max (node);
  channels = gsk_displacement_node_get_channels (node);
  offset = gsk_displacement_node_get_offset (node);

  child_bounds = bounds;
  graphene_rect_inset (&child_bounds, - max->width, - max->height);
  child_image = gsk_gpu_node_processor_get_node_as_image (self,
                                                          0,
                                                          &child_bounds,
                                                          child,
                                                          0,
                                                          &child_rect);
  if (child_image == NULL)
    return;

  displacement_image = gsk_gpu_node_processor_get_node_as_image (self,
                                                                 0,
                                                                 &bounds,
                                                                 displacement_child,
                                                                 1,
                                                                 &displacement_rect);
  if (displacement_image == NULL)
    return; /* technically we have to render TRANSPARENT everywhere */

  gsk_gpu_displacement_op (self,
                           self->ccs,
                           &bounds,
                           displacement_image,
                           GSK_GPU_SAMPLER_TRANSPARENT,
                           child_image,
                           GSK_GPU_SAMPLER_TRANSPARENT,
                           &displacement_rect,
                           &child_rect,
                           channels[0],
                           channels[1],
                           max,
                           gsk_displacement_node_get_scale (node),
                           &GRAPHENE_SIZE_INIT (offset->x, offset->y));

  g_object_unref (displacement_image);
  g_object_unref (child_image);
}

static void
gsk_gpu_node_processor_add_mask_node (GskGpuRenderPass *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,
                                                         1,
                                                         &mask_rect);
  if (mask_image == NULL)
    {
      if (mask_mode == GSK_MASK_MODE_INVERTED_ALPHA)
        gsk_gpu_node_processor_add_node (self, source_child, 0);
      return;
    }

  if (mask_mode == GSK_MASK_MODE_ALPHA)
    {
      if (gsk_render_node_get_node_type (source_child) == GSK_COLOR_NODE)
        {
          const GdkColor *color = gsk_color_node_get_gdk_color (source_child);
          gsk_gpu_colorize_op (self,
                               self->ccs,
                               gsk_gpu_color_states_find (self->ccs, color),
                               &bounds,
                               mask_image,
                               GSK_GPU_SAMPLER_DEFAULT,
                               &mask_rect,
                               color);
        }
      else
        {
          GskGpuRenderPassClipStorage storage;

          gsk_gpu_render_pass_push_clip_mask (self,
                                              &bounds,
                                              mask_image,
                                              &mask_rect,
                                              !gsk_render_node_is_bilevel_opacity (mask_child),
                                              &storage);
          gsk_gpu_node_processor_add_node (self, source_child, 0);
          gsk_gpu_render_pass_pop_clip_mask (self, &storage);
        }
    }
  else
    {
      GskGpuImage *source_image;
      graphene_rect_t source_rect;

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

      gsk_gpu_mask_op (self,
                       self->ccs,
                       &bounds,
                       source_image,
                       GSK_GPU_SAMPLER_DEFAULT,
                       mask_image,
                       GSK_GPU_SAMPLER_DEFAULT,
                       mask_mode,
                       &source_rect,
                       &mask_rect);

      g_object_unref (source_image);
    }

  g_object_unref (mask_image);
}

static void
gsk_gpu_node_processor_add_glyph_node (GskGpuRenderPass *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 *acs;
  GdkColor color2;

  if (gsk_gpu_render_pass_has_opacity (self) &&
      gsk_text_node_has_color_glyphs (node))
    {
      gsk_gpu_node_processor_add_with_offscreen (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);

  acs = gsk_gpu_color_states_find (self->ccs, color);
  gdk_color_convert (&color2, acs, color);

  scale = MAX (self->scale.width, self->scale.height);

  if (gsk_transform_get_fine_category (self->modelview) <= GSK_FINE_TRANSFORM_CATEGORY_2D)
    {
      scale = exp2f (ceilf (log2f (scale)));
      align_scale_x = align_scale_y = 1;
      flags_mask = 0;
    }
  else 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;

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

      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 = glyph_origin.x / align_scale_x - self->offset.x;
      glyph_origin.y = glyph_origin.y / align_scale_y - self->offset.y;

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

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

          if (glyphs[i].attr.is_color)
            gsk_gpu_texture_op (self,
                                self->ccs,
                                &glyph_bounds,
                                image,
                                GSK_GPU_SAMPLER_DEFAULT,
                                &glyph_tex_rect);
          else
            gsk_gpu_colorize_op (self,
                                 self->ccs,
                                 acs,
                                 &glyph_bounds,
                                 image,
                                 GSK_GPU_SAMPLER_DEFAULT,
                                 &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 (GskGpuRenderPass *self,
                                              GskRenderNode       *node)
{
  GskGpuImage *image;
  GskRenderNode *child;
  const graphene_matrix_t *color_matrix;
  graphene_rect_t tex_rect, bounds;

  child = gsk_color_matrix_node_get_child (node);
  color_matrix = gsk_color_matrix_node_get_color_matrix (node);

  if (!gsk_gpu_node_processor_clip_bounds (self,
                                           &node->bounds,
                                           gsk_color_matrix_node_get_snap (node),
                                           &bounds))
    return;

  image = gsk_gpu_node_processor_get_node_as_image (self,
                                                    0,
                                                    NULL,
                                                    child,
                                                    0,
                                                    &tex_rect);
  if (image == NULL)
    {
      GdkColor color;
      float values[4];

      graphene_vec4_to_float (gsk_color_matrix_node_get_color_offset (node),
                              values);
      gdk_color_init (&color,
                      gsk_color_matrix_node_get_color_state (node),
                      values);

      gsk_gpu_color_op (self,
                        self->ccs,
                        gsk_gpu_color_states_find (self->ccs, &color),
                        &bounds,
                        &color);
      gdk_color_finish (&color);

      return;
    }

  gsk_gpu_color_matrix_op (self,
                           self->ccs,
                           gsk_color_matrix_node_get_color_state (node),
                           &bounds,
                           image,
                           GSK_GPU_SAMPLER_DEFAULT,
                           color_matrix,
                           gsk_color_matrix_node_get_color_offset (node),
                           &tex_rect);

  g_object_unref (image);
}

static void
copy_component_transfer (const GskComponentTransfer *transfer,
                         float                       params[4],
                         float                       table[32],
                         guint                      *n)
{
  params[0] = transfer->kind;
  switch (transfer->kind)
    {
    case GSK_COMPONENT_TRANSFER_IDENTITY:
      break;
    case GSK_COMPONENT_TRANSFER_LEVELS:
      params[1] = transfer->levels.n;
      break;
    case GSK_COMPONENT_TRANSFER_LINEAR:
      params[1] = transfer->linear.m;
      params[2] = transfer->linear.b;
      break;
    case GSK_COMPONENT_TRANSFER_GAMMA:
      params[1] = transfer->gamma.amp;
      params[2] = transfer->gamma.exp;
      params[3] = transfer->gamma.ofs;
      break;
    case GSK_COMPONENT_TRANSFER_DISCRETE:
    case GSK_COMPONENT_TRANSFER_TABLE:
      if (*n + transfer->table.n >= 32)
        g_warning ("tables too big in component transfer");
      params[1] = transfer->table.n;
      params[2] = *n;
      for (guint i = 0; i < transfer->table.n && *n + i < 32; i++)
        table[*n + i] = transfer->table.values[i];
      *n += transfer->table.n;
      break;
    default:
      g_assert_not_reached ();
    }
}

static void
gsk_gpu_node_processor_add_component_transfer_node (GskGpuRenderPass *self,
                                                    GskRenderNode       *node)
{
  GskGpuImage *image;
  GskRenderNode *child;
  graphene_rect_t tex_rect;
  float params[4];
  graphene_vec4_t params_vec[4];
  float table[32];
  graphene_vec4_t table_vec[8];
  guint i, n;

  child = gsk_component_transfer_node_get_child (node);

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

  n = 0;
  for (i = 0; i < 4; i++)
    {
      copy_component_transfer (gsk_component_transfer_node_get_transfer (node, i),
                               params,
                               table,
                               &n);
      graphene_vec4_init_from_float (&params_vec[i], params);
    }
  for (i = 0; i < 8; i++)
    {
      graphene_vec4_init_from_float (&table_vec[i], &table[4 * i]);
    }

  gsk_gpu_component_transfer_op (self,
                                 self->ccs,
                                 gsk_component_transfer_node_get_color_state (node),
                                 &node->bounds,
                                 image,
                                 GSK_GPU_SAMPLER_DEFAULT,
                                 &params_vec[0],
                                 &params_vec[1],
                                 &params_vec[2],
                                 &params_vec[3],
                                 &table_vec[0],
                                 &table_vec[1],
                                 &table_vec[2],
                                 &table_vec[3],
                                 &table_vec[4],
                                 &table_vec[5],
                                 &table_vec[6],
                                 &table_vec[7],
                                 &node->bounds,
                                 &tex_rect);

  g_object_unref (image);
}

static void
gsk_gpu_node_processor_repeat_tile (GskGpuRenderPass    *self,
                                    const graphene_rect_t  *rect,
                                    float                   x,
                                    gboolean                snap_x,
                                    float                   y,
                                    gboolean                snap_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;
    }
  if (snap_x || snap_y)
    {
      graphene_rect_t snapped;
      if (!gsk_gpu_render_pass_snap_rect (self, &clipped_child_bounds, GSK_RECT_SNAP_GROW, &snapped))
        return;

      if (snap_x)
        {
          clipped_child_bounds.origin.x = snapped.origin.x;
          clipped_child_bounds.size.width = snapped.size.width;
        }
      if (snap_y)
        {
          clipped_child_bounds.origin.y = snapped.origin.y;
          clipped_child_bounds.size.height = snapped.size.height;
        }
    }

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

  gsk_gpu_texture_op (self,
                      self->ccs,
                      rect,
                      image,
                      GSK_GPU_SAMPLER_REPEAT,
                      &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 (GskGpuRenderPass *self,
                                        GskRenderNode       *node)
{
  GskRenderNode *child;
  graphene_rect_t child_bounds, bounds;
  float tile_left, tile_right, tile_top, tile_bottom;
  GskRepeat repeat;
  GskRectSnap snap, child_snap;
  gboolean avoid_offscreen;

  child = gsk_repeat_node_get_child (node);
  snap = gsk_repeat_node_get_snap (node);
  child_snap = gsk_repeat_node_get_child_snap (node);
  if (!gsk_gpu_render_pass_snap_rect (self,
                                      gsk_repeat_node_get_child_bounds (node),
                                      child_snap,
                                      &child_bounds))
    return;

  repeat = gsk_repeat_node_get_repeat (node);
  if (repeat == GSK_REPEAT_NONE)
    {
      if (!gsk_gpu_render_pass_snap_rect (self, &node->bounds, snap, &bounds) ||
          !gsk_rect_intersection (&bounds, &child_bounds, &bounds))
        return;
      gsk_gpu_node_processor_add_node_clipped (self,
                                               child,
                                               0,
                                               &bounds,
                                               GSK_RECT_SNAP_NONE);
      return;
    }

  if (!gsk_gpu_node_processor_clip_bounds (self, &node->bounds, snap, &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);

  if (repeat == GSK_REPEAT_PAD)
    {
      graphene_rect_t clipped_child_bounds;
      GskGpuImage *image;

      gsk_repeat_node_compute_rect_for_pad (&bounds,
                                            &child_bounds,
                                            &clipped_child_bounds);
      if (!gsk_gpu_render_pass_snap_rect (self, &clipped_child_bounds, GSK_RECT_SNAP_GROW, &clipped_child_bounds))
        return;
      image = gsk_gpu_node_processor_get_node_as_image (self,
                                                        GSK_GPU_AS_IMAGE_EXACT_SIZE,
                                                        &clipped_child_bounds,
                                                        child,
                                                        0,
                                                        &clipped_child_bounds);
      g_return_if_fail (image);
      gsk_gpu_texture_op (self,
                          self->ccs,
                          &bounds,
                          image,
                          GSK_GPU_SAMPLER_DEFAULT,
                          &clipped_child_bounds);
      g_object_unref (image);
    }
  else if (repeat == GSK_REPEAT_REFLECT)
    {
      graphene_rect_t clipped_child_bounds, snapped_child_bounds;
      graphene_point_t pos;
      GskGpuImage *image;

      gsk_repeat_node_compute_rect_for_reflect (&bounds,
                                                &child_bounds,
                                                &clipped_child_bounds,
                                                &pos);
      if (!gsk_gpu_render_pass_snap_rect (self,
                                          &clipped_child_bounds,
                                          GSK_RECT_SNAP_GROW,
                                          &snapped_child_bounds))
        return;

      if (gsk_rect_contains_rect (&child_bounds, &snapped_child_bounds))
        {
          pos.x += snapped_child_bounds.origin.x - clipped_child_bounds.origin.x;
          pos.y += snapped_child_bounds.origin.y - clipped_child_bounds.origin.y;
          clipped_child_bounds = snapped_child_bounds;
        }

      image = gsk_gpu_node_processor_get_node_as_image (self,
                                                        GSK_GPU_AS_IMAGE_EXACT_SIZE,
                                                        &clipped_child_bounds,
                                                        child,
                                                        0,
                                                        &clipped_child_bounds);
      g_return_if_fail (image);
      clipped_child_bounds.origin = pos;
      gsk_gpu_texture_op (self,
                          self->ccs,
                          &bounds,
                          image,
                          GSK_GPU_SAMPLER_REFLECT,
                          &clipped_child_bounds);
      g_object_unref (image);
    }
  else
    {
      /* 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),
                                                  FALSE,
                                                  ceilf (tile_top),
                                                  FALSE,
                                                  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),
                                                      FALSE,
                                                      y,
                                                      TRUE,
                                                      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,
                                                  TRUE,
                                                  ceilf (tile_top),
                                                  FALSE,
                                                  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,
                                                           0,
                                                           &clip_bounds,
                                                           GSK_RECT_SNAP_NONE);
                }
            }

          self->offset = old_offset;
        }
    }
}

static void
gsk_gpu_node_processor_add_fill_node (GskGpuRenderPass *self,
                                      GskRenderNode       *node)
{
  graphene_rect_t clip_bounds;
  GskGpuImage *mask_image;
  GskRenderNode *child;
  graphene_rect_t mask_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,
                                           self->modelview,
                                           gsk_fill_node_get_path (node),
                                           gsk_fill_node_get_fill_rule (node),
                                           &mask_rect);
  if (mask_image == NULL)
    return;

  if (GSK_RENDER_NODE_TYPE (child) == GSK_COLOR_NODE)
    {
      const GdkColor *color = gsk_color_node_get_gdk_color (child);

      gsk_gpu_colorize_op (self,
                           self->ccs,
                           gsk_gpu_color_states_find (self->ccs, color),
                           &clip_bounds,
                           mask_image,
                           GSK_GPU_SAMPLER_DEFAULT,
                           &mask_rect,
                           color);
    }
  else
    {
      GskGpuRenderPassClipStorage storage;

      gsk_gpu_render_pass_push_clip_mask (self, &clip_bounds, mask_image, &mask_rect, FALSE, &storage);
      gsk_gpu_node_processor_add_node (self, child, 0);
      gsk_gpu_render_pass_pop_clip_mask (self, &storage);
    }

  g_object_unref (mask_image);
}

static void
gsk_gpu_node_processor_add_stroke_node (GskGpuRenderPass *self,
                                        GskRenderNode       *node)
{
  graphene_rect_t clip_bounds;
  GskGpuImage *mask_image;
  GskRenderNode *child;
  graphene_rect_t mask_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,
                                             self->modelview,
                                             gsk_stroke_node_get_path (node),
                                             gsk_stroke_node_get_stroke (node),
                                             &mask_rect);
  if (mask_image == NULL)
    return;

  if (GSK_RENDER_NODE_TYPE (child) == GSK_COLOR_NODE)
    {
      const GdkColor *color = gsk_color_node_get_gdk_color (child);

      gsk_gpu_colorize_op (self,
                           self->ccs,
                           gsk_gpu_color_states_find (self->ccs, color),
                           &clip_bounds,
                           mask_image,
                           GSK_GPU_SAMPLER_DEFAULT,
                           &mask_rect,
                           color);
    }
  else
    {
      GskGpuRenderPassClipStorage storage;

      gsk_gpu_render_pass_push_clip_mask (self, &clip_bounds, mask_image, &mask_rect, FALSE, &storage);
      gsk_gpu_node_processor_add_node (self, child, 0);
      gsk_gpu_render_pass_pop_clip_mask (self, &storage);
    }

  g_object_unref (mask_image);
}

static void
gsk_gpu_node_processor_add_subsurface_node (GskGpuRenderPass *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), 0);
      return;
    }

  if (!gdk_subsurface_is_above_parent (subsurface))
    {
      cairo_rectangle_int_t device_clipped;
      graphene_rect_t clipped;

      if (!gsk_gpu_node_processor_clip_bounds (self, &node->bounds, GSK_RECT_SNAP_NONE, &clipped))
        return;

      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, &self->offset, &clipped)) &&
          gsk_gpu_render_pass_user_to_device_exact (self, &clipped, &device_clipped))
        {
          float color[4] = { 0, 0, 0, 0 };
          gsk_gpu_clear_op (self->frame, &device_clipped, color);
        }
      else
        {
          GskGpuRenderPassBlendStorage storage;
          GdkColor white;

          gsk_gpu_render_pass_push_blend (self, GSK_GPU_BLEND_CLEAR, &storage);

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

          gsk_gpu_render_pass_pop_blend (self, &storage);
        }
    }
}

static GskGpuImage *
gsk_gpu_get_subsurface_node_as_image (GskGpuFrame           *frame,
                                      GskGpuAsImageFlags     flags,
                                      GdkColorState         *ccs,
                                      const graphene_rect_t *clip_bounds,
                                      const graphene_size_t *scale,
                                      GskRenderNode         *node,
                                      graphene_rect_t       *out_bounds)
{
  GskGpuImage *result;
  GskRenderNode *child;
#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

  child = gsk_subsurface_node_get_child (node);

  gsk_gpu_frame_start_node (frame, child, 0);

  result = gsk_gpu_get_node_as_image (frame,
                                      flags,
                                      ccs,
                                      clip_bounds,
                                      scale,
                                      child,
                                      out_bounds);

  gsk_gpu_frame_end_node (frame);

  return result;
}

static void
gsk_gpu_node_processor_add_copy_node (GskGpuRenderPass *self,
                                      GskRenderNode       *node)
{
  g_warning_once ("Bug: The GPU renderer should never see copy nodes");
}

static void
gsk_gpu_node_processor_add_paste_node (GskGpuRenderPass *self,
                                       GskRenderNode       *node)
{
  g_warning_once ("Bug: The GPU renderer should never see paste nodes");
}

static gboolean
gsk_gpu_porter_duff_needs_dual_blend (GskPorterDuff op)
{
  switch (op)
  {
    case GSK_PORTER_DUFF_DEST:
    case GSK_PORTER_DUFF_SOURCE_OVER_DEST:
    case GSK_PORTER_DUFF_DEST_IN_SOURCE:
    case GSK_PORTER_DUFF_DEST_OUT_SOURCE:
    case GSK_PORTER_DUFF_CLEAR:
      return FALSE;

    case GSK_PORTER_DUFF_SOURCE:
    case GSK_PORTER_DUFF_DEST_OVER_SOURCE:
    case GSK_PORTER_DUFF_SOURCE_IN_DEST:
    case GSK_PORTER_DUFF_SOURCE_OUT_DEST:
    case GSK_PORTER_DUFF_SOURCE_ATOP_DEST:
    case GSK_PORTER_DUFF_DEST_ATOP_SOURCE:
    case GSK_PORTER_DUFF_XOR:
      return TRUE;

    default:
      g_assert_not_reached ();
      return FALSE;
  }
}

static GskGpuBlend
gsk_gpu_blend_for_porter_duff (GskPorterDuff op)
{
  switch (op)
    {
    case GSK_PORTER_DUFF_SOURCE:
      return GSK_GPU_BLEND_MASK_ONE;

    case GSK_PORTER_DUFF_DEST_OVER_SOURCE:
    case GSK_PORTER_DUFF_SOURCE_OUT_DEST:
    case GSK_PORTER_DUFF_DEST_ATOP_SOURCE:
    case GSK_PORTER_DUFF_XOR:
      return GSK_GPU_BLEND_MASK_INV_ALPHA;

    case GSK_PORTER_DUFF_SOURCE_IN_DEST:
    case GSK_PORTER_DUFF_SOURCE_ATOP_DEST:
      return GSK_GPU_BLEND_MASK_ALPHA;

    case GSK_PORTER_DUFF_CLEAR:
    case GSK_PORTER_DUFF_DEST_IN_SOURCE:
    case GSK_PORTER_DUFF_DEST_OUT_SOURCE:
      return GSK_GPU_BLEND_CLEAR;

    case GSK_PORTER_DUFF_SOURCE_OVER_DEST:
      return GSK_GPU_BLEND_OVER;

    case GSK_PORTER_DUFF_DEST:
    default:
      g_assert_not_reached ();
      return GSK_GPU_BLEND_OVER;
    }
}

static void
gsk_gpu_node_processor_add_composite_node (GskGpuRenderPass *self,
                                           GskRenderNode       *node)
{
  GskRenderNode *child;
  GskGpuImage *mask_image;
  graphene_rect_t bounds, mask_rect;
  GskPorterDuff op;
  GskGpuRenderPassBlendStorage storage;

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

  op = gsk_composite_node_get_operator (node);
  child = gsk_composite_node_get_child (node);

  /* There is a no-op operator... */
  if (op == GSK_PORTER_DUFF_DEST)
    return;
  
  gsk_gpu_render_pass_push_blend (self,
                                  gsk_gpu_blend_for_porter_duff (op),
                                  &storage);

  mask_image = gsk_gpu_node_processor_get_node_as_image (self,
                                                         0,
                                                         &bounds,
                                                         gsk_composite_node_get_mask (node),
                                                         1,
                                                         &mask_rect);
  if (mask_image == NULL)
    return;

  if (op == GSK_PORTER_DUFF_CLEAR)
    {
      gsk_gpu_texture_op (self,
                          self->ccs,
                          &mask_rect,
                          mask_image,
                          GSK_GPU_SAMPLER_DEFAULT,
                          &mask_rect);
    }
  else
    {
      GskGpuImage *child_image;
      graphene_rect_t child_rect;

      child_image = gsk_gpu_node_processor_get_node_as_image (self,
                                                              0,
                                                              &bounds,
                                                              child,
                                                              0,
                                                              &child_rect);
      if (child_image == NULL)
        {
          /* FIXME */
          child_image = g_object_ref (mask_image);
          /* put it far away so it won't get sampled */
          child_rect = mask_rect;
          child_rect.origin.x += 2 * mask_rect.size.width;
        }

      if (op == GSK_PORTER_DUFF_DEST_IN_SOURCE)
        {
          gsk_gpu_mask_op (self,
                           self->ccs,
                           &bounds,
                           mask_image,
                           GSK_GPU_SAMPLER_DEFAULT,
                           child_image,
                           GSK_GPU_SAMPLER_TRANSPARENT,
                           GSK_MASK_MODE_INVERTED_ALPHA,
                           &mask_rect,
                           &child_rect);
        }
      else if (!gsk_gpu_porter_duff_needs_dual_blend (op))
        {
          gsk_gpu_mask_op (self,
                           self->ccs,
                           &bounds,
                           child_image,
                           GSK_GPU_SAMPLER_DEFAULT,
                           mask_image,
                           GSK_GPU_SAMPLER_DEFAULT,
                           GSK_MASK_MODE_ALPHA,
                           &child_rect,
                           &mask_rect);
        }
      else if (gsk_gpu_frame_should_optimize (self->frame, GSK_GPU_OPTIMIZE_DUAL_BLEND))
        {
          gsk_gpu_composite_op (self,
                                self->ccs,
                                &bounds,
                                child_image,
                                GSK_GPU_SAMPLER_DEFAULT,
                                mask_image,
                                GSK_GPU_SAMPLER_DEFAULT,
                                op,
                                &child_rect,
                                &mask_rect);
        }
      else if (op == GSK_PORTER_DUFF_SOURCE)
        {
          /* SOURCE = CLEAR in mask
           *          + ADD source in mask */
          gsk_gpu_render_pass_pop_blend (self, &storage);
          gsk_gpu_render_pass_push_blend (self,
                                          GSK_GPU_BLEND_CLEAR,
                                          &storage);
          gsk_gpu_texture_op (self,
                              self->ccs,
                              &mask_rect,
                              mask_image,
                              GSK_GPU_SAMPLER_DEFAULT,
                              &mask_rect);
          gsk_gpu_render_pass_pop_blend (self, &storage);
          gsk_gpu_render_pass_push_blend (self,
                                          GSK_GPU_BLEND_ADD,
                                          &storage);
          gsk_gpu_mask_op (self,
                           self->ccs,
                           &bounds,
                           child_image,
                           GSK_GPU_SAMPLER_DEFAULT,
                           mask_image,
                           GSK_GPU_SAMPLER_DEFAULT,
                           GSK_MASK_MODE_ALPHA,
                           &child_rect,
                           &mask_rect);
        }
      else
        {
          g_warning_once ("FIXME: Implement compositing without dual blending support.");
        }

      g_object_unref (child_image);
    }

  g_object_unref (mask_image);

  gsk_gpu_render_pass_pop_blend (self, &storage);
}

static void
gsk_gpu_node_processor_add_isolation_node (GskGpuRenderPass *self,
                                           GskRenderNode       *node)
{
  GskRenderNode *child = gsk_isolation_node_get_child (node);
  GskIsolation isolations = gsk_isolation_node_get_isolations (node);

  if (isolations & GSK_ISOLATION_BACKGROUND)
    {
      if (gsk_render_node_get_copy_mode (child) != GSK_COPY_NONE ||
          gsk_render_node_clears_background (child))
        {
          gsk_gpu_frame_start_node (self->frame, child, 0);
          gsk_gpu_node_processor_add_with_offscreen (self, child);
          gsk_gpu_frame_end_node (self->frame);
          return;
        }
    }
  
  gsk_gpu_node_processor_add_node (self, child, 0);
}

static void
gsk_gpu_node_processor_add_container_node (GskGpuRenderPass *self,
                                           GskRenderNode       *node)
{
  GskRenderNode **children;
  gsize i, n_children;

  if (gsk_gpu_render_pass_has_opacity (self) &&
      !gsk_container_node_is_disjoint (node))
    {
      gsk_gpu_node_processor_add_with_offscreen (self, node);
      return;
    }

  children = gsk_render_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], i);
}

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

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

  child = gsk_debug_node_get_child (node);

  gsk_gpu_frame_start_node (frame, child, 0);

  result = gsk_gpu_get_node_as_image (frame,
                                      flags,
                                      ccs,
                                      clip_bounds,
                                      scale,
                                      child,
                                      out_bounds);

  gsk_gpu_frame_end_node (frame);

  return result;
}

static void
gsk_gpu_node_processor_add_turbulence_node (GskGpuRenderPass *self,
                                            GskRenderNode    *node)
{
  graphene_rect_t bounds;
  GskGpuImage *lookup_image;
  GdkTexture *lookup_texture;
  GBytes *lookup_bytes;
  const float *data;
  size_t stride;
  GdkColorState *image_cs;

  /* Note: We don't clip here to avoid the turbulence pattern shifting */
  if (!gsk_gpu_render_pass_snap_rect (self,
                                      &node->bounds,
                                      gsk_turbulence_node_get_snap (node),
                                      &bounds))
    return;

  stride = GSK_TURBULENCE_TABLE_WIDTH * 4 * sizeof (float);
  data = gsk_turbulence_node_get_lookup_table (node);

  lookup_bytes = g_bytes_new_static (data, GSK_TURBULENCE_TABLE_HEIGHT * stride);
  lookup_texture = GDK_TEXTURE (gdk_memory_texture_new (GSK_TURBULENCE_TABLE_WIDTH,
                                                        GSK_TURBULENCE_TABLE_HEIGHT,
                                                        GDK_MEMORY_R32G32B32A32_FLOAT_PREMULTIPLIED,
                                                        lookup_bytes,
                                                        stride));
  g_bytes_unref (lookup_bytes);

  lookup_image = gsk_gpu_lookup_texture (self->frame, self->ccs, lookup_texture, FALSE, &image_cs);
  g_object_unref (lookup_texture);

  if (lookup_image == NULL)
    return;

  gsk_gpu_turbulence_op (self,
                         self->ccs,
                         gsk_turbulence_node_get_color_state (node),
                         &bounds,
                         lookup_image,
                         GSK_GPU_SAMPLER_NEAREST,
                         gsk_turbulence_node_get_noise_type (node) == GSK_NOISE_FRACTAL_NOISE,
                         gsk_turbulence_node_get_stitch_tiles (node),
                         gsk_turbulence_node_get_frequency (node),
                         (float) gsk_turbulence_node_get_octaves (node),
                         bounds.origin.x,
                         bounds.origin.y,
                         bounds.size.width,
                         bounds.size.height);

  g_object_unref (lookup_image);
  gdk_color_state_unref (image_cs);
}

static const struct
{
  void                  (* process_node)                        (GskGpuRenderPass    *self,
                                                                 GskRenderNode          *node);
  GskGpuImage *         (* get_node_as_image)                   (GskGpuFrame            *self,
                                                                 GskGpuAsImageFlags      flags,
                                                                 GdkColorState          *ccs,
                                                                 const graphene_rect_t  *clip_bounds,
                                                                 const graphene_size_t  *scale,
                                                                 GskRenderNode          *node,
                                                                 graphene_rect_t        *out_bounds);
} nodes_vtable[] = {
  [GSK_NOT_A_RENDER_NODE] = {
    NULL,
    NULL,
  },
  [GSK_CONTAINER_NODE] = {
    gsk_gpu_node_processor_add_container_node,
    NULL,
  },
  [GSK_CAIRO_NODE] = {
    gsk_gpu_node_processor_add_cairo_node,
    gsk_gpu_get_cairo_node_as_image,
  },
  [GSK_COLOR_NODE] = {
    gsk_gpu_node_processor_add_color_node,
    NULL,
  },
  [GSK_LINEAR_GRADIENT_NODE] = {
    gsk_gpu_node_processor_add_linear_gradient_node,
    NULL,
  },
  [GSK_REPEATING_LINEAR_GRADIENT_NODE] = {
    gsk_gpu_node_processor_add_linear_gradient_node,
    NULL,
  },
  [GSK_RADIAL_GRADIENT_NODE] = {
    gsk_gpu_node_processor_add_radial_gradient_node,
    NULL,
  },
  [GSK_REPEATING_RADIAL_GRADIENT_NODE] = {
    gsk_gpu_node_processor_add_radial_gradient_node,
    NULL,
  },
  [GSK_CONIC_GRADIENT_NODE] = {
    gsk_gpu_node_processor_add_conic_gradient_node,
    NULL,
  },
  [GSK_BORDER_NODE] = {
    gsk_gpu_node_processor_add_border_node,
    NULL,
  },
  [GSK_TEXTURE_NODE] = {
    gsk_gpu_node_processor_add_texture_node,
    gsk_gpu_get_texture_node_as_image,
  },
  [GSK_INSET_SHADOW_NODE] = {
    gsk_gpu_node_processor_add_inset_shadow_node,
    NULL,
  },
  [GSK_OUTSET_SHADOW_NODE] = {
    gsk_gpu_node_processor_add_outset_shadow_node,
    NULL,
  },
  [GSK_TRANSFORM_NODE] = {
    gsk_gpu_node_processor_add_transform_node,
    NULL,
  },
  [GSK_OPACITY_NODE] = {
    gsk_gpu_node_processor_add_opacity_node,
    NULL,
  },
  [GSK_COLOR_MATRIX_NODE] = {
    gsk_gpu_node_processor_add_color_matrix_node,
    NULL,
  },
  [GSK_REPEAT_NODE] = {
    gsk_gpu_node_processor_add_repeat_node,
    NULL,
  },
  [GSK_CLIP_NODE] = {
    gsk_gpu_node_processor_add_clip_node,
    NULL,
  },
  [GSK_ROUNDED_CLIP_NODE] = {
    gsk_gpu_node_processor_add_rounded_clip_node,
    NULL,
  },
  [GSK_SHADOW_NODE] = {
    gsk_gpu_node_processor_add_shadow_node,
    NULL,
  },
  [GSK_BLEND_NODE] = {
    gsk_gpu_node_processor_add_blend_node,
    NULL,
  },
  [GSK_CROSS_FADE_NODE] = {
    gsk_gpu_node_processor_add_cross_fade_node,
    NULL,
  },
  [GSK_TEXT_NODE] = {
    gsk_gpu_node_processor_add_glyph_node,
    NULL,
  },
  [GSK_BLUR_NODE] = {
    gsk_gpu_node_processor_add_blur_node,
    NULL,
  },
  [GSK_DEBUG_NODE] = {
    gsk_gpu_node_processor_add_debug_node,
    gsk_gpu_get_debug_node_as_image,
  },
  [GSK_GL_SHADER_NODE] = {
    gsk_gpu_node_processor_add_gl_shader_node,
    NULL,
  },
  [GSK_TEXTURE_SCALE_NODE] = {
    gsk_gpu_node_processor_add_texture_scale_node,
    NULL,
  },
  [GSK_MASK_NODE] = {
    gsk_gpu_node_processor_add_mask_node,
    NULL,
  },
  [GSK_FILL_NODE] = {
    gsk_gpu_node_processor_add_fill_node,
    NULL,
  },
  [GSK_STROKE_NODE] = {
    gsk_gpu_node_processor_add_stroke_node,
    NULL,
  },
  [GSK_SUBSURFACE_NODE] = {
    gsk_gpu_node_processor_add_subsurface_node,
    gsk_gpu_get_subsurface_node_as_image,
  },
  [GSK_COMPONENT_TRANSFER_NODE] = {
    gsk_gpu_node_processor_add_component_transfer_node,
    NULL,
  },
  [GSK_COPY_NODE] = {
    gsk_gpu_node_processor_add_copy_node,
    NULL,
  },
  [GSK_PASTE_NODE] = {
    gsk_gpu_node_processor_add_paste_node,
    NULL,
  },
  [GSK_COMPOSITE_NODE] = {
    gsk_gpu_node_processor_add_composite_node,
    NULL,
  },
  [GSK_ISOLATION_NODE] = {
    gsk_gpu_node_processor_add_isolation_node,
    NULL,
  },
  [GSK_DISPLACEMENT_NODE] = {
    gsk_gpu_node_processor_add_displacement_node,
    NULL,
  },
  [GSK_ARITHMETIC_NODE] = {
    gsk_gpu_node_processor_add_arithmetic_node,
    NULL,
  },
  [GSK_TURBULENCE_NODE] = {
    gsk_gpu_node_processor_add_turbulence_node,
    NULL,
  },
};

static void
gsk_gpu_node_processor_add_node_untracked (GskGpuRenderPass *self,
                                           GskRenderNode       *node)
{
  GskRenderNodeType node_type;

  if (!gsk_gpu_render_pass_in_clip_fast (self, &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 (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);
    }
}

/*
 * 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
 * @pos: position in child to do tracking with or -1 for no tracking
 * @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_size_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_convert_to (GskGpuRenderPass   *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 ();
    }

  if (GDK_IS_BUILTIN_COLOR_STATE (self->ccs))
    {
      gsk_gpu_convert_builtin_op (self,
                                  image_color_state,
                                  gsk_gpu_get_acs_for_builtin (self->ccs),
                                  rect,
                                  image,
                                  GSK_GPU_SAMPLER_DEFAULT,
                                  GDK_BUILTIN_COLOR_STATE_ID (self->ccs),
                                  target_premultiplied,
                                  TRUE,
                                  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_cicp_op (self,
                               image_color_state,
                               gsk_gpu_get_acs_for_cicp (self->ccs, image_color_state),
                               rect,
                               image,
                               GSK_GPU_SAMPLER_DEFAULT,
                               target_premultiplied,
                               TRUE,
                               tex_rect,
                               cicp->color_primaries,
                               cicp->transfer_function,
                               cicp->matrix_coefficients,
                               cicp->range == GDK_CICP_RANGE_NARROW ? 0 : 1);
    }
  else
    {
      gsk_gpu_convert_op (self,
                          self->ccs,
                          target_premultiplied,
                          image_color_state,
                          rect,
                          image,
                          GSK_GPU_SAMPLER_DEFAULT,
                          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)
{
  GskGpuRenderPass *self;
  GdkColorState *ccs;
  GskGpuImage *image;
  graphene_rect_t clip_bounds, tex_rect;
  int i;

  ccs = gdk_color_state_get_rendering_color_state (target_color_state);

  if (gdk_color_state_equal (ccs, target_color_state))
    {
      gsk_gpu_occlusion_render_node (frame, target, target_color_state, pass_type, clip, viewport, node);
    }
  else
    {
      GskGpuRenderPassBlendStorage blend_storage;
      cairo_rectangle_int_t extents;

      cairo_region_get_extents (clip, &extents);

      self = gsk_gpu_render_pass_new (frame,
                                      target,
                                      target_color_state,
                                      pass_type,
                                      gdk_cairo_region_is_rectangle (clip)
                                      ? GSK_GPU_LOAD_OP_DONT_CARE
                                      : GSK_GPU_LOAD_OP_LOAD,
                                      NULL,
                                      &extents,
                                      viewport);

      gsk_gpu_render_pass_push_blend (self, GSK_GPU_BLEND_NONE, &blend_storage);

      for (i = 0; i < cairo_region_num_rectangles (clip); i++)
        {
          GskGpuRenderPassClipStorage clip_storage;
          cairo_rectangle_int_t rect;

          cairo_region_get_rectangle (clip, i, &rect);
          gsk_gpu_render_pass_push_clip_device_rect (self, &rect, &clip_storage);

          /* 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))
            {
              gsk_gpu_render_pass_pop_clip_device_rect (self, &clip_storage);
              continue;
            }

          image = gsk_gpu_get_node_as_image (self->frame,
                                             0,
                                             ccs,
                                             &clip_bounds,
                                             &self->scale,
                                             node,
                                             &tex_rect);
          if (image == NULL)
            {
              gsk_gpu_render_pass_pop_clip_device_rect (self, &clip_storage);
              continue;
            }

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

          g_object_unref (image);
          gsk_gpu_render_pass_pop_clip_device_rect (self, &clip_storage);
        }

      gsk_gpu_render_pass_pop_blend (self, &blend_storage);
      gsk_gpu_render_pass_free (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)
{
  GskGpuRenderPass *self;
  GskGpuImage *target, *intermediate = NULL;
  gsize width, height;
  gboolean target_shader_op, image_shader_op;
  GskGpuRenderPassBlendStorage storage;

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

  target = create_offscreen_image (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, g_object_ref (image), image_color_state, FALSE);
      if (intermediate == NULL)
        return NULL;
      image = intermediate;
      image_color_state = ccs;
      image_shader_op = GDK_SHADER_DEFAULT;
    }

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

  gsk_gpu_render_pass_push_blend (self, GSK_GPU_BLEND_NONE, &storage);

  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_pop_blend (self, &storage);
  gsk_gpu_render_pass_free (self);

  g_clear_object (&intermediate);

  return target;
}

void
gsk_gpu_node_processor_add_first_node_untracked (GskGpuRenderPass *self,
                                                 GskRenderNode    *node)
{
  if (gsk_render_node_needs_blending (node))
    { 
      gsk_gpu_node_processor_add_node_untracked (self, node);
    }
  else
    {
      GskGpuRenderPassBlendStorage storage;

      gsk_gpu_render_pass_push_blend (self, GSK_GPU_BLEND_NONE, &storage);

      gsk_gpu_node_processor_add_node_untracked (self, node);

      gsk_gpu_render_pass_pop_blend (self, &storage);
    }
}

GskGpuRenderPass *
gsk_render_node_default_occlusion (GskRenderNode   *self,
                                   GskGpuOcclusion *occlusion)
{
  GskGpuRenderPass *result;

  if (gsk_render_node_needs_blending (self))
    { 
      result = gsk_gpu_occlusion_begin_rendering_transparent (occlusion);
    }
  else
    {
      /* Note that checking needs_blending alone is not enough to guarantee
       * that the background can be DONT_CARE.
       * But we also know that this rect is opaque, and that together is enough.
       */
      result = gsk_gpu_occlusion_begin_rendering_whatever (occlusion);
    }

  gsk_gpu_node_processor_add_first_node_untracked (result, self);  

  return result;
}

GskGpuRenderPass *
gsk_container_node_occlusion (GskRenderNode   *node,
                              GskGpuOcclusion *occlusion)
{
  GskGpuRenderPass *result = NULL;
  GskRenderNode **children;
  gsize n_children;
  int i;

  children = gsk_render_node_get_children (node, &n_children);

  if (n_children == 0)
    return NULL;

  for (i = n_children; i-- > 0; )
    {
      result = gsk_gpu_occlusion_try_node (occlusion, children[i], i);
      if (result)
        break;
    }

  if (i < 0)
    result = gsk_gpu_occlusion_begin_rendering_transparent (occlusion);

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

  return result;
}

