Merge branch 'ngl-is-the-new-gl' into 'master'
authorMatthias Clasen <mclasen@redhat.com>
Mon, 11 Oct 2021 06:43:41 +0000 (06:43 +0000)
committerMatthias Clasen <mclasen@redhat.com>
Mon, 11 Oct 2021 06:43:41 +0000 (06:43 +0000)
Rename ngl to gl

Closes #4318

See merge request GNOME/gtk!4037

1  2 
gdk/gdkmemoryformat.c
gsk/gl/gskglcommandqueue.c
gsk/gl/gskglrenderjob.c
testsuite/gdk/memorytexture.c

Simple merge
index 0000000000000000000000000000000000000000,13256cb23572d14e949816f4a3aa027e620eb5d0..2571804726cddd7fdf0e4caa094e5597c850c38a
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,1433 +1,1449 @@@
 -  if (gdk_gl_context_get_use_es (self->context))
 -    glTexImage2D (GL_TEXTURE_2D, 0, format, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
 -  else
 -    glTexImage2D (GL_TEXTURE_2D, 0, format, width, height, 0, GL_BGRA, GL_UNSIGNED_BYTE, NULL);
+ /* gskglcommandqueue.c
+  *
+  * Copyright 2017 Timm Bäder <mail@baedert.org>
+  * Copyright 2018 Matthias Clasen <mclasen@redhat.com>
+  * Copyright 2018 Alexander Larsson <alexl@redhat.com>
+  * Copyright 2020 Christian Hergert <chergert@redhat.com>
+  *
+  * This library is free software; you can redistribute it and/or
+  * modify it under the terms of the GNU Lesser General Public
+  * License as published by the Free Software Foundation; either
+  * version 2.1 of the License, or (at your option) any later version.
+  *
+  * This library is distributed in the hope that it will be useful,
+  * but WITHOUT ANY WARRANTY; without even the implied warranty of
+  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+  * Lesser General Public License for more details.
+  *
+  * You should have received a copy of the GNU Lesser General Public
+  * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+  *
+  * SPDX-License-Identifier: LGPL-2.1-or-later
+  */
+ #include "config.h"
+ #include <string.h>
+ #include <gdk/gdkglcontextprivate.h>
+ #include <gdk/gdkmemoryformatprivate.h>
+ #include <gdk/gdkmemorytextureprivate.h>
+ #include <gdk/gdkprofilerprivate.h>
+ #include <gsk/gskdebugprivate.h>
+ #include <gsk/gskroundedrectprivate.h>
+ #include "gskglattachmentstateprivate.h"
+ #include "gskglbufferprivate.h"
+ #include "gskglcommandqueueprivate.h"
+ #include "gskgluniformstateprivate.h"
+ #include "inlinearray.h"
+ G_DEFINE_TYPE (GskGLCommandQueue, gsk_gl_command_queue, G_TYPE_OBJECT)
+ G_GNUC_UNUSED static inline void
+ print_uniform (GskGLUniformFormat format,
+                guint              array_count,
+                gconstpointer      valueptr)
+ {
+   const union {
+     graphene_matrix_t matrix[0];
+     GskRoundedRect rounded_rect[0];
+     float fval[0];
+     int ival[0];
+     guint uval[0];
+   } *data = valueptr;
+   switch (format)
+     {
+     case GSK_GL_UNIFORM_FORMAT_1F:
+       g_printerr ("1f<%f>", data->fval[0]);
+       break;
+     case GSK_GL_UNIFORM_FORMAT_2F:
+       g_printerr ("2f<%f,%f>", data->fval[0], data->fval[1]);
+       break;
+     case GSK_GL_UNIFORM_FORMAT_3F:
+       g_printerr ("3f<%f,%f,%f>", data->fval[0], data->fval[1], data->fval[2]);
+       break;
+     case GSK_GL_UNIFORM_FORMAT_4F:
+       g_printerr ("4f<%f,%f,%f,%f>", data->fval[0], data->fval[1], data->fval[2], data->fval[3]);
+       break;
+     case GSK_GL_UNIFORM_FORMAT_1I:
+     case GSK_GL_UNIFORM_FORMAT_TEXTURE:
+       g_printerr ("1i<%d>", data->ival[0]);
+       break;
+     case GSK_GL_UNIFORM_FORMAT_1UI:
+       g_printerr ("1ui<%u>", data->uval[0]);
+       break;
+     case GSK_GL_UNIFORM_FORMAT_COLOR: {
+       char *str = gdk_rgba_to_string (valueptr);
+       g_printerr ("%s", str);
+       g_free (str);
+       break;
+     }
+     case GSK_GL_UNIFORM_FORMAT_ROUNDED_RECT: {
+       char *str = gsk_rounded_rect_to_string (valueptr);
+       g_printerr ("%s", str);
+       g_free (str);
+       break;
+     }
+     case GSK_GL_UNIFORM_FORMAT_MATRIX: {
+       float mat[16];
+       graphene_matrix_to_float (&data->matrix[0], mat);
+       g_printerr ("matrix<");
+       for (guint i = 0; i < G_N_ELEMENTS (mat)-1; i++)
+         g_printerr ("%f,", mat[i]);
+       g_printerr ("%f>", mat[G_N_ELEMENTS (mat)-1]);
+       break;
+     }
+     case GSK_GL_UNIFORM_FORMAT_1FV:
+     case GSK_GL_UNIFORM_FORMAT_2FV:
+     case GSK_GL_UNIFORM_FORMAT_3FV:
+     case GSK_GL_UNIFORM_FORMAT_4FV:
+       /* non-V variants are -4 from V variants */
+       format -= 4;
+       g_printerr ("[");
+       for (guint i = 0; i < array_count; i++)
+         {
+           print_uniform (format, 0, valueptr);
+           if (i + 1 != array_count)
+             g_printerr (",");
+           valueptr = ((guint8*)valueptr + gsk_gl_uniform_format_size (format));
+         }
+       g_printerr ("]");
+       break;
+     case GSK_GL_UNIFORM_FORMAT_2I:
+       g_printerr ("2i<%d,%d>", data->ival[0], data->ival[1]);
+       break;
+     case GSK_GL_UNIFORM_FORMAT_3I:
+       g_printerr ("3i<%d,%d,%d>", data->ival[0], data->ival[1], data->ival[2]);
+       break;
+     case GSK_GL_UNIFORM_FORMAT_4I:
+       g_printerr ("3i<%d,%d,%d,%d>", data->ival[0], data->ival[1], data->ival[2], data->ival[3]);
+       break;
+     case GSK_GL_UNIFORM_FORMAT_LAST:
+     default:
+       g_assert_not_reached ();
+     }
+ }
+ G_GNUC_UNUSED static inline void
+ gsk_gl_command_queue_print_batch (GskGLCommandQueue       *self,
+                                   const GskGLCommandBatch *batch)
+ {
+   static const char *command_kinds[] = { "Clear", "Draw", };
+   guint framebuffer_id;
+   g_assert (GSK_IS_GL_COMMAND_QUEUE (self));
+   g_assert (batch != NULL);
+   if (batch->any.kind == GSK_GL_COMMAND_KIND_CLEAR)
+     framebuffer_id = batch->clear.framebuffer;
+   else if (batch->any.kind == GSK_GL_COMMAND_KIND_DRAW)
+     framebuffer_id = batch->draw.framebuffer;
+   else
+     return;
+   g_printerr ("Batch {\n");
+   g_printerr ("         Kind: %s\n", command_kinds[batch->any.kind]);
+   g_printerr ("     Viewport: %dx%d\n", batch->any.viewport.width, batch->any.viewport.height);
+   g_printerr ("  Framebuffer: %d\n", framebuffer_id);
+   if (batch->any.kind == GSK_GL_COMMAND_KIND_DRAW)
+     {
+       g_printerr ("      Program: %d\n", batch->any.program);
+       g_printerr ("     Vertices: %d\n", batch->draw.vbo_count);
+       for (guint i = 0; i < batch->draw.bind_count; i++)
+         {
+           const GskGLCommandBind *bind = &self->batch_binds.items[batch->draw.bind_offset + i];
+           g_printerr ("      Bind[%d]: %u\n", bind->texture, bind->id);
+         }
+       for (guint i = 0; i < batch->draw.uniform_count; i++)
+         {
+           const GskGLCommandUniform *uniform = &self->batch_uniforms.items[batch->draw.uniform_offset + i];
+           g_printerr ("  Uniform[%02d]: ", uniform->location);
+           print_uniform (uniform->info.format,
+                          uniform->info.array_count,
+                          gsk_gl_uniform_state_get_uniform_data (self->uniforms, uniform->info.offset));
+           g_printerr ("\n");
+         }
+     }
+   else if (batch->any.kind == GSK_GL_COMMAND_KIND_CLEAR)
+     {
+       g_printerr ("         Bits: 0x%x\n", batch->clear.bits);
+     }
+   g_printerr ("}\n");
+ }
+ G_GNUC_UNUSED static inline void
+ gsk_gl_command_queue_capture_png (GskGLCommandQueue *self,
+                                   const char        *filename,
+                                   guint              width,
+                                   guint              height,
+                                   gboolean           flip_y)
+ {
+   guint stride;
+   guint8 *data;
+   GBytes *bytes;
+   GdkTexture *texture;
+   g_assert (GSK_IS_GL_COMMAND_QUEUE (self));
+   g_assert (filename != NULL);
+   stride = cairo_format_stride_for_width (CAIRO_FORMAT_ARGB32, width);
+   data = g_malloc_n (height, stride);
+   glReadPixels (0, 0, width, height, GL_BGRA, GL_UNSIGNED_BYTE, data);
+   if (flip_y)
+     {
+       guint8 *flipped = g_malloc_n (height, stride);
+       for (guint i = 0; i < height; i++)
+         memcpy (flipped + (height * stride) - ((i + 1) * stride),
+                 data + (stride * i),
+                 stride);
+       g_free (data);
+       data = flipped;
+     }
+   bytes = g_bytes_new_take (data, height * stride);
+   texture = gdk_memory_texture_new (width, height, GDK_MEMORY_DEFAULT, bytes, stride);
+   g_bytes_unref (bytes);
+   gdk_texture_save_to_png (texture, filename);
+   g_object_unref (texture);
+ }
+ static inline gboolean
+ will_ignore_batch (GskGLCommandQueue *self)
+ {
+   if G_LIKELY (self->batches.len < G_MAXINT16)
+     return FALSE;
+   if (!self->have_truncated)
+     {
+       self->have_truncated = TRUE;
+       g_critical ("GL command queue too large, truncating further batches.");
+     }
+   return TRUE;
+ }
+ static inline guint
+ snapshot_attachments (const GskGLAttachmentState *state,
+                       GskGLCommandBinds          *array)
+ {
+   GskGLCommandBind *bind = gsk_gl_command_binds_append_n (array, G_N_ELEMENTS (state->textures));
+   guint count = 0;
+   for (guint i = 0; i < G_N_ELEMENTS (state->textures); i++)
+     {
+       if (state->textures[i].id)
+         {
+           bind[count].id = state->textures[i].id;
+           bind[count].texture = state->textures[i].texture;
+           count++;
+         }
+     }
+   if (count != G_N_ELEMENTS (state->textures))
+     array->len -= G_N_ELEMENTS (state->textures) - count;
+   return count;
+ }
+ static inline guint
+ snapshot_uniforms (GskGLUniformState    *state,
+                    GskGLUniformProgram  *program,
+                    GskGLCommandUniforms *array)
+ {
+   GskGLCommandUniform *uniform = gsk_gl_command_uniforms_append_n (array, program->n_mappings);
+   guint count = 0;
+   for (guint i = 0; i < program->n_mappings; i++)
+     {
+       const GskGLUniformMapping *mapping = &program->mappings[i];
+       if (!mapping->info.initial && mapping->location > -1)
+         {
+           uniform[count].location = mapping->location;
+           uniform[count].info = mapping->info;
+           count++;
+         }
+     }
+   if (count != program->n_mappings)
+     array->len -= program->n_mappings - count;
+   return count;
+ }
+ static inline gboolean
+ snapshots_equal (GskGLCommandQueue *self,
+                  GskGLCommandBatch *first,
+                  GskGLCommandBatch *second)
+ {
+   if (first->draw.bind_count != second->draw.bind_count ||
+       first->draw.uniform_count != second->draw.uniform_count)
+     return FALSE;
+   for (guint i = 0; i < first->draw.bind_count; i++)
+     {
+       const GskGLCommandBind *fb = &self->batch_binds.items[first->draw.bind_offset+i];
+       const GskGLCommandBind *sb = &self->batch_binds.items[second->draw.bind_offset+i];
+       if (fb->id != sb->id || fb->texture != sb->texture)
+         return FALSE;
+     }
+   for (guint i = 0; i < first->draw.uniform_count; i++)
+     {
+       const GskGLCommandUniform *fu = &self->batch_uniforms.items[first->draw.uniform_offset+i];
+       const GskGLCommandUniform *su = &self->batch_uniforms.items[second->draw.uniform_offset+i];
+       gconstpointer fdata;
+       gconstpointer sdata;
+       gsize len;
+       /* Short circuit if we'd end up with the same memory */
+       if (fu->info.offset == su->info.offset)
+         continue;
+       if (fu->info.format != su->info.format ||
+           fu->info.array_count != su->info.array_count)
+         return FALSE;
+       fdata = gsk_gl_uniform_state_get_uniform_data (self->uniforms, fu->info.offset);
+       sdata = gsk_gl_uniform_state_get_uniform_data (self->uniforms, su->info.offset);
+       switch (fu->info.format)
+         {
+         case GSK_GL_UNIFORM_FORMAT_1F:
+         case GSK_GL_UNIFORM_FORMAT_1FV:
+         case GSK_GL_UNIFORM_FORMAT_1I:
+         case GSK_GL_UNIFORM_FORMAT_TEXTURE:
+         case GSK_GL_UNIFORM_FORMAT_1UI:
+           len = 4;
+           break;
+         case GSK_GL_UNIFORM_FORMAT_2F:
+         case GSK_GL_UNIFORM_FORMAT_2FV:
+         case GSK_GL_UNIFORM_FORMAT_2I:
+           len = 8;
+           break;
+         case GSK_GL_UNIFORM_FORMAT_3F:
+         case GSK_GL_UNIFORM_FORMAT_3FV:
+         case GSK_GL_UNIFORM_FORMAT_3I:
+           len = 12;
+           break;
+         case GSK_GL_UNIFORM_FORMAT_4F:
+         case GSK_GL_UNIFORM_FORMAT_4FV:
+         case GSK_GL_UNIFORM_FORMAT_4I:
+           len = 16;
+           break;
+         case GSK_GL_UNIFORM_FORMAT_MATRIX:
+           len = sizeof (float) * 16;
+           break;
+         case GSK_GL_UNIFORM_FORMAT_ROUNDED_RECT:
+           len = sizeof (float) * 12;
+           break;
+         case GSK_GL_UNIFORM_FORMAT_COLOR:
+           len = sizeof (float) * 4;
+           break;
+         default:
+           g_assert_not_reached ();
+         }
+       len *= fu->info.array_count;
+       if (memcmp (fdata, sdata, len) != 0)
+         return FALSE;
+     }
+   return TRUE;
+ }
+ static void
+ gsk_gl_command_queue_dispose (GObject *object)
+ {
+   GskGLCommandQueue *self = (GskGLCommandQueue *)object;
+   g_assert (GSK_IS_GL_COMMAND_QUEUE (self));
+   g_clear_object (&self->profiler);
+   g_clear_object (&self->gl_profiler);
+   g_clear_object (&self->context);
+   g_clear_pointer (&self->attachments, gsk_gl_attachment_state_unref);
+   g_clear_pointer (&self->uniforms, gsk_gl_uniform_state_unref);
+   gsk_gl_command_batches_clear (&self->batches);
+   gsk_gl_command_binds_clear (&self->batch_binds);
+   gsk_gl_command_uniforms_clear (&self->batch_uniforms);
+   gsk_gl_buffer_destroy (&self->vertices);
+   G_OBJECT_CLASS (gsk_gl_command_queue_parent_class)->dispose (object);
+ }
+ static void
+ gsk_gl_command_queue_class_init (GskGLCommandQueueClass *klass)
+ {
+   GObjectClass *object_class = G_OBJECT_CLASS (klass);
+   object_class->dispose = gsk_gl_command_queue_dispose;
+ }
+ static void
+ gsk_gl_command_queue_init (GskGLCommandQueue *self)
+ {
+   self->max_texture_size = -1;
+   gsk_gl_command_batches_init (&self->batches, 128);
+   gsk_gl_command_binds_init (&self->batch_binds, 1024);
+   gsk_gl_command_uniforms_init (&self->batch_uniforms, 2048);
+   gsk_gl_buffer_init (&self->vertices, GL_ARRAY_BUFFER, sizeof (GskGLDrawVertex));
+ }
+ GskGLCommandQueue *
+ gsk_gl_command_queue_new (GdkGLContext      *context,
+                           GskGLUniformState *uniforms)
+ {
+   GskGLCommandQueue *self;
+   g_return_val_if_fail (GDK_IS_GL_CONTEXT (context), NULL);
+   self = g_object_new (GSK_TYPE_GL_COMMAND_QUEUE, NULL);
+   self->context = g_object_ref (context);
+   self->attachments = gsk_gl_attachment_state_new ();
+   /* Use shared uniform state if we're provided one */
+   if (uniforms != NULL)
+     self->uniforms = gsk_gl_uniform_state_ref (uniforms);
+   else
+     self->uniforms = gsk_gl_uniform_state_new ();
+   /* Determine max texture size immediately and restore context */
+   gdk_gl_context_make_current (context);
+   glGetIntegerv (GL_MAX_TEXTURE_SIZE, &self->max_texture_size);
+   return g_steal_pointer (&self);
+ }
+ static inline GskGLCommandBatch *
+ begin_next_batch (GskGLCommandQueue *self)
+ {
+   GskGLCommandBatch *batch;
+   g_assert (GSK_IS_GL_COMMAND_QUEUE (self));
+   /* GskGLCommandBatch contains an embedded linked list using integers into the
+    * self->batches array. We can't use pointer because the batches could be
+    * realloc()'d at runtime.
+    *
+    * Before we execute the command queue, we sort the batches by framebuffer but
+    * leave the batches in place as we can just tweak the links via prev/next.
+    *
+    * Generally we only traverse forwards, so we could ignore the previous field.
+    * But to optimize the reordering of batches by framebuffer we walk backwards
+    * so we sort by most-recently-seen framebuffer to ensure draws happen in the
+    * proper order.
+    */
+   batch = gsk_gl_command_batches_append (&self->batches);
+   batch->any.next_batch_index = -1;
+   batch->any.prev_batch_index = self->tail_batch_index;
+   return batch;
+ }
+ static void
+ enqueue_batch (GskGLCommandQueue *self)
+ {
+   guint index;
+   g_assert (GSK_IS_GL_COMMAND_QUEUE (self));
+   g_assert (self->batches.len > 0);
+   /* Batches are linked lists but using indexes into the batches array instead
+    * of pointers. This is for two main reasons. First, 16-bit indexes allow us
+    * to store the information in 4 bytes, where as two pointers would take 16
+    * bytes.  Furthermore, we have an array here so pointers would get
+    * invalidated if we realloc()'d (and that can happen from time to time).
+    */
+   index = self->batches.len - 1;
+   if (self->head_batch_index == -1)
+     self->head_batch_index = index;
+   if (self->tail_batch_index != -1)
+     {
+       GskGLCommandBatch *prev = &self->batches.items[self->tail_batch_index];
+       prev->any.next_batch_index = index;
+     }
+   self->tail_batch_index = index;
+ }
+ static void
+ discard_batch (GskGLCommandQueue *self)
+ {
+   g_assert (GSK_IS_GL_COMMAND_QUEUE (self));
+   g_assert (self->batches.len > 0);
+   self->batches.len--;
+ }
+ void
+ gsk_gl_command_queue_begin_draw (GskGLCommandQueue   *self,
+                                  GskGLUniformProgram *program,
+                                  guint                width,
+                                  guint                height)
+ {
+   GskGLCommandBatch *batch;
+   g_assert (GSK_IS_GL_COMMAND_QUEUE (self));
+   g_assert (self->in_draw == FALSE);
+   g_assert (width <= G_MAXUINT16);
+   g_assert (height <= G_MAXUINT16);
+   /* Our internal links use 16-bits, so that is our max number
+    * of batches we can have in one frame.
+    */
+   if (will_ignore_batch (self))
+     return;
+   self->program_info = program;
+   batch = begin_next_batch (self);
+   batch->any.kind = GSK_GL_COMMAND_KIND_DRAW;
+   batch->any.program = program->program_id;
+   batch->any.next_batch_index = -1;
+   batch->any.viewport.width = width;
+   batch->any.viewport.height = height;
+   batch->draw.framebuffer = 0;
+   batch->draw.uniform_count = 0;
+   batch->draw.uniform_offset = self->batch_uniforms.len;
+   batch->draw.bind_count = 0;
+   batch->draw.bind_offset = self->batch_binds.len;
+   batch->draw.vbo_count = 0;
+   batch->draw.vbo_offset = gsk_gl_buffer_get_offset (&self->vertices);
+   self->fbo_max = MAX (self->fbo_max, batch->draw.framebuffer);
+   self->in_draw = TRUE;
+ }
+ void
+ gsk_gl_command_queue_end_draw (GskGLCommandQueue *self)
+ {
+   GskGLCommandBatch *last_batch;
+   GskGLCommandBatch *batch;
+   g_assert (GSK_IS_GL_COMMAND_QUEUE (self));
+   g_assert (self->batches.len > 0);
+   if (will_ignore_batch (self))
+     return;
+   batch = gsk_gl_command_batches_tail (&self->batches);
+   g_assert (self->in_draw == TRUE);
+   g_assert (batch->any.kind == GSK_GL_COMMAND_KIND_DRAW);
+   if G_UNLIKELY (batch->draw.vbo_count == 0)
+     {
+       discard_batch (self);
+       self->in_draw = FALSE;
+       return;
+     }
+   /* Track the destination framebuffer in case it changed */
+   batch->draw.framebuffer = self->attachments->fbo.id;
+   self->attachments->fbo.changed = FALSE;
+   self->fbo_max = MAX (self->fbo_max, self->attachments->fbo.id);
+   /* Save our full uniform state for this draw so we can possibly
+    * reorder the draw later.
+    */
+   batch->draw.uniform_offset = self->batch_uniforms.len;
+   batch->draw.uniform_count = snapshot_uniforms (self->uniforms, self->program_info, &self->batch_uniforms);
+   /* Track the bind attachments that changed */
+   if (self->program_info->has_attachments)
+     {
+       batch->draw.bind_offset = self->batch_binds.len;
+       batch->draw.bind_count = snapshot_attachments (self->attachments, &self->batch_binds);
+     }
+   else
+     {
+       batch->draw.bind_offset = 0;
+       batch->draw.bind_count = 0;
+     }
+   if (self->batches.len > 1)
+     last_batch = &self->batches.items[self->batches.len - 2];
+   else
+     last_batch = NULL;
+   /* Do simple chaining of draw to last batch. */
+   if (last_batch != NULL &&
+       last_batch->any.kind == GSK_GL_COMMAND_KIND_DRAW &&
+       last_batch->any.program == batch->any.program &&
+       last_batch->any.viewport.width == batch->any.viewport.width &&
+       last_batch->any.viewport.height == batch->any.viewport.height &&
+       last_batch->draw.framebuffer == batch->draw.framebuffer &&
+       last_batch->draw.vbo_offset + last_batch->draw.vbo_count == batch->draw.vbo_offset &&
+       last_batch->draw.vbo_count + batch->draw.vbo_count <= 0xffff &&
+       snapshots_equal (self, last_batch, batch))
+     {
+       last_batch->draw.vbo_count += batch->draw.vbo_count;
+       discard_batch (self);
+     }
+   else
+     {
+       enqueue_batch (self);
+     }
+   self->in_draw = FALSE;
+   self->program_info = NULL;
+ }
+ /**
+  * gsk_gl_command_queue_split_draw:
+  * @self a `GskGLCommandQueue`
+  *
+  * This function is like calling gsk_gl_command_queue_end_draw() followed by
+  * a gsk_gl_command_queue_begin_draw() with the same parameters as a
+  * previous begin draw (if shared uniforms where not changed further).
+  *
+  * This is useful to avoid comparisons inside of loops where we know shared
+  * uniforms are not changing.
+  *
+  * This generally should just be called from gsk_gl_program_split_draw()
+  * as that is where the begin/end flow happens from the render job.
+  */
+ void
+ gsk_gl_command_queue_split_draw (GskGLCommandQueue *self)
+ {
+   GskGLCommandBatch *batch;
+   GskGLUniformProgram *program;
+   guint width;
+   guint height;
+   g_assert (GSK_IS_GL_COMMAND_QUEUE (self));
+   g_assert (self->batches.len > 0);
+   g_assert (self->in_draw == TRUE);
+   program = self->program_info;
+   batch = gsk_gl_command_batches_tail (&self->batches);
+   g_assert (batch->any.kind == GSK_GL_COMMAND_KIND_DRAW);
+   width = batch->any.viewport.width;
+   height = batch->any.viewport.height;
+   gsk_gl_command_queue_end_draw (self);
+   gsk_gl_command_queue_begin_draw (self, program, width, height);
+ }
+ void
+ gsk_gl_command_queue_clear (GskGLCommandQueue    *self,
+                              guint                  clear_bits,
+                              const graphene_rect_t *viewport)
+ {
+   GskGLCommandBatch *batch;
+   g_assert (GSK_IS_GL_COMMAND_QUEUE (self));
+   g_assert (self->in_draw == FALSE);
+   if (will_ignore_batch (self))
+     return;
+   if (clear_bits == 0)
+     clear_bits = GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT;
+   batch = begin_next_batch (self);
+   batch->any.kind = GSK_GL_COMMAND_KIND_CLEAR;
+   batch->any.viewport.width = viewport->size.width;
+   batch->any.viewport.height = viewport->size.height;
+   batch->clear.bits = clear_bits;
+   batch->clear.framebuffer = self->attachments->fbo.id;
+   batch->any.next_batch_index = -1;
+   batch->any.program = 0;
+   self->fbo_max = MAX (self->fbo_max, batch->clear.framebuffer);
+   enqueue_batch (self);
+   self->attachments->fbo.changed = FALSE;
+ }
+ GdkGLContext *
+ gsk_gl_command_queue_get_context (GskGLCommandQueue *self)
+ {
+   g_return_val_if_fail (GSK_IS_GL_COMMAND_QUEUE (self), NULL);
+   return self->context;
+ }
+ void
+ gsk_gl_command_queue_make_current (GskGLCommandQueue *self)
+ {
+   g_assert (GSK_IS_GL_COMMAND_QUEUE (self));
+   g_assert (GDK_IS_GL_CONTEXT (self->context));
+   gdk_gl_context_make_current (self->context);
+ }
+ void
+ gsk_gl_command_queue_delete_program (GskGLCommandQueue *self,
+                                      guint              program)
+ {
+   g_assert (GSK_IS_GL_COMMAND_QUEUE (self));
+   glDeleteProgram (program);
+ }
+ static inline void
+ apply_viewport (guint *current_width,
+                 guint *current_height,
+                 guint  width,
+                 guint  height)
+ {
+   if G_UNLIKELY (*current_width != width || *current_height != height)
+     {
+       *current_width = width;
+       *current_height = height;
+       glViewport (0, 0, width, height);
+     }
+ }
+ static inline void
+ apply_scissor (gboolean              *state,
+                guint                  framebuffer,
+                const graphene_rect_t *scissor,
+                gboolean               has_scissor)
+ {
+   g_assert (framebuffer != (guint)-1);
+   if (framebuffer != 0 || !has_scissor)
+     {
+       if (*state != FALSE)
+         {
+           glDisable (GL_SCISSOR_TEST);
+           *state = FALSE;
+         }
+     }
+   else
+     {
+       if (*state != TRUE)
+         {
+           glEnable (GL_SCISSOR_TEST);
+           glScissor (scissor->origin.x,
+                      scissor->origin.y,
+                      scissor->size.width,
+                      scissor->size.height);
+           *state = TRUE;
+         }
+     }
+ }
+ static inline gboolean
+ apply_framebuffer (int   *framebuffer,
+                    guint  new_framebuffer)
+ {
+   if G_UNLIKELY (new_framebuffer != *framebuffer)
+     {
+       *framebuffer = new_framebuffer;
+       glBindFramebuffer (GL_FRAMEBUFFER, new_framebuffer);
+       return TRUE;
+     }
+   return FALSE;
+ }
+ static inline void
+ gsk_gl_command_queue_unlink (GskGLCommandQueue *self,
+                              GskGLCommandBatch *batch)
+ {
+   if (batch->any.prev_batch_index == -1)
+     self->head_batch_index = batch->any.next_batch_index;
+   else
+     self->batches.items[batch->any.prev_batch_index].any.next_batch_index = batch->any.next_batch_index;
+   if (batch->any.next_batch_index == -1)
+     self->tail_batch_index = batch->any.prev_batch_index;
+   else
+     self->batches.items[batch->any.next_batch_index].any.prev_batch_index = batch->any.prev_batch_index;
+   batch->any.prev_batch_index = -1;
+   batch->any.next_batch_index = -1;
+ }
+ static inline void
+ gsk_gl_command_queue_insert_before (GskGLCommandQueue *self,
+                                     GskGLCommandBatch *batch,
+                                     GskGLCommandBatch *sibling)
+ {
+   int sibling_index;
+   int index;
+   g_assert (batch >= self->batches.items);
+   g_assert (batch < &self->batches.items[self->batches.len]);
+   g_assert (sibling >= self->batches.items);
+   g_assert (sibling < &self->batches.items[self->batches.len]);
+   index = gsk_gl_command_batches_index_of (&self->batches, batch);
+   sibling_index = gsk_gl_command_batches_index_of (&self->batches, sibling);
+   batch->any.next_batch_index = sibling_index;
+   batch->any.prev_batch_index = sibling->any.prev_batch_index;
+   if (batch->any.prev_batch_index > -1)
+     self->batches.items[batch->any.prev_batch_index].any.next_batch_index = index;
+   sibling->any.prev_batch_index = index;
+   if (batch->any.prev_batch_index == -1)
+     self->head_batch_index = index;
+ }
+ static void
+ gsk_gl_command_queue_sort_batches (GskGLCommandQueue *self)
+ {
+   int *seen;
+   int *seen_free = NULL;
+   int index;
+   g_assert (GSK_IS_GL_COMMAND_QUEUE (self));
+   g_assert (self->tail_batch_index >= 0);
+   g_assert (self->fbo_max >= 0);
+   /* Create our seen list with most recent index set to -1,
+    * meaning we haven't yet seen that framebuffer.
+    */
+   if (self->fbo_max < 1024)
+     seen = g_alloca (sizeof (int) * (self->fbo_max + 1));
+   else
+     seen = seen_free = g_new0 (int, (self->fbo_max + 1));
+   for (int i = 0; i <= self->fbo_max; i++)
+     seen[i] = -1;
+   /* Walk in reverse, and if we've seen that framebuffer before, we want to
+    * delay this operation until right before the last batch we saw for that
+    * framebuffer.
+    *
+    * We can do this because we don't use a framebuffer's texture until it has
+    * been completely drawn.
+    */
+   index = self->tail_batch_index;
+   while (index >= 0)
+     {
+       GskGLCommandBatch *batch = &self->batches.items[index];
+       int cur_index = index;
+       int fbo = -1;
+       g_assert (index > -1);
+       g_assert (index < self->batches.len);
+       switch (batch->any.kind)
+         {
+         case GSK_GL_COMMAND_KIND_DRAW:
+           fbo = batch->draw.framebuffer;
+           break;
+         case GSK_GL_COMMAND_KIND_CLEAR:
+           fbo = batch->clear.framebuffer;
+           break;
+         default:
+           g_assert_not_reached ();
+         }
+       index = batch->any.prev_batch_index;
+       g_assert (index >= -1);
+       g_assert (index < (int)self->batches.len);
+       g_assert (fbo >= -1);
+       if (fbo == -1)
+         continue;
+       g_assert (fbo <= self->fbo_max);
+       g_assert (seen[fbo] >= -1);
+       g_assert (seen[fbo] < (int)self->batches.len);
+       if (seen[fbo] != -1 && seen[fbo] != batch->any.next_batch_index)
+         {
+           int mru_index = seen[fbo];
+           GskGLCommandBatch *mru = &self->batches.items[mru_index];
+           g_assert (mru_index > -1);
+           gsk_gl_command_queue_unlink (self, batch);
+           g_assert (batch->any.prev_batch_index == -1);
+           g_assert (batch->any.next_batch_index == -1);
+           gsk_gl_command_queue_insert_before (self, batch, mru);
+           g_assert (batch->any.prev_batch_index > -1 ||
+                     self->head_batch_index == cur_index);
+           g_assert (batch->any.next_batch_index == seen[fbo]);
+         }
+       g_assert (cur_index > -1);
+       g_assert (seen[fbo] >= -1);
+       seen[fbo] = cur_index;
+     }
+   g_free (seen_free);
+ }
+ /**
+  * gsk_gl_command_queue_execute:
+  * @self: a `GskGLCommandQueue`
+  * @surface_height: the height of the backing surface
+  * @scale_factor: the scale factor of the backing surface
+  * #scissor: (nullable): the scissor clip if any
+  *
+  * Executes all of the batches in the command queue.
+  */
+ void
+ gsk_gl_command_queue_execute (GskGLCommandQueue    *self,
+                               guint                 surface_height,
+                               guint                 scale_factor,
+                               const cairo_region_t *scissor)
+ {
+   G_GNUC_UNUSED guint count = 0;
+   graphene_rect_t scissor_test;
+   gboolean has_scissor = scissor != NULL;
+   gboolean scissor_state = -1;
+   guint program = 0;
+   guint width = 0;
+   guint height = 0;
+   guint n_binds = 0;
+   guint n_fbos = 0;
+   guint n_uniforms = 0;
+   guint n_programs = 0;
+   guint vao_id;
+   guint vbo_id;
+   int textures[4];
+   int framebuffer = -1;
+   int next_batch_index;
+   int active = -1;
+   g_assert (GSK_IS_GL_COMMAND_QUEUE (self));
+   g_assert (self->in_draw == FALSE);
+   if (self->batches.len == 0)
+     return;
+   for (guint i = 0; i < G_N_ELEMENTS (textures); i++)
+     textures[i] = -1;
+   gsk_gl_command_queue_sort_batches (self);
+   gsk_gl_command_queue_make_current (self);
+ #ifdef G_ENABLE_DEBUG
+   gsk_gl_profiler_begin_gpu_region (self->gl_profiler);
+   gsk_profiler_timer_begin (self->profiler, self->metrics.cpu_time);
+ #endif
+   glEnable (GL_DEPTH_TEST);
+   glDepthFunc (GL_LEQUAL);
+   /* Pre-multiplied alpha */
+   glEnable (GL_BLEND);
+   glBlendFunc (GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
+   glBlendEquation (GL_FUNC_ADD);
+   glGenVertexArrays (1, &vao_id);
+   glBindVertexArray (vao_id);
+   vbo_id = gsk_gl_buffer_submit (&self->vertices);
+   /* 0 = position location */
+   glEnableVertexAttribArray (0);
+   glVertexAttribPointer (0, 2, GL_FLOAT, GL_FALSE,
+                          sizeof (GskGLDrawVertex),
+                          (void *) G_STRUCT_OFFSET (GskGLDrawVertex, position));
+   /* 1 = texture coord location */
+   glEnableVertexAttribArray (1);
+   glVertexAttribPointer (1, 2, GL_FLOAT, GL_FALSE,
+                          sizeof (GskGLDrawVertex),
+                          (void *) G_STRUCT_OFFSET (GskGLDrawVertex, uv));
+   /* 2 = color location */
+   glEnableVertexAttribArray (2);
+   glVertexAttribPointer (2, 4, GL_HALF_FLOAT, GL_FALSE,
+                          sizeof (GskGLDrawVertex),
+                          (void *) G_STRUCT_OFFSET (GskGLDrawVertex, color));
+   /* 3 = color2 location */
+   glEnableVertexAttribArray (3);
+   glVertexAttribPointer (3, 4, GL_HALF_FLOAT, GL_FALSE,
+                          sizeof (GskGLDrawVertex),
+                          (void *) G_STRUCT_OFFSET (GskGLDrawVertex, color2));
+   /* Setup initial scissor clip */
+   if (scissor != NULL)
+     {
+       cairo_rectangle_int_t r;
+       g_assert (cairo_region_num_rectangles (scissor) == 1);
+       cairo_region_get_rectangle (scissor, 0, &r);
+       scissor_test.origin.x = r.x * scale_factor;
+       scissor_test.origin.y = surface_height - (r.height * scale_factor) - (r.y * scale_factor);
+       scissor_test.size.width = r.width * scale_factor;
+       scissor_test.size.height = r.height * scale_factor;
+     }
+   next_batch_index = self->head_batch_index;
+   while (next_batch_index >= 0)
+     {
+       const GskGLCommandBatch *batch = &self->batches.items[next_batch_index];
+       g_assert (next_batch_index >= 0);
+       g_assert (next_batch_index < self->batches.len);
+       g_assert (batch->any.next_batch_index != next_batch_index);
+       count++;
+       switch (batch->any.kind)
+         {
+         case GSK_GL_COMMAND_KIND_CLEAR:
+           if (apply_framebuffer (&framebuffer, batch->clear.framebuffer))
+             {
+               apply_scissor (&scissor_state, framebuffer, &scissor_test, has_scissor);
+               n_fbos++;
+             }
+           apply_viewport (&width,
+                           &height,
+                           batch->any.viewport.width,
+                           batch->any.viewport.height);
+           glClearColor (0, 0, 0, 0);
+           glClear (batch->clear.bits);
+         break;
+         case GSK_GL_COMMAND_KIND_DRAW:
+           if (batch->any.program != program)
+             {
+               program = batch->any.program;
+               glUseProgram (program);
+               n_programs++;
+             }
+           if (apply_framebuffer (&framebuffer, batch->draw.framebuffer))
+             {
+               apply_scissor (&scissor_state, framebuffer, &scissor_test, has_scissor);
+               n_fbos++;
+             }
+           apply_viewport (&width,
+                           &height,
+                           batch->any.viewport.width,
+                           batch->any.viewport.height);
+           if G_UNLIKELY (batch->draw.bind_count > 0)
+             {
+               const GskGLCommandBind *bind = &self->batch_binds.items[batch->draw.bind_offset];
+               for (guint i = 0; i < batch->draw.bind_count; i++)
+                 {
+                   if (textures[bind->texture] != bind->id)
+                     {
+                       if (active != bind->texture)
+                         {
+                           active = bind->texture;
+                           glActiveTexture (GL_TEXTURE0 + bind->texture);
+                         }
+                       glBindTexture (GL_TEXTURE_2D, bind->id);
+                       textures[bind->texture] = bind->id;
+                     }
+                   bind++;
+                 }
+               n_binds += batch->draw.bind_count;
+             }
+           if (batch->draw.uniform_count > 0)
+             {
+               const GskGLCommandUniform *u = &self->batch_uniforms.items[batch->draw.uniform_offset];
+               for (guint i = 0; i < batch->draw.uniform_count; i++, u++)
+                 gsk_gl_uniform_state_apply (self->uniforms, program, u->location, u->info);
+               n_uniforms += batch->draw.uniform_count;
+             }
+           glDrawArrays (GL_TRIANGLES, batch->draw.vbo_offset, batch->draw.vbo_count);
+         break;
+         default:
+           g_assert_not_reached ();
+         }
+ #if 0
+       if (batch->any.kind == GSK_GL_COMMAND_KIND_DRAW ||
+           batch->any.kind == GSK_GL_COMMAND_KIND_CLEAR)
+         {
+           char filename[128];
+           g_snprintf (filename, sizeof filename,
+                       "capture%03u_batch%03d_kind%u_program%u_u%u_b%u_fb%u_ctx%p.png",
+                       count, next_batch_index,
+                       batch->any.kind, batch->any.program,
+                       batch->any.kind == GSK_GL_COMMAND_KIND_DRAW ? batch->draw.uniform_count : 0,
+                       batch->any.kind == GSK_GL_COMMAND_KIND_DRAW ? batch->draw.bind_count : 0,
+                       framebuffer,
+                       gdk_gl_context_get_current ());
+           gsk_gl_command_queue_capture_png (self, filename, width, height, TRUE);
+           gsk_gl_command_queue_print_batch (self, batch);
+         }
+ #endif
+       next_batch_index = batch->any.next_batch_index;
+     }
+   glDeleteBuffers (1, &vbo_id);
+   glDeleteVertexArrays (1, &vao_id);
+   gdk_profiler_set_int_counter (self->metrics.n_binds, n_binds);
+   gdk_profiler_set_int_counter (self->metrics.n_uniforms, n_uniforms);
+   gdk_profiler_set_int_counter (self->metrics.n_fbos, n_fbos);
+   gdk_profiler_set_int_counter (self->metrics.n_programs, n_programs);
+   gdk_profiler_set_int_counter (self->metrics.n_uploads, self->n_uploads);
+   gdk_profiler_set_int_counter (self->metrics.queue_depth, self->batches.len);
+ #ifdef G_ENABLE_DEBUG
+   {
+     gint64 start_time G_GNUC_UNUSED = gsk_profiler_timer_get_start (self->profiler, self->metrics.cpu_time);
+     gint64 cpu_time = gsk_profiler_timer_end (self->profiler, self->metrics.cpu_time);
+     gint64 gpu_time = gsk_gl_profiler_end_gpu_region (self->gl_profiler);
+     gsk_profiler_timer_set (self->profiler, self->metrics.gpu_time, gpu_time);
+     gsk_profiler_timer_set (self->profiler, self->metrics.cpu_time, cpu_time);
+     gsk_profiler_counter_inc (self->profiler, self->metrics.n_frames);
+     gsk_profiler_push_samples (self->profiler);
+   }
+ #endif
+ }
+ void
+ gsk_gl_command_queue_begin_frame (GskGLCommandQueue *self)
+ {
+   g_assert (GSK_IS_GL_COMMAND_QUEUE (self));
+   g_assert (self->batches.len == 0);
+   gsk_gl_command_queue_make_current (self);
+   self->fbo_max = 0;
+   self->tail_batch_index = -1;
+   self->head_batch_index = -1;
+   self->in_frame = TRUE;
+ }
+ /**
+  * gsk_gl_command_queue_end_frame:
+  * @self: a `GskGLCommandQueue`
+  *
+  * This function performs cleanup steps that need to be done after
+  * a frame has finished. This is not performed as part of the command
+  * queue execution to allow for the frame to be submitted as soon
+  * as possible.
+  *
+  * However, it should be executed after the draw contexts end_frame
+  * has been called to swap the OpenGL framebuffers.
+  */
+ void
+ gsk_gl_command_queue_end_frame (GskGLCommandQueue *self)
+ {
+   g_assert (GSK_IS_GL_COMMAND_QUEUE (self));
+   gsk_gl_command_queue_make_current (self);
+   gsk_gl_uniform_state_end_frame (self->uniforms);
+   /* Reset attachments so we don't hold on to any textures
+    * that might be released after the frame.
+    */
+   for (guint i = 0; i < G_N_ELEMENTS (self->attachments->textures); i++)
+     {
+       if (self->attachments->textures[i].id != 0)
+         {
+           glActiveTexture (GL_TEXTURE0 + i);
+           glBindTexture (GL_TEXTURE_2D, 0);
+           self->attachments->textures[i].id = 0;
+           self->attachments->textures[i].changed = FALSE;
+           self->attachments->textures[i].initial = TRUE;
+         }
+     }
+   self->batches.len = 0;
+   self->batch_binds.len = 0;
+   self->batch_uniforms.len = 0;
+   self->n_uploads = 0;
+   self->tail_batch_index = -1;
+   self->in_frame = FALSE;
+ }
+ gboolean
+ gsk_gl_command_queue_create_render_target (GskGLCommandQueue *self,
+                                            int                width,
+                                            int                height,
+                                            int                format,
+                                            int                min_filter,
+                                            int                mag_filter,
+                                            guint             *out_fbo_id,
+                                            guint             *out_texture_id)
+ {
+   GLuint fbo_id = 0;
+   GLint texture_id;
+   g_assert (GSK_IS_GL_COMMAND_QUEUE (self));
+   g_assert (width > 0);
+   g_assert (height > 0);
+   g_assert (out_fbo_id != NULL);
+   g_assert (out_texture_id != NULL);
+   texture_id = gsk_gl_command_queue_create_texture (self,
+                                                      width, height,
+                                                      format,
+                                                      min_filter, mag_filter);
+   if (texture_id == -1)
+     {
+       *out_fbo_id = 0;
+       *out_texture_id = 0;
+       return FALSE;
+     }
+   fbo_id = gsk_gl_command_queue_create_framebuffer (self);
+   glBindFramebuffer (GL_FRAMEBUFFER, fbo_id);
+   glFramebufferTexture2D (GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture_id, 0);
+   g_assert_cmphex (glCheckFramebufferStatus (GL_FRAMEBUFFER), ==, GL_FRAMEBUFFER_COMPLETE);
+   *out_fbo_id = fbo_id;
+   *out_texture_id = texture_id;
+   return TRUE;
+ }
+ int
+ gsk_gl_command_queue_create_texture (GskGLCommandQueue *self,
+                                      int                width,
+                                      int                height,
+                                      int                format,
+                                      int                min_filter,
+                                      int                mag_filter)
+ {
+   GLuint texture_id = 0;
+   g_assert (GSK_IS_GL_COMMAND_QUEUE (self));
+   if G_UNLIKELY (self->max_texture_size == -1)
+     glGetIntegerv (GL_MAX_TEXTURE_SIZE, &self->max_texture_size);
+   if (width > self->max_texture_size || height > self->max_texture_size)
+     return -1;
+   glGenTextures (1, &texture_id);
+   glActiveTexture (GL_TEXTURE0);
+   glBindTexture (GL_TEXTURE_2D, texture_id);
+   glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, min_filter);
+   glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, mag_filter);
+   glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+   glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
++  switch (format)
++  {
++    case GL_RGBA8:
++      glTexImage2D (GL_TEXTURE_2D, 0, format, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
++      break;
++    case GL_RGBA16F:
++      glTexImage2D (GL_TEXTURE_2D, 0, format, width, height, 0, GL_RGBA, GL_HALF_FLOAT, NULL);
++      break;
++    case GL_RGBA32F:
++      glTexImage2D (GL_TEXTURE_2D, 0, format, width, height, 0, GL_RGBA, GL_FLOAT, NULL);
++      break;
++    default:
++      /* If you add new formats, make sure to set the correct format and type here
++       * so that GLES doesn't barf invalid operations at you.
++       * Because it is very important that these 3 values match when data is set to
++       * NULL, do you hear me?
++       */
++      g_assert_not_reached ();
++      break;
++  }
+   /* Restore the previous texture if it was set */
+   if (self->attachments->textures[0].id != 0)
+     glBindTexture (GL_TEXTURE_2D, self->attachments->textures[0].id);
+   return (int)texture_id;
+ }
+ guint
+ gsk_gl_command_queue_create_framebuffer (GskGLCommandQueue *self)
+ {
+   GLuint fbo_id;
+   g_assert (GSK_IS_GL_COMMAND_QUEUE (self));
+   glGenFramebuffers (1, &fbo_id);
+   return fbo_id;
+ }
+ int
+ gsk_gl_command_queue_upload_texture (GskGLCommandQueue *self,
+                                      GdkTexture        *texture,
+                                      guint              x_offset,
+                                      guint              y_offset,
+                                      guint              width,
+                                      guint              height,
+                                      int                min_filter,
+                                      int                mag_filter)
+ {
+   G_GNUC_UNUSED gint64 start_time = GDK_PROFILER_CURRENT_TIME;
+   cairo_surface_t *surface = NULL;
+   GdkMemoryFormat data_format;
+   const guchar *data;
+   gsize data_stride;
+   gsize bpp;
+   int texture_id;
+   g_assert (GSK_IS_GL_COMMAND_QUEUE (self));
+   g_assert (!GDK_IS_GL_TEXTURE (texture));
+   g_assert (x_offset + width <= gdk_texture_get_width (texture));
+   g_assert (y_offset + height <= gdk_texture_get_height (texture));
+   g_assert (min_filter == GL_LINEAR || min_filter == GL_NEAREST);
+   g_assert (mag_filter == GL_LINEAR || min_filter == GL_NEAREST);
+   if (width > self->max_texture_size || height > self->max_texture_size)
+     {
+       g_warning ("Attempt to create texture of size %ux%u but max size is %d. "
+                  "Clipping will occur.",
+                  width, height, self->max_texture_size);
+       width = MAX (width, self->max_texture_size);
+       height = MAX (height, self->max_texture_size);
+     }
+   texture_id = gsk_gl_command_queue_create_texture (self, width, height, GL_RGBA8, min_filter, mag_filter);
+   if (texture_id == -1)
+     return texture_id;
+   if (GDK_IS_MEMORY_TEXTURE (texture))
+     {
+       GdkMemoryTexture *memory_texture = GDK_MEMORY_TEXTURE (texture);
+       data = gdk_memory_texture_get_data (memory_texture);
+       data_format = gdk_texture_get_format (texture);
+       data_stride = gdk_memory_texture_get_stride (memory_texture);
+     }
+   else
+     {
+       /* Fall back to downloading to a surface */
+       surface = gdk_texture_download_surface (texture);
+       cairo_surface_flush (surface);
+       data = cairo_image_surface_get_data (surface);
+       data_format = GDK_MEMORY_DEFAULT;
+       data_stride = cairo_image_surface_get_stride (surface);
+     }
+   self->n_uploads++;
+   bpp = gdk_memory_format_bytes_per_pixel (data_format);
+   /* Switch to texture0 as 2D. We'll restore it later. */
+   glActiveTexture (GL_TEXTURE0);
+   glBindTexture (GL_TEXTURE_2D, texture_id);
+   gdk_gl_context_upload_texture (gdk_gl_context_get_current (),
+                                  data + x_offset * bpp + y_offset * data_stride,
+                                  width, height, data_stride,
+                                  data_format, GL_TEXTURE_2D);
+   /* Restore previous texture state if any */
+   if (self->attachments->textures[0].id > 0)
+     glBindTexture (self->attachments->textures[0].target,
+                    self->attachments->textures[0].id);
+   g_clear_pointer (&surface, cairo_surface_destroy);
+   if (gdk_profiler_is_running ())
+     gdk_profiler_add_markf (start_time, GDK_PROFILER_CURRENT_TIME-start_time,
+                             "Upload Texture",
+                             "Size %dx%d", width, height);
+   return texture_id;
+ }
+ void
+ gsk_gl_command_queue_set_profiler (GskGLCommandQueue *self,
+                                    GskProfiler       *profiler)
+ {
+ #ifdef G_ENABLE_DEBUG
+   g_assert (GSK_IS_GL_COMMAND_QUEUE (self));
+   g_assert (GSK_IS_PROFILER (profiler));
+   if (g_set_object (&self->profiler, profiler))
+     {
+       self->gl_profiler = gsk_gl_profiler_new (self->context);
+       self->metrics.n_frames = gsk_profiler_add_counter (profiler, "frames", "Frames", FALSE);
+       self->metrics.cpu_time = gsk_profiler_add_timer (profiler, "cpu-time", "CPU Time", FALSE, TRUE);
+       self->metrics.gpu_time = gsk_profiler_add_timer (profiler, "gpu-time", "GPU Time", FALSE, TRUE);
+       self->metrics.n_binds = gdk_profiler_define_int_counter ("attachments", "Number of texture attachments");
+       self->metrics.n_fbos = gdk_profiler_define_int_counter ("fbos", "Number of framebuffers attached");
+       self->metrics.n_uniforms = gdk_profiler_define_int_counter ("uniforms", "Number of uniforms changed");
+       self->metrics.n_uploads = gdk_profiler_define_int_counter ("uploads", "Number of texture uploads");
+       self->metrics.n_programs = gdk_profiler_define_int_counter ("programs", "Number of program changes");
+       self->metrics.queue_depth = gdk_profiler_define_int_counter ("gl-queue-depth", "Depth of GL command batches");
+     }
+ #endif
+ }
index 0000000000000000000000000000000000000000,b35b1158acc2d14e473341003f10ce9fdaabc0eb..a904cf09d48021d189081391fa955600d95dca2c
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,4163 +1,4166 @@@
 -  glGetFramebufferAttachmentParameteriv (GL_FRAMEBUFFER,  GL_COLOR_ATTACHMENT0, GL_FRAMEBUFFER_ATTACHMENT_RED_SIZE, &size);
+ /* gskglrenderjob.c
+  *
+  * Copyright 2017 Timm Bäder <mail@baedert.org>
+  * Copyright 2018 Matthias Clasen <mclasen@redhat.com>
+  * Copyright 2018 Alexander Larsson <alexl@redhat.com>
+  * Copyright 2020 Christian Hergert <chergert@redhat.com>
+  *
+  * This library is free software; you can redistribute it and/or
+  * modify it under the terms of the GNU Lesser General Public
+  * License as published by the Free Software Foundation; either
+  * version 2.1 of the License, or (at your option) any later version.
+  *
+  * This library is distributed in the hope that it will be useful,
+  * but WITHOUT ANY WARRANTY; without even the implied warranty of
+  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+  * Lesser General Public License for more details.
+  *
+  * You should have received a copy of the GNU Lesser General Public
+  * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+  *
+  * SPDX-License-Identifier: LGPL-2.1-or-later
+  */
+ #include "config.h"
+ #include <gdk/gdkglcontextprivate.h>
+ #include <gdk/gdkprofilerprivate.h>
+ #include <gdk/gdkrgbaprivate.h>
+ #include <gsk/gskrendernodeprivate.h>
+ #include <gsk/gskglshaderprivate.h>
+ #include <gdk/gdktextureprivate.h>
+ #include <gsk/gsktransformprivate.h>
+ #include <gsk/gskroundedrectprivate.h>
+ #include <math.h>
+ #include <string.h>
+ #include "gskglcommandqueueprivate.h"
+ #include "gskgldriverprivate.h"
+ #include "gskglglyphlibraryprivate.h"
+ #include "gskgliconlibraryprivate.h"
+ #include "gskglprogramprivate.h"
+ #include "gskglrenderjobprivate.h"
+ #include "gskglshadowlibraryprivate.h"
+ #include "ninesliceprivate.h"
+ #include "fp16private.h"
+ #define ORTHO_NEAR_PLANE   -10000
+ #define ORTHO_FAR_PLANE     10000
+ #define MAX_GRADIENT_STOPS  6
+ #define SHADOW_EXTRA_SIZE   4
+ /* Make sure gradient stops fits in packed array_count */
+ G_STATIC_ASSERT ((MAX_GRADIENT_STOPS * 5) < (1 << GSK_GL_UNIFORM_ARRAY_BITS));
+ #define rounded_rect_top_left(r)                                                        \
+   (GRAPHENE_RECT_INIT(r->bounds.origin.x,                                               \
+                       r->bounds.origin.y,                                               \
+                       r->corner[0].width, r->corner[0].height))
+ #define rounded_rect_top_right(r) \
+   (GRAPHENE_RECT_INIT(r->bounds.origin.x + r->bounds.size.width - r->corner[1].width,   \
+                       r->bounds.origin.y, \
+                       r->corner[1].width, r->corner[1].height))
+ #define rounded_rect_bottom_right(r) \
+   (GRAPHENE_RECT_INIT(r->bounds.origin.x + r->bounds.size.width - r->corner[2].width,   \
+                       r->bounds.origin.y + r->bounds.size.height - r->corner[2].height, \
+                       r->corner[2].width, r->corner[2].height))
+ #define rounded_rect_bottom_left(r)                                                     \
+   (GRAPHENE_RECT_INIT(r->bounds.origin.x,                                               \
+                       r->bounds.origin.y + r->bounds.size.height - r->corner[2].height, \
+                       r->corner[3].width, r->corner[3].height))
+ #define rounded_rect_corner0(r)   rounded_rect_top_left(r)
+ #define rounded_rect_corner1(r)   rounded_rect_top_right(r)
+ #define rounded_rect_corner2(r)   rounded_rect_bottom_right(r)
+ #define rounded_rect_corner3(r)   rounded_rect_bottom_left(r)
+ #define rounded_rect_corner(r, i) (rounded_rect_corner##i(r))
+ #define ALPHA_IS_CLEAR(alpha) ((alpha) < ((float) 0x00ff / (float) 0xffff))
+ #define RGBA_IS_CLEAR(rgba) ALPHA_IS_CLEAR((rgba)->alpha)
+ typedef struct _GskGLRenderClip
+ {
+   GskRoundedRect rect;
+   guint          is_rectilinear : 1;
+   guint          is_fully_contained : 1;
+ } GskGLRenderClip;
+ typedef struct _GskGLRenderModelview
+ {
+   GskTransform *transform;
+   float scale_x;
+   float scale_y;
+   float dx;
+   float dy;
+   float offset_x_before;
+   float offset_y_before;
+   graphene_matrix_t matrix;
+ } GskGLRenderModelview;
+ struct _GskGLRenderJob
+ {
+   /* The context containing the framebuffer we are drawing to. Generally this
+    * is the context of the surface but may be a shared context if rendering to
+    * an offscreen texture such as gsk_gl_renderer_render_texture().
+    */
+   GdkGLContext *context;
+   /* The driver to be used. This is shared among all the renderers on a given
+    * GdkDisplay and uses the shared GL context to send commands.
+    */
+   GskGLDriver *driver;
+   /* The command queue (which is just a faster pointer to the driver's
+    * command queue.
+    */
+   GskGLCommandQueue *command_queue;
+   /* The region that we are clipping. Normalized to a single rectangle region. */
+   cairo_region_t *region;
+   /* The framebuffer to draw to in the @context GL context. So 0 would be the
+    * default framebuffer of @context. This is important to note as many other
+    * operations could be done using objects shared from the command queues
+    * GL context.
+    */
+   guint framebuffer;
+   /* The viewport we are using. This state is updated as we process render
+    * nodes in the specific visitor callbacks.
+    */
+   graphene_rect_t viewport;
+   /* The current projection, updated as we process nodes */
+   graphene_matrix_t projection;
+   /* An array of GskGLRenderModelview updated as nodes are processed. The
+    * current modelview is the last element.
+    */
+   GArray *modelview;
+   /* An array of GskGLRenderClip updated as nodes are processed. The
+    * current clip is the last element.
+    */
+   GArray *clip;
+   /* Our current alpha state as we process nodes */
+   float alpha;
+   /* Offset (delta x,y) as we process nodes. Occasionally this is merged into
+    * a transform that is referenced from child transform nodes.
+    */
+   float offset_x;
+   float offset_y;
+   /* The scale we are processing, possibly updated by transforms */
+   float scale_x;
+   float scale_y;
+   /* Cached pointers */
+   const GskGLRenderClip *current_clip;
+   const GskGLRenderModelview *current_modelview;
+   GskGLProgram *current_program;
+   /* If we should be rendering red zones over fallback nodes */
+   guint debug_fallback : 1;
+   /* Format we want to use for intermediate textures, determined by
+    * looking at the format of the framebuffer we are rendering on.
+    */
+   int target_format;
+ };
+ typedef struct _GskGLRenderOffscreen
+ {
+   /* The bounds to render */
+   const graphene_rect_t *bounds;
+   /* Return location for texture coordinates */
+   struct {
+     float x;
+     float y;
+     float x2;
+     float y2;
+   } area;
+   /* Return location for texture ID */
+   guint texture_id;
+   /* Whether to force creating a new texture, even if the
+    * input already is a texture
+    */
+   guint force_offscreen : 1;
+   guint reset_clip : 1;
+   guint do_not_cache : 1;
+   guint linear_filter : 1;
+   /* Return location for whether we created a texture */
+   guint was_offscreen : 1;
+ } GskGLRenderOffscreen;
+ static void     gsk_gl_render_job_visit_node                (GskGLRenderJob       *job,
+                                                              const GskRenderNode  *node);
+ static gboolean gsk_gl_render_job_visit_node_with_offscreen (GskGLRenderJob       *job,
+                                                              const GskRenderNode  *node,
+                                                              GskGLRenderOffscreen *offscreen);
+ static inline int
+ get_target_format (GskGLRenderJob      *job,
+                    const GskRenderNode *node)
+ {
+   if (gsk_render_node_prefers_high_depth (node))
+     return job->target_format;
+   return GL_RGBA8;
+ }
+ static inline void
+ init_full_texture_region (GskGLRenderOffscreen *offscreen)
+ {
+   offscreen->area.x = 0;
+   offscreen->area.y = 0;
+   offscreen->area.x2 = 1;
+   offscreen->area.y2 = 1;
+ }
+ static inline gboolean G_GNUC_PURE
+ node_is_invisible (const GskRenderNode *node)
+ {
+   return node->bounds.size.width == 0.0f ||
+          node->bounds.size.height == 0.0f;
+ }
+ static inline gboolean G_GNUC_PURE
+ rounded_rect_equal (const GskRoundedRect *r1,
+                     const GskRoundedRect *r2)
+ {
+   return memcmp (r1, r2, sizeof (GskRoundedRect)) == 0;
+ }
+ static inline void
+ gsk_rounded_rect_shrink_to_minimum (GskRoundedRect *self)
+ {
+   self->bounds.size.width  = MAX (self->corner[0].width + self->corner[1].width,
+                                   self->corner[3].width + self->corner[2].width);
+   self->bounds.size.height = MAX (self->corner[0].height + self->corner[3].height,
+                                   self->corner[1].height + self->corner[2].height);
+ }
+ static inline gboolean G_GNUC_PURE
+ node_supports_transform (const GskRenderNode *node)
+ {
+   /* Some nodes can't handle non-trivial transforms without being
+    * rendered to a texture (e.g. rotated clips, etc.). Some however work
+    * just fine, mostly because they already draw their child to a
+    * texture and just render the texture manipulated in some way, think
+    * opacity or color matrix.
+    */
+   switch ((int)gsk_render_node_get_node_type (node))
+     {
+       case GSK_COLOR_NODE:
+       case GSK_OPACITY_NODE:
+       case GSK_COLOR_MATRIX_NODE:
+       case GSK_TEXTURE_NODE:
+       case GSK_CROSS_FADE_NODE:
+       case GSK_LINEAR_GRADIENT_NODE:
+       case GSK_DEBUG_NODE:
+       case GSK_TEXT_NODE:
+         return TRUE;
+       case GSK_SHADOW_NODE:
+         return node_supports_transform (gsk_shadow_node_get_child (node));
+       case GSK_TRANSFORM_NODE:
+         return node_supports_transform (gsk_transform_node_get_child (node));
+       default:
+         return FALSE;
+     }
+ }
+ static inline gboolean G_GNUC_PURE
+ color_matrix_modifies_alpha (const GskRenderNode *node)
+ {
+   const graphene_matrix_t *matrix = gsk_color_matrix_node_get_color_matrix (node);
+   const graphene_vec4_t *offset = gsk_color_matrix_node_get_color_offset (node);
+   graphene_vec4_t row3;
+   if (graphene_vec4_get_w (offset) != 0.0f)
+     return TRUE;
+   graphene_matrix_get_row (matrix, 3, &row3);
+   return !graphene_vec4_equal (graphene_vec4_w_axis (), &row3);
+ }
+ static inline gboolean G_GNUC_PURE
+ rect_contains_rect (const graphene_rect_t *r1,
+                     const graphene_rect_t *r2)
+ {
+   return r2->origin.x >= r1->origin.x &&
+          (r2->origin.x + r2->size.width) <= (r1->origin.x + r1->size.width) &&
+          r2->origin.y >= r1->origin.y &&
+          (r2->origin.y + r2->size.height) <= (r1->origin.y + r1->size.height);
+ }
+ static inline gboolean
+ rounded_inner_rect_contains_rect (const GskRoundedRect  *rounded,
+                                   const graphene_rect_t *rect)
+ {
+   const graphene_rect_t *rounded_bounds = &rounded->bounds;
+   graphene_rect_t inner;
+   float offset_x;
+   float offset_y;
+   /* TODO: This is pretty conservative and we could go further,
+    *       more fine-grained checks to avoid offscreen drawing.
+    */
+   offset_x = MAX (rounded->corner[GSK_CORNER_TOP_LEFT].width,
+                   rounded->corner[GSK_CORNER_BOTTOM_LEFT].width);
+   offset_y = MAX (rounded->corner[GSK_CORNER_TOP_LEFT].height,
+                   rounded->corner[GSK_CORNER_TOP_RIGHT].height);
+   inner.origin.x = rounded_bounds->origin.x + offset_x;
+   inner.origin.y = rounded_bounds->origin.y + offset_y;
+   inner.size.width = rounded_bounds->size.width - offset_x -
+                      MAX (rounded->corner[GSK_CORNER_TOP_RIGHT].width,
+                           rounded->corner[GSK_CORNER_BOTTOM_RIGHT].width);
+   inner.size.height = rounded_bounds->size.height - offset_y -
+                       MAX (rounded->corner[GSK_CORNER_BOTTOM_LEFT].height,
+                            rounded->corner[GSK_CORNER_BOTTOM_RIGHT].height);
+   return rect_contains_rect (&inner, rect);
+ }
+ static inline gboolean G_GNUC_PURE
+ rect_intersects (const graphene_rect_t *r1,
+                  const graphene_rect_t *r2)
+ {
+   /* Assume both rects are already normalized, as they usually are */
+   if (r1->origin.x > (r2->origin.x + r2->size.width) ||
+       (r1->origin.x + r1->size.width) < r2->origin.x)
+     return FALSE;
+   else if (r1->origin.y > (r2->origin.y + r2->size.height) ||
+       (r1->origin.y + r1->size.height) < r2->origin.y)
+     return FALSE;
+   else
+     return TRUE;
+ }
+ static inline gboolean
+ rounded_rect_has_corner (const GskRoundedRect *r,
+                          guint                 i)
+ {
+   return r->corner[i].width > 0 && r->corner[i].height > 0;
+ }
+ /* Current clip is NOT rounded but new one is definitely! */
+ static inline gboolean
+ intersect_rounded_rectilinear (const graphene_rect_t *non_rounded,
+                                const GskRoundedRect  *rounded,
+                                GskRoundedRect        *result)
+ {
+   gboolean corners[4];
+   /* Intersects with top left corner? */
+   corners[0] = rounded_rect_has_corner (rounded, 0) &&
+                rect_intersects (non_rounded,
+                                 &rounded_rect_corner (rounded, 0));
+   if (corners[0] && !rect_contains_rect (non_rounded,
+                                          &rounded_rect_corner (rounded, 0)))
+     return FALSE;
+   /* top right ? */
+   corners[1] = rounded_rect_has_corner (rounded, 1) &&
+                rect_intersects (non_rounded,
+                                 &rounded_rect_corner (rounded, 1));
+   if (corners[1] && !rect_contains_rect (non_rounded,
+                                          &rounded_rect_corner (rounded, 1)))
+     return FALSE;
+   /* bottom right ? */
+   corners[2] = rounded_rect_has_corner (rounded, 2) &&
+                rect_intersects (non_rounded,
+                                 &rounded_rect_corner (rounded, 2));
+   if (corners[2] && !rect_contains_rect (non_rounded,
+                                          &rounded_rect_corner (rounded, 2)))
+     return FALSE;
+   /* bottom left ? */
+   corners[3] = rounded_rect_has_corner (rounded, 3) &&
+                rect_intersects (non_rounded,
+                                 &rounded_rect_corner (rounded, 3));
+   if (corners[3] && !rect_contains_rect (non_rounded,
+                                          &rounded_rect_corner (rounded, 3)))
+     return FALSE;
+   /* We do intersect with at least one of the corners, but in such a way that the
+    * intersection between the two clips can still be represented by a single rounded
+    * rect in a trivial way. do that.
+    */
+   graphene_rect_intersection (non_rounded, &rounded->bounds, &result->bounds);
+   for (guint i = 0; i < 4; i++)
+     {
+       if (corners[i])
+         result->corner[i] = rounded->corner[i];
+       else
+         result->corner[i].width = result->corner[i].height = 0;
+     }
+   return TRUE;
+ }
+ static inline void
+ init_projection_matrix (graphene_matrix_t     *projection,
+                         const graphene_rect_t *viewport)
+ {
+   graphene_matrix_init_ortho (projection,
+                               viewport->origin.x,
+                               viewport->origin.x + viewport->size.width,
+                               viewport->origin.y,
+                               viewport->origin.y + viewport->size.height,
+                               ORTHO_NEAR_PLANE,
+                               ORTHO_FAR_PLANE);
+   graphene_matrix_scale (projection, 1, -1, 1);
+ }
+ static inline float
+ gsk_gl_render_job_set_alpha (GskGLRenderJob *job,
+                              float           alpha)
+ {
+   if (job->alpha != alpha)
+     {
+       float ret = job->alpha;
+       job->alpha = alpha;
+       job->driver->stamps[UNIFORM_SHARED_ALPHA]++;
+       return ret;
+     }
+   return alpha;
+ }
+ static void
+ extract_matrix_metadata (GskGLRenderModelview *modelview)
+ {
+   gsk_transform_to_matrix (modelview->transform, &modelview->matrix);
+   switch (gsk_transform_get_category (modelview->transform))
+     {
+     case GSK_TRANSFORM_CATEGORY_IDENTITY:
+       modelview->scale_x = 1;
+       modelview->scale_y = 1;
+       modelview->dx = 0;
+       modelview->dy = 0;
+       break;
+     case GSK_TRANSFORM_CATEGORY_2D_TRANSLATE:
+       modelview->scale_x = 1;
+       modelview->scale_y = 1;
+       gsk_transform_to_translate (modelview->transform,
+                                   &modelview->dx, &modelview->dy);
+       break;
+     case GSK_TRANSFORM_CATEGORY_2D_AFFINE:
+       gsk_transform_to_affine (modelview->transform,
+                                &modelview->scale_x, &modelview->scale_y,
+                                &modelview->dx, &modelview->dy);
+       break;
+     case GSK_TRANSFORM_CATEGORY_2D:
+       {
+         float xx, xy, yx, yy, dx, dy;
+         gsk_transform_to_2d (modelview->transform,
+                              &xx, &xy, &yx, &yy, &dx, &dy);
+         modelview->scale_x = sqrtf (xx * xx + xy * xy);
+         modelview->scale_y = sqrtf (yx * yx + yy * yy);
+       }
+       break;
+     case GSK_TRANSFORM_CATEGORY_UNKNOWN:
+     case GSK_TRANSFORM_CATEGORY_ANY:
+     case GSK_TRANSFORM_CATEGORY_3D:
+       {
+         graphene_vec3_t col1;
+         graphene_vec3_t col2;
+         /* TODO: 90% sure this is incorrect. But we should never hit this code
+          * path anyway. */
+         graphene_vec3_init (&col1,
+                             graphene_matrix_get_value (&modelview->matrix, 0, 0),
+                             graphene_matrix_get_value (&modelview->matrix, 1, 0),
+                             graphene_matrix_get_value (&modelview->matrix, 2, 0));
+         graphene_vec3_init (&col2,
+                             graphene_matrix_get_value (&modelview->matrix, 0, 1),
+                             graphene_matrix_get_value (&modelview->matrix, 1, 1),
+                             graphene_matrix_get_value (&modelview->matrix, 2, 1));
+         modelview->scale_x = graphene_vec3_length (&col1);
+         modelview->scale_y = graphene_vec3_length (&col2);
+         modelview->dx = 0;
+         modelview->dy = 0;
+       }
+       break;
+     default:
+       break;
+     }
+ }
+ static void
+ gsk_gl_render_job_set_modelview (GskGLRenderJob *job,
+                                  GskTransform   *transform)
+ {
+   GskGLRenderModelview *modelview;
+   g_assert (job != NULL);
+   g_assert (job->modelview != NULL);
+   job->driver->stamps[UNIFORM_SHARED_MODELVIEW]++;
+   g_array_set_size (job->modelview, job->modelview->len + 1);
+   modelview = &g_array_index (job->modelview,
+                               GskGLRenderModelview,
+                               job->modelview->len - 1);
+   modelview->transform = transform;
+   modelview->offset_x_before = job->offset_x;
+   modelview->offset_y_before = job->offset_y;
+   extract_matrix_metadata (modelview);
+   job->offset_x = 0;
+   job->offset_y = 0;
+   job->scale_x = modelview->scale_x;
+   job->scale_y = modelview->scale_y;
+   job->current_modelview = modelview;
+ }
+ static void
+ gsk_gl_render_job_push_modelview (GskGLRenderJob *job,
+                                   GskTransform   *transform)
+ {
+   GskGLRenderModelview *modelview;
+   g_assert (job != NULL);
+   g_assert (job->modelview != NULL);
+   g_assert (transform != NULL);
+   job->driver->stamps[UNIFORM_SHARED_MODELVIEW]++;
+   g_array_set_size (job->modelview, job->modelview->len + 1);
+   modelview = &g_array_index (job->modelview,
+                               GskGLRenderModelview,
+                               job->modelview->len - 1);
+   if G_LIKELY (job->modelview->len > 1)
+     {
+       GskGLRenderModelview *last;
+       GskTransform *t = NULL;
+       last = &g_array_index (job->modelview,
+                              GskGLRenderModelview,
+                              job->modelview->len - 2);
+       /* Multiply given matrix with our previous modelview */
+       t = gsk_transform_translate (gsk_transform_ref (last->transform),
+                                    &(graphene_point_t) {
+                                      job->offset_x,
+                                      job->offset_y
+                                    });
+       t = gsk_transform_transform (t, transform);
+       modelview->transform = t;
+     }
+   else
+     {
+       modelview->transform = gsk_transform_ref (transform);
+     }
+   modelview->offset_x_before = job->offset_x;
+   modelview->offset_y_before = job->offset_y;
+   extract_matrix_metadata (modelview);
+   job->offset_x = 0;
+   job->offset_y = 0;
+   job->scale_x = modelview->scale_x;
+   job->scale_y = modelview->scale_y;
+   job->current_modelview = modelview;
+ }
+ static void
+ gsk_gl_render_job_pop_modelview (GskGLRenderJob *job)
+ {
+   const GskGLRenderModelview *head;
+   g_assert (job != NULL);
+   g_assert (job->modelview);
+   g_assert (job->modelview->len > 0);
+   job->driver->stamps[UNIFORM_SHARED_MODELVIEW]++;
+   head = job->current_modelview;
+   job->offset_x = head->offset_x_before;
+   job->offset_y = head->offset_y_before;
+   gsk_transform_unref (head->transform);
+   job->modelview->len--;
+   if (job->modelview->len >= 1)
+     {
+       head = &g_array_index (job->modelview, GskGLRenderModelview, job->modelview->len - 1);
+       job->scale_x = head->scale_x;
+       job->scale_y = head->scale_y;
+       job->current_modelview = head;
+     }
+   else
+     {
+       job->current_modelview = NULL;
+     }
+ }
+ static void
+ gsk_gl_render_job_push_clip (GskGLRenderJob       *job,
+                              const GskRoundedRect *rect)
+ {
+   GskGLRenderClip *clip;
+   g_assert (job != NULL);
+   g_assert (job->clip != NULL);
+   g_assert (rect != NULL);
+   job->driver->stamps[UNIFORM_SHARED_CLIP_RECT]++;
+   g_array_set_size (job->clip, job->clip->len + 1);
+   clip = &g_array_index (job->clip, GskGLRenderClip, job->clip->len - 1);
+   memcpy (&clip->rect, rect, sizeof *rect);
+   clip->is_rectilinear = gsk_rounded_rect_is_rectilinear (rect);
+   clip->is_fully_contained = FALSE;
+   job->current_clip = clip;
+ }
+ static void
+ gsk_gl_render_job_push_contained_clip (GskGLRenderJob *job)
+ {
+   GskGLRenderClip *clip;
+   GskGLRenderClip *old_clip;
+   g_assert (job != NULL);
+   g_assert (job->clip != NULL);
+   g_assert (job->clip->len > 0);
+   job->driver->stamps[UNIFORM_SHARED_CLIP_RECT]++;
+   old_clip = &g_array_index (job->clip, GskGLRenderClip, job->clip->len - 1);
+   g_array_set_size (job->clip, job->clip->len + 1);
+   clip = &g_array_index (job->clip, GskGLRenderClip, job->clip->len - 1);
+   memcpy (&clip->rect.bounds, &old_clip->rect.bounds, sizeof (graphene_rect_t));
+   memset (clip->rect.corner, 0, sizeof clip->rect.corner);
+   clip->is_rectilinear = TRUE;
+   clip->is_fully_contained = TRUE;
+   job->current_clip = clip;
+ }
+ static void
+ gsk_gl_render_job_pop_clip (GskGLRenderJob *job)
+ {
+   g_assert (job != NULL);
+   g_assert (job->clip != NULL);
+   g_assert (job->clip->len > 0);
+   job->driver->stamps[UNIFORM_SHARED_CLIP_RECT]++;
+   job->current_clip--;
+   job->clip->len--;
+ }
+ static inline void
+ gsk_gl_render_job_offset (GskGLRenderJob *job,
+                           float           offset_x,
+                           float           offset_y)
+ {
+   if (offset_x || offset_y)
+     {
+       job->offset_x += offset_x;
+       job->offset_y += offset_y;
+     }
+ }
+ static inline void
+ gsk_gl_render_job_set_projection (GskGLRenderJob          *job,
+                                   const graphene_matrix_t *projection)
+ {
+   memcpy (&job->projection, projection, sizeof job->projection);
+   job->driver->stamps[UNIFORM_SHARED_PROJECTION]++;
+ }
+ static inline void
+ gsk_gl_render_job_set_projection_from_rect (GskGLRenderJob        *job,
+                                             const graphene_rect_t *rect,
+                                             graphene_matrix_t     *prev_projection)
+ {
+   if (prev_projection)
+     memcpy (prev_projection, &job->projection, sizeof *prev_projection);
+   init_projection_matrix (&job->projection, rect);
+   job->driver->stamps[UNIFORM_SHARED_PROJECTION]++;
+ }
+ static inline void
+ gsk_gl_render_job_set_projection_for_size (GskGLRenderJob    *job,
+                                            float              width,
+                                            float              height,
+                                            graphene_matrix_t *prev_projection)
+ {
+   if (prev_projection)
+     memcpy (prev_projection, &job->projection, sizeof *prev_projection);
+   graphene_matrix_init_ortho (&job->projection, 0, width, 0, height, ORTHO_NEAR_PLANE, ORTHO_FAR_PLANE);
+   graphene_matrix_scale (&job->projection, 1, -1, 1);
+   job->driver->stamps[UNIFORM_SHARED_PROJECTION]++;
+ }
+ static inline void
+ gsk_gl_render_job_set_viewport (GskGLRenderJob        *job,
+                                 const graphene_rect_t *viewport,
+                                 graphene_rect_t       *prev_viewport)
+ {
+   if (prev_viewport)
+     memcpy (prev_viewport, &job->viewport, sizeof *prev_viewport);
+   memcpy (&job->viewport, viewport, sizeof job->viewport);
+   job->driver->stamps[UNIFORM_SHARED_VIEWPORT]++;
+ }
+ static inline void
+ gsk_gl_render_job_set_viewport_for_size (GskGLRenderJob  *job,
+                                          float            width,
+                                          float            height,
+                                          graphene_rect_t *prev_viewport)
+ {
+   if (prev_viewport)
+     memcpy (prev_viewport, &job->viewport, sizeof *prev_viewport);
+   job->viewport.origin.x = 0;
+   job->viewport.origin.y = 0;
+   job->viewport.size.width = width;
+   job->viewport.size.height = height;
+   job->driver->stamps[UNIFORM_SHARED_VIEWPORT]++;
+ }
+ static inline void
+ gsk_gl_render_job_transform_bounds (GskGLRenderJob        *job,
+                                     const graphene_rect_t *rect,
+                                     graphene_rect_t       *out_rect)
+ {
+   GskTransform *transform;
+   GskTransformCategory category;
+   g_assert (job != NULL);
+   g_assert (job->modelview->len > 0);
+   g_assert (rect != NULL);
+   g_assert (out_rect != NULL);
+   transform = job->current_modelview->transform;
+   category = gsk_transform_get_category (transform);
+   /* Our most common transform is 2d-affine, so inline it.
+    * Both identity and 2d-translate are virtually unseen here.
+    */
+   if G_LIKELY (category >= GSK_TRANSFORM_CATEGORY_2D_AFFINE)
+     {
+       float scale_x = job->current_modelview->scale_x;
+       float scale_y = job->current_modelview->scale_y;
+       float dx = job->current_modelview->dx;
+       float dy = job->current_modelview->dy;
+       /* Init directly into out rect */
+       out_rect->origin.x = ((rect->origin.x + job->offset_x) * scale_x) + dx;
+       out_rect->origin.y = ((rect->origin.y + job->offset_y) * scale_y) + dy;
+       out_rect->size.width = rect->size.width * scale_x;
+       out_rect->size.height = rect->size.height * scale_y;
+       /* Normalize in place */
+       if (out_rect->size.width < 0.f)
+         {
+           float size = fabsf (out_rect->size.width);
+           out_rect->origin.x -= size;
+           out_rect->size.width = size;
+         }
+       if (out_rect->size.height < 0.f)
+         {
+           float size = fabsf (out_rect->size.height);
+           out_rect->origin.y -= size;
+           out_rect->size.height = size;
+         }
+     }
+   else
+     {
+       graphene_rect_t r;
+       r.origin.x = rect->origin.x + job->offset_x;
+       r.origin.y = rect->origin.y + job->offset_y;
+       r.size.width = rect->size.width;
+       r.size.height = rect->size.height;
+       gsk_transform_transform_bounds (transform, &r, out_rect);
+     }
+ }
+ static inline void
+ gsk_gl_render_job_transform_rounded_rect (GskGLRenderJob       *job,
+                                           const GskRoundedRect *rect,
+                                           GskRoundedRect       *out_rect)
+ {
+   out_rect->bounds.origin.x = job->offset_x + rect->bounds.origin.x;
+   out_rect->bounds.origin.y = job->offset_y + rect->bounds.origin.y;
+   out_rect->bounds.size.width = rect->bounds.size.width;
+   out_rect->bounds.size.height = rect->bounds.size.height;
+   memcpy (out_rect->corner, rect->corner, sizeof rect->corner);
+ }
+ static inline void
+ rounded_rect_get_inner (const GskRoundedRect *rect,
+                         graphene_rect_t      *inner)
+ {
+   float left = MAX (rect->corner[GSK_CORNER_TOP_LEFT].width, rect->corner[GSK_CORNER_BOTTOM_LEFT].width);
+   float right = MAX (rect->corner[GSK_CORNER_TOP_RIGHT].width, rect->corner[GSK_CORNER_BOTTOM_RIGHT].width);
+   float top = MAX (rect->corner[GSK_CORNER_TOP_LEFT].height, rect->corner[GSK_CORNER_TOP_RIGHT].height);
+   float bottom = MAX (rect->corner[GSK_CORNER_BOTTOM_LEFT].height, rect->corner[GSK_CORNER_BOTTOM_RIGHT].height);
+   inner->origin.x = rect->bounds.origin.x + left;
+   inner->size.width = rect->bounds.size.width - (left + right);
+   inner->origin.y = rect->bounds.origin.y + top;
+   inner->size.height = rect->bounds.size.height - (top + bottom);
+ }
+ static inline gboolean
+ interval_contains (float p1, float w1,
+                    float p2, float w2)
+ {
+   if (p2 < p1)
+     return FALSE;
+   if (p2 + w2 > p1 + w1)
+     return FALSE;
+   return TRUE;
+ }
+ static inline gboolean
+ gsk_gl_render_job_update_clip (GskGLRenderJob        *job,
+                                const graphene_rect_t *bounds,
+                                gboolean              *pushed_clip)
+ {
+   graphene_rect_t transformed_bounds;
+   gboolean no_clip = FALSE;
+   gboolean rect_clip = FALSE;
+   *pushed_clip = FALSE;
+   if (job->current_clip->is_fully_contained)
+     {
+       /* Already fully contained - no further checks needed */
+       return TRUE;
+     }
+   gsk_gl_render_job_transform_bounds (job, bounds, &transformed_bounds);
+   if (!rect_intersects (&job->current_clip->rect.bounds, &transformed_bounds))
+     {
+       /* Completely clipped away */
+       return FALSE;
+     }
+   if (job->current_clip->is_rectilinear)
+     {
+       if (rect_contains_rect (&job->current_clip->rect.bounds, &transformed_bounds))
+         no_clip = TRUE;
+       else
+         rect_clip = TRUE;
+     }
+   else if (gsk_rounded_rect_contains_rect (&job->current_clip->rect, &transformed_bounds))
+     {
+       no_clip = TRUE;
+     }
+   else
+     {
+       graphene_rect_t inner;
+       rounded_rect_get_inner (&job->current_clip->rect, &inner);
+       if (interval_contains (inner.origin.x, inner.size.width,
+                              transformed_bounds.origin.x, transformed_bounds.size.width) ||
+           interval_contains (inner.origin.y, inner.size.height,
+                              transformed_bounds.origin.y, transformed_bounds.size.height))
+         rect_clip = TRUE;
+     }
+   if (no_clip)
+     {
+       /* This node is completely contained inside the clip.
+        * Record this fact on the clip stack, so we don't do more work
+        * for child nodes.
+        */
+       gsk_gl_render_job_push_contained_clip (job);
+       *pushed_clip = TRUE;
+     }
+   else if (rect_clip && !job->current_clip->is_rectilinear)
+     {
+       graphene_rect_t rect;
+       /* The clip gets simpler for this node */
+       graphene_rect_intersection (&job->current_clip->rect.bounds, &transformed_bounds, &rect);
+       gsk_gl_render_job_push_clip (job, &GSK_ROUNDED_RECT_INIT_FROM_RECT (rect));
+       *pushed_clip = TRUE;
+     }
+   return TRUE;
+ }
+ static inline void
+ rgba_to_half (const GdkRGBA *rgba,
+               guint16        h[4])
+ {
+   float_to_half4 ((const float *)rgba, h);
+ }
+ /* fill_vertex_data */
+ static void
+ gsk_gl_render_job_draw_coords (GskGLRenderJob *job,
+                                float           min_x,
+                                float           min_y,
+                                float           max_x,
+                                float           max_y,
+                                float           min_u,
+                                float           min_v,
+                                float           max_u,
+                                float           max_v,
+                                guint16         c[4])
+ {
+   GskGLDrawVertex *vertices = gsk_gl_command_queue_add_vertices (job->command_queue);
+   vertices[0] = (GskGLDrawVertex) { .position = { min_x, min_y }, .uv = { min_u, min_v }, .color = { c[0], c[1], c[2], c[3] } };
+   vertices[1] = (GskGLDrawVertex) { .position = { min_x, max_y }, .uv = { min_u, max_v }, .color = { c[0], c[1], c[2], c[3] } };
+   vertices[2] = (GskGLDrawVertex) { .position = { max_x, min_y }, .uv = { max_u, min_v }, .color = { c[0], c[1], c[2], c[3] } };
+   vertices[3] = (GskGLDrawVertex) { .position = { max_x, max_y }, .uv = { max_u, max_v }, .color = { c[0], c[1], c[2], c[3] } };
+   vertices[4] = (GskGLDrawVertex) { .position = { min_x, max_y }, .uv = { min_u, max_v }, .color = { c[0], c[1], c[2], c[3] } };
+   vertices[5] = (GskGLDrawVertex) { .position = { max_x, min_y }, .uv = { max_u, min_v }, .color = { c[0], c[1], c[2], c[3] } };
+ }
+ /* load_vertex_data_with_region */
+ static inline void
+ gsk_gl_render_job_draw_offscreen_with_color (GskGLRenderJob             *job,
+                                              const graphene_rect_t      *bounds,
+                                              const GskGLRenderOffscreen *offscreen,
+                                              guint16                     color[4])
+ {
+   float min_x = job->offset_x + bounds->origin.x;
+   float min_y = job->offset_y + bounds->origin.y;
+   float max_x = min_x + bounds->size.width;
+   float max_y = min_y + bounds->size.height;
+   float y1 = offscreen->was_offscreen ? offscreen->area.y2 : offscreen->area.y;
+   float y2 = offscreen->was_offscreen ? offscreen->area.y : offscreen->area.y2;
+   gsk_gl_render_job_draw_coords (job,
+                                  min_x, min_y, max_x, max_y,
+                                  offscreen->area.x, y1, offscreen->area.x2, y2,
+                                  color);
+ }
+ static inline void
+ gsk_gl_render_job_draw_offscreen (GskGLRenderJob             *job,
+                                   const graphene_rect_t      *bounds,
+                                   const GskGLRenderOffscreen *offscreen)
+ {
+   gsk_gl_render_job_draw_offscreen_with_color (job, bounds, offscreen,
+                                                (guint16[]) { FP16_ZERO, FP16_ZERO, FP16_ZERO, FP16_ZERO });
+ }
+ /* load_float_vertex_data */
+ static inline void
+ gsk_gl_render_job_draw_with_color (GskGLRenderJob *job,
+                                    float           x,
+                                    float           y,
+                                    float           width,
+                                    float           height,
+                                    guint16         color[4])
+ {
+   float min_x = job->offset_x + x;
+   float min_y = job->offset_y + y;
+   float max_x = min_x + width;
+   float max_y = min_y + height;
+   gsk_gl_render_job_draw_coords (job, min_x, min_y, max_x, max_y, 0, 0, 1, 1, color);
+ }
+ static inline void
+ gsk_gl_render_job_draw (GskGLRenderJob *job,
+                         float           x,
+                         float           y,
+                         float           width,
+                         float           height)
+ {
+   gsk_gl_render_job_draw_with_color (job, x, y, width, height,
+                                      (guint16[]) { FP_ZERO, FP_ZERO, FP_ZERO, FP_ZERO });
+ }
+ /* load_vertex_data */
+ static inline void
+ gsk_gl_render_job_draw_rect_with_color (GskGLRenderJob        *job,
+                                         const graphene_rect_t *bounds,
+                                         guint16                color[4])
+ {
+   gsk_gl_render_job_draw_with_color (job,
+                                      bounds->origin.x,
+                                      bounds->origin.y,
+                                      bounds->size.width,
+                                      bounds->size.height,
+                                      color);
+ }
+ static inline void
+ gsk_gl_render_job_draw_rect (GskGLRenderJob        *job,
+                              const graphene_rect_t *bounds)
+ {
+   gsk_gl_render_job_draw (job,
+                           bounds->origin.x,
+                           bounds->origin.y,
+                           bounds->size.width,
+                           bounds->size.height);
+ }
+ /* load_offscreen_vertex_data */
+ static inline void
+ gsk_gl_render_job_draw_offscreen_rect (GskGLRenderJob        *job,
+                                        const graphene_rect_t *bounds)
+ {
+   float min_x = job->offset_x + bounds->origin.x;
+   float min_y = job->offset_y + bounds->origin.y;
+   float max_x = min_x + bounds->size.width;
+   float max_y = min_y + bounds->size.height;
+   guint16 color[4] = { FP16_ZERO, FP16_ZERO, FP16_ZERO, FP16_ZERO };
+   gsk_gl_render_job_draw_coords (job,
+                                  min_x, min_y, max_x, max_y,
+                                  0, 1, 1, 0,
+                                  color);
+ }
+ static inline void
+ gsk_gl_render_job_begin_draw (GskGLRenderJob *job,
+                               GskGLProgram   *program)
+ {
+   job->current_program = program;
+   gsk_gl_command_queue_begin_draw (job->command_queue,
+                                    program->program_info,
+                                    job->viewport.size.width,
+                                    job->viewport.size.height);
+   gsk_gl_uniform_state_set4fv (program->uniforms,
+                                program->program_info,
+                                UNIFORM_SHARED_VIEWPORT,
+                                job->driver->stamps[UNIFORM_SHARED_VIEWPORT],
+                                1,
+                                (const float *)&job->viewport);
+   gsk_gl_uniform_state_set_matrix (program->uniforms,
+                                    program->program_info,
+                                    UNIFORM_SHARED_MODELVIEW,
+                                    job->driver->stamps[UNIFORM_SHARED_MODELVIEW],
+                                    &job->current_modelview->matrix);
+   gsk_gl_uniform_state_set_matrix (program->uniforms,
+                                    program->program_info,
+                                    UNIFORM_SHARED_PROJECTION,
+                                    job->driver->stamps[UNIFORM_SHARED_PROJECTION],
+                                    &job->projection);
+   gsk_gl_uniform_state_set_rounded_rect (program->uniforms,
+                                          program->program_info,
+                                          UNIFORM_SHARED_CLIP_RECT,
+                                          job->driver->stamps[UNIFORM_SHARED_CLIP_RECT],
+                                          &job->current_clip->rect);
+   gsk_gl_uniform_state_set1f (program->uniforms,
+                               program->program_info,
+                               UNIFORM_SHARED_ALPHA,
+                               job->driver->stamps[UNIFORM_SHARED_ALPHA],
+                               job->alpha);
+ }
+ #define CHOOSE_PROGRAM(job,name) \
+   (job->current_clip->is_fully_contained \
+       ? job->driver->name ## _no_clip \
+       : (job->current_clip->is_rectilinear \
+         ? job->driver->name ## _rect_clip \
+         : job->driver->name))
+ static inline void
+ gsk_gl_render_job_split_draw (GskGLRenderJob *job)
+ {
+   gsk_gl_command_queue_split_draw (job->command_queue);
+ }
+ static inline void
+ gsk_gl_render_job_end_draw (GskGLRenderJob *job)
+ {
+   gsk_gl_command_queue_end_draw (job->command_queue);
+   job->current_program = NULL;
+ }
+ static inline void
+ gsk_gl_render_job_visit_as_fallback (GskGLRenderJob      *job,
+                                      const GskRenderNode *node)
+ {
+   float scale_x = job->scale_x;
+   float scale_y = job->scale_y;
+   int surface_width = ceilf (node->bounds.size.width * scale_x);
+   int surface_height = ceilf (node->bounds.size.height * scale_y);
+   GdkTexture *texture;
+   cairo_surface_t *surface;
+   cairo_surface_t *rendered_surface;
+   cairo_t *cr;
+   int cached_id;
+   int texture_id;
+   GskTextureKey key;
+   if (surface_width <= 0 || surface_height <= 0)
+     return;
+   key.pointer = node;
+   key.pointer_is_child = FALSE;
+   key.scale_x = scale_x;
+   key.scale_y = scale_y;
+   key.filter = GL_NEAREST;
+   cached_id = gsk_gl_driver_lookup_texture (job->driver, &key);
+   if (cached_id != 0)
+     {
+       gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, blit));
+       gsk_gl_program_set_uniform_texture (job->current_program,
+                                           UNIFORM_SHARED_SOURCE, 0,
+                                           GL_TEXTURE_2D, GL_TEXTURE0, cached_id);
+       gsk_gl_render_job_draw_offscreen_rect (job, &node->bounds);
+       gsk_gl_render_job_end_draw (job);
+       return;
+     }
+   /* We first draw the recording surface on an image surface,
+    * just because the scaleY(-1) later otherwise screws up the
+    * rendering... */
+   {
+     rendered_surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32,
+                                                    surface_width,
+                                                    surface_height);
+     cairo_surface_set_device_scale (rendered_surface, scale_x, scale_y);
+     cr = cairo_create (rendered_surface);
+     cairo_save (cr);
+     cairo_translate (cr, - floorf (node->bounds.origin.x), - floorf (node->bounds.origin.y));
+     /* Render nodes don't modify state, so casting away the const is fine here */
+     gsk_render_node_draw ((GskRenderNode *)node, cr);
+     cairo_restore (cr);
+     cairo_destroy (cr);
+   }
+   surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32,
+                                         surface_width,
+                                         surface_height);
+   cairo_surface_set_device_scale (surface, scale_x, scale_y);
+   cr = cairo_create (surface);
+   /* We draw upside down here, so it matches what GL does. */
+   cairo_save (cr);
+   cairo_scale (cr, 1, -1);
+   cairo_translate (cr, 0, - surface_height / scale_y);
+   cairo_set_source_surface (cr, rendered_surface, 0, 0);
+   cairo_rectangle (cr, 0, 0, surface_width / scale_x, surface_height / scale_y);
+   cairo_fill (cr);
+   cairo_restore (cr);
+ #ifdef G_ENABLE_DEBUG
+   if (job->debug_fallback)
+     {
+       cairo_move_to (cr, 0, 0);
+       cairo_rectangle (cr, 0, 0, node->bounds.size.width, node->bounds.size.height);
+       if (gsk_render_node_get_node_type (node) == GSK_CAIRO_NODE)
+         cairo_set_source_rgba (cr, 0.3, 0, 1, 0.25);
+       else
+         cairo_set_source_rgba (cr, 1, 0, 0, 0.25);
+       cairo_fill_preserve (cr);
+       if (gsk_render_node_get_node_type (node) == GSK_CAIRO_NODE)
+         cairo_set_source_rgba (cr, 0.3, 0, 1, 1);
+       else
+         cairo_set_source_rgba (cr, 1, 0, 0, 1);
+       cairo_stroke (cr);
+     }
+ #endif
+   cairo_destroy (cr);
+   /* Create texture to upload */
+   texture = gdk_texture_new_for_surface (surface);
+   texture_id = gsk_gl_driver_load_texture (job->driver, texture,
+                                            GL_NEAREST, GL_NEAREST);
+   if (gdk_gl_context_has_debug (job->command_queue->context))
+     gdk_gl_context_label_object_printf (job->command_queue->context, GL_TEXTURE, texture_id,
+                                         "Fallback %s %d",
+                                         g_type_name_from_instance ((GTypeInstance *) node),
+                                         texture_id);
+   g_object_unref (texture);
+   cairo_surface_destroy (surface);
+   cairo_surface_destroy (rendered_surface);
+   gsk_gl_driver_cache_texture (job->driver, &key, texture_id);
+   gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, blit));
+   gsk_gl_program_set_uniform_texture (job->current_program,
+                                       UNIFORM_SHARED_SOURCE, 0,
+                                       GL_TEXTURE_2D,
+                                       GL_TEXTURE0,
+                                       texture_id);
+   gsk_gl_render_job_draw_offscreen_rect (job, &node->bounds);
+   gsk_gl_render_job_end_draw (job);
+ }
+ static guint
+ blur_offscreen (GskGLRenderJob       *job,
+                 GskGLRenderOffscreen *offscreen,
+                 int                   texture_to_blur_width,
+                 int                   texture_to_blur_height,
+                 float                 blur_radius_x,
+                 float                 blur_radius_y)
+ {
+   const GskRoundedRect new_clip = GSK_ROUNDED_RECT_INIT (0, 0, texture_to_blur_width, texture_to_blur_height);
+   GskGLRenderTarget *pass1;
+   GskGLRenderTarget *pass2;
+   graphene_matrix_t prev_projection;
+   graphene_rect_t prev_viewport;
+   guint prev_fbo;
+   g_assert (blur_radius_x > 0);
+   g_assert (blur_radius_y > 0);
+   g_assert (offscreen->texture_id > 0);
+   g_assert (offscreen->area.x2 > offscreen->area.x);
+   g_assert (offscreen->area.y2 > offscreen->area.y);
+   if (!gsk_gl_driver_create_render_target (job->driver,
+                                            MAX (texture_to_blur_width, 1),
+                                            MAX (texture_to_blur_height, 1),
+                                            job->target_format,
+                                            GL_NEAREST, GL_NEAREST,
+                                            &pass1))
+     return 0;
+   if (texture_to_blur_width <= 0 || texture_to_blur_height <= 0)
+     return gsk_gl_driver_release_render_target (job->driver, pass1, FALSE);
+   if (!gsk_gl_driver_create_render_target (job->driver,
+                                            texture_to_blur_width,
+                                            texture_to_blur_height,
+                                            job->target_format,
+                                            GL_NEAREST, GL_NEAREST,
+                                            &pass2))
+     return gsk_gl_driver_release_render_target (job->driver, pass1, FALSE);
+   gsk_gl_render_job_set_viewport (job, &new_clip.bounds, &prev_viewport);
+   gsk_gl_render_job_set_projection_from_rect (job, &new_clip.bounds, &prev_projection);
+   gsk_gl_render_job_set_modelview (job, NULL);
+   gsk_gl_render_job_push_clip (job, &new_clip);
+   /* Bind new framebuffer and clear it */
+   prev_fbo = gsk_gl_command_queue_bind_framebuffer (job->command_queue, pass1->framebuffer_id);
+   gsk_gl_command_queue_clear (job->command_queue, 0, &job->viewport);
+   /* Begin drawing the first horizontal pass, using offscreen as the
+    * source texture for the program.
+    */
+   gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, blur));
+   gsk_gl_program_set_uniform_texture (job->current_program,
+                                       UNIFORM_SHARED_SOURCE, 0,
+                                       GL_TEXTURE_2D,
+                                       GL_TEXTURE0,
+                                       offscreen->texture_id);
+   gsk_gl_program_set_uniform1f (job->current_program,
+                                 UNIFORM_BLUR_RADIUS, 0,
+                                 blur_radius_x);
+   gsk_gl_program_set_uniform2f (job->current_program,
+                                 UNIFORM_BLUR_SIZE, 0,
+                                 texture_to_blur_width,
+                                 texture_to_blur_height);
+   gsk_gl_program_set_uniform2f (job->current_program,
+                                 UNIFORM_BLUR_DIR, 0,
+                                 1, 0);
+   gsk_gl_render_job_draw_coords (job,
+                                  0, 0, texture_to_blur_width, texture_to_blur_height,
+                                  0, 1, 1, 0,
+                                  (guint16[]) { FP16_ZERO, FP16_ZERO, FP16_ZERO, FP16_ZERO });
+   gsk_gl_render_job_end_draw (job);
+   /* Bind second pass framebuffer and clear it */
+   gsk_gl_command_queue_bind_framebuffer (job->command_queue, pass2->framebuffer_id);
+   gsk_gl_command_queue_clear (job->command_queue, 0, &job->viewport);
+   /* Draw using blur program with first pass as source texture */
+   gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, blur));
+   gsk_gl_program_set_uniform_texture (job->current_program,
+                                       UNIFORM_SHARED_SOURCE, 0,
+                                       GL_TEXTURE_2D,
+                                       GL_TEXTURE0,
+                                       pass1->texture_id);
+   gsk_gl_program_set_uniform1f (job->current_program,
+                                 UNIFORM_BLUR_RADIUS, 0,
+                                 blur_radius_y);
+   gsk_gl_program_set_uniform2f (job->current_program,
+                                 UNIFORM_BLUR_SIZE, 0,
+                                 texture_to_blur_width,
+                                 texture_to_blur_height);
+   gsk_gl_program_set_uniform2f (job->current_program,
+                                 UNIFORM_BLUR_DIR, 0,
+                                 0, 1);
+   gsk_gl_render_job_draw_coords (job,
+                                  0, 0, texture_to_blur_width, texture_to_blur_height,
+                                  0, 1, 1, 0,
+                                  (guint16[]) { FP16_ZERO, FP16_ZERO, FP16_ZERO, FP16_ZERO });
+   gsk_gl_render_job_end_draw (job);
+   gsk_gl_render_job_pop_modelview (job);
+   gsk_gl_render_job_pop_clip (job);
+   gsk_gl_render_job_set_viewport (job, &prev_viewport, NULL);
+   gsk_gl_render_job_set_projection (job, &prev_projection);
+   gsk_gl_command_queue_bind_framebuffer (job->command_queue, prev_fbo);
+   gsk_gl_driver_release_render_target (job->driver, pass1, TRUE);
+   return gsk_gl_driver_release_render_target (job->driver, pass2, FALSE);
+ }
+ static void
+ blur_node (GskGLRenderJob       *job,
+            GskGLRenderOffscreen *offscreen,
+            const GskRenderNode  *node,
+            float                 blur_radius,
+            float                *min_x,
+            float                *max_x,
+            float                *min_y,
+            float                *max_y)
+ {
+   const float blur_extra = blur_radius * 2.0; /* 2.0 = shader radius_multiplier */
+   const float half_blur_extra = (blur_extra / 2.0);
+   float scale_x = job->scale_x;
+   float scale_y = job->scale_y;
+   float texture_width;
+   float texture_height;
+   g_assert (blur_radius > 0);
+   /* Increase texture size for the given blur radius and scale it */
+   texture_width  = ceilf ((node->bounds.size.width  + blur_extra));
+   texture_height = ceilf ((node->bounds.size.height + blur_extra));
+   /* Only blur this if the out region has no texture id yet */
+   if (offscreen->texture_id == 0)
+     {
+       const graphene_rect_t bounds = GRAPHENE_RECT_INIT (node->bounds.origin.x - half_blur_extra,
+                                                          node->bounds.origin.y - half_blur_extra,
+                                                          texture_width, texture_height);
+       offscreen->bounds = &bounds;
+       offscreen->reset_clip = TRUE;
+       offscreen->force_offscreen = TRUE;
+       if (!gsk_gl_render_job_visit_node_with_offscreen (job, node, offscreen))
+         g_assert_not_reached ();
+       /* Ensure that we actually got a real texture_id */
+       g_assert (offscreen->texture_id != 0);
+       offscreen->texture_id = blur_offscreen (job,
+                                               offscreen,
+                                               texture_width * scale_x,
+                                               texture_height * scale_y,
+                                               blur_radius * scale_x,
+                                               blur_radius * scale_y);
+       init_full_texture_region (offscreen);
+     }
+   *min_x = job->offset_x + node->bounds.origin.x - half_blur_extra;
+   *max_x = job->offset_x + node->bounds.origin.x + node->bounds.size.width + half_blur_extra;
+   *min_y = job->offset_y + node->bounds.origin.y - half_blur_extra;
+   *max_y = job->offset_y + node->bounds.origin.y + node->bounds.size.height + half_blur_extra;
+ }
+ #define ATLAS_SIZE 512
+ static inline void
+ gsk_gl_render_job_visit_color_node (GskGLRenderJob      *job,
+                                     const GskRenderNode *node)
+ {
+   const GdkRGBA *rgba;
+   guint16 color[4];
+   GskGLProgram *program;
+   GskGLCommandBatch *batch;
+   rgba = gsk_color_node_get_color (node);
+   if (RGBA_IS_CLEAR (rgba))
+     return;
+   rgba_to_half (rgba, color);
+   /* Avoid switching away from the coloring program for
+    * rendering a solid color.
+    */
+   program = CHOOSE_PROGRAM (job, coloring);
+   batch = gsk_gl_command_queue_get_batch (job->command_queue);
+   /* Limit the size, or we end up with a coordinate overflow somwhere. */
+   if (node->bounds.size.width < 300 &&
+       node->bounds.size.height < 300 &&
+       batch->any.kind == GSK_GL_COMMAND_KIND_DRAW &&
+       batch->any.program == program->id)
+     {
+       GskGLRenderOffscreen offscreen = {0};
+       gsk_gl_render_job_begin_draw (job, program);
+       /* The top left few pixels in our atlases are always
+        * solid white, so we can use it here, without
+        * having to choose any particular atlas texture.
+        */
+       offscreen.was_offscreen = FALSE;
+       offscreen.area.x = 1.f / ATLAS_SIZE;
+       offscreen.area.y = 1.f / ATLAS_SIZE;
+       offscreen.area.x2 = 2.f / ATLAS_SIZE;
+       offscreen.area.y2 = 2.f / ATLAS_SIZE;
+       gsk_gl_render_job_draw_offscreen_with_color (job,
+                                                    &node->bounds,
+                                                    &offscreen,
+                                                    color);
+       gsk_gl_render_job_end_draw (job);
+     }
+   else
+     {
+       gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, color));
+       gsk_gl_render_job_draw_rect_with_color (job, &node->bounds, color);
+       gsk_gl_render_job_end_draw (job);
+     }
+ }
+ static inline void
+ gsk_gl_render_job_visit_linear_gradient_node (GskGLRenderJob      *job,
+                                               const GskRenderNode *node)
+ {
+   const GskColorStop *stops = gsk_linear_gradient_node_get_color_stops (node, NULL);
+   const graphene_point_t *start = gsk_linear_gradient_node_get_start (node);
+   const graphene_point_t *end = gsk_linear_gradient_node_get_end (node);
+   int n_color_stops = gsk_linear_gradient_node_get_n_color_stops (node);
+   gboolean repeat = gsk_render_node_get_node_type (node) == GSK_REPEATING_LINEAR_GRADIENT_NODE;
+   float x1 = job->offset_x + start->x;
+   float x2 = job->offset_x + end->x;
+   float y1 = job->offset_y + start->y;
+   float y2 = job->offset_y + end->y;
+   g_assert (n_color_stops < MAX_GRADIENT_STOPS);
+   gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, linear_gradient));
+   gsk_gl_program_set_uniform1i (job->current_program,
+                                 UNIFORM_LINEAR_GRADIENT_NUM_COLOR_STOPS, 0,
+                                 n_color_stops);
+   gsk_gl_program_set_uniform1fv (job->current_program,
+                                  UNIFORM_LINEAR_GRADIENT_COLOR_STOPS, 0,
+                                  n_color_stops * 5,
+                                  (const float *)stops);
+   gsk_gl_program_set_uniform4f (job->current_program,
+                                 UNIFORM_LINEAR_GRADIENT_POINTS, 0,
+                                 x1, y1, x2 - x1, y2 - y1);
+   gsk_gl_program_set_uniform1i (job->current_program,
+                                 UNIFORM_LINEAR_GRADIENT_REPEAT, 0,
+                                 repeat);
+   gsk_gl_render_job_draw_rect (job, &node->bounds);
+   gsk_gl_render_job_end_draw (job);
+ }
+ static inline void
+ gsk_gl_render_job_visit_conic_gradient_node (GskGLRenderJob      *job,
+                                              const GskRenderNode *node)
+ {
+   static const float scale = 0.5f * M_1_PI;
+   const GskColorStop *stops = gsk_conic_gradient_node_get_color_stops (node, NULL);
+   const graphene_point_t *center = gsk_conic_gradient_node_get_center (node);
+   int n_color_stops = gsk_conic_gradient_node_get_n_color_stops (node);
+   float angle = gsk_conic_gradient_node_get_angle (node);
+   float bias = angle * scale + 2.0f;
+   g_assert (n_color_stops < MAX_GRADIENT_STOPS);
+   gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, conic_gradient));
+   gsk_gl_program_set_uniform1i (job->current_program,
+                                 UNIFORM_CONIC_GRADIENT_NUM_COLOR_STOPS, 0,
+                                 n_color_stops);
+   gsk_gl_program_set_uniform1fv (job->current_program,
+                                  UNIFORM_CONIC_GRADIENT_COLOR_STOPS, 0,
+                                  n_color_stops * 5,
+                                  (const float *)stops);
+   gsk_gl_program_set_uniform4f (job->current_program,
+                                 UNIFORM_CONIC_GRADIENT_GEOMETRY, 0,
+                                 job->offset_x + center->x,
+                                 job->offset_y + center->y,
+                                 scale,
+                                 bias);
+   gsk_gl_render_job_draw_rect (job, &node->bounds);
+   gsk_gl_render_job_end_draw (job);
+ }
+ static inline void
+ gsk_gl_render_job_visit_radial_gradient_node (GskGLRenderJob      *job,
+                                               const GskRenderNode *node)
+ {
+   int n_color_stops = gsk_radial_gradient_node_get_n_color_stops (node);
+   const GskColorStop *stops = gsk_radial_gradient_node_get_color_stops (node, NULL);
+   const graphene_point_t *center = gsk_radial_gradient_node_get_center (node);
+   float start = gsk_radial_gradient_node_get_start (node);
+   float end = gsk_radial_gradient_node_get_end (node);
+   float hradius = gsk_radial_gradient_node_get_hradius (node);
+   float vradius = gsk_radial_gradient_node_get_vradius (node);
+   gboolean repeat = gsk_render_node_get_node_type (node) == GSK_REPEATING_RADIAL_GRADIENT_NODE;
+   float scale = 1.0f / (end - start);
+   float bias = -start * scale;
+   g_assert (n_color_stops < MAX_GRADIENT_STOPS);
+   gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, radial_gradient));
+   gsk_gl_program_set_uniform1i (job->current_program,
+                                 UNIFORM_RADIAL_GRADIENT_NUM_COLOR_STOPS, 0,
+                                 n_color_stops);
+   gsk_gl_program_set_uniform1fv (job->current_program,
+                                  UNIFORM_RADIAL_GRADIENT_COLOR_STOPS, 0,
+                                  n_color_stops * 5,
+                                  (const float *)stops);
+   gsk_gl_program_set_uniform1i (job->current_program,
+                                 UNIFORM_RADIAL_GRADIENT_REPEAT, 0,
+                                 repeat);
+   gsk_gl_program_set_uniform2f (job->current_program,
+                                 UNIFORM_RADIAL_GRADIENT_RANGE, 0,
+                                 scale, bias);
+   gsk_gl_program_set_uniform4f (job->current_program,
+                                 UNIFORM_RADIAL_GRADIENT_GEOMETRY, 0,
+                                 job->offset_x + center->x,
+                                 job->offset_y + center->y,
+                                 1.0f / (hradius * job->scale_x),
+                                 1.0f / (vradius * job->scale_y));
+   gsk_gl_render_job_draw_rect (job, &node->bounds);
+   gsk_gl_render_job_end_draw (job);
+ }
+ static inline void
+ gsk_gl_render_job_visit_clipped_child (GskGLRenderJob        *job,
+                                        const GskRenderNode   *child,
+                                        const graphene_rect_t *clip)
+ {
+   graphene_rect_t transformed_clip;
+   GskRoundedRect intersection;
+   gsk_gl_render_job_transform_bounds (job, clip, &transformed_clip);
+   if (job->current_clip->is_rectilinear)
+     {
+       memset (&intersection.corner, 0, sizeof intersection.corner);
+       graphene_rect_intersection (&transformed_clip,
+                                   &job->current_clip->rect.bounds,
+                                   &intersection.bounds);
+       gsk_gl_render_job_push_clip (job, &intersection);
+       gsk_gl_render_job_visit_node (job, child);
+       gsk_gl_render_job_pop_clip (job);
+     }
+   else if (intersect_rounded_rectilinear (&transformed_clip,
+                                           &job->current_clip->rect,
+                                           &intersection))
+     {
+       gsk_gl_render_job_push_clip (job, &intersection);
+       gsk_gl_render_job_visit_node (job, child);
+       gsk_gl_render_job_pop_clip (job);
+     }
+   else
+     {
+       GskGLRenderOffscreen offscreen = {0};
+       offscreen.bounds = clip;
+       offscreen.force_offscreen = TRUE;
+       offscreen.reset_clip = TRUE;
+       offscreen.do_not_cache = TRUE;
+       gsk_gl_render_job_visit_node_with_offscreen (job, child, &offscreen);
+       g_assert (offscreen.texture_id);
+       gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, blit));
+       gsk_gl_program_set_uniform_texture (job->current_program,
+                                           UNIFORM_SHARED_SOURCE, 0,
+                                           GL_TEXTURE_2D,
+                                           GL_TEXTURE0,
+                                           offscreen.texture_id);
+       gsk_gl_render_job_draw_offscreen_rect (job, clip);
+       gsk_gl_render_job_end_draw (job);
+     }
+ }
+ static inline void
+ gsk_gl_render_job_visit_clip_node (GskGLRenderJob      *job,
+                                    const GskRenderNode *node)
+ {
+   const graphene_rect_t *clip = gsk_clip_node_get_clip (node);
+   const GskRenderNode *child = gsk_clip_node_get_child (node);
+   gsk_gl_render_job_visit_clipped_child (job, child, clip);
+ }
+ static inline void
+ gsk_gl_render_job_visit_rounded_clip_node (GskGLRenderJob      *job,
+                                            const GskRenderNode *node)
+ {
+   const GskRenderNode *child = gsk_rounded_clip_node_get_child (node);
+   const GskRoundedRect *clip = gsk_rounded_clip_node_get_clip (node);
+   GskRoundedRect transformed_clip;
+   float scale_x = job->scale_x;
+   float scale_y = job->scale_y;
+   gboolean need_offscreen;
+   if (node_is_invisible (child))
+     return;
+   gsk_gl_render_job_transform_bounds (job, &clip->bounds, &transformed_clip.bounds);
+   for (guint i = 0; i < G_N_ELEMENTS (transformed_clip.corner); i++)
+     {
+       transformed_clip.corner[i].width = clip->corner[i].width * scale_x;
+       transformed_clip.corner[i].height = clip->corner[i].height * scale_y;
+     }
+   if (job->current_clip->is_rectilinear)
+     {
+       GskRoundedRect intersected_clip;
+       if (intersect_rounded_rectilinear (&job->current_clip->rect.bounds,
+                                          &transformed_clip,
+                                          &intersected_clip))
+         {
+           gsk_gl_render_job_push_clip (job, &intersected_clip);
+           gsk_gl_render_job_visit_node (job, child);
+           gsk_gl_render_job_pop_clip (job);
+           return;
+         }
+     }
+   /* After this point we are really working with a new and a current clip
+    * which both have rounded corners.
+    */
+   if (job->clip->len <= 1)
+     need_offscreen = FALSE;
+   else if (rounded_inner_rect_contains_rect (&job->current_clip->rect, &transformed_clip.bounds))
+     need_offscreen = FALSE;
+   else
+     need_offscreen = TRUE;
+   if (!need_offscreen)
+     {
+       /* If the new clip entirely contains the current clip, the intersection is simply
+        * the current clip, so we can ignore the new one.
+        */
+       if (rounded_inner_rect_contains_rect (&transformed_clip, &job->current_clip->rect.bounds))
+         {
+           gsk_gl_render_job_visit_node (job, child);
+           return;
+         }
+       gsk_gl_render_job_push_clip (job, &transformed_clip);
+       gsk_gl_render_job_visit_node (job, child);
+       gsk_gl_render_job_pop_clip (job);
+     }
+   else
+     {
+       GskGLRenderOffscreen offscreen = {0};
+       offscreen.bounds = &node->bounds;
+       offscreen.force_offscreen = TRUE;
+       offscreen.reset_clip = FALSE;
+       gsk_gl_render_job_push_clip (job, &transformed_clip);
+       if (!gsk_gl_render_job_visit_node_with_offscreen (job, child, &offscreen))
+         g_assert_not_reached ();
+       gsk_gl_render_job_pop_clip (job);
+       g_assert (offscreen.texture_id);
+       gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, blit));
+       gsk_gl_program_set_uniform_texture (job->current_program,
+                                           UNIFORM_SHARED_SOURCE, 0,
+                                           GL_TEXTURE_2D,
+                                           GL_TEXTURE0,
+                                           offscreen.texture_id);
+       gsk_gl_render_job_draw_offscreen (job, &node->bounds, &offscreen);
+       gsk_gl_render_job_end_draw (job);
+     }
+ }
+ static inline void
+ gsk_gl_render_job_visit_rect_border_node (GskGLRenderJob      *job,
+                                           const GskRenderNode *node)
+ {
+   const GdkRGBA *colors = gsk_border_node_get_colors (node);
+   const float *widths = gsk_border_node_get_widths (node);
+   const graphene_point_t *origin = &node->bounds.origin;
+   const graphene_size_t *size = &node->bounds.size;
+   guint16 color[4];
+   gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, color));
+   if (widths[0] > 0)
+     {
+       rgba_to_half (&colors[0], color);
+       gsk_gl_render_job_draw_rect_with_color (job,
+                                               &GRAPHENE_RECT_INIT (origin->x, origin->y, size->width - widths[1], widths[0]),
+                                               color);
+     }
+   if (widths[1] > 0)
+     {
+       rgba_to_half (&colors[1], color);
+       gsk_gl_render_job_draw_rect_with_color (job,
+                                               &GRAPHENE_RECT_INIT (origin->x + size->width - widths[1], origin->y, widths[1], size->height - widths[2]),
+                                               color);
+     }
+   if (widths[2] > 0)
+     {
+       rgba_to_half (&colors[2], color);
+       gsk_gl_render_job_draw_rect_with_color (job,
+                                               &GRAPHENE_RECT_INIT (origin->x + widths[3], origin->y + size->height - widths[2], size->width - widths[3], widths[2]),
+                                               color);
+     }
+   if (widths[3] > 0)
+     {
+       rgba_to_half (&colors[3], color);
+       gsk_gl_render_job_draw_rect_with_color (job,
+                                               &GRAPHENE_RECT_INIT (origin->x, origin->y + widths[0], widths[3], size->height - widths[0]),
+                                               color);
+     }
+   gsk_gl_render_job_end_draw (job);
+ }
+ static inline void
+ gsk_gl_render_job_visit_border_node (GskGLRenderJob      *job,
+                                      const GskRenderNode *node)
+ {
+   const GskRoundedRect *rounded_outline = gsk_border_node_get_outline (node);
+   const GdkRGBA *colors = gsk_border_node_get_colors (node);
+   const float *widths = gsk_border_node_get_widths (node);
+   struct {
+     float w;
+     float h;
+   } sizes[4];
+   float min_x = job->offset_x + node->bounds.origin.x;
+   float min_y = job->offset_y + node->bounds.origin.y;
+   float max_x = min_x + node->bounds.size.width;
+   float max_y = min_y + node->bounds.size.height;
+   GskRoundedRect outline;
+   guint16 color[4];
+   memset (sizes, 0, sizeof sizes);
+   if (widths[0] > 0)
+     {
+       sizes[0].h = MAX (widths[0], rounded_outline->corner[0].height);
+       sizes[1].h = MAX (widths[0], rounded_outline->corner[1].height);
+     }
+   if (widths[1] > 0)
+     {
+       sizes[1].w = MAX (widths[1], rounded_outline->corner[1].width);
+       sizes[2].w = MAX (widths[1], rounded_outline->corner[2].width);
+     }
+   if (widths[2] > 0)
+     {
+       sizes[2].h = MAX (widths[2], rounded_outline->corner[2].height);
+       sizes[3].h = MAX (widths[2], rounded_outline->corner[3].height);
+     }
+   if (widths[3] > 0)
+     {
+       sizes[0].w = MAX (widths[3], rounded_outline->corner[0].width);
+       sizes[3].w = MAX (widths[3], rounded_outline->corner[3].width);
+     }
+   gsk_gl_render_job_transform_rounded_rect (job, rounded_outline, &outline);
+   gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, border));
+   gsk_gl_program_set_uniform4fv (job->current_program,
+                                  UNIFORM_BORDER_WIDTHS, 0,
+                                  1,
+                                  widths);
+   gsk_gl_program_set_uniform_rounded_rect (job->current_program,
+                                            UNIFORM_BORDER_OUTLINE_RECT, 0,
+                                            &outline);
+   if (widths[0] > 0)
+     {
+       GskGLDrawVertex *vertices = gsk_gl_command_queue_add_vertices (job->command_queue);
+       rgba_to_half (&colors[0], color);
+       vertices[0] = (GskGLDrawVertex) { .position = { min_x,              min_y              }, .uv = { 0, 1 }, .color = { color[0], color[1], color[2], color[3] } };
+       vertices[1] = (GskGLDrawVertex) { .position = { min_x + sizes[0].w, min_y + sizes[0].h }, .uv = { 0, 0 }, .color = { color[0], color[1], color[2], color[3] } };
+       vertices[2] = (GskGLDrawVertex) { .position = { max_x,              min_y              }, .uv = { 1, 1 }, .color = { color[0], color[1], color[2], color[3] } };
+       vertices[3] = (GskGLDrawVertex) { .position = { max_x - sizes[1].w, min_y + sizes[1].h }, .uv = { 1, 0 }, .color = { color[0], color[1], color[2], color[3] } };
+       vertices[4] = (GskGLDrawVertex) { .position = { min_x + sizes[0].w, min_y + sizes[0].h }, .uv = { 0, 0 }, .color = { color[0], color[1], color[2], color[3] } };
+       vertices[5] = (GskGLDrawVertex) { .position = { max_x,              min_y              }, .uv = { 1, 1 }, .color = { color[0], color[1], color[2], color[3] } };
+     }
+   if (widths[1] > 0)
+     {
+       GskGLDrawVertex *vertices = gsk_gl_command_queue_add_vertices (job->command_queue);
+       rgba_to_half (&colors[1], color);
+       vertices[0] = (GskGLDrawVertex) { .position = { max_x - sizes[1].w, min_y + sizes[1].h }, .uv = { 0, 1 }, .color = { color[0], color[1], color[2], color[3] } };
+       vertices[1] = (GskGLDrawVertex) { .position = { max_x - sizes[2].w, max_y - sizes[2].h }, .uv = { 0, 0 }, .color = { color[0], color[1], color[2], color[3] } };
+       vertices[2] = (GskGLDrawVertex) { .position = { max_x,              min_y              }, .uv = { 1, 1 }, .color = { color[0], color[1], color[2], color[3] } };
+       vertices[3] = (GskGLDrawVertex) { .position = { max_x,              max_y              }, .uv = { 1, 0 }, .color = { color[0], color[1], color[2], color[3] } };
+       vertices[4] = (GskGLDrawVertex) { .position = { max_x - sizes[2].w, max_y - sizes[2].h }, .uv = { 0, 0 }, .color = { color[0], color[1], color[2], color[3] } };
+       vertices[5] = (GskGLDrawVertex) { .position = { max_x,              min_y              }, .uv = { 1, 1 }, .color = { color[0], color[1], color[2], color[3] } };
+     }
+   if (widths[2] > 0)
+     {
+       GskGLDrawVertex *vertices = gsk_gl_command_queue_add_vertices (job->command_queue);
+       rgba_to_half (&colors[2], color);
+       vertices[0] = (GskGLDrawVertex) { .position = { min_x + sizes[3].w, max_y - sizes[3].h }, .uv = { 0, 1 }, .color = { color[0], color[1], color[2], color[3] } };
+       vertices[1] = (GskGLDrawVertex) { .position = { min_x,              max_y              }, .uv = { 0, 0 }, .color = { color[0], color[1], color[2], color[3] } };
+       vertices[2] = (GskGLDrawVertex) { .position = { max_x - sizes[2].w, max_y - sizes[2].h }, .uv = { 1, 1 }, .color = { color[0], color[1], color[2], color[3] } };
+       vertices[3] = (GskGLDrawVertex) { .position = { max_x,              max_y              }, .uv = { 1, 0 }, .color = { color[0], color[1], color[2], color[3] } };
+       vertices[4] = (GskGLDrawVertex) { .position = { min_x            ,  max_y              }, .uv = { 0, 0 }, .color = { color[0], color[1], color[2], color[3] } };
+       vertices[5] = (GskGLDrawVertex) { .position = { max_x - sizes[2].w, max_y - sizes[2].h }, .uv = { 1, 1 }, .color = { color[0], color[1], color[2], color[3] } };
+     }
+   if (widths[3] > 0)
+     {
+       GskGLDrawVertex *vertices = gsk_gl_command_queue_add_vertices (job->command_queue);
+       rgba_to_half (&colors[3], color);
+       vertices[0] = (GskGLDrawVertex) { .position = { min_x,              min_y              }, .uv = { 0, 1 }, .color = { color[0], color[1], color[2], color[3] } };
+       vertices[1] = (GskGLDrawVertex) { .position = { min_x,              max_y              }, .uv = { 0, 0 }, .color = { color[0], color[1], color[2], color[3] } };
+       vertices[2] = (GskGLDrawVertex) { .position = { min_x + sizes[0].w, min_y + sizes[0].h }, .uv = { 1, 1 }, .color = { color[0], color[1], color[2], color[3] } };
+       vertices[3] = (GskGLDrawVertex) { .position = { min_x + sizes[3].w, max_y - sizes[3].h }, .uv = { 1, 0 }, .color = { color[0], color[1], color[2], color[3] } };
+       vertices[4] = (GskGLDrawVertex) { .position = { min_x,              max_y              }, .uv = { 0, 0 }, .color = { color[0], color[1], color[2], color[3] } };
+       vertices[5] = (GskGLDrawVertex) { .position = { min_x + sizes[0].w, min_y + sizes[0].h }, .uv = { 1, 1 }, .color = { color[0], color[1], color[2], color[3] } };
+     }
+   gsk_gl_render_job_end_draw (job);
+ }
+ /* A special case for a pattern that occurs frequently with CSS
+  * backgrounds: two sibling nodes, the first of which is a rounded
+  * clip node with a color node as child, and the second one is a
+  * border node, with the same outline as the clip node. We render
+  * this using the filled_border shader.
+  */
+ static void
+ gsk_gl_render_job_visit_css_background (GskGLRenderJob      *job,
+                                         const GskRenderNode *node,
+                                         const GskRenderNode *node2)
+ {
+   const GskRenderNode *child = gsk_rounded_clip_node_get_child (node);
+   const GskRoundedRect *rounded_outline = gsk_border_node_get_outline (node2);
+   const float *widths = gsk_border_node_get_widths (node2);
+   float min_x = job->offset_x + node2->bounds.origin.x;
+   float min_y = job->offset_y + node2->bounds.origin.y;
+   float max_x = min_x + node2->bounds.size.width;
+   float max_y = min_y + node2->bounds.size.height;
+   GskRoundedRect outline;
+   GskGLDrawVertex *vertices;
+   guint16 color[4];
+   guint16 color2[4];
+   if (node_is_invisible (node2))
+     return;
+   rgba_to_half (&gsk_border_node_get_colors (node2)[0], color);
+   rgba_to_half (gsk_color_node_get_color (child), color2);
+   gsk_gl_render_job_transform_rounded_rect (job, rounded_outline, &outline);
+   gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, filled_border));
+   gsk_gl_program_set_uniform4fv (job->current_program,
+                                  UNIFORM_FILLED_BORDER_WIDTHS, 0,
+                                  1,
+                                  widths);
+   gsk_gl_program_set_uniform_rounded_rect (job->current_program,
+                                            UNIFORM_FILLED_BORDER_OUTLINE_RECT, 0,
+                                            &outline);
+   vertices = gsk_gl_command_queue_add_vertices (job->command_queue);
+   vertices[0] = (GskGLDrawVertex) { .position = { min_x, min_y }, .color = { color[0], color[1], color[2], color[3] }, .color2 = { color2[0], color2[1], color2[2], color2[3] } };
+   vertices[1] = (GskGLDrawVertex) { .position = { min_x, max_y }, .color = { color[0], color[1], color[2], color[3] }, .color2 = { color2[0], color2[1], color2[2], color2[3] } };
+   vertices[2] = (GskGLDrawVertex) { .position = { max_x, min_y }, .color = { color[0], color[1], color[2], color[3] }, .color2 = { color2[0], color2[1], color2[2], color2[3] } };
+   vertices[3] = (GskGLDrawVertex) { .position = { max_x, max_y }, .color = { color[0], color[1], color[2], color[3] }, .color2 = { color2[0], color2[1], color2[2], color2[3] } };
+   vertices[4] = (GskGLDrawVertex) { .position = { min_x, max_y }, .color = { color[0], color[1], color[2], color[3] }, .color2 = { color2[0], color2[1], color2[2], color2[3] } };
+   vertices[5] = (GskGLDrawVertex) { .position = { max_x, min_y }, .color = { color[0], color[1], color[2], color[3] }, .color2 = { color2[0], color2[1], color2[2], color2[3] } };
+   gsk_gl_render_job_end_draw (job);
+ }
+ /* Returns TRUE if applying @transform to @bounds
+  * yields an axis-aligned rectangle
+  */
+ static gboolean
+ result_is_axis_aligned (GskTransform          *transform,
+                         const graphene_rect_t *bounds)
+ {
+   graphene_matrix_t m;
+   graphene_quad_t q;
+   graphene_rect_t b;
+   graphene_point_t b1, b2;
+   const graphene_point_t *p;
+   gsk_transform_to_matrix (transform, &m);
+   gsk_matrix_transform_rect (&m, bounds, &q);
+   graphene_quad_bounds (&q, &b);
+   graphene_rect_get_top_left (&b, &b1);
+   graphene_rect_get_bottom_right (&b, &b2);
+   for (guint i = 0; i < 4; i++)
+     {
+       p = graphene_quad_get_point (&q, i);
+       if (fabs (p->x - b1.x) > FLT_EPSILON && fabs (p->x - b2.x) > FLT_EPSILON)
+         return FALSE;
+       if (fabs (p->y - b1.y) > FLT_EPSILON && fabs (p->y - b2.y) > FLT_EPSILON)
+         return FALSE;
+     }
+   return TRUE;
+ }
+ static inline void
+ gsk_gl_render_job_visit_transform_node (GskGLRenderJob      *job,
+                                         const GskRenderNode *node)
+ {
+   GskTransform *transform = gsk_transform_node_get_transform (node);
+   const GskTransformCategory category = gsk_transform_get_category (transform);
+   const GskRenderNode *child = gsk_transform_node_get_child (node);
+   switch (category)
+     {
+     case GSK_TRANSFORM_CATEGORY_IDENTITY:
+       gsk_gl_render_job_visit_node (job, child);
+     break;
+     case GSK_TRANSFORM_CATEGORY_2D_TRANSLATE:
+       {
+         float dx, dy;
+         gsk_transform_node_get_translate (node, &dx, &dy);
+         gsk_gl_render_job_offset (job, dx, dy);
+         gsk_gl_render_job_visit_node (job, child);
+         gsk_gl_render_job_offset (job, -dx, -dy);
+       }
+     break;
+     case GSK_TRANSFORM_CATEGORY_2D_AFFINE:
+       {
+         gsk_gl_render_job_push_modelview (job, transform);
+         gsk_gl_render_job_visit_node (job, child);
+         gsk_gl_render_job_pop_modelview (job);
+       }
+     break;
+     case GSK_TRANSFORM_CATEGORY_2D:
+     case GSK_TRANSFORM_CATEGORY_3D:
+     case GSK_TRANSFORM_CATEGORY_ANY:
+     case GSK_TRANSFORM_CATEGORY_UNKNOWN:
+       if (node_supports_transform (child))
+         {
+           gsk_gl_render_job_push_modelview (job, transform);
+           gsk_gl_render_job_visit_node (job, child);
+           gsk_gl_render_job_pop_modelview (job);
+         }
+       else
+         {
+           GskGLRenderOffscreen offscreen = {0};
+           float sx = 1, sy  = 1;
+           offscreen.bounds = &child->bounds;
+           offscreen.force_offscreen = FALSE;
+           offscreen.reset_clip = TRUE;
+           if (!result_is_axis_aligned (transform, &child->bounds))
+             offscreen.linear_filter = TRUE;
+           if (category == GSK_TRANSFORM_CATEGORY_2D)
+             {
+               graphene_matrix_t m;
+               double a, b, c, d, tx, ty;
+               g_assert (transform != NULL);
+               gsk_transform_to_matrix (transform, &m);
+               if (graphene_matrix_to_2d (&m, &a, &b, &c, &d, &tx, &ty))
+                 {
+                   sx = sqrt (a * a + b * b);
+                   sy = sqrt (c * c + d * d);
+                 }
+               else
+                 sx = sy = 1;
+               if (sx != 1 || sy != 1)
+                 {
+                   GskTransform *scale;
+                   scale = gsk_transform_translate (gsk_transform_scale (NULL, sx, sy), &GRAPHENE_POINT_INIT (tx, ty));
+                   gsk_gl_render_job_push_modelview (job, scale);
+                   transform = gsk_transform_transform (gsk_transform_invert (scale), transform);
+                 }
+             }
+           if (gsk_gl_render_job_visit_node_with_offscreen (job, child, &offscreen))
+             {
+               /* For non-trivial transforms, we draw everything on a texture and then
+                * draw the texture transformed. */
+               if (transform)
+                 gsk_gl_render_job_push_modelview (job, transform);
+               gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, blit));
+               gsk_gl_program_set_uniform_texture (job->current_program,
+                                                   UNIFORM_SHARED_SOURCE, 0,
+                                                   GL_TEXTURE_2D,
+                                                   GL_TEXTURE0,
+                                                   offscreen.texture_id);
+               gsk_gl_render_job_draw_offscreen (job, &child->bounds, &offscreen);
+               gsk_gl_render_job_end_draw (job);
+               if (transform)
+                 gsk_gl_render_job_pop_modelview (job);
+             }
+           if (category == GSK_TRANSFORM_CATEGORY_2D)
+             {
+               if (sx != 1 || sy != 1)
+                 {
+                   gsk_gl_render_job_pop_modelview (job);
+                   gsk_transform_unref (transform);
+                 }
+             }
+         }
+     break;
+     default:
+       g_assert_not_reached ();
+     }
+ }
+ static inline void
+ gsk_gl_render_job_visit_unblurred_inset_shadow_node (GskGLRenderJob      *job,
+                                                      const GskRenderNode *node)
+ {
+   const GskRoundedRect *outline = gsk_inset_shadow_node_get_outline (node);
+   GskRoundedRect transformed_outline;
+   guint16 color[4];
+   gsk_gl_render_job_transform_rounded_rect (job, outline, &transformed_outline);
+   gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, inset_shadow));
+   gsk_gl_program_set_uniform_rounded_rect (job->current_program,
+                                            UNIFORM_INSET_SHADOW_OUTLINE_RECT, 0,
+                                            &transformed_outline);
+   gsk_gl_program_set_uniform1f (job->current_program,
+                                 UNIFORM_INSET_SHADOW_SPREAD, 0,
+                                 gsk_inset_shadow_node_get_spread (node));
+   gsk_gl_program_set_uniform2f (job->current_program,
+                                 UNIFORM_INSET_SHADOW_OFFSET, 0,
+                                 gsk_inset_shadow_node_get_dx (node),
+                                 gsk_inset_shadow_node_get_dy (node));
+   rgba_to_half (gsk_inset_shadow_node_get_color (node), color);
+   gsk_gl_render_job_draw_rect_with_color (job, &node->bounds, color);
+   gsk_gl_render_job_end_draw (job);
+ }
+ static inline void
+ gsk_gl_render_job_visit_blurred_inset_shadow_node (GskGLRenderJob      *job,
+                                                    const GskRenderNode *node)
+ {
+   const GskRoundedRect *node_outline = gsk_inset_shadow_node_get_outline (node);
+   float blur_radius = gsk_inset_shadow_node_get_blur_radius (node);
+   float offset_x = gsk_inset_shadow_node_get_dx (node);
+   float offset_y = gsk_inset_shadow_node_get_dy (node);
+   float scale_x = job->scale_x;
+   float scale_y = job->scale_y;
+   float blur_extra = blur_radius * 2.0; /* 2.0 = shader radius_multiplier */
+   float half_blur_extra = blur_radius;
+   float texture_width;
+   float texture_height;
+   int blurred_texture_id;
+   GskTextureKey key;
+   GskGLRenderOffscreen offscreen = {0};
+   guint16 color[4];
+   g_assert (blur_radius > 0);
+   texture_width = ceilf ((node_outline->bounds.size.width + blur_extra) * scale_x);
+   texture_height = ceilf ((node_outline->bounds.size.height + blur_extra) * scale_y);
+   key.pointer = node;
+   key.pointer_is_child = FALSE;
+   key.scale_x = scale_x;
+   key.scale_y = scale_y;
+   key.filter = GL_NEAREST;
+   blurred_texture_id = gsk_gl_driver_lookup_texture (job->driver, &key);
+   if (blurred_texture_id == 0)
+     {
+       float spread = gsk_inset_shadow_node_get_spread (node) + half_blur_extra;
+       GskRoundedRect transformed_outline;
+       GskRoundedRect outline_to_blur;
+       GskGLRenderTarget *render_target;
+       graphene_matrix_t prev_projection;
+       graphene_rect_t prev_viewport;
+       guint prev_fbo;
+       /* TODO: In the following code, we have to be careful about where we apply the scale.
+        * We're manually scaling stuff (e.g. the outline) so we can later use texture_width
+        * and texture_height (which are already scaled) as the geometry and keep the modelview
+        * at a scale of 1. That's kinda complicated though... */
+       /* Outline of what we actually want to blur later.
+        * Spread grows inside, so we don't need to account for that. But the blur will need
+        * to read outside of the inset shadow, so we need to draw some color in there. */
+       outline_to_blur = *node_outline;
+       gsk_rounded_rect_shrink (&outline_to_blur,
+                                -half_blur_extra,
+                                -half_blur_extra,
+                                -half_blur_extra,
+                                -half_blur_extra);
+       /* Fit to our texture */
+       outline_to_blur.bounds.origin.x = 0;
+       outline_to_blur.bounds.origin.y = 0;
+       outline_to_blur.bounds.size.width *= scale_x;
+       outline_to_blur.bounds.size.height *= scale_y;
+       for (guint i = 0; i < 4; i ++)
+         {
+           outline_to_blur.corner[i].width *= scale_x;
+           outline_to_blur.corner[i].height *= scale_y;
+         }
+       if (!gsk_gl_driver_create_render_target (job->driver,
+                                                texture_width, texture_height,
+                                                get_target_format (job, node),
+                                                GL_NEAREST, GL_NEAREST,
+                                                &render_target))
+         g_assert_not_reached ();
+       gsk_gl_render_job_set_viewport_for_size (job, texture_width, texture_height, &prev_viewport);
+       gsk_gl_render_job_set_projection_for_size (job, texture_width, texture_height, &prev_projection);
+       gsk_gl_render_job_set_modelview (job, NULL);
+       gsk_gl_render_job_push_clip (job, &GSK_ROUNDED_RECT_INIT (0, 0, texture_width, texture_height));
+       prev_fbo = gsk_gl_command_queue_bind_framebuffer (job->command_queue, render_target->framebuffer_id);
+       gsk_gl_command_queue_clear (job->command_queue, 0, &job->viewport);
+       gsk_gl_render_job_transform_rounded_rect (job, &outline_to_blur, &transformed_outline);
+       /* Actual inset shadow outline drawing */
+       gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, inset_shadow));
+       gsk_gl_program_set_uniform_rounded_rect (job->current_program,
+                                                UNIFORM_INSET_SHADOW_OUTLINE_RECT, 0,
+                                                &transformed_outline);
+       gsk_gl_program_set_uniform1f (job->current_program,
+                                     UNIFORM_INSET_SHADOW_SPREAD, 0,
+                                     spread * MAX (scale_x, scale_y));
+       gsk_gl_program_set_uniform2f (job->current_program,
+                                     UNIFORM_INSET_SHADOW_OFFSET, 0,
+                                     offset_x * scale_x,
+                                     offset_y * scale_y);
+       rgba_to_half (gsk_inset_shadow_node_get_color (node), color);
+       gsk_gl_render_job_draw_with_color (job,
+                                          0, 0, texture_width, texture_height,
+                                          color);
+       gsk_gl_render_job_end_draw (job);
+       gsk_gl_render_job_pop_modelview (job);
+       gsk_gl_render_job_pop_clip (job);
+       gsk_gl_render_job_set_projection (job, &prev_projection);
+       gsk_gl_render_job_set_viewport (job, &prev_viewport, NULL);
+       gsk_gl_command_queue_bind_framebuffer (job->command_queue, prev_fbo);
+       offscreen.texture_id = render_target->texture_id;
+       init_full_texture_region (&offscreen);
+       blurred_texture_id = blur_offscreen (job,
+                                            &offscreen,
+                                            texture_width,
+                                            texture_height,
+                                            blur_radius * scale_x,
+                                            blur_radius * scale_y);
+       gsk_gl_driver_release_render_target (job->driver, render_target, TRUE);
+       gsk_gl_driver_cache_texture (job->driver, &key, blurred_texture_id);
+     }
+   g_assert (blurred_texture_id != 0);
+   /* Blur the rendered unblurred inset shadow */
+   /* Use a clip to cut away the unwanted parts outside of the original outline */
+   {
+     const gboolean needs_clip = !gsk_rounded_rect_is_rectilinear (node_outline);
+     const float tx1 = half_blur_extra * scale_x / texture_width;
+     const float tx2 = 1.0 - tx1;
+     const float ty1 = half_blur_extra * scale_y / texture_height;
+     const float ty2 = 1.0 - ty1;
+     if (needs_clip)
+       {
+         GskRoundedRect node_clip;
+         gsk_gl_render_job_transform_bounds (job, &node_outline->bounds, &node_clip.bounds);
+         for (guint i = 0; i < 4; i ++)
+           {
+             node_clip.corner[i].width = node_outline->corner[i].width * scale_x;
+             node_clip.corner[i].height = node_outline->corner[i].height * scale_y;
+           }
+         gsk_gl_render_job_push_clip (job, &node_clip);
+       }
+     offscreen.was_offscreen = TRUE;
+     offscreen.area.x = tx1;
+     offscreen.area.y = ty1;
+     offscreen.area.x2 = tx2;
+     offscreen.area.y2 = ty2;
+     gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, blit));
+     gsk_gl_program_set_uniform_texture (job->current_program,
+                                         UNIFORM_SHARED_SOURCE, 0,
+                                         GL_TEXTURE_2D,
+                                         GL_TEXTURE0,
+                                         blurred_texture_id);
+     gsk_gl_render_job_draw_offscreen (job, &node->bounds, &offscreen);
+     gsk_gl_render_job_end_draw (job);
+     if (needs_clip)
+       gsk_gl_render_job_pop_clip (job);
+   }
+ }
+ static inline void
+ gsk_gl_render_job_visit_unblurred_outset_shadow_node (GskGLRenderJob      *job,
+                                                       const GskRenderNode *node)
+ {
+   const GskRoundedRect *outline = gsk_outset_shadow_node_get_outline (node);
+   GskRoundedRect transformed_outline;
+   float x = node->bounds.origin.x;
+   float y = node->bounds.origin.y;
+   float w = node->bounds.size.width;
+   float h = node->bounds.size.height;
+   float spread = gsk_outset_shadow_node_get_spread (node);
+   float dx = gsk_outset_shadow_node_get_dx (node);
+   float dy = gsk_outset_shadow_node_get_dy (node);
+   guint16 color[4];
+   const float edge_sizes[] = { // Top, right, bottom, left
+     spread - dy, spread + dx, spread + dy, spread - dx
+   };
+   const float corner_sizes[][2] = { // top left, top right, bottom right, bottom left
+     { outline->corner[0].width + spread - dx, outline->corner[0].height + spread - dy },
+     { outline->corner[1].width + spread + dx, outline->corner[1].height + spread - dy },
+     { outline->corner[2].width + spread + dx, outline->corner[2].height + spread + dy },
+     { outline->corner[3].width + spread - dx, outline->corner[3].height + spread + dy },
+   };
+   rgba_to_half (gsk_outset_shadow_node_get_color (node), color);
+   gsk_gl_render_job_transform_rounded_rect (job, outline, &transformed_outline);
+   gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, unblurred_outset_shadow));
+   gsk_gl_program_set_uniform_rounded_rect (job->current_program,
+                                            UNIFORM_UNBLURRED_OUTSET_SHADOW_OUTLINE_RECT, 0,
+                                            &transformed_outline);
+   gsk_gl_program_set_uniform1f (job->current_program,
+                                 UNIFORM_UNBLURRED_OUTSET_SHADOW_SPREAD, 0,
+                                 spread);
+   gsk_gl_program_set_uniform2f (job->current_program,
+                                 UNIFORM_UNBLURRED_OUTSET_SHADOW_OFFSET, 0,
+                                 dx, dy);
+   /* Corners... */
+   if (corner_sizes[0][0] > 0 && corner_sizes[0][1] > 0) /* Top left */
+     gsk_gl_render_job_draw_with_color (job,
+                                        x, y, corner_sizes[0][0], corner_sizes[0][1],
+                                        color);
+   if (corner_sizes[1][0] > 0 && corner_sizes[1][1] > 0) /* Top right */
+     gsk_gl_render_job_draw_with_color (job,
+                                        x + w - corner_sizes[1][0], y,
+                                        corner_sizes[1][0], corner_sizes[1][1],
+                                        color);
+   if (corner_sizes[2][0] > 0 && corner_sizes[2][1] > 0) /* Bottom right */
+     gsk_gl_render_job_draw_with_color (job,
+                                        x + w - corner_sizes[2][0], y + h - corner_sizes[2][1],
+                                        corner_sizes[2][0], corner_sizes[2][1],
+                                        color);
+   if (corner_sizes[3][0] > 0 && corner_sizes[3][1] > 0) /* Bottom left */
+     gsk_gl_render_job_draw_with_color (job,
+                                        x, y + h - corner_sizes[3][1],
+                                        corner_sizes[3][0], corner_sizes[3][1],
+                                        color);
+   /* Edges... */;
+   if (edge_sizes[0] > 0) /* Top */
+     gsk_gl_render_job_draw_with_color (job,
+                                        x + corner_sizes[0][0], y,
+                                        w - corner_sizes[0][0] - corner_sizes[1][0], edge_sizes[0],
+                                        color);
+   if (edge_sizes[1] > 0) /* Right */
+     gsk_gl_render_job_draw_with_color (job,
+                                        x + w - edge_sizes[1], y + corner_sizes[1][1],
+                                        edge_sizes[1], h - corner_sizes[1][1] - corner_sizes[2][1],
+                                        color);
+   if (edge_sizes[2] > 0) /* Bottom */
+     gsk_gl_render_job_draw_with_color (job,
+                                        x + corner_sizes[3][0], y + h - edge_sizes[2],
+                                        w - corner_sizes[3][0] - corner_sizes[2][0], edge_sizes[2],
+                                        color);
+   if (edge_sizes[3] > 0) /* Left */
+     gsk_gl_render_job_draw_with_color (job,
+                                        x, y + corner_sizes[0][1],
+                                        edge_sizes[3], h - corner_sizes[0][1] - corner_sizes[3][1],
+                                        color);
+   gsk_gl_render_job_end_draw (job);
+ }
+ static inline void
+ gsk_gl_render_job_visit_blurred_outset_shadow_node (GskGLRenderJob      *job,
+                                                     const GskRenderNode *node)
+ {
+   const GskRoundedRect *outline = gsk_outset_shadow_node_get_outline (node);
+   float scale_x = job->scale_x;
+   float scale_y = job->scale_y;
+   float blur_radius = gsk_outset_shadow_node_get_blur_radius (node);
+   float blur_extra = blur_radius * 2.0f; /* 2.0 = shader radius_multiplier */
+   float half_blur_extra = blur_extra / 2.0f;
+   int extra_blur_pixels_x = ceilf (half_blur_extra * scale_x);
+   int extra_blur_pixels_y = ceilf (half_blur_extra * scale_y);
+   float spread = gsk_outset_shadow_node_get_spread (node);
+   float dx = gsk_outset_shadow_node_get_dx (node);
+   float dy = gsk_outset_shadow_node_get_dy (node);
+   GskRoundedRect scaled_outline;
+   GskRoundedRect transformed_outline;
+   GskGLRenderOffscreen offscreen = {0};
+   int texture_width, texture_height;
+   int blurred_texture_id;
+   int cached_tid;
+   gboolean do_slicing;
+   guint16 color[4];
+   float half_width = outline->bounds.size.width / 2;
+   float half_height = outline->bounds.size.height / 2;
+   rgba_to_half (gsk_outset_shadow_node_get_color (node), color);
+   /* scaled_outline is the minimal outline we need to draw the given drop shadow,
+    * enlarged by the spread and offset by the blur radius. */
+   scaled_outline = *outline;
+   if (outline->bounds.size.width < blur_extra ||
+       outline->bounds.size.height < blur_extra ||
+       outline->corner[0].width >= half_width ||
+       outline->corner[1].width >= half_width ||
+       outline->corner[2].width >= half_width ||
+       outline->corner[3].width >= half_width ||
+       outline->corner[0].height >= half_height ||
+       outline->corner[1].height >= half_height ||
+       outline->corner[2].height >= half_height ||
+       outline->corner[3].height >= half_height)
+     {
+       do_slicing = FALSE;
+       gsk_rounded_rect_shrink (&scaled_outline, -spread, -spread, -spread, -spread);
+     }
+   else
+     {
+       /* Shrink our outline to the minimum size that can still hold all the border radii */
+       gsk_rounded_rect_shrink_to_minimum (&scaled_outline);
+       /* Increase by the spread */
+       gsk_rounded_rect_shrink (&scaled_outline, -spread, -spread, -spread, -spread);
+       /* Grow bounds but don't grow corners */
+       graphene_rect_inset (&scaled_outline.bounds, - blur_extra / 2.0, - blur_extra / 2.0);
+       /* For the center part, we add a few pixels */
+       scaled_outline.bounds.size.width += SHADOW_EXTRA_SIZE;
+       scaled_outline.bounds.size.height += SHADOW_EXTRA_SIZE;
+       do_slicing = TRUE;
+     }
+   texture_width = (int)ceil ((scaled_outline.bounds.size.width + blur_extra) * scale_x);
+   texture_height = (int)ceil ((scaled_outline.bounds.size.height + blur_extra) * scale_y);
+   scaled_outline.bounds.origin.x = extra_blur_pixels_x;
+   scaled_outline.bounds.origin.y = extra_blur_pixels_y;
+   scaled_outline.bounds.size.width = texture_width - (extra_blur_pixels_x * 2);
+   scaled_outline.bounds.size.height = texture_height - (extra_blur_pixels_y * 2);
+   for (guint i = 0; i < G_N_ELEMENTS (scaled_outline.corner); i++)
+     {
+       scaled_outline.corner[i].width *= scale_x;
+       scaled_outline.corner[i].height *= scale_y;
+     }
+   cached_tid = gsk_gl_shadow_library_lookup (job->driver->shadows, &scaled_outline, blur_radius);
+   if (cached_tid == 0)
+     {
+       GdkGLContext *context = job->command_queue->context;
+       GskGLRenderTarget *render_target;
+       graphene_matrix_t prev_projection;
+       graphene_rect_t prev_viewport;
+       guint prev_fbo;
+       gsk_gl_driver_create_render_target (job->driver,
+                                           texture_width, texture_height,
+                                           get_target_format (job, node),
+                                           GL_NEAREST, GL_NEAREST,
+                                           &render_target);
+       if (gdk_gl_context_has_debug (context))
+         {
+           gdk_gl_context_label_object_printf (context,
+                                               GL_TEXTURE,
+                                               render_target->texture_id,
+                                               "Outset Shadow Temp %d",
+                                               render_target->texture_id);
+           gdk_gl_context_label_object_printf (context,
+                                               GL_FRAMEBUFFER,
+                                               render_target->framebuffer_id,
+                                               "Outset Shadow FB Temp %d",
+                                               render_target->framebuffer_id);
+         }
+       /* Change state for offscreen */
+       gsk_gl_render_job_set_projection_for_size (job, texture_width, texture_height, &prev_projection);
+       gsk_gl_render_job_set_viewport_for_size (job, texture_width, texture_height, &prev_viewport);
+       gsk_gl_render_job_set_modelview (job, NULL);
+       gsk_gl_render_job_push_clip (job, &scaled_outline);
+       /* Bind render target and clear it */
+       prev_fbo = gsk_gl_command_queue_bind_framebuffer (job->command_queue, render_target->framebuffer_id);
+       gsk_gl_command_queue_clear (job->command_queue, 0, &job->viewport);
+       /* Draw the outline using color program */
+       gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, color));
+       gsk_gl_render_job_draw_with_color (job, 0, 0, texture_width, texture_height,
+                                          (guint16[]){ FP16_ONE, FP16_ONE, FP16_ONE, FP16_ONE });
+       gsk_gl_render_job_end_draw (job);
+       /* Reset state from offscreen */
+       gsk_gl_render_job_pop_clip (job);
+       gsk_gl_render_job_pop_modelview (job);
+       gsk_gl_render_job_set_viewport (job, &prev_viewport, NULL);
+       gsk_gl_render_job_set_projection (job, &prev_projection);
+       /* Now blur the outline */
+       init_full_texture_region (&offscreen);
+       offscreen.texture_id = gsk_gl_driver_release_render_target (job->driver, render_target, FALSE);
+       blurred_texture_id = blur_offscreen (job,
+                                            &offscreen,
+                                            texture_width,
+                                            texture_height,
+                                            blur_radius * scale_x,
+                                            blur_radius * scale_y);
+       gsk_gl_shadow_library_insert (job->driver->shadows,
+                                     &scaled_outline,
+                                     blur_radius,
+                                     blurred_texture_id);
+       gsk_gl_command_queue_bind_framebuffer (job->command_queue, prev_fbo);
+     }
+   else
+     {
+       blurred_texture_id = cached_tid;
+     }
+   gsk_gl_render_job_transform_rounded_rect (job, outline, &transformed_outline);
+   if (!do_slicing)
+     {
+       float min_x = floorf (outline->bounds.origin.x - spread - half_blur_extra + dx);
+       float min_y = floorf (outline->bounds.origin.y - spread - half_blur_extra + dy);
+       offscreen.was_offscreen = TRUE;
+       offscreen.texture_id = blurred_texture_id;
+       init_full_texture_region (&offscreen);
+       gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, outset_shadow));
+       gsk_gl_program_set_uniform_texture (job->current_program,
+                                           UNIFORM_SHARED_SOURCE, 0,
+                                           GL_TEXTURE_2D,
+                                           GL_TEXTURE0,
+                                           blurred_texture_id);
+       gsk_gl_program_set_uniform_rounded_rect (job->current_program,
+                                                UNIFORM_OUTSET_SHADOW_OUTLINE_RECT, 0,
+                                                &transformed_outline);
+       gsk_gl_render_job_draw_offscreen_with_color (job,
+                                                    &GRAPHENE_RECT_INIT (min_x,
+                                                                         min_y,
+                                                                         texture_width / scale_x,
+                                                                         texture_height / scale_y),
+                                                    &offscreen,
+                                                    color);
+       gsk_gl_render_job_end_draw (job);
+       return;
+     }
+   /* slicing */
+   gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, outset_shadow));
+   gsk_gl_program_set_uniform_texture (job->current_program,
+                                       UNIFORM_SHARED_SOURCE, 0,
+                                       GL_TEXTURE_2D,
+                                       GL_TEXTURE0,
+                                       blurred_texture_id);
+   gsk_gl_program_set_uniform_rounded_rect (job->current_program,
+                                            UNIFORM_OUTSET_SHADOW_OUTLINE_RECT, 0,
+                                            &transformed_outline);
+   {
+     float min_x = floorf (outline->bounds.origin.x - spread - half_blur_extra + dx);
+     float min_y = floorf (outline->bounds.origin.y - spread - half_blur_extra + dy);
+     float max_x = ceilf (outline->bounds.origin.x + outline->bounds.size.width +
+                          half_blur_extra + dx + spread);
+     float max_y = ceilf (outline->bounds.origin.y + outline->bounds.size.height +
+                          half_blur_extra + dy + spread);
+     const GskGLTextureNineSlice *slices;
+     float left_width, center_width, right_width;
+     float top_height, center_height, bottom_height;
+     GskGLTexture *texture;
+     texture = gsk_gl_driver_get_texture_by_id (job->driver, blurred_texture_id);
+     slices = gsk_gl_texture_get_nine_slice (texture, &scaled_outline, extra_blur_pixels_x, extra_blur_pixels_y);
+     offscreen.was_offscreen = TRUE;
+     /* Our texture coordinates MUST be scaled, while the actual vertex coords
+      * MUST NOT be scaled.
+      */
+     left_width = slices[NINE_SLICE_TOP_LEFT].rect.width / scale_x;
+     right_width = slices[NINE_SLICE_TOP_RIGHT].rect.width / scale_x;
+     center_width = (max_x - min_x) - (left_width + right_width);
+     top_height = slices[NINE_SLICE_TOP_LEFT].rect.height / scale_y;
+     bottom_height = slices[NINE_SLICE_BOTTOM_LEFT].rect.height / scale_y;
+     center_height = (max_y - min_y) - (top_height + bottom_height);
+     /* Top left */
+     if (nine_slice_is_visible (&slices[NINE_SLICE_TOP_LEFT]))
+       {
+         memcpy (&offscreen.area, &slices[NINE_SLICE_TOP_LEFT].area, sizeof offscreen.area);
+         gsk_gl_render_job_draw_offscreen_with_color (job,
+                                                      &GRAPHENE_RECT_INIT (min_x,
+                                                                           min_y,
+                                                                           left_width,
+                                                                           top_height),
+                                                      &offscreen,
+                                                      color);
+       }
+     /* Top center */
+     if (nine_slice_is_visible (&slices[NINE_SLICE_TOP_CENTER]))
+     {
+       memcpy (&offscreen.area, &slices[NINE_SLICE_TOP_CENTER].area, sizeof offscreen.area);
+       gsk_gl_render_job_draw_offscreen_with_color (job,
+                                                    &GRAPHENE_RECT_INIT (min_x + left_width,
+                                                                         min_y,
+                                                                         center_width,
+                                                                         top_height),
+                                                    &offscreen,
+                                                    color);
+     }
+     /* Top right */
+     if (nine_slice_is_visible (&slices[NINE_SLICE_TOP_RIGHT]))
+     {
+       memcpy (&offscreen.area, &slices[NINE_SLICE_TOP_RIGHT].area, sizeof offscreen.area);
+       gsk_gl_render_job_draw_offscreen_with_color (job,
+                                                    &GRAPHENE_RECT_INIT (max_x - right_width,
+                                                                         min_y,
+                                                                         right_width,
+                                                                         top_height),
+                                                    &offscreen,
+                                                    color);
+     }
+     /* Bottom right */
+     if (nine_slice_is_visible (&slices[NINE_SLICE_BOTTOM_RIGHT]))
+     {
+       memcpy (&offscreen.area, &slices[NINE_SLICE_BOTTOM_RIGHT].area, sizeof offscreen.area);
+       gsk_gl_render_job_draw_offscreen_with_color (job,
+                                                    &GRAPHENE_RECT_INIT (max_x - right_width,
+                                                                         max_y - bottom_height,
+                                                                         right_width,
+                                                                         bottom_height),
+                                                    &offscreen,
+                                                    color);
+     }
+     /* Bottom left */
+     if (nine_slice_is_visible (&slices[NINE_SLICE_BOTTOM_LEFT]))
+       {
+         memcpy (&offscreen.area, &slices[NINE_SLICE_BOTTOM_LEFT].area, sizeof offscreen.area);
+         gsk_gl_render_job_draw_offscreen_with_color (job,
+                                                      &GRAPHENE_RECT_INIT (min_x,
+                                                                           max_y - bottom_height,
+                                                                           left_width,
+                                                                           bottom_height),
+                                                      &offscreen,
+                                                      color);
+       }
+     /* Left side */
+     if (nine_slice_is_visible (&slices[NINE_SLICE_LEFT_CENTER]))
+       {
+         memcpy (&offscreen.area, &slices[NINE_SLICE_LEFT_CENTER].area, sizeof offscreen.area);
+         gsk_gl_render_job_draw_offscreen_with_color (job,
+                                                      &GRAPHENE_RECT_INIT (min_x,
+                                                                           min_y + top_height,
+                                                                           left_width,
+                                                                           center_height),
+                                                      &offscreen,
+                                                      color);
+       }
+     /* Right side */
+     if (nine_slice_is_visible (&slices[NINE_SLICE_RIGHT_CENTER]))
+       {
+         memcpy (&offscreen.area, &slices[NINE_SLICE_RIGHT_CENTER].area, sizeof offscreen.area);
+         gsk_gl_render_job_draw_offscreen_with_color (job,
+                                                      &GRAPHENE_RECT_INIT (max_x - right_width,
+                                                                           min_y + top_height,
+                                                                           right_width,
+                                                                           center_height),
+                                                      &offscreen,
+                                                      color);
+       }
+     /* Bottom side */
+     if (nine_slice_is_visible (&slices[NINE_SLICE_BOTTOM_CENTER]))
+       {
+         memcpy (&offscreen.area, &slices[NINE_SLICE_BOTTOM_CENTER].area, sizeof offscreen.area);
+         gsk_gl_render_job_draw_offscreen_with_color (job,
+                                                      &GRAPHENE_RECT_INIT (min_x + left_width,
+                                                                           max_y - bottom_height,
+                                                                           center_width,
+                                                                           bottom_height),
+                                                      &offscreen,
+                                                      color);
+       }
+     /* Middle */
+     if (nine_slice_is_visible (&slices[NINE_SLICE_CENTER]))
+       {
+         if (!gsk_rounded_rect_contains_rect (outline, &GRAPHENE_RECT_INIT (min_x + left_width,
+                                                                            min_y + top_height,
+                                                                            center_width,
+                                                                            center_height)))
+           {
+             memcpy (&offscreen.area, &slices[NINE_SLICE_CENTER].area, sizeof offscreen.area);
+             gsk_gl_render_job_draw_offscreen_with_color (job,
+                                                          &GRAPHENE_RECT_INIT (min_x + left_width,
+                                                                               min_y + top_height,
+                                                                               center_width,
+                                                                               center_height),
+                                                          &offscreen,
+                                                          color);
+           }
+       }
+   }
+   gsk_gl_render_job_end_draw (job);
+ }
+ static inline gboolean G_GNUC_PURE
+ equal_texture_nodes (const GskRenderNode *node1,
+                      const GskRenderNode *node2)
+ {
+   if (gsk_render_node_get_node_type (node1) != GSK_TEXTURE_NODE ||
+       gsk_render_node_get_node_type (node2) != GSK_TEXTURE_NODE)
+     return FALSE;
+   if (gsk_texture_node_get_texture (node1) !=
+       gsk_texture_node_get_texture (node2))
+     return FALSE;
+   return graphene_rect_equal (&node1->bounds, &node2->bounds);
+ }
+ static inline void
+ gsk_gl_render_job_visit_cross_fade_node (GskGLRenderJob      *job,
+                                          const GskRenderNode *node)
+ {
+   const GskRenderNode *start_node = gsk_cross_fade_node_get_start_child (node);
+   const GskRenderNode *end_node = gsk_cross_fade_node_get_end_child (node);
+   float progress = gsk_cross_fade_node_get_progress (node);
+   GskGLRenderOffscreen offscreen_start = {0};
+   GskGLRenderOffscreen offscreen_end = {0};
+   g_assert (progress > 0.0);
+   g_assert (progress < 1.0);
+   offscreen_start.force_offscreen = TRUE;
+   offscreen_start.reset_clip = TRUE;
+   offscreen_start.bounds = &node->bounds;
+   offscreen_end.force_offscreen = TRUE;
+   offscreen_end.reset_clip = TRUE;
+   offscreen_end.bounds = &node->bounds;
+   if (!gsk_gl_render_job_visit_node_with_offscreen (job, start_node, &offscreen_start))
+     {
+       gsk_gl_render_job_visit_node (job, end_node);
+       return;
+     }
+   g_assert (offscreen_start.texture_id);
+   if (!gsk_gl_render_job_visit_node_with_offscreen (job, end_node, &offscreen_end))
+     {
+       float prev_alpha = gsk_gl_render_job_set_alpha (job, job->alpha * progress);
+       gsk_gl_render_job_visit_node (job, start_node);
+       gsk_gl_render_job_set_alpha (job, prev_alpha);
+       return;
+     }
+   g_assert (offscreen_end.texture_id);
+   gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, cross_fade));
+   gsk_gl_program_set_uniform_texture (job->current_program,
+                                       UNIFORM_SHARED_SOURCE, 0,
+                                       GL_TEXTURE_2D,
+                                       GL_TEXTURE0,
+                                       offscreen_start.texture_id);
+   gsk_gl_program_set_uniform_texture (job->current_program,
+                                       UNIFORM_CROSS_FADE_SOURCE2, 0,
+                                       GL_TEXTURE_2D,
+                                       GL_TEXTURE1,
+                                       offscreen_end.texture_id);
+   gsk_gl_program_set_uniform1f (job->current_program,
+                                 UNIFORM_CROSS_FADE_PROGRESS, 0,
+                                 progress);
+   gsk_gl_render_job_draw_offscreen (job, &node->bounds, &offscreen_end);
+   gsk_gl_render_job_end_draw (job);
+ }
+ static gboolean
+ is_non_branching (const GskRenderNode *node)
+ {
+   switch ((int)gsk_render_node_get_node_type (node))
+     {
+     case GSK_COLOR_NODE:
+     case GSK_LINEAR_GRADIENT_NODE:
+     case GSK_REPEATING_LINEAR_GRADIENT_NODE:
+     case GSK_RADIAL_GRADIENT_NODE:
+     case GSK_REPEATING_RADIAL_GRADIENT_NODE:
+     case GSK_CONIC_GRADIENT_NODE:
+     case GSK_BORDER_NODE:
+     case GSK_TEXTURE_NODE:
+     case GSK_INSET_SHADOW_NODE:
+     case GSK_OUTSET_SHADOW_NODE:
+     case GSK_TEXT_NODE:
+     case GSK_CAIRO_NODE:
+       return TRUE;
+     case GSK_TRANSFORM_NODE:
+       return is_non_branching (gsk_transform_node_get_child (node));
+     case GSK_OPACITY_NODE:
+       return is_non_branching (gsk_opacity_node_get_child (node));
+     case GSK_COLOR_MATRIX_NODE:
+       return is_non_branching (gsk_color_matrix_node_get_child (node));
+     case GSK_CLIP_NODE:
+       return is_non_branching (gsk_clip_node_get_child (node));
+     case GSK_ROUNDED_CLIP_NODE:
+       return is_non_branching (gsk_rounded_clip_node_get_child (node));
+     case GSK_SHADOW_NODE:
+       return is_non_branching (gsk_shadow_node_get_child (node));
+     case GSK_BLUR_NODE:
+       return is_non_branching (gsk_shadow_node_get_child (node));
+     case GSK_DEBUG_NODE:
+       return is_non_branching (gsk_debug_node_get_child (node));
+     case GSK_CONTAINER_NODE:
+       return gsk_container_node_get_n_children (node) == 1 &&
+              is_non_branching (gsk_container_node_get_child (node, 0));
+     default:
+       return FALSE;
+     }
+ }
+ static inline void
+ gsk_gl_render_job_visit_opacity_node (GskGLRenderJob      *job,
+                                       const GskRenderNode *node)
+ {
+   const GskRenderNode *child = gsk_opacity_node_get_child (node);
+   float opacity = gsk_opacity_node_get_opacity (node);
+   float new_alpha = job->alpha * opacity;
+   if (!ALPHA_IS_CLEAR (new_alpha))
+     {
+       float prev_alpha = gsk_gl_render_job_set_alpha (job, new_alpha);
+       /* Handle a few easy cases without offscreen. We bail out
+        * as soon as we see nodes with multiple children - in theory,
+        * we would only need offscreens for overlapping children.
+        */
+       if (is_non_branching (child))
+         {
+           gsk_gl_render_job_visit_node (job, child);
+           gsk_gl_render_job_set_alpha (job, prev_alpha);
+         }
+       else
+         {
+           GskGLRenderOffscreen offscreen = {0};
+           offscreen.bounds = &child->bounds;
+           offscreen.force_offscreen = TRUE;
+           offscreen.reset_clip = TRUE;
+           /* Note: offscreen rendering resets alpha to 1.0 */
+           if (!gsk_gl_render_job_visit_node_with_offscreen (job, child, &offscreen))
+             return;
+           g_assert (offscreen.texture_id);
+           gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, blit));
+           gsk_gl_program_set_uniform_texture (job->current_program,
+                                               UNIFORM_SHARED_SOURCE, 0,
+                                               GL_TEXTURE_2D,
+                                               GL_TEXTURE0,
+                                               offscreen.texture_id);
+           gsk_gl_render_job_draw_offscreen (job, &node->bounds, &offscreen);
+           gsk_gl_render_job_end_draw (job);
+         }
+       gsk_gl_render_job_set_alpha (job, prev_alpha);
+     }
+ }
+ static inline int
+ compute_phase_and_pos (float value, float *pos)
+ {
+   float v;
+   *pos = floorf (value);
+   v = value - *pos;
+   if (v < 0.125)
+     return 0;
+   else if (v < 0.375)
+     return 1;
+   else if (v < 0.625)
+     return 2;
+   else if (v < 0.875)
+     return 3;
+   else
+     {
+       *pos += 1;
+       return 0;
+     }
+ }
+ static inline void
+ gsk_gl_render_job_visit_text_node (GskGLRenderJob      *job,
+                                    const GskRenderNode *node,
+                                    const GdkRGBA       *color,
+                                    gboolean             force_color)
+ {
+   const PangoFont *font = gsk_text_node_get_font (node);
+   const PangoGlyphInfo *glyphs = gsk_text_node_get_glyphs (node, NULL);
+   const graphene_point_t *offset = gsk_text_node_get_offset (node);
+   float text_scale = MAX (job->scale_x, job->scale_y); /* TODO: Fix for uneven scales? */
+   guint num_glyphs = gsk_text_node_get_num_glyphs (node);
+   float x = offset->x + job->offset_x;
+   float y = offset->y + job->offset_y;
+   GskGLGlyphLibrary *library = job->driver->glyphs;
+   GskGLCommandBatch *batch;
+   int x_position = 0;
+   GskGLGlyphKey lookup;
+   guint last_texture = 0;
+   GskGLDrawVertex *vertices;
+   guint used = 0;
+   guint16 nc[4] = { FP16_MINUS_ONE, FP16_MINUS_ONE, FP16_MINUS_ONE, FP16_MINUS_ONE };
+   guint16 cc[4];
+   const guint16 *c;
+   const PangoGlyphInfo *gi;
+   guint i;
+   int yshift;
+   float ypos;
+   if (num_glyphs == 0)
+     return;
+   if ((force_color || !gsk_text_node_has_color_glyphs (node)) &&
+       RGBA_IS_CLEAR (color))
+     return;
+   rgba_to_half (color, cc);
+   lookup.font = (PangoFont *)font;
+   lookup.scale = (guint) (text_scale * 1024);
+   yshift = compute_phase_and_pos (y, &ypos);
+   gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, coloring));
+   batch = gsk_gl_command_queue_get_batch (job->command_queue);
+   vertices = gsk_gl_command_queue_add_n_vertices (job->command_queue, num_glyphs);
+   /* We use one quad per character */
+   for (i = 0, gi = glyphs; i < num_glyphs; i++, gi++)
+     {
+       const GskGLGlyphValue *glyph;
+       float glyph_x, glyph_y, glyph_x2, glyph_y2;
+       float tx, ty, tx2, ty2;
+       float cx;
+       float cy;
+       guint texture_id;
+       lookup.glyph = gi->glyph;
+       /* If the glyph has color, we don't need to recolor anything.
+        * We tell the shader by setting the color to vec4(-1).
+        */
+       if (!force_color && gi->attr.is_color)
+         c = nc;
+       else
+         c = cc;
+       cx = (float)(x_position + gi->geometry.x_offset) / PANGO_SCALE;
+       lookup.xshift = compute_phase_and_pos (x + cx, &cx);
+       if G_UNLIKELY (gi->geometry.y_offset != 0)
+         {
+           cy = (float)(gi->geometry.y_offset) / PANGO_SCALE;
+           lookup.yshift = compute_phase_and_pos (y + cy, &cy);
+         }
+       else
+         {
+           lookup.yshift = yshift;
+           cy = ypos;
+         }
+       x_position += gi->geometry.width;
+       texture_id = gsk_gl_glyph_library_lookup_or_add (library, &lookup, &glyph);
+       if G_UNLIKELY (texture_id == 0)
+         continue;
+       if G_UNLIKELY (last_texture != texture_id || batch->draw.vbo_count + GSK_GL_N_VERTICES > 0xffff)
+         {
+           if G_LIKELY (last_texture != 0)
+             {
+               guint vbo_offset = batch->draw.vbo_offset + batch->draw.vbo_count;
+               /* Since we have batched added our VBO vertices to avoid repeated
+                * calls to the buffer, we need to manually tweak the vbo offset
+                * of the new batch as otherwise it will point at the end of our
+                * vbo array.
+                */
+               gsk_gl_render_job_split_draw (job);
+               batch = gsk_gl_command_queue_get_batch (job->command_queue);
+               batch->draw.vbo_offset = vbo_offset;
+             }
+           gsk_gl_program_set_uniform_texture (job->current_program,
+                                               UNIFORM_SHARED_SOURCE, 0,
+                                               GL_TEXTURE_2D,
+                                               GL_TEXTURE0,
+                                               texture_id);
+           last_texture = texture_id;
+         }
+       tx = glyph->entry.area.x;
+       ty = glyph->entry.area.y;
+       tx2 = glyph->entry.area.x2;
+       ty2 = glyph->entry.area.y2;
+       glyph_x = cx + glyph->ink_rect.x;
+       glyph_y = cy + glyph->ink_rect.y;
+       glyph_x2 = glyph_x + glyph->ink_rect.width;
+       glyph_y2 = glyph_y + glyph->ink_rect.height;
+       *(vertices++) = (GskGLDrawVertex) { .position = { glyph_x,  glyph_y  }, .uv = { tx,  ty  }, .color = { c[0], c[1], c[2], c[3] } };
+       *(vertices++) = (GskGLDrawVertex) { .position = { glyph_x,  glyph_y2 }, .uv = { tx,  ty2 }, .color = { c[0], c[1], c[2], c[3] } };
+       *(vertices++) = (GskGLDrawVertex) { .position = { glyph_x2, glyph_y  }, .uv = { tx2, ty  }, .color = { c[0], c[1], c[2], c[3] } };
+       *(vertices++) = (GskGLDrawVertex) { .position = { glyph_x2, glyph_y2 }, .uv = { tx2, ty2 }, .color = { c[0], c[1], c[2], c[3] } };
+       *(vertices++) = (GskGLDrawVertex) { .position = { glyph_x,  glyph_y2 }, .uv = { tx,  ty2 }, .color = { c[0], c[1], c[2], c[3] } };
+       *(vertices++) = (GskGLDrawVertex) { .position = { glyph_x2, glyph_y  }, .uv = { tx2, ty  }, .color = { c[0], c[1], c[2], c[3] } };
+       batch->draw.vbo_count += GSK_GL_N_VERTICES;
+       used++;
+     }
+   if (used != num_glyphs)
+     gsk_gl_command_queue_retract_n_vertices (job->command_queue, num_glyphs - used);
+   gsk_gl_render_job_end_draw (job);
+ }
+ static inline void
+ gsk_gl_render_job_visit_shadow_node (GskGLRenderJob      *job,
+                                      const GskRenderNode *node)
+ {
+   const gsize n_shadows = gsk_shadow_node_get_n_shadows (node);
+   const GskRenderNode *original_child = gsk_shadow_node_get_child (node);
+   const GskRenderNode *shadow_child = original_child;
+   /* Shadow nodes recolor every pixel of the source texture, but leave the alpha in tact.
+    * If the child is a color matrix node that doesn't touch the alpha, we can throw that away. */
+   if (gsk_render_node_get_node_type (shadow_child) == GSK_COLOR_MATRIX_NODE &&
+       !color_matrix_modifies_alpha (shadow_child))
+     shadow_child = gsk_color_matrix_node_get_child (shadow_child);
+   for (guint i = 0; i < n_shadows; i++)
+     {
+       const GskShadow *shadow = gsk_shadow_node_get_shadow (node, i);
+       const float dx = shadow->dx;
+       const float dy = shadow->dy;
+       GskGLRenderOffscreen offscreen = {0};
+       graphene_rect_t bounds;
+       guint16 color[4];
+       if (RGBA_IS_CLEAR (&shadow->color))
+         continue;
+       if (node_is_invisible (shadow_child))
+         continue;
+       if (shadow->radius == 0 &&
+           gsk_render_node_get_node_type (shadow_child) == GSK_TEXT_NODE)
+         {
+           if (dx != 0 || dy != 0)
+             {
+               gsk_gl_render_job_offset (job, dx, dy);
+               gsk_gl_render_job_visit_text_node (job, shadow_child, &shadow->color, TRUE);
+               gsk_gl_render_job_offset (job, -dx, -dy);
+             }
+           continue;
+         }
+       if (shadow->radius > 0)
+         {
+           float min_x;
+           float min_y;
+           float max_x;
+           float max_y;
+           offscreen.do_not_cache = TRUE;
+           blur_node (job,
+                      &offscreen,
+                      shadow_child,
+                      shadow->radius,
+                      &min_x, &max_x,
+                      &min_y, &max_y);
+           bounds.origin.x = min_x - job->offset_x;
+           bounds.origin.y = min_y - job->offset_y;
+           bounds.size.width = max_x - min_x;
+           bounds.size.height = max_y - min_y;
+           offscreen.was_offscreen = TRUE;
+         }
+       else if (dx == 0 && dy == 0)
+         {
+           continue; /* Invisible anyway */
+         }
+       else
+         {
+           offscreen.bounds = &shadow_child->bounds;
+           offscreen.reset_clip = TRUE;
+           offscreen.do_not_cache = TRUE;
+           if (!gsk_gl_render_job_visit_node_with_offscreen (job, shadow_child, &offscreen))
+             g_assert_not_reached ();
+           bounds = shadow_child->bounds;
+         }
+       gsk_gl_render_job_offset (job, dx, dy);
+       gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, coloring));
+       gsk_gl_program_set_uniform_texture (job->current_program,
+                                           UNIFORM_SHARED_SOURCE, 0,
+                                           GL_TEXTURE_2D,
+                                           GL_TEXTURE0,
+                                           offscreen.texture_id);
+       rgba_to_half (&shadow->color, color);
+       gsk_gl_render_job_draw_offscreen_with_color (job, &bounds, &offscreen, color);
+       gsk_gl_render_job_end_draw (job);
+       gsk_gl_render_job_offset (job, -dx, -dy);
+     }
+   /* Now draw the child normally */
+   gsk_gl_render_job_visit_node (job, original_child);
+ }
+ static inline void
+ gsk_gl_render_job_visit_blur_node (GskGLRenderJob      *job,
+                                    const GskRenderNode *node)
+ {
+   const GskRenderNode *child = gsk_blur_node_get_child (node);
+   float blur_radius = gsk_blur_node_get_radius (node);
+   GskGLRenderOffscreen offscreen = {0};
+   GskTextureKey key;
+   gboolean cache_texture;
+   float min_x;
+   float max_x;
+   float min_y;
+   float max_y;
+   g_assert (blur_radius > 0);
+   if (node_is_invisible (child))
+     return;
+   key.pointer = node;
+   key.pointer_is_child = FALSE;
+   key.scale_x = job->scale_x;
+   key.scale_y = job->scale_y;
+   key.filter = GL_NEAREST;
+   offscreen.texture_id = gsk_gl_driver_lookup_texture (job->driver, &key);
+   cache_texture = offscreen.texture_id == 0;
+   blur_node (job,
+              &offscreen,
+              child,
+              blur_radius,
+              &min_x, &max_x, &min_y, &max_y);
+   g_assert (offscreen.texture_id != 0);
+   if (cache_texture)
+     gsk_gl_driver_cache_texture (job->driver, &key, offscreen.texture_id);
+   gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, blit));
+   gsk_gl_program_set_uniform_texture (job->current_program,
+                                       UNIFORM_SHARED_SOURCE, 0,
+                                       GL_TEXTURE_2D,
+                                       GL_TEXTURE0,
+                                       offscreen.texture_id);
+   gsk_gl_render_job_draw_coords (job,
+                                  min_x, min_y, max_x, max_y,
+                                  0, 1, 1, 0,
+                                  (guint16[]) { FP16_ZERO, FP16_ZERO, FP16_ZERO, FP16_ZERO } );
+   gsk_gl_render_job_end_draw (job);
+ }
+ static inline void
+ gsk_gl_render_job_visit_blend_node (GskGLRenderJob      *job,
+                                     const GskRenderNode *node)
+ {
+   const GskRenderNode *top_child = gsk_blend_node_get_top_child (node);
+   const GskRenderNode *bottom_child = gsk_blend_node_get_bottom_child (node);
+   GskGLRenderOffscreen top_offscreen = {0};
+   GskGLRenderOffscreen bottom_offscreen = {0};
+   top_offscreen.bounds = &node->bounds;
+   top_offscreen.force_offscreen = TRUE;
+   top_offscreen.reset_clip = TRUE;
+   bottom_offscreen.bounds = &node->bounds;
+   bottom_offscreen.force_offscreen = TRUE;
+   bottom_offscreen.reset_clip = TRUE;
+   /* TODO: We create 2 textures here as big as the blend node, but both the
+    * start and the end node might be a lot smaller than that. */
+   if (!gsk_gl_render_job_visit_node_with_offscreen (job, bottom_child, &bottom_offscreen))
+     {
+       gsk_gl_render_job_visit_node (job, top_child);
+       return;
+     }
+   g_assert (bottom_offscreen.was_offscreen);
+   if (!gsk_gl_render_job_visit_node_with_offscreen (job, top_child, &top_offscreen))
+     {
+       gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, blit));
+       gsk_gl_program_set_uniform_texture (job->current_program,
+                                           UNIFORM_SHARED_SOURCE, 0,
+                                           GL_TEXTURE_2D,
+                                           GL_TEXTURE0,
+                                           bottom_offscreen.texture_id);
+       gsk_gl_render_job_draw_offscreen (job, &node->bounds, &bottom_offscreen);
+       gsk_gl_render_job_end_draw (job);
+       return;
+     }
+   g_assert (top_offscreen.was_offscreen);
+   gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, blend));
+   gsk_gl_program_set_uniform_texture (job->current_program,
+                                       UNIFORM_SHARED_SOURCE, 0,
+                                       GL_TEXTURE_2D,
+                                       GL_TEXTURE0,
+                                       bottom_offscreen.texture_id);
+   gsk_gl_program_set_uniform_texture (job->current_program,
+                                       UNIFORM_BLEND_SOURCE2, 0,
+                                       GL_TEXTURE_2D,
+                                       GL_TEXTURE1,
+                                       top_offscreen.texture_id);
+   gsk_gl_program_set_uniform1i (job->current_program,
+                                 UNIFORM_BLEND_MODE, 0,
+                                 gsk_blend_node_get_blend_mode (node));
+   gsk_gl_render_job_draw_offscreen_rect (job, &node->bounds);
+   gsk_gl_render_job_end_draw (job);
+ }
+ static inline void
+ gsk_gl_render_job_visit_color_matrix_node (GskGLRenderJob      *job,
+                                            const GskRenderNode *node)
+ {
+   const GskRenderNode *child = gsk_color_matrix_node_get_child (node);
+   GskGLRenderOffscreen offscreen = {0};
+   float offset[4];
+   if (node_is_invisible (child))
+     return;
+   offscreen.bounds = &node->bounds;
+   offscreen.reset_clip = TRUE;
+   if (!gsk_gl_render_job_visit_node_with_offscreen (job, child, &offscreen))
+     g_assert_not_reached ();
+   g_assert (offscreen.texture_id > 0);
+   graphene_vec4_to_float (gsk_color_matrix_node_get_color_offset (node), offset);
+   gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, color_matrix));
+   gsk_gl_program_set_uniform_texture (job->current_program,
+                                       UNIFORM_SHARED_SOURCE, 0,
+                                       GL_TEXTURE_2D,
+                                       GL_TEXTURE0,
+                                       offscreen.texture_id);
+   gsk_gl_program_set_uniform_matrix (job->current_program,
+                                      UNIFORM_COLOR_MATRIX_COLOR_MATRIX, 0,
+                                      gsk_color_matrix_node_get_color_matrix (node));
+   gsk_gl_program_set_uniform4fv (job->current_program,
+                                  UNIFORM_COLOR_MATRIX_COLOR_OFFSET, 0,
+                                  1,
+                                  offset);
+   gsk_gl_render_job_draw_offscreen (job, &node->bounds, &offscreen);
+   gsk_gl_render_job_end_draw (job);
+ }
+ static inline void
+ gsk_gl_render_job_visit_gl_shader_node_fallback (GskGLRenderJob      *job,
+                                                  const GskRenderNode *node)
+ {
+   guint16 pink[4] = { 15360, 13975, 14758, 15360 }; /* 255 105 180 */
+   gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, color));
+   gsk_gl_render_job_draw_rect_with_color (job, &node->bounds, pink);
+   gsk_gl_render_job_end_draw (job);
+ }
+ static inline void
+ gsk_gl_render_job_visit_gl_shader_node (GskGLRenderJob      *job,
+                                         const GskRenderNode *node)
+ {
+   GError *error = NULL;
+   GskGLShader *shader;
+   GskGLProgram *program;
+   int n_children;
+   shader = gsk_gl_shader_node_get_shader (node);
+   program = gsk_gl_driver_lookup_shader (job->driver, shader, &error);
+   n_children = gsk_gl_shader_node_get_n_children (node);
+   if G_UNLIKELY (program == NULL)
+     {
+       if (g_object_get_data (G_OBJECT (shader), "gsk-did-warn") == NULL)
+         {
+           g_object_set_data (G_OBJECT (shader), "gsk-did-warn", GUINT_TO_POINTER (1));
+           g_warning ("Failed to compile gl shader: %s", error->message);
+         }
+       gsk_gl_render_job_visit_gl_shader_node_fallback (job, node);
+       g_clear_error (&error);
+     }
+   else
+     {
+       GskGLRenderOffscreen offscreens[4] = {{0}};
+       const GskGLUniform *uniforms;
+       const guint8 *base;
+       GBytes *args;
+       int n_uniforms;
+       g_assert (n_children < G_N_ELEMENTS (offscreens));
+       for (guint i = 0; i < n_children; i++)
+         {
+           const GskRenderNode *child = gsk_gl_shader_node_get_child (node, i);
+           offscreens[i].bounds = &node->bounds;
+           offscreens[i].force_offscreen = TRUE;
+           offscreens[i].reset_clip = TRUE;
+           if (!gsk_gl_render_job_visit_node_with_offscreen (job, child, &offscreens[i]))
+             return;
+         }
+       args = gsk_gl_shader_node_get_args (node);
+       base = g_bytes_get_data (args, NULL);
+       uniforms = gsk_gl_shader_get_uniforms (shader, &n_uniforms);
+       gsk_gl_render_job_begin_draw (job, program);
+       for (guint i = 0; i < n_children; i++)
+         gsk_gl_program_set_uniform_texture (program,
+                                             UNIFORM_CUSTOM_TEXTURE1 + i, 0,
+                                             GL_TEXTURE_2D,
+                                             GL_TEXTURE0 + i,
+                                             offscreens[i].texture_id);
+       gsk_gl_program_set_uniform2f (program,
+                                     UNIFORM_CUSTOM_SIZE, 0,
+                                     node->bounds.size.width,
+                                     node->bounds.size.height);
+       for (guint i = 0; i < n_uniforms; i++)
+         {
+           const GskGLUniform *u = &uniforms[i];
+           const guint8 *data = base + u->offset;
+           switch (u->type)
+             {
+             default:
+             case GSK_GL_UNIFORM_TYPE_NONE:
+               break;
+             case GSK_GL_UNIFORM_TYPE_FLOAT:
+               gsk_gl_uniform_state_set1fv (job->command_queue->uniforms,
+                                            program->program_info,
+                                            UNIFORM_CUSTOM_ARG0 + i,
+                                            0, 1, (const float *)data);
+               break;
+             case GSK_GL_UNIFORM_TYPE_INT:
+               gsk_gl_uniform_state_set1i (job->command_queue->uniforms,
+                                           program->program_info,
+                                           UNIFORM_CUSTOM_ARG0 + i,
+                                           0, *(const gint32 *)data);
+               break;
+             case GSK_GL_UNIFORM_TYPE_UINT:
+             case GSK_GL_UNIFORM_TYPE_BOOL:
+               gsk_gl_uniform_state_set1ui (job->command_queue->uniforms,
+                                            program->program_info,
+                                            UNIFORM_CUSTOM_ARG0 + i,
+                                            0, *(const guint32 *)data);
+               break;
+             case GSK_GL_UNIFORM_TYPE_VEC2:
+               gsk_gl_uniform_state_set2fv (job->command_queue->uniforms,
+                                            program->program_info,
+                                            UNIFORM_CUSTOM_ARG0 + i,
+                                            0, 1, (const float *)data);
+               break;
+             case GSK_GL_UNIFORM_TYPE_VEC3:
+               gsk_gl_uniform_state_set3fv (job->command_queue->uniforms,
+                                            program->program_info,
+                                            UNIFORM_CUSTOM_ARG0 + i,
+                                            0, 1, (const float *)data);
+               break;
+             case GSK_GL_UNIFORM_TYPE_VEC4:
+               gsk_gl_uniform_state_set4fv (job->command_queue->uniforms,
+                                            program->program_info,
+                                            UNIFORM_CUSTOM_ARG0 + i,
+                                            0, 1, (const float *)data);
+               break;
+             }
+         }
+       gsk_gl_render_job_draw_offscreen_rect (job, &node->bounds);
+       gsk_gl_render_job_end_draw (job);
+     }
+ }
+ static void
+ gsk_gl_render_job_upload_texture (GskGLRenderJob       *job,
+                                   GdkTexture           *texture,
+                                   GskGLRenderOffscreen *offscreen)
+ {
+   if (gsk_gl_texture_library_can_cache ((GskGLTextureLibrary *)job->driver->icons,
+                                         texture->width,
+                                         texture->height) &&
+       !GDK_IS_GL_TEXTURE (texture))
+     {
+       const GskGLIconData *icon_data;
+       gsk_gl_icon_library_lookup_or_add (job->driver->icons, texture, &icon_data);
+       offscreen->texture_id = GSK_GL_TEXTURE_ATLAS_ENTRY_TEXTURE (icon_data);
+       memcpy (&offscreen->area, &icon_data->entry.area, sizeof offscreen->area);
+     }
+   else
+     {
+       offscreen->texture_id = gsk_gl_driver_load_texture (job->driver, texture, GL_LINEAR, GL_LINEAR);
+       init_full_texture_region (offscreen);
+     }
+ }
+ static inline void
+ gsk_gl_render_job_visit_texture_node (GskGLRenderJob      *job,
+                                       const GskRenderNode *node)
+ {
+   GdkTexture *texture = gsk_texture_node_get_texture (node);
+   int max_texture_size = job->command_queue->max_texture_size;
+   if G_LIKELY (texture->width <= max_texture_size &&
+                texture->height <= max_texture_size)
+     {
+       GskGLRenderOffscreen offscreen = {0};
+       gsk_gl_render_job_upload_texture (job, texture, &offscreen);
+       g_assert (offscreen.texture_id);
+       g_assert (offscreen.was_offscreen == FALSE);
+       gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, blit));
+       gsk_gl_program_set_uniform_texture (job->current_program,
+                                           UNIFORM_SHARED_SOURCE, 0,
+                                           GL_TEXTURE_2D,
+                                           GL_TEXTURE0,
+                                           offscreen.texture_id);
+       gsk_gl_render_job_draw_offscreen (job, &node->bounds, &offscreen);
+       gsk_gl_render_job_end_draw (job);
+     }
+   else
+     {
+       float min_x = job->offset_x + node->bounds.origin.x;
+       float min_y = job->offset_y + node->bounds.origin.y;
+       float max_x = min_x + node->bounds.size.width;
+       float max_y = min_y + node->bounds.size.height;
+       float scale_x = (max_x - min_x) / texture->width;
+       float scale_y = (max_y - min_y) / texture->height;
+       GskGLTextureSlice *slices = NULL;
+       guint n_slices = 0;
+       gsk_gl_driver_slice_texture (job->driver, texture, &slices, &n_slices);
+       g_assert (slices != NULL);
+       g_assert (n_slices > 0);
+       gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, blit));
+       for (guint i = 0; i < n_slices; i ++)
+         {
+           const GskGLTextureSlice *slice = &slices[i];
+           float x1, x2, y1, y2;
+           x1 = min_x + (scale_x * slice->rect.x);
+           x2 = x1 + (slice->rect.width * scale_x);
+           y1 = min_y + (scale_y * slice->rect.y);
+           y2 = y1 + (slice->rect.height * scale_y);
+           if (i > 0)
+             gsk_gl_render_job_split_draw (job);
+           gsk_gl_program_set_uniform_texture (job->current_program,
+                                               UNIFORM_SHARED_SOURCE, 0,
+                                               GL_TEXTURE_2D,
+                                               GL_TEXTURE0,
+                                               slice->texture_id);
+           gsk_gl_render_job_draw_coords (job,
+                                          x1, y1, x2, y2,
+                                          0, 0, 1, 1,
+                                          (guint16[]) { FP16_ZERO, FP16_ZERO, FP16_ZERO, FP16_ZERO });
+         }
+       gsk_gl_render_job_end_draw (job);
+     }
+ }
+ static inline void
+ gsk_gl_render_job_visit_repeat_node (GskGLRenderJob      *job,
+                                      const GskRenderNode *node)
+ {
+   const GskRenderNode *child = gsk_repeat_node_get_child (node);
+   const graphene_rect_t *child_bounds = gsk_repeat_node_get_child_bounds (node);
+   GskGLRenderOffscreen offscreen = {0};
+   if (node_is_invisible (child))
+     return;
+   if (!graphene_rect_equal (child_bounds, &child->bounds))
+     {
+       /* TODO: implement these repeat nodes. */
+       gsk_gl_render_job_visit_as_fallback (job, node);
+       return;
+     }
+   /* If the size of the repeat node is smaller than the size of the
+    * child node, we don't repeat at all and can just draw that part
+    * of the child texture... */
+   if (rect_contains_rect (child_bounds, &node->bounds))
+     {
+       gsk_gl_render_job_visit_clipped_child (job, child, &node->bounds);
+       return;
+     }
+   offscreen.bounds = &child->bounds;
+   offscreen.reset_clip = TRUE;
+   if (!gsk_gl_render_job_visit_node_with_offscreen (job, child, &offscreen))
+     g_assert_not_reached ();
+   gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, repeat));
+   gsk_gl_program_set_uniform_texture (job->current_program,
+                                       UNIFORM_SHARED_SOURCE, 0,
+                                       GL_TEXTURE_2D,
+                                       GL_TEXTURE0,
+                                       offscreen.texture_id);
+   gsk_gl_program_set_uniform4f (job->current_program,
+                                 UNIFORM_REPEAT_CHILD_BOUNDS, 0,
+                                 (node->bounds.origin.x - child_bounds->origin.x) / child_bounds->size.width,
+                                 (node->bounds.origin.y - child_bounds->origin.y) / child_bounds->size.height,
+                                 node->bounds.size.width / child_bounds->size.width,
+                                 node->bounds.size.height / child_bounds->size.height);
+   gsk_gl_program_set_uniform4f (job->current_program,
+                                 UNIFORM_REPEAT_TEXTURE_RECT, 0,
+                                 offscreen.area.x,
+                                 offscreen.was_offscreen ? offscreen.area.y2 : offscreen.area.y,
+                                 offscreen.area.x2,
+                                 offscreen.was_offscreen ? offscreen.area.y : offscreen.area.y2);
+   gsk_gl_render_job_draw_offscreen (job, &node->bounds, &offscreen);
+   gsk_gl_render_job_end_draw (job);
+ }
+ static void
+ gsk_gl_render_job_visit_node (GskGLRenderJob      *job,
+                               const GskRenderNode *node)
+ {
+   gboolean has_clip;
+   g_assert (job != NULL);
+   g_assert (node != NULL);
+   g_assert (GSK_IS_GL_DRIVER (job->driver));
+   g_assert (GSK_IS_GL_COMMAND_QUEUE (job->command_queue));
+   if (node_is_invisible (node))
+     return;
+   if (!gsk_gl_render_job_update_clip (job, &node->bounds, &has_clip))
+     return;
+   switch (gsk_render_node_get_node_type (node))
+     {
+     case GSK_BLEND_NODE:
+       gsk_gl_render_job_visit_blend_node (job, node);
+     break;
+     case GSK_BLUR_NODE:
+       if (gsk_blur_node_get_radius (node) > 0)
+         gsk_gl_render_job_visit_blur_node (job, node);
+       else
+         gsk_gl_render_job_visit_node (job, gsk_blur_node_get_child (node));
+     break;
+     case GSK_BORDER_NODE:
+       if (gsk_border_node_get_uniform_color (node) &&
+           gsk_rounded_rect_is_rectilinear (gsk_border_node_get_outline (node)))
+         gsk_gl_render_job_visit_rect_border_node (job, node);
+       else
+         gsk_gl_render_job_visit_border_node (job, node);
+     break;
+     case GSK_CLIP_NODE:
+       gsk_gl_render_job_visit_clip_node (job, node);
+     break;
+     case GSK_COLOR_NODE:
+       gsk_gl_render_job_visit_color_node (job, node);
+     break;
+     case GSK_COLOR_MATRIX_NODE:
+       gsk_gl_render_job_visit_color_matrix_node (job, node);
+     break;
+     case GSK_CONIC_GRADIENT_NODE:
+       if (gsk_conic_gradient_node_get_n_color_stops (node) < MAX_GRADIENT_STOPS)
+         gsk_gl_render_job_visit_conic_gradient_node (job, node);
+       else
+         gsk_gl_render_job_visit_as_fallback (job, node);
+     break;
+     case GSK_CONTAINER_NODE:
+       {
+         GskRenderNode **children;
+         guint n_children;
+         children = gsk_container_node_get_children (node, &n_children);
+         for (guint i = 0; i < n_children; i++)
+           {
+             const GskRenderNode *child = children[i];
+             if (i + 1 < n_children &&
+                 job->current_clip->is_fully_contained &&
+                 gsk_render_node_get_node_type (child) == GSK_ROUNDED_CLIP_NODE)
+               {
+                 const GskRenderNode *grandchild = gsk_rounded_clip_node_get_child (child);
+                 const GskRenderNode *child2 = children[i + 1];
+                 if (gsk_render_node_get_node_type (grandchild) == GSK_COLOR_NODE &&
+                     gsk_render_node_get_node_type (child2) == GSK_BORDER_NODE &&
+                     gsk_border_node_get_uniform_color (child2) &&
+                     rounded_rect_equal (gsk_rounded_clip_node_get_clip (child),
+                                         gsk_border_node_get_outline (child2)))
+                   {
+                     gsk_gl_render_job_visit_css_background (job, child, child2);
+                     i++; /* skip the border node */
+                     continue;
+                   }
+               }
+             gsk_gl_render_job_visit_node (job, child);
+           }
+       }
+     break;
+     case GSK_CROSS_FADE_NODE:
+       {
+         const GskRenderNode *start_node = gsk_cross_fade_node_get_start_child (node);
+         const GskRenderNode *end_node = gsk_cross_fade_node_get_end_child (node);
+         float progress = gsk_cross_fade_node_get_progress (node);
+         if (progress <= 0.0f)
+           gsk_gl_render_job_visit_node (job, gsk_cross_fade_node_get_start_child (node));
+         else if (progress >= 1.0f || equal_texture_nodes (start_node, end_node))
+           gsk_gl_render_job_visit_node (job, gsk_cross_fade_node_get_end_child (node));
+         else
+           gsk_gl_render_job_visit_cross_fade_node (job, node);
+       }
+     break;
+     case GSK_DEBUG_NODE:
+       /* Debug nodes are ignored because draws get reordered anyway */
+       gsk_gl_render_job_visit_node (job, gsk_debug_node_get_child (node));
+     break;
+     case GSK_GL_SHADER_NODE:
+       gsk_gl_render_job_visit_gl_shader_node (job, node);
+     break;
+     case GSK_INSET_SHADOW_NODE:
+       if (gsk_inset_shadow_node_get_blur_radius (node) > 0)
+         gsk_gl_render_job_visit_blurred_inset_shadow_node (job, node);
+       else
+         gsk_gl_render_job_visit_unblurred_inset_shadow_node (job, node);
+     break;
+     case GSK_LINEAR_GRADIENT_NODE:
+     case GSK_REPEATING_LINEAR_GRADIENT_NODE:
+       if (gsk_linear_gradient_node_get_n_color_stops (node) < MAX_GRADIENT_STOPS)
+         gsk_gl_render_job_visit_linear_gradient_node (job, node);
+       else
+         gsk_gl_render_job_visit_as_fallback (job, node);
+     break;
+     case GSK_OPACITY_NODE:
+       gsk_gl_render_job_visit_opacity_node (job, node);
+     break;
+     case GSK_OUTSET_SHADOW_NODE:
+       if (gsk_outset_shadow_node_get_blur_radius (node) > 0)
+         gsk_gl_render_job_visit_blurred_outset_shadow_node (job, node);
+       else
+         gsk_gl_render_job_visit_unblurred_outset_shadow_node (job, node);
+     break;
+     case GSK_RADIAL_GRADIENT_NODE:
+     case GSK_REPEATING_RADIAL_GRADIENT_NODE:
+       if (gsk_radial_gradient_node_get_n_color_stops (node) < MAX_GRADIENT_STOPS)
+         gsk_gl_render_job_visit_radial_gradient_node (job, node);
+       else
+         gsk_gl_render_job_visit_as_fallback (job, node);
+     break;
+     case GSK_REPEAT_NODE:
+       gsk_gl_render_job_visit_repeat_node (job, node);
+     break;
+     case GSK_ROUNDED_CLIP_NODE:
+       gsk_gl_render_job_visit_rounded_clip_node (job, node);
+     break;
+     case GSK_SHADOW_NODE:
+       gsk_gl_render_job_visit_shadow_node (job, node);
+     break;
+     case GSK_TEXT_NODE:
+       gsk_gl_render_job_visit_text_node (job,
+                                          node,
+                                          gsk_text_node_get_color (node),
+                                          FALSE);
+     break;
+     case GSK_TEXTURE_NODE:
+       gsk_gl_render_job_visit_texture_node (job, node);
+     break;
+     case GSK_TRANSFORM_NODE:
+       gsk_gl_render_job_visit_transform_node (job, node);
+     break;
+     case GSK_CAIRO_NODE:
+       gsk_gl_render_job_visit_as_fallback (job, node);
+     break;
+     case GSK_NOT_A_RENDER_NODE:
+     default:
+       g_assert_not_reached ();
+     break;
+     }
+   if (has_clip)
+     gsk_gl_render_job_pop_clip (job);
+ }
+ static gboolean
+ gsk_gl_render_job_visit_node_with_offscreen (GskGLRenderJob       *job,
+                                              const GskRenderNode  *node,
+                                              GskGLRenderOffscreen *offscreen)
+ {
+   GskTextureKey key;
+   guint cached_id;
+   int filter;
+   g_assert (job != NULL);
+   g_assert (node != NULL);
+   g_assert (offscreen != NULL);
+   g_assert (offscreen->texture_id == 0);
+   g_assert (offscreen->bounds != NULL);
+   if (node_is_invisible (node))
+     {
+       /* Just to be safe. */
+       offscreen->texture_id = 0;
+       init_full_texture_region (offscreen);
+       offscreen->was_offscreen = FALSE;
+       return FALSE;
+     }
+   if (gsk_render_node_get_node_type (node) == GSK_TEXTURE_NODE &&
+       offscreen->force_offscreen == FALSE)
+     {
+       GdkTexture *texture = gsk_texture_node_get_texture (node);
+       gsk_gl_render_job_upload_texture (job, texture, offscreen);
+       g_assert (offscreen->was_offscreen == FALSE);
+       return TRUE;
+     }
+   filter = offscreen->linear_filter ? GL_LINEAR : GL_NEAREST;
+   /* Check if we've already cached the drawn texture. */
+   key.pointer = node;
+   key.pointer_is_child = TRUE; /* Don't conflict with the child using the cache too */
+   key.parent_rect = *offscreen->bounds;
+   key.scale_x = job->scale_x;
+   key.scale_y = job->scale_y;
+   key.filter = filter;
+   cached_id = gsk_gl_driver_lookup_texture (job->driver, &key);
+   if (cached_id != 0)
+     {
+       offscreen->texture_id = cached_id;
+       init_full_texture_region (offscreen);
+       /* We didn't render it offscreen, but hand out an offscreen texture id */
+       offscreen->was_offscreen = TRUE;
+       return TRUE;
+     }
+   float scaled_width;
+   float scaled_height;
+   float downscale_x = 1;
+   float downscale_y = 1;
+   g_assert (job->command_queue->max_texture_size > 0);
+   /* Tweak the scale factor so that the required texture doesn't
+    * exceed the max texture limit. This will render with a lower
+    * resolution, but this is better than clipping.
+    */
+   {
+     int max_texture_size = job->command_queue->max_texture_size;
+     scaled_width = ceilf (offscreen->bounds->size.width * fabs (job->scale_x));
+     if (scaled_width > max_texture_size)
+       {
+         downscale_x = (float)max_texture_size / scaled_width;
+         scaled_width = max_texture_size;
+       }
+     if (job->scale_x < 0)
+       downscale_x = -downscale_x;
+     scaled_height = ceilf (offscreen->bounds->size.height * fabs (job->scale_y));
+     if (scaled_height > max_texture_size)
+       {
+         downscale_y = (float)max_texture_size / scaled_height;
+         scaled_height = max_texture_size;
+       }
+     if (job->scale_y < 0)
+       downscale_y = -downscale_y;
+   }
+   GskGLRenderTarget *render_target;
+   graphene_matrix_t prev_projection;
+   graphene_rect_t prev_viewport;
+   graphene_rect_t viewport;
+   float offset_x = job->offset_x;
+   float offset_y = job->offset_y;
+   float prev_alpha;
+   guint prev_fbo;
+   if (!gsk_gl_driver_create_render_target (job->driver,
+                                            scaled_width, scaled_height,
+                                            get_target_format (job, node),
+                                            filter, filter,
+                                            &render_target))
+     g_assert_not_reached ();
+   if (gdk_gl_context_has_debug (job->command_queue->context))
+     {
+       gdk_gl_context_label_object_printf (job->command_queue->context,
+                                           GL_TEXTURE,
+                                           render_target->texture_id,
+                                           "Offscreen<%s> %d",
+                                           g_type_name_from_instance ((GTypeInstance *) node),
+                                           render_target->texture_id);
+       gdk_gl_context_label_object_printf (job->command_queue->context,
+                                           GL_FRAMEBUFFER,
+                                           render_target->framebuffer_id,
+                                           "Offscreen<%s> FB %d",
+                                           g_type_name_from_instance ((GTypeInstance *) node),
+                                           render_target->framebuffer_id);
+     }
+   if (downscale_x != 1 || downscale_y != 1)
+     {
+       GskTransform *transform = gsk_transform_scale (NULL, downscale_x, downscale_y);
+       gsk_gl_render_job_push_modelview (job, transform);
+       gsk_transform_unref (transform);
+     }
+   gsk_gl_render_job_transform_bounds (job, offscreen->bounds, &viewport);
+   /* Code above will scale the size with the scale we use in the render ops,
+    * but for the viewport size, we need our own size limited by the texture size */
+   viewport.size.width = scaled_width;
+   viewport.size.height = scaled_height;
+   gsk_gl_render_job_set_viewport (job, &viewport, &prev_viewport);
+   gsk_gl_render_job_set_projection_from_rect (job, &job->viewport, &prev_projection);
+   prev_alpha = gsk_gl_render_job_set_alpha (job, 1.0f);
+   prev_fbo = gsk_gl_command_queue_bind_framebuffer (job->command_queue, render_target->framebuffer_id);
+   gsk_gl_command_queue_clear (job->command_queue, 0, &job->viewport);
+   if (offscreen->reset_clip)
+     gsk_gl_render_job_push_clip (job, &GSK_ROUNDED_RECT_INIT_FROM_RECT (job->viewport));
+   gsk_gl_render_job_visit_node (job, node);
+   if (offscreen->reset_clip)
+     gsk_gl_render_job_pop_clip (job);
+   if (downscale_x != 1 || downscale_y != 1)
+     gsk_gl_render_job_pop_modelview (job);
+   gsk_gl_render_job_set_viewport (job, &prev_viewport, NULL);
+   gsk_gl_render_job_set_projection (job, &prev_projection);
+   gsk_gl_render_job_set_alpha (job, prev_alpha);
+   gsk_gl_command_queue_bind_framebuffer (job->command_queue, prev_fbo);
+   job->offset_x = offset_x;
+   job->offset_y = offset_y;
+   offscreen->was_offscreen = TRUE;
+   offscreen->texture_id = gsk_gl_driver_release_render_target (job->driver,
+                                                                render_target,
+                                                                FALSE);
+   init_full_texture_region (offscreen);
+   if (!offscreen->do_not_cache)
+     gsk_gl_driver_cache_texture (job->driver, &key, offscreen->texture_id);
+   return TRUE;
+ }
+ void
+ gsk_gl_render_job_render_flipped (GskGLRenderJob *job,
+                                   GskRenderNode  *root)
+ {
+   graphene_matrix_t proj;
+   guint framebuffer_id;
+   guint texture_id;
+   guint surface_height;
+   g_return_if_fail (job != NULL);
+   g_return_if_fail (root != NULL);
+   g_return_if_fail (GSK_IS_GL_DRIVER (job->driver));
+   surface_height = job->viewport.size.height;
+   graphene_matrix_init_ortho (&proj,
+                               job->viewport.origin.x,
+                               job->viewport.origin.x + job->viewport.size.width,
+                               job->viewport.origin.y,
+                               job->viewport.origin.y + job->viewport.size.height,
+                               ORTHO_NEAR_PLANE,
+                               ORTHO_FAR_PLANE);
+   graphene_matrix_scale (&proj, 1, -1, 1);
+   if (!gsk_gl_command_queue_create_render_target (job->command_queue,
+                                                   MAX (1, job->viewport.size.width),
+                                                   MAX (1, job->viewport.size.height),
+                                                   job->target_format,
+                                                   GL_NEAREST, GL_NEAREST,
+                                                   &framebuffer_id, &texture_id))
+     return;
+   /* Setup drawing to our offscreen texture/framebuffer which is flipped */
+   gsk_gl_command_queue_bind_framebuffer (job->command_queue, framebuffer_id);
+   gsk_gl_command_queue_clear (job->command_queue, 0, &job->viewport);
+   /* Visit all nodes creating batches */
+   gdk_gl_context_push_debug_group (job->command_queue->context, "Building command queue");
+   gsk_gl_render_job_visit_node (job, root);
+   gdk_gl_context_pop_debug_group (job->command_queue->context);
+   /* Now draw to our real destination, but flipped */
+   gsk_gl_render_job_set_alpha (job, 1.0f);
+   gsk_gl_command_queue_bind_framebuffer (job->command_queue, job->framebuffer);
+   gsk_gl_command_queue_clear (job->command_queue, 0, &job->viewport);
+   gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, blit));
+   gsk_gl_program_set_uniform_texture (job->current_program,
+                                       UNIFORM_SHARED_SOURCE, 0,
+                                       GL_TEXTURE_2D,
+                                       GL_TEXTURE0,
+                                       texture_id);
+   gsk_gl_render_job_draw_rect (job, &job->viewport);
+   gsk_gl_render_job_end_draw (job);
+   gdk_gl_context_push_debug_group (job->command_queue->context, "Executing command queue");
+   gsk_gl_command_queue_execute (job->command_queue, surface_height, 1, NULL);
+   gdk_gl_context_pop_debug_group (job->command_queue->context);
+   glDeleteFramebuffers (1, &framebuffer_id);
+   glDeleteTextures (1, &texture_id);
+ }
+ void
+ gsk_gl_render_job_render (GskGLRenderJob *job,
+                           GskRenderNode  *root)
+ {
+   G_GNUC_UNUSED gint64 start_time;
+   guint scale_factor;
+   guint surface_height;
+   g_return_if_fail (job != NULL);
+   g_return_if_fail (root != NULL);
+   g_return_if_fail (GSK_IS_GL_DRIVER (job->driver));
+   scale_factor = MAX (job->scale_x, job->scale_y);
+   surface_height = job->viewport.size.height;
+   gsk_gl_command_queue_make_current (job->command_queue);
+   /* Build the command queue using the shared GL context for all renderers
+    * on the same display.
+    */
+   start_time = GDK_PROFILER_CURRENT_TIME;
+   gdk_gl_context_push_debug_group (job->command_queue->context, "Building command queue");
+   gsk_gl_command_queue_bind_framebuffer (job->command_queue, job->framebuffer);
+   gsk_gl_command_queue_clear (job->command_queue, 0, &job->viewport);
+   gsk_gl_render_job_visit_node (job, root);
+   gdk_gl_context_pop_debug_group (job->command_queue->context);
+   gdk_profiler_add_mark (start_time, GDK_PROFILER_CURRENT_TIME-start_time, "Build GL command queue", "");
+ #if 0
+   /* At this point the atlases have uploaded content while we processed
+    * nodes but have not necessarily been used by the commands in the queue.
+    */
+   gsk_gl_driver_save_atlases_to_png (job->driver, NULL);
+ #endif
+   /* But now for executing the command queue, we want to use the context
+    * that was provided to us when creating the render job as framebuffer 0
+    * is bound to that context.
+    */
+   start_time = GDK_PROFILER_CURRENT_TIME;
+   gsk_gl_command_queue_make_current (job->command_queue);
+   gdk_gl_context_push_debug_group (job->command_queue->context, "Executing command queue");
+   gsk_gl_command_queue_execute (job->command_queue, surface_height, scale_factor, job->region);
+   gdk_gl_context_pop_debug_group (job->command_queue->context);
+   gdk_profiler_add_mark (start_time, GDK_PROFILER_CURRENT_TIME-start_time, "Execute GL command queue", "");
+ }
+ void
+ gsk_gl_render_job_set_debug_fallback (GskGLRenderJob *job,
+                                       gboolean        debug_fallback)
+ {
+   g_return_if_fail (job != NULL);
+   job->debug_fallback = !!debug_fallback;
+ }
+ static int
+ get_framebuffer_format (guint framebuffer)
+ {
+   int size;
+   glBindFramebuffer (GL_FRAMEBUFFER, framebuffer);
 -  if (size >= 32)
++  glGetFramebufferAttachmentParameteriv (GL_FRAMEBUFFER,
++                                         framebuffer ? GL_COLOR_ATTACHMENT0
++                                                     : GL_BACK,
++                                         GL_FRAMEBUFFER_ATTACHMENT_RED_SIZE, &size);
 -  else if (size >= 16)
++  if (size > 16)
+     return GL_RGBA32F;
++  else if (size > 8)
+     return GL_RGBA16F;
+   else
+     return GL_RGBA8;
+ }
+ GskGLRenderJob *
+ gsk_gl_render_job_new (GskGLDriver           *driver,
+                        const graphene_rect_t *viewport,
+                        float                  scale_factor,
+                        const cairo_region_t  *region,
+                        guint                  framebuffer)
+ {
+   const graphene_rect_t *clip_rect = viewport;
+   graphene_rect_t transformed_extents;
+   GskGLRenderJob *job;
+   g_return_val_if_fail (GSK_IS_GL_DRIVER (driver), NULL);
+   g_return_val_if_fail (viewport != NULL, NULL);
+   g_return_val_if_fail (scale_factor > 0, NULL);
+   job = g_slice_new0 (GskGLRenderJob);
+   job->driver = g_object_ref (driver);
+   job->command_queue = job->driver->command_queue;
+   job->clip = g_array_sized_new (FALSE, FALSE, sizeof (GskGLRenderClip), 16);
+   job->modelview = g_array_sized_new (FALSE, FALSE, sizeof (GskGLRenderModelview), 16);
+   job->framebuffer = framebuffer;
+   job->offset_x = 0;
+   job->offset_y = 0;
+   job->scale_x = scale_factor;
+   job->scale_y = scale_factor;
+   job->viewport = *viewport;
+   job->target_format = get_framebuffer_format (framebuffer);
+   gsk_gl_render_job_set_alpha (job, 1.0f);
+   gsk_gl_render_job_set_projection_from_rect (job, viewport, NULL);
+   gsk_gl_render_job_set_modelview (job, gsk_transform_scale (NULL, scale_factor, scale_factor));
+   /* Setup our initial clip. If region is NULL then we are drawing the
+    * whole viewport. Otherwise, we need to convert the region to a
+    * bounding box and clip based on that.
+    */
+   if (region != NULL)
+     {
+       cairo_rectangle_int_t extents;
+       cairo_region_get_extents (region, &extents);
+       gsk_gl_render_job_transform_bounds (job,
+                                           &GRAPHENE_RECT_INIT (extents.x,
+                                                                extents.y,
+                                                                extents.width,
+                                                                extents.height),
+                                           &transformed_extents);
+       clip_rect = &transformed_extents;
+       job->region = cairo_region_create_rectangle (&extents);
+     }
+   gsk_gl_render_job_push_clip (job,
+                                &GSK_ROUNDED_RECT_INIT (clip_rect->origin.x,
+                                                        clip_rect->origin.y,
+                                                        clip_rect->size.width,
+                                                        clip_rect->size.height));
+   return job;
+ }
+ void
+ gsk_gl_render_job_free (GskGLRenderJob *job)
+ {
+   job->current_modelview = NULL;
+   job->current_clip = NULL;
+   while (job->modelview->len > 0)
+     {
+       GskGLRenderModelview *modelview = &g_array_index (job->modelview, GskGLRenderModelview, job->modelview->len-1);
+       g_clear_pointer (&modelview->transform, gsk_transform_unref);
+       job->modelview->len--;
+     }
+   g_clear_object (&job->driver);
+   g_clear_pointer (&job->region, cairo_region_destroy);
+   g_clear_pointer (&job->modelview, g_array_unref);
+   g_clear_pointer (&job->clip, g_array_unref);
+   g_slice_free (GskGLRenderJob, job);
+ }
Simple merge