[gtk/wip/chergert/glproto] gsk: add OpenGL based GskNextRenderer




commit 893a77e22c9cd9af9f3ebe6a9d11fc7f6d425723
Author: Christian Hergert <chergert redhat com>
Date:   Fri Dec 18 17:36:59 2020 -0800

    gsk: add OpenGL based GskNextRenderer
    
    The primary goal here was to cleanup the current GL renderer to make
    maintenance easier going forward. Furthermore, it tracks state to allow
    us to implement more advanced renderer features going forward.
    
    ## Reordering
    
    This renderer will reorder batches by render target to reduce the number
    of times render targets are changed.
    
    In the future, we could also reorder by program within the render target
    if we can determine that vertices do not overlap.
    
    ## Uniform Snapshots
    
    To allow for reordering of batches all uniforms need to be tracked for
    the programs. This allows us to create the full uniform state when the
    batch has been moved into a new position.
    
    Some care was taken as it can be performance sensitive.
    
    ## Attachment Snapshots
    
    Similar to uniform snapshots, we need to know all of the texture
    attachments so that we can rebind them when necessary.
    
    ## Render Jobs
    
    To help isolate the process of creating GL commands from the renderer
    abstraction a render job abstraction was added. This could be extended
    in the future if we decided to do tiling.
    
    ## Command Queue
    
    Render jobs create batches using the command queue. The command queue
    will snapshot uniform and attachment state so that it can reorder
    batches right before executing them.
    
    Currently, the only reordering done is to ensure that we only visit
    each render target once. We could extend this by tracking vertices,
    attachments, and others.
    
    This code currently uses an inline array helper to reduce overhead
    from GArray which was showing up on profiles. It could be changed to
    use GdkArray without too much work, but had roughly double the
    instructions. Cycle counts have not yet been determined.
    
    ## GLSL Programs
    
    This was simplified to use XMACROS so that we can just extend one file
    (gskglprograms.defs) instead of multiple places. The programs are added
    as fields in the driver for easy access.
    
    ## Driver
    
    The driver manages textures, render targets, access to atlases,
    programs, and more. There is one driver per display, by using the
    shared GL context.
    
    Some work could be done here to batch uploads so that we make fewer
    calls to upload when sending icon theme data to the GPU. We'd need
    to keep a copy of the atlas data for such purposes.

 demos/node-editor/node-editor-window.c |    4 +
 gsk/gskglshader.c                      |    7 +-
 gsk/gskrenderer.c                      |    4 +
 gsk/meson.build                        |   17 +-
 gsk/next/gskglattachmentstate.c        |  106 +
 gsk/next/gskglattachmentstateprivate.h |   71 +
 gsk/next/gskglbuffer.c                 |   66 +
 gsk/next/gskglbufferprivate.h          |   81 +
 gsk/next/gskglcommandqueue.c           | 1467 +++++++++++++
 gsk/next/gskglcommandqueueprivate.h    |  357 +++
 gsk/next/gskglcompiler.c               |  678 ++++++
 gsk/next/gskglcompilerprivate.h        |   69 +
 gsk/next/gskgldriver.c                 | 1344 ++++++++++++
 gsk/next/gskgldriverprivate.h          |  236 ++
 gsk/next/gskglglyphlibrary.c           |  325 +++
 gsk/next/gskglglyphlibraryprivate.h    |  119 +
 gsk/next/gskgliconlibrary.c            |  213 ++
 gsk/next/gskgliconlibraryprivate.h     |   60 +
 gsk/next/gskglprogram.c                |  176 ++
 gsk/next/gskglprogramprivate.h         |  273 +++
 gsk/next/gskglprograms.defs            |   83 +
 gsk/next/gskglrenderer.c               |  312 +++
 gsk/next/gskglrenderer.h               |   47 +
 gsk/next/gskglrendererprivate.h        |   34 +
 gsk/next/gskglrenderjob.c              | 3756 ++++++++++++++++++++++++++++++++
 gsk/next/gskglrenderjobprivate.h       |   39 +
 gsk/next/gskglshadowlibrary.c          |  228 ++
 gsk/next/gskglshadowlibraryprivate.h   |   44 +
 gsk/next/gskgltexturelibrary.c         |  312 +++
 gsk/next/gskgltexturelibraryprivate.h  |  201 ++
 gsk/next/gskgltexturepool.c            |  273 +++
 gsk/next/gskgltexturepoolprivate.h     |  105 +
 gsk/next/gskgltypesprivate.h           |   62 +
 gsk/next/gskgluniformstate.c           |  270 +++
 gsk/next/gskgluniformstateprivate.h    |  685 ++++++
 gsk/next/inlinearray.h                 |   77 +
 gsk/next/ninesliceprivate.h            |  309 +++
 gtk/gtktestutils.c                     |    1 +
 testsuite/gsk/meson.build              |    1 +
 39 files changed, 12509 insertions(+), 3 deletions(-)
---
diff --git a/demos/node-editor/node-editor-window.c b/demos/node-editor/node-editor-window.c
index 0335a28c46..84c9141c71 100644
--- a/demos/node-editor/node-editor-window.c
+++ b/demos/node-editor/node-editor-window.c
@@ -25,6 +25,7 @@
 
 #include "gsk/gskrendernodeparserprivate.h"
 #include "gsk/gl/gskglrenderer.h"
+#include "gsk/next/gskglrenderer.h"
 #ifdef GDK_WINDOWING_BROADWAY
 #include "gsk/broadway/gskbroadwayrenderer.h"
 #endif
@@ -762,6 +763,9 @@ node_editor_window_realize (GtkWidget *widget)
   node_editor_window_add_renderer (self,
                                    gsk_gl_renderer_new (),
                                    "OpenGL");
+  node_editor_window_add_renderer (self,
+                                   gsk_next_renderer_new (),
+                                   "Next");
 #ifdef GDK_RENDERING_VULKAN
   node_editor_window_add_renderer (self,
                                    gsk_vulkan_renderer_new (),
diff --git a/gsk/gskglshader.c b/gsk/gskglshader.c
index 181ae32a2d..e5c2786213 100644
--- a/gsk/gskglshader.c
+++ b/gsk/gskglshader.c
@@ -139,7 +139,9 @@
 #include "gskglshader.h"
 #include "gskglshaderprivate.h"
 #include "gskdebugprivate.h"
+
 #include "gl/gskglrendererprivate.h"
+#include "next/gskglrendererprivate.h"
 
 static GskGLUniformType
 uniform_type_from_glsl (const char *str)
@@ -542,8 +544,9 @@ gsk_gl_shader_compile (GskGLShader  *shader,
   g_return_val_if_fail (GSK_IS_GL_SHADER (shader), FALSE);
 
   if (GSK_IS_GL_RENDERER (renderer))
-    return gsk_gl_renderer_try_compile_gl_shader (GSK_GL_RENDERER (renderer),
-                                                  shader, error);
+    return gsk_gl_renderer_try_compile_gl_shader (GSK_GL_RENDERER (renderer), shader, error);
+  else if (GSK_IS_NEXT_RENDERER (renderer))
+    return gsk_next_renderer_try_compile_gl_shader (GSK_NEXT_RENDERER (renderer), shader, error);
 
   g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
                "The renderer does not support gl shaders");
diff --git a/gsk/gskrenderer.c b/gsk/gskrenderer.c
index dbbfd28f35..255f92d11a 100644
--- a/gsk/gskrenderer.c
+++ b/gsk/gskrenderer.c
@@ -39,6 +39,7 @@
 #include "gskcairorenderer.h"
 #include "gskdebugprivate.h"
 #include "gl/gskglrenderer.h"
+#include "next/gskglrenderer.h"
 #include "gskprofilerprivate.h"
 #include "gskrendernodeprivate.h"
 
@@ -496,6 +497,8 @@ get_renderer_for_name (const char *renderer_name)
   else if (g_ascii_strcasecmp (renderer_name, "opengl") == 0
            || g_ascii_strcasecmp (renderer_name, "gl") == 0)
     return GSK_TYPE_GL_RENDERER;
+  else if (g_ascii_strcasecmp (renderer_name, "next") == 0)
+    return GSK_TYPE_NEXT_RENDERER;
 #ifdef GDK_RENDERING_VULKAN
   else if (g_ascii_strcasecmp (renderer_name, "vulkan") == 0)
     return GSK_TYPE_VULKAN_RENDERER;
@@ -511,6 +514,7 @@ get_renderer_for_name (const char *renderer_name)
       g_print ("   cairo - Use the Cairo fallback renderer\n");
       g_print ("  opengl - Use the default OpenGL renderer\n");
       g_print ("      gl - Same as opengl\n");
+      g_print ("    next - Another OpenGL renderer\n");
 #ifdef GDK_RENDERING_VULKAN
       g_print ("  vulkan - Use the Vulkan renderer\n");
 #else
diff --git a/gsk/meson.build b/gsk/meson.build
index 748f4738a1..62193a79da 100644
--- a/gsk/meson.build
+++ b/gsk/meson.build
@@ -31,6 +31,7 @@ gsk_public_sources = files([
   'gskroundedrect.c',
   'gsktransform.c',
   'gl/gskglrenderer.c',
+  'next/gskglrenderer.c',
 ])
 
 gsk_private_sources = files([
@@ -48,6 +49,19 @@ gsk_private_sources = files([
   'gl/gskgliconcache.c',
   'gl/opbuffer.c',
   'gl/stb_rect_pack.c',
+  'next/gskglattachmentstate.c',
+  'next/gskglbuffer.c',
+  'next/gskglcommandqueue.c',
+  'next/gskglcompiler.c',
+  'next/gskgldriver.c',
+  'next/gskglglyphlibrary.c',
+  'next/gskgliconlibrary.c',
+  'next/gskglprogram.c',
+  'next/gskglrenderjob.c',
+  'next/gskglshadowlibrary.c',
+  'next/gskgltexturelibrary.c',
+  'next/gskgluniformstate.c',
+  'next/gskgltexturepool.c',
 ])
 
 gsk_public_headers = files([
@@ -64,7 +78,8 @@ gsk_public_headers = files([
 install_headers(gsk_public_headers, 'gsk.h', subdir: 'gtk-4.0/gsk')
 
 gsk_public_gl_headers = files([
-  'gl/gskglrenderer.h'
+  'gl/gskglrenderer.h',
+  'next/gskglrenderer.h',
 ])
 install_headers(gsk_public_gl_headers, subdir: 'gtk-4.0/gsk/gl')
 gsk_public_headers += gsk_public_gl_headers
diff --git a/gsk/next/gskglattachmentstate.c b/gsk/next/gskglattachmentstate.c
new file mode 100644
index 0000000000..b05cc975a1
--- /dev/null
+++ b/gsk/next/gskglattachmentstate.c
@@ -0,0 +1,106 @@
+/* gskglattachmentstate.c
+ *
+ * Copyright 2020 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU Lesser General Public License as published by the Free
+ * Software Foundation; either version 2.1 of the License, or (at your option)
+ * any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "config.h"
+
+#include "gskglattachmentstateprivate.h"
+
+GskGLAttachmentState *
+gsk_gl_attachment_state_new (void)
+{
+  GskGLAttachmentState *self;
+
+  self = g_atomic_rc_box_new0 (GskGLAttachmentState);
+
+  self->fbo.changed = FALSE;
+  self->fbo.id = 0;
+  self->n_changed = 0;
+
+  /* Initialize textures, assume we are 2D by default since it
+   * doesn't really matter until we bind something other than
+   * GL_TEXTURE0 to it anyway.
+   */
+  for (guint i = 0; i < G_N_ELEMENTS (self->textures); i++)
+    {
+      self->textures[i].target = GL_TEXTURE_2D;
+      self->textures[i].texture = GL_TEXTURE0;
+      self->textures[i].id = 0;
+      self->textures[i].changed = FALSE;
+      self->textures[i].initial = TRUE;
+    }
+
+  return self;
+}
+
+GskGLAttachmentState *
+gsk_gl_attachment_state_ref (GskGLAttachmentState *self)
+{
+  return g_atomic_rc_box_acquire (self);
+}
+
+void
+gsk_gl_attachment_state_unref (GskGLAttachmentState *self)
+{
+  g_atomic_rc_box_release (self);
+}
+
+void
+gsk_gl_attachment_state_bind_texture (GskGLAttachmentState *self,
+                                      GLenum                target,
+                                      GLenum                texture,
+                                      guint                 id)
+{
+  GskGLBindTexture *attach;
+
+  g_assert (self != NULL);
+  g_assert (target == GL_TEXTURE_1D ||
+            target == GL_TEXTURE_2D ||
+            target == GL_TEXTURE_3D);
+  g_assert (texture >= GL_TEXTURE0 && texture <= GL_TEXTURE16);
+
+  attach = &self->textures[texture - GL_TEXTURE0];
+
+  if (attach->target != target || attach->texture != texture || attach->id != id)
+    {
+      attach->target = target;
+      attach->texture = texture;
+      attach->id = id;
+      attach->initial = FALSE;
+
+      if (attach->changed == FALSE)
+        {
+          attach->changed = TRUE;
+          self->n_changed++;
+        }
+    }
+}
+
+void
+gsk_gl_attachment_state_bind_framebuffer (GskGLAttachmentState *self,
+                                          guint                 id)
+{
+  g_assert (self != NULL);
+
+  if (self->fbo.id != id)
+    {
+      self->fbo.id = id;
+      self->fbo.changed = TRUE;
+    }
+}
diff --git a/gsk/next/gskglattachmentstateprivate.h b/gsk/next/gskglattachmentstateprivate.h
new file mode 100644
index 0000000000..cc71648d17
--- /dev/null
+++ b/gsk/next/gskglattachmentstateprivate.h
@@ -0,0 +1,71 @@
+/* gskglattachmentstateprivate.h
+ *
+ * Copyright 2020 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU Lesser General Public License as published by the Free
+ * Software Foundation; either version 2.1 of the License, or (at your option)
+ * any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#ifndef __GSK_GL_ATTACHMENT_STATE_PRIVATE_H__
+#define __GSK_GL_ATTACHMENT_STATE_PRIVATE_H__
+
+#include "gskgltypesprivate.h"
+
+G_BEGIN_DECLS
+
+typedef struct _GskGLAttachmentState GskGLAttachmentState;
+typedef struct _GskGLBindFramebuffer GskGLBindFramebuffer;
+typedef struct _GskGLBindTexture     GskGLBindTexture;
+
+struct _GskGLBindTexture
+{
+  guint changed : 1;
+  guint initial : 1;
+  GLenum target : 30;
+  GLenum texture;
+  guint id;
+};
+
+G_STATIC_ASSERT (sizeof (GskGLBindTexture) == 12);
+
+struct _GskGLBindFramebuffer
+{
+  guint changed : 1;
+  guint id : 31;
+};
+
+G_STATIC_ASSERT (sizeof (GskGLBindFramebuffer) == 4);
+
+struct _GskGLAttachmentState
+{
+  GskGLBindFramebuffer fbo;
+  /* Increase if shaders add more textures */
+  GskGLBindTexture textures[4];
+  guint n_changed;
+};
+
+GskGLAttachmentState *gsk_gl_attachment_state_new              (void);
+GskGLAttachmentState *gsk_gl_attachment_state_ref              (GskGLAttachmentState *self);
+void                  gsk_gl_attachment_state_unref            (GskGLAttachmentState *self);
+void                  gsk_gl_attachment_state_bind_texture     (GskGLAttachmentState *self,
+                                                                GLenum                target,
+                                                                GLenum                texture,
+                                                                guint                 id);
+void                  gsk_gl_attachment_state_bind_framebuffer (GskGLAttachmentState *self,
+                                                                guint                 id);
+
+G_END_DECLS
+
+#endif /* __GSK_GL_ATTACHMENT_STATE_PRIVATE_H__ */
diff --git a/gsk/next/gskglbuffer.c b/gsk/next/gskglbuffer.c
new file mode 100644
index 0000000000..c285ae69e7
--- /dev/null
+++ b/gsk/next/gskglbuffer.c
@@ -0,0 +1,66 @@
+/* gskglbufferprivate.h
+ *
+ * Copyright 2020 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU Lesser General Public License as published by the Free
+ * Software Foundation; either version 2.1 of the License, or (at your option)
+ * any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "config.h"
+
+#include "gskglbufferprivate.h"
+
+/**
+ * gsk_gl_buffer_init:
+ * @target: the target buffer such as %GL_ARRAY_BUFFER or %GL_UNIFORM_BUFFER
+ * @element_size: the size of elements within the buffer
+ *
+ * Creates a new #GskGLBuffer which can be used to deliver data to shaders
+ * within a GLSL program. You can use this to store vertices such as with
+ * %GL_ARRAY_BUFFER or uniform data with %GL_UNIFORM_BUFFER.
+ */
+void
+gsk_gl_buffer_init (GskGLBuffer *self,
+                    GLenum       target,
+                    guint        element_size)
+{
+  memset (self, 0, sizeof *self);
+
+  self->buffer = g_malloc (8092);
+  self->buffer_len = 8092;
+  self->target = target;
+  self->element_size = element_size;
+}
+
+GLuint
+gsk_gl_buffer_submit (GskGLBuffer *buffer)
+{
+  GLuint id;
+
+  glGenBuffers (1, &id);
+  glBindBuffer (buffer->target, id);
+  glBufferData (buffer->target, buffer->buffer_pos, buffer->buffer, GL_STATIC_DRAW);
+
+  buffer->buffer_pos = 0;
+  buffer->count = 0;
+
+  return id;
+}
+
+void
+gsk_gl_buffer_destroy (GskGLBuffer *buffer)
+{
+  g_clear_pointer (&buffer->buffer, g_free);
+}
diff --git a/gsk/next/gskglbufferprivate.h b/gsk/next/gskglbufferprivate.h
new file mode 100644
index 0000000000..b0315724f9
--- /dev/null
+++ b/gsk/next/gskglbufferprivate.h
@@ -0,0 +1,81 @@
+/* gskglbufferprivate.h
+ *
+ * Copyright 2020 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU Lesser General Public License as published by the Free
+ * Software Foundation; either version 2.1 of the License, or (at your option)
+ * any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#ifndef __GSK_GL_BUFFER_PRIVATE_H__
+#define __GSK_GL_BUFFER_PRIVATE_H__
+
+#include "gskgltypesprivate.h"
+
+G_BEGIN_DECLS
+
+typedef struct _GskGLBuffer
+{
+  guint8 *buffer;
+  gsize   buffer_pos;
+  gsize   buffer_len;
+  guint   count;
+  GLenum  target;
+  guint   element_size;
+} GskGLBuffer;
+
+void         gsk_gl_buffer_init    (GskGLBuffer *self,
+                                    GLenum       target,
+                                    guint        element_size);
+void         gsk_gl_buffer_destroy (GskGLBuffer *buffer);
+GLuint       gsk_gl_buffer_submit  (GskGLBuffer *buffer);
+
+static inline gpointer
+gsk_gl_buffer_advance (GskGLBuffer *buffer,
+                       guint        count)
+{
+  gpointer ret;
+  gsize to_alloc = count * buffer->element_size;
+
+  if G_UNLIKELY (buffer->buffer_pos + to_alloc > buffer->buffer_len)
+    {
+      buffer->buffer_len *= 2;
+      buffer->buffer = g_realloc (buffer->buffer, buffer->buffer_len);
+    }
+
+  ret = buffer->buffer + buffer->buffer_pos;
+
+  buffer->buffer_pos += to_alloc;
+  buffer->count += count;
+
+  return ret;
+}
+
+static inline void
+gsk_gl_buffer_retract (GskGLBuffer *buffer,
+                       guint        count)
+{
+  buffer->buffer_pos -= count * buffer->element_size;
+  buffer->count -= count;
+}
+
+static inline guint
+gsk_gl_buffer_get_offset (GskGLBuffer *buffer)
+{
+  return buffer->count;
+}
+
+G_END_DECLS
+
+#endif /* __GSK_GL_BUFFER_PRIVATE_H__ */
diff --git a/gsk/next/gskglcommandqueue.c b/gsk/next/gskglcommandqueue.c
new file mode 100644
index 0000000000..f5677a76ac
--- /dev/null
+++ b/gsk/next/gskglcommandqueue.c
@@ -0,0 +1,1467 @@
+/* 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/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)
+
+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 ();
+    }
+}
+
+static inline void
+gsk_gl_command_queue_print_batch (GskGLCommandQueue       *self,
+                                  const GskGLCommandBatch *batch)
+{
+  static const char *command_kinds[] = { "Clear", NULL, NULL, "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_print ("      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");
+}
+
+static inline void
+gsk_gl_command_queue_capture_png (GskGLCommandQueue *self,
+                                  const char        *filename,
+                                  guint              width,
+                                  guint              height,
+                                  gboolean           flip_y)
+{
+  cairo_surface_t *surface;
+  guint8 *data;
+  guint stride;
+
+  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;
+    }
+
+  surface = cairo_image_surface_create_for_data (data, CAIRO_FORMAT_ARGB32, width, height, stride);
+  cairo_surface_write_to_png (surface, filename);
+
+  cairo_surface_destroy (surface);
+  g_free (data);
+}
+
+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_sparse);
+  guint count = 0;
+
+  for (guint i = 0; i < program->n_sparse; i++)
+    {
+      guint location = program->sparse[i];
+      const GskGLUniformInfo *info = &program->uniforms[location].info;
+
+      if (!info->initial)
+        {
+          uniform[count].location = location;
+          uniform[count].info = *info;
+          count++;
+        }
+    }
+
+  if (count != program->n_sparse)
+    array->len -= program->n_sparse - 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);
+
+  self->debug_groups = g_string_chunk_new (4096);
+
+  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));
+
+  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);
+
+  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,
+                                 const graphene_rect_t *viewport)
+{
+  GskGLCommandBatch *batch;
+
+  g_assert (GSK_IS_GL_COMMAND_QUEUE (self));
+  g_assert (self->in_draw == FALSE);
+  g_assert (viewport != NULL);
+
+  /* Our internal links use 16-bits, so that is our max number
+   * of batches we can have in one frame.
+   */
+  if (self->batches.len == G_MAXINT16)
+    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 = viewport->size.width;
+  batch->any.viewport.height = viewport->size.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);
+
+  /* Max batches is 16-bit */
+  if (self->batches.len == G_MAXINT16)
+    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 &&
+      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;
+  graphene_rect_t viewport;
+
+  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);
+
+  viewport.origin.x = 0;
+  viewport.origin.y = 0;
+  viewport.size.width = batch->any.viewport.width;
+  viewport.size.height = batch->any.viewport.height;
+
+  gsk_gl_command_queue_end_draw (self);
+  gsk_gl_command_queue_begin_draw (self, program, &viewport);
+}
+
+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 (self->batches.len == G_MAXINT16)
+    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_uniform (gconstpointer    dataptr,
+               GskGLUniformInfo info,
+               guint            location)
+{
+  g_assert (dataptr != NULL);
+  g_assert (info.format > 0);
+  g_assert (location < GL_MAX_UNIFORM_LOCATIONS);
+
+  switch (info.format)
+    {
+    case GSK_GL_UNIFORM_FORMAT_1F:
+      glUniform1fv (location, 1, dataptr);
+    break;
+
+    case GSK_GL_UNIFORM_FORMAT_2F:
+      glUniform2fv (location, 1, dataptr);
+    break;
+
+    case GSK_GL_UNIFORM_FORMAT_3F:
+      glUniform3fv (location, 1, dataptr);
+    break;
+
+    case GSK_GL_UNIFORM_FORMAT_4F:
+      glUniform4fv (location, 1, dataptr);
+    break;
+
+    case GSK_GL_UNIFORM_FORMAT_1FV:
+      glUniform1fv (location, info.array_count, dataptr);
+    break;
+
+    case GSK_GL_UNIFORM_FORMAT_2FV:
+      glUniform2fv (location, info.array_count, dataptr);
+    break;
+
+    case GSK_GL_UNIFORM_FORMAT_3FV:
+      glUniform3fv (location, info.array_count, dataptr);
+    break;
+
+    case GSK_GL_UNIFORM_FORMAT_4FV:
+      glUniform4fv (location, info.array_count, dataptr);
+    break;
+
+    case GSK_GL_UNIFORM_FORMAT_1I:
+    case GSK_GL_UNIFORM_FORMAT_TEXTURE:
+      glUniform1iv (location, 1, dataptr);
+    break;
+
+    case GSK_GL_UNIFORM_FORMAT_2I:
+      glUniform2iv (location, 1, dataptr);
+    break;
+
+    case GSK_GL_UNIFORM_FORMAT_3I:
+      glUniform3iv (location, 1, dataptr);
+    break;
+
+    case GSK_GL_UNIFORM_FORMAT_4I:
+      glUniform4iv (location, 1, dataptr);
+    break;
+
+    case GSK_GL_UNIFORM_FORMAT_1UI:
+      glUniform1uiv (location, 1, dataptr);
+    break;
+
+    case GSK_GL_UNIFORM_FORMAT_MATRIX: {
+      float mat[16];
+      graphene_matrix_to_float (dataptr, mat);
+      glUniformMatrix4fv (location, 1, GL_FALSE, mat);
+#if 0
+      /* TODO: If Graphene can give us a peek here on platforms
+       * where the format is float[16] (most/all x86_64?) then
+       * We can avoid the SIMD operation to convert the format.
+       */
+      G_STATIC_ASSERT (sizeof (graphene_matrix_t) == 16*4);
+      glUniformMatrix4fv (location, 1, GL_FALSE, dataptr);
+#endif
+    }
+    break;
+
+    case GSK_GL_UNIFORM_FORMAT_COLOR:
+      glUniform4fv (location, 1, dataptr);
+    break;
+
+    case GSK_GL_UNIFORM_FORMAT_ROUNDED_RECT:
+      glUniform4fv (location, 3, dataptr);
+    break;
+
+    default:
+      g_assert_not_reached ();
+    }
+}
+
+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.
+   */
+  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 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));
+
+  /* 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);
+            }
+
+          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++)
+                apply_uniform (gsk_gl_uniform_state_get_uniform_data (self->uniforms, u->info.offset),
+                               u->info, u->location);
+
+              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_uploads, self->n_uploads);
+
+#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;
+        }
+    }
+
+  g_string_chunk_clear (self->debug_groups);
+
+  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                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,
+                                                    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                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);
+
+  if (gdk_gl_context_get_use_es (self->context))
+    glTexImage2D (GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
+  else
+    glTexImage2D (GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_BGRA, GL_UNSIGNED_BYTE, NULL);
+
+  /* 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, 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_memory_texture_get_format (memory_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);
+
+  /* Swtich 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");
+    }
+#endif
+}
diff --git a/gsk/next/gskglcommandqueueprivate.h b/gsk/next/gskglcommandqueueprivate.h
new file mode 100644
index 0000000000..39d6fd0315
--- /dev/null
+++ b/gsk/next/gskglcommandqueueprivate.h
@@ -0,0 +1,357 @@
+/* gskglcommandqueueprivate.h
+ *
+ * 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
+ */
+
+#ifndef __GSK_GL_COMMAND_QUEUE_PRIVATE_H__
+#define __GSK_GL_COMMAND_QUEUE_PRIVATE_H__
+
+#include <gsk/gskprofilerprivate.h>
+
+#include "gskgltypesprivate.h"
+#include "gskglbufferprivate.h"
+#include "gskglattachmentstateprivate.h"
+#include "gskgluniformstateprivate.h"
+
+#include "inlinearray.h"
+
+#include "../gl/gskglprofilerprivate.h"
+
+G_BEGIN_DECLS
+
+#define GSK_TYPE_GL_COMMAND_QUEUE (gsk_gl_command_queue_get_type())
+
+G_DECLARE_FINAL_TYPE (GskGLCommandQueue, gsk_gl_command_queue, GSK, GL_COMMAND_QUEUE, GObject)
+
+typedef enum _GskGLCommandKind
+{
+  /* The batch will perform a glClear() */
+  GSK_GL_COMMAND_KIND_CLEAR,
+
+  /* The batch will perform a glDrawArrays() */
+  GSK_GL_COMMAND_KIND_DRAW,
+} GskGLCommandKind;
+
+typedef struct _GskGLCommandBind
+{
+  /* @texture is the value passed to glActiveTexture(), the "slot" the
+   * texture will be placed into. We always use GL_TEXTURE_2D so we don't
+   * waste any bits here to indicate that.
+   */
+  guint texture : 5;
+
+  /* The identifier for the texture created with glGenTextures(). */
+  guint id : 27;
+} GskGLCommandBind;
+
+G_STATIC_ASSERT (sizeof (GskGLCommandBind) == 4);
+
+typedef struct _GskGLCommandBatchAny
+{
+  /* A GskGLCommandKind indicating what the batch will do */
+  guint kind : 8;
+
+  /* The program's identifier to use for determining if we can merge two
+   * batches together into a single set of draw operations. We put this
+   * here instead of the GskGLCommandDraw so that we can use the extra
+   * bits here without making the structure larger.
+   */
+  guint program : 24;
+
+  /* The index of the next batch following this one. This is used
+   * as a sort of integer-based linked list to simplify out-of-order
+   * batching without moving memory around. -1 indicates last batch.
+   */
+  gint16 next_batch_index;
+
+  /* Same but for reverse direction as we sort in reverse to get the
+   * batches ordered by framebuffer.
+   */
+  gint16 prev_batch_index;
+
+  /* The viewport size of the batch. We check this as we process
+   * batches to determine if we need to resize the viewport.
+   */
+  struct {
+    guint16 width;
+    guint16 height;
+  } viewport;
+} GskGLCommandBatchAny;
+
+G_STATIC_ASSERT (sizeof (GskGLCommandBatchAny) == 12);
+
+typedef struct _GskGLCommandDraw
+{
+  GskGLCommandBatchAny head;
+
+  /* There doesn't seem to be a limit on the framebuffer identifier that
+   * can be returned, so we have to use a whole unsigned for the framebuffer
+   * we are drawing to. When processing batches, we check to see if this
+   * changes and adjust the render target accordingly. Some sorting is
+   * performed to reduce the amount we change framebuffers.
+   */
+  guint framebuffer;
+
+  /* The number of uniforms to change. This must be less than or equal to
+   * GL_MAX_UNIFORM_LOCATIONS but only guaranteed up to 1024 by any OpenGL
+   * implementation to be conformant.
+   */
+  guint uniform_count : 11;
+
+  /* The number of textures to bind, which is only guaranteed up to 16
+   * by the OpenGL specification to be conformant.
+   */
+  guint bind_count : 5;
+
+  /* GL_MAX_ELEMENTS_VERTICES specifies 33000 for this which requires 16-bit
+   * to address all possible counts <= GL_MAX_ELEMENTS_VERTICES.
+   */
+  guint vbo_count : 16;
+
+  /* The offset within the VBO containing @vbo_count vertices to send with
+   * glDrawArrays().
+   */
+  guint vbo_offset;
+
+  /* The offset within the array of uniform changes to be made containing
+   * @uniform_count #GskGLCommandUniform elements to apply.
+   */
+  guint uniform_offset;
+
+  /* The offset within the array of bind changes to be made containing
+   * @bind_count #GskGLCommandBind elements to apply.
+   */
+  guint bind_offset;
+} GskGLCommandDraw;
+
+G_STATIC_ASSERT (sizeof (GskGLCommandDraw) == 32);
+
+typedef struct _GskGLCommandClear
+{
+  GskGLCommandBatchAny  any;
+  guint                 bits;
+  guint                 framebuffer;
+} GskGLCommandClear;
+
+G_STATIC_ASSERT (sizeof (GskGLCommandClear) == 20);
+
+typedef struct _GskGLCommandUniform
+{
+  GskGLUniformInfo info;
+  guint            location;
+} GskGLCommandUniform;
+
+G_STATIC_ASSERT (sizeof (GskGLCommandUniform) == 8);
+
+typedef union _GskGLCommandBatch
+{
+  GskGLCommandBatchAny any;
+  GskGLCommandDraw     draw;
+  GskGLCommandClear    clear;
+} GskGLCommandBatch;
+
+G_STATIC_ASSERT (sizeof (GskGLCommandBatch) == 32);
+
+DEFINE_INLINE_ARRAY (GskGLCommandBatches, gsk_gl_command_batches, GskGLCommandBatch)
+DEFINE_INLINE_ARRAY (GskGLCommandBinds, gsk_gl_command_binds, GskGLCommandBind)
+DEFINE_INLINE_ARRAY (GskGLCommandUniforms, gsk_gl_command_uniforms, GskGLCommandUniform)
+
+struct _GskGLCommandQueue
+{
+  GObject parent_instance;
+
+  /* The GdkGLContext we make current before executing GL commands. */
+  GdkGLContext *context;
+
+  /* Array of GskGLCommandBatch which is a fixed size structure that will
+   * point into offsets of other arrays so that all similar data is stored
+   * together. The idea here is that we reduce the need for pointers so that
+   * using g_realloc()'d arrays is fine.
+   */
+  GskGLCommandBatches batches;
+
+  /* Contains array of vertices and some wrapper code to help upload them
+   * to the GL driver. We can also tweak this to use double buffered arrays
+   * if we find that to be faster on some hardware and/or drivers.
+   */
+  GskGLBuffer vertices;
+
+  /* The GskGLAttachmentState contains information about our FBO and texture
+   * attachments as we process incoming operations. We snapshot them into
+   * various batches so that we can compare differences between merge
+   * candidates.
+   */
+  GskGLAttachmentState *attachments;
+
+  /* The uniform state across all programs. We snapshot this into batches so
+   * that we can compare uniform state between batches to give us more
+   * chances at merging draw commands.
+   */
+  GskGLUniformState *uniforms;
+
+  /* Current program if we are in a draw so that we can send commands
+   * to the uniform state as needed.
+   */
+  GskGLUniformProgram *program_info;
+
+  /* The profiler instance to deliver timing/etc data */
+  GskProfiler *profiler;
+  GskGLProfiler *gl_profiler;
+
+  /* Array of GskGLCommandBind which denote what textures need to be attached
+   * to which slot. GskGLCommandDraw.bind_offset and bind_count reference this
+   * array to determine what to attach.
+   */
+  GskGLCommandBinds batch_binds;
+
+  /* Array of GskGLCommandUniform denoting which uniforms must be updated
+   * before the glDrawArrays() may be called. These are referenced from the
+   * GskGLCommandDraw.uniform_offset and uniform_count fields.
+   */
+  GskGLCommandUniforms batch_uniforms;
+
+  /* String storage for debug groups */
+  GStringChunk *debug_groups;
+
+  /* Discovered max texture size when loading the command queue so that we
+   * can either scale down or slice textures to fit within this size. Assumed
+   * to be both height and width.
+   */
+  int max_texture_size;
+
+  /* The index of the last batch in @batches, which may not be the element
+   * at the end of the array, as batches can be reordered. This is used to
+   * update the "next" index when adding a new batch.
+   */
+  gint16 tail_batch_index;
+  gint16 head_batch_index;
+
+  /* Max framebuffer we used, so we can sort items faster */
+  guint fbo_max;
+
+  /* Various GSK and GDK metric counter ids */
+  struct {
+    GQuark n_frames;
+    GQuark cpu_time;
+    GQuark gpu_time;
+    guint n_binds;
+    guint n_fbos;
+    guint n_uniforms;
+    guint n_uploads;
+  } metrics;
+
+  /* Counter for uploads on the frame */
+  guint n_uploads;
+
+  /* If we're inside a begin/end_frame pair */
+  guint in_frame : 1;
+
+  /* If we're inside of a begin_draw()/end_draw() pair. */
+  guint in_draw : 1;
+};
+
+GskGLCommandQueue *gsk_gl_command_queue_new                  (GdkGLContext             *context,
+                                                              GskGLUniformState        *uniforms);
+void               gsk_gl_command_queue_set_profiler         (GskGLCommandQueue        *self,
+                                                              GskProfiler              *profiler);
+GdkGLContext      *gsk_gl_command_queue_get_context          (GskGLCommandQueue        *self);
+void               gsk_gl_command_queue_make_current         (GskGLCommandQueue        *self);
+void               gsk_gl_command_queue_begin_frame          (GskGLCommandQueue        *self);
+void               gsk_gl_command_queue_end_frame            (GskGLCommandQueue        *self);
+void               gsk_gl_command_queue_execute              (GskGLCommandQueue        *self,
+                                                              guint                     surface_height,
+                                                              guint                     scale_factor,
+                                                              const cairo_region_t     *scissor);
+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);
+int                gsk_gl_command_queue_create_texture       (GskGLCommandQueue        *self,
+                                                              int                       width,
+                                                              int                       height,
+                                                              int                       min_filter,
+                                                              int                       mag_filter);
+guint              gsk_gl_command_queue_create_framebuffer   (GskGLCommandQueue        *self);
+gboolean           gsk_gl_command_queue_create_render_target (GskGLCommandQueue        *self,
+                                                              int                       width,
+                                                              int                       height,
+                                                              int                       min_filter,
+                                                              int                       mag_filter,
+                                                              guint                    *out_fbo_id,
+                                                              guint                    *out_texture_id);
+void               gsk_gl_command_queue_delete_program       (GskGLCommandQueue        *self,
+                                                              guint                     program_id);
+void               gsk_gl_command_queue_clear                (GskGLCommandQueue        *self,
+                                                              guint                     clear_bits,
+                                                              const graphene_rect_t    *viewport);
+void               gsk_gl_command_queue_begin_draw           (GskGLCommandQueue        *self,
+                                                              GskGLUniformProgram      *program_info,
+                                                              const graphene_rect_t    *viewport);
+void               gsk_gl_command_queue_end_draw             (GskGLCommandQueue        *self);
+void               gsk_gl_command_queue_split_draw           (GskGLCommandQueue        *self);
+
+static inline GskGLCommandBatch *
+gsk_gl_command_queue_get_batch (GskGLCommandQueue *self)
+{
+  return gsk_gl_command_batches_tail (&self->batches);
+}
+
+static inline GskGLDrawVertex *
+gsk_gl_command_queue_add_vertices (GskGLCommandQueue *self)
+{
+  gsk_gl_command_queue_get_batch (self)->draw.vbo_count += GSK_GL_N_VERTICES;
+  return gsk_gl_buffer_advance (&self->vertices, GSK_GL_N_VERTICES);
+}
+
+static inline GskGLDrawVertex *
+gsk_gl_command_queue_add_n_vertices (GskGLCommandQueue *self,
+                                     guint              count)
+{
+  /* This is a batch form of gsk_gl_command_queue_add_vertices(). Note that
+   * it does *not* add the count to .draw.vbo_count as the caller is responsible
+   * for that.
+   */
+  return gsk_gl_buffer_advance (&self->vertices, GSK_GL_N_VERTICES * count);
+}
+
+static inline void
+gsk_gl_command_queue_retract_n_vertices (GskGLCommandQueue *self,
+                                         guint              count)
+{
+  /* Like gsk_gl_command_queue_add_n_vertices(), this does not tweak
+   * the draw vbo_count.
+   */
+  gsk_gl_buffer_retract (&self->vertices, GSK_GL_N_VERTICES * count);
+}
+
+static inline guint
+gsk_gl_command_queue_bind_framebuffer (GskGLCommandQueue *self,
+                                       guint              framebuffer)
+{
+  guint ret = self->attachments->fbo.id;
+  gsk_gl_attachment_state_bind_framebuffer (self->attachments, framebuffer);
+  return ret;
+}
+
+G_END_DECLS
+
+#endif /* __GSK_GL_COMMAND_QUEUE_PRIVATE_H__ */
diff --git a/gsk/next/gskglcompiler.c b/gsk/next/gskglcompiler.c
new file mode 100644
index 0000000000..d51d10fc3a
--- /dev/null
+++ b/gsk/next/gskglcompiler.c
@@ -0,0 +1,678 @@
+/* gskglcompiler.c
+ *
+ * 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 <gsk/gskdebugprivate.h>
+#include <gio/gio.h>
+#include <string.h>
+
+#include "gskglcommandqueueprivate.h"
+#include "gskglcompilerprivate.h"
+#include "gskglprogramprivate.h"
+
+#define SHADER_VERSION_GLES       100
+#define SHADER_VERSION_GL2_LEGACY 110
+#define SHADER_VERSION_GL3_LEGACY 130
+#define SHADER_VERSION_GL3        150
+
+struct _GskGLCompiler
+{
+  GObject parent_instance;
+
+  GskNextDriver *driver;
+
+  GBytes *all_preamble;
+  GBytes *fragment_preamble;
+  GBytes *vertex_preamble;
+  GBytes *fragment_source;
+  GBytes *fragment_suffix;
+  GBytes *vertex_source;
+  GBytes *vertex_suffix;
+
+  GArray *attrib_locations;
+
+  int glsl_version;
+
+  guint gl3 : 1;
+  guint gles : 1;
+  guint legacy : 1;
+  guint debug_shaders : 1;
+};
+
+typedef struct _GskGLProgramAttrib
+{
+  const char *name;
+  guint       location;
+} GskGLProgramAttrib;
+
+static GBytes *empty_bytes;
+
+G_DEFINE_TYPE (GskGLCompiler, gsk_gl_compiler, G_TYPE_OBJECT)
+
+static void
+gsk_gl_compiler_finalize (GObject *object)
+{
+  GskGLCompiler *self = (GskGLCompiler *)object;
+
+  g_clear_pointer (&self->all_preamble, g_bytes_unref);
+  g_clear_pointer (&self->fragment_preamble, g_bytes_unref);
+  g_clear_pointer (&self->vertex_preamble, g_bytes_unref);
+  g_clear_pointer (&self->vertex_suffix, g_bytes_unref);
+  g_clear_pointer (&self->fragment_source, g_bytes_unref);
+  g_clear_pointer (&self->fragment_suffix, g_bytes_unref);
+  g_clear_pointer (&self->vertex_source, g_bytes_unref);
+  g_clear_pointer (&self->attrib_locations, g_array_unref);
+  g_clear_object (&self->driver);
+
+  G_OBJECT_CLASS (gsk_gl_compiler_parent_class)->finalize (object);
+}
+
+static void
+gsk_gl_compiler_class_init (GskGLCompilerClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = gsk_gl_compiler_finalize;
+
+  empty_bytes = g_bytes_new (NULL, 0);
+}
+
+static void
+gsk_gl_compiler_init (GskGLCompiler *self)
+{
+  self->glsl_version = 150;
+  self->attrib_locations = g_array_new (FALSE, FALSE, sizeof (GskGLProgramAttrib));
+  self->all_preamble = g_bytes_ref (empty_bytes);
+  self->vertex_preamble = g_bytes_ref (empty_bytes);
+  self->fragment_preamble = g_bytes_ref (empty_bytes);
+  self->vertex_source = g_bytes_ref (empty_bytes);
+  self->vertex_suffix = g_bytes_ref (empty_bytes);
+  self->fragment_source = g_bytes_ref (empty_bytes);
+  self->fragment_suffix = g_bytes_ref (empty_bytes);
+}
+
+GskGLCompiler *
+gsk_gl_compiler_new (GskNextDriver *driver,
+                     gboolean       debug_shaders)
+{
+  GskGLCompiler *self;
+  GdkGLContext *context;
+
+  g_return_val_if_fail (GSK_IS_NEXT_DRIVER (driver), NULL);
+  g_return_val_if_fail (driver->shared_command_queue != NULL, NULL);
+
+  self = g_object_new (GSK_TYPE_GL_COMPILER, NULL);
+  self->driver = g_object_ref (driver);
+  self->debug_shaders = !!debug_shaders;
+
+  context = gsk_gl_command_queue_get_context (self->driver->shared_command_queue);
+
+  if (gdk_gl_context_get_use_es (context))
+    {
+      self->glsl_version = SHADER_VERSION_GLES;
+      self->gles = TRUE;
+    }
+  else if (gdk_gl_context_is_legacy (context))
+    {
+      int maj, min;
+
+      gdk_gl_context_get_version (context, &maj, &min);
+
+      if (maj == 3)
+        self->glsl_version = SHADER_VERSION_GL3_LEGACY;
+      else
+        self->glsl_version = SHADER_VERSION_GL2_LEGACY;
+
+      self->legacy = TRUE;
+    }
+  else
+    {
+      self->glsl_version = SHADER_VERSION_GL3;
+      self->gl3 = TRUE;
+    }
+
+  gsk_gl_command_queue_make_current (self->driver->shared_command_queue);
+
+  return g_steal_pointer (&self);
+}
+
+void
+gsk_gl_compiler_bind_attribute (GskGLCompiler *self,
+                                const char    *name,
+                                guint          location)
+{
+  GskGLProgramAttrib attrib;
+
+  g_return_if_fail (GSK_IS_GL_COMPILER (self));
+  g_return_if_fail (name != NULL);
+  g_return_if_fail (location < 32);
+
+  attrib.name = g_intern_string (name);
+  attrib.location = location;
+
+  g_array_append_val (self->attrib_locations, attrib);
+}
+
+void
+gsk_gl_compiler_clear_attributes (GskGLCompiler *self)
+{
+  g_return_if_fail (GSK_IS_GL_COMPILER (self));
+
+  g_array_set_size (self->attrib_locations, 0);
+}
+
+void
+gsk_gl_compiler_set_preamble (GskGLCompiler     *self,
+                              GskGLCompilerKind  kind,
+                              GBytes            *preamble_bytes)
+{
+  GBytes **loc = NULL;
+
+  g_return_if_fail (GSK_IS_GL_COMPILER (self));
+  g_return_if_fail (preamble_bytes != NULL);
+
+  if (kind == GSK_GL_COMPILER_ALL)
+    loc = &self->all_preamble;
+  else if (kind == GSK_GL_COMPILER_FRAGMENT)
+    loc = &self->fragment_preamble;
+  else if (kind == GSK_GL_COMPILER_VERTEX)
+    loc = &self->vertex_preamble;
+  else
+    g_return_if_reached ();
+
+  g_assert (loc != NULL);
+
+  if (*loc != preamble_bytes)
+    {
+      g_clear_pointer (loc, g_bytes_unref);
+      *loc = preamble_bytes ? g_bytes_ref (preamble_bytes) : NULL;
+    }
+}
+
+void
+gsk_gl_compiler_set_preamble_from_resource (GskGLCompiler     *self,
+                                            GskGLCompilerKind  kind,
+                                            const char        *resource_path)
+{
+  GError *error = NULL;
+  GBytes *bytes;
+
+  g_return_if_fail (GSK_IS_GL_COMPILER (self));
+  g_return_if_fail (kind == GSK_GL_COMPILER_ALL ||
+                    kind == GSK_GL_COMPILER_VERTEX ||
+                    kind == GSK_GL_COMPILER_FRAGMENT);
+  g_return_if_fail (resource_path != NULL);
+
+  bytes = g_resources_lookup_data (resource_path,
+                                   G_RESOURCE_LOOKUP_FLAGS_NONE,
+                                   &error);
+
+  if (bytes == NULL)
+    g_warning ("Cannot set shader from resource: %s", error->message);
+  else
+    gsk_gl_compiler_set_preamble (self, kind, bytes);
+
+  g_clear_pointer (&bytes, g_bytes_unref);
+  g_clear_error (&error);
+}
+
+void
+gsk_gl_compiler_set_source (GskGLCompiler     *self,
+                            GskGLCompilerKind  kind,
+                            GBytes            *source_bytes)
+{
+  GBytes **loc = NULL;
+
+  g_return_if_fail (GSK_IS_GL_COMPILER (self));
+  g_return_if_fail (kind == GSK_GL_COMPILER_ALL ||
+                    kind == GSK_GL_COMPILER_VERTEX ||
+                    kind == GSK_GL_COMPILER_FRAGMENT);
+
+  if (source_bytes == NULL)
+    source_bytes = empty_bytes;
+
+  /* If kind is ALL, then we need to split the fragment and
+   * vertex shaders from the bytes and assign them individually.
+   * This safely scans for FRAGMENT_SHADER and VERTEX_SHADER as
+   * specified within the GLSL resources. Some care is taken to
+   * use GBytes which reference the original bytes instead of
+   * copying them.
+   */
+  if (kind == GSK_GL_COMPILER_ALL)
+    {
+      gsize len = 0;
+      const char *source;
+      const char *vertex_shader_start;
+      const char *fragment_shader_start;
+      const char *endpos;
+      GBytes *fragment_bytes;
+      GBytes *vertex_bytes;
+
+      g_clear_pointer (&self->fragment_source, g_bytes_unref);
+      g_clear_pointer (&self->vertex_source, g_bytes_unref);
+
+      source = g_bytes_get_data (source_bytes, &len);
+      endpos = source + len;
+      vertex_shader_start = g_strstr_len (source, len, "VERTEX_SHADER");
+      fragment_shader_start = g_strstr_len (source, len, "FRAGMENT_SHADER");
+
+      if (vertex_shader_start == NULL)
+        {
+          g_warning ("Failed to locate VERTEX_SHADER in shader source");
+          return;
+        }
+
+      if (fragment_shader_start == NULL)
+        {
+          g_warning ("Failed to locate FRAGMENT_SHADER in shader source");
+          return;
+        }
+
+      if (vertex_shader_start > fragment_shader_start)
+        {
+          g_warning ("VERTEX_SHADER must come before FRAGMENT_SHADER");
+          return;
+        }
+
+      /* Locate next newlines */
+      while (vertex_shader_start < endpos && vertex_shader_start[0] != '\n')
+        vertex_shader_start++;
+      while (fragment_shader_start < endpos && fragment_shader_start[0] != '\n')
+        fragment_shader_start++;
+
+      vertex_bytes = g_bytes_new_from_bytes (source_bytes,
+                                             vertex_shader_start - source,
+                                             fragment_shader_start - vertex_shader_start);
+      fragment_bytes = g_bytes_new_from_bytes (source_bytes,
+                                               fragment_shader_start - source,
+                                               endpos - fragment_shader_start);
+
+      gsk_gl_compiler_set_source (self, GSK_GL_COMPILER_VERTEX, vertex_bytes);
+      gsk_gl_compiler_set_source (self, GSK_GL_COMPILER_FRAGMENT, fragment_bytes);
+
+      g_bytes_unref (fragment_bytes);
+      g_bytes_unref (vertex_bytes);
+
+      return;
+    }
+
+  if (kind == GSK_GL_COMPILER_FRAGMENT)
+    loc = &self->fragment_source;
+  else if (kind == GSK_GL_COMPILER_VERTEX)
+    loc = &self->vertex_source;
+  else
+    g_return_if_reached ();
+
+  if (*loc != source_bytes)
+    {
+      g_clear_pointer (loc, g_bytes_unref);
+      *loc = g_bytes_ref (source_bytes);
+    }
+}
+
+void
+gsk_gl_compiler_set_source_from_resource (GskGLCompiler     *self,
+                                          GskGLCompilerKind  kind,
+                                          const char        *resource_path)
+{
+  GError *error = NULL;
+  GBytes *bytes;
+
+  g_return_if_fail (GSK_IS_GL_COMPILER (self));
+  g_return_if_fail (kind == GSK_GL_COMPILER_ALL ||
+                    kind == GSK_GL_COMPILER_VERTEX ||
+                    kind == GSK_GL_COMPILER_FRAGMENT);
+  g_return_if_fail (resource_path != NULL);
+
+  bytes = g_resources_lookup_data (resource_path,
+                                   G_RESOURCE_LOOKUP_FLAGS_NONE,
+                                   &error);
+
+  if (bytes == NULL)
+    g_warning ("Cannot set shader from resource: %s", error->message);
+  else
+    gsk_gl_compiler_set_source (self, kind, bytes);
+
+  g_clear_pointer (&bytes, g_bytes_unref);
+  g_clear_error (&error);
+}
+
+void
+gsk_gl_compiler_set_suffix (GskGLCompiler     *self,
+                            GskGLCompilerKind  kind,
+                            GBytes            *suffix_bytes)
+{
+  GBytes **loc;
+
+  g_return_if_fail (GSK_IS_GL_COMPILER (self));
+  g_return_if_fail (kind == GSK_GL_COMPILER_VERTEX ||
+                    kind == GSK_GL_COMPILER_FRAGMENT);
+  g_return_if_fail (suffix_bytes != NULL);
+
+  if (suffix_bytes == NULL)
+    suffix_bytes = empty_bytes;
+
+  if (kind == GSK_GL_COMPILER_FRAGMENT)
+    loc = &self->fragment_suffix;
+  else if (kind == GSK_GL_COMPILER_VERTEX)
+    loc = &self->vertex_suffix;
+  else
+    g_return_if_reached ();
+
+  if (*loc != suffix_bytes)
+    {
+      g_clear_pointer (loc, g_bytes_unref);
+      *loc = g_bytes_ref (suffix_bytes);
+    }
+}
+
+void
+gsk_gl_compiler_set_suffix_from_resource (GskGLCompiler     *self,
+                                          GskGLCompilerKind  kind,
+                                          const char        *resource_path)
+{
+  GError *error = NULL;
+  GBytes *bytes;
+
+  g_return_if_fail (GSK_IS_GL_COMPILER (self));
+  g_return_if_fail (kind == GSK_GL_COMPILER_VERTEX ||
+                    kind == GSK_GL_COMPILER_FRAGMENT);
+  g_return_if_fail (resource_path != NULL);
+
+  bytes = g_resources_lookup_data (resource_path,
+                                   G_RESOURCE_LOOKUP_FLAGS_NONE,
+                                   &error);
+
+  if (bytes == NULL)
+    g_warning ("Cannot set suffix from resource: %s", error->message);
+  else
+    gsk_gl_compiler_set_suffix (self, kind, bytes);
+
+  g_clear_pointer (&bytes, g_bytes_unref);
+  g_clear_error (&error);
+}
+
+static void
+prepend_line_numbers (char    *code,
+                      GString *s)
+{
+  char *p;
+  int line;
+
+  p = code;
+  line = 1;
+  while (*p)
+    {
+      char *end = strchr (p, '\n');
+      if (end)
+        end = end + 1; /* Include newline */
+      else
+        end = p + strlen (p);
+
+      g_string_append_printf (s, "%3d| ", line++);
+      g_string_append_len (s, p, end - p);
+
+      p = end;
+    }
+}
+
+static gboolean
+check_shader_error (int     shader_id,
+                    GError **error)
+{
+  GLint status;
+  GLint log_len;
+  GLint code_len;
+  char *buffer;
+  char *code;
+  GString *s;
+
+  glGetShaderiv (shader_id, GL_COMPILE_STATUS, &status);
+
+  if G_LIKELY (status == GL_TRUE)
+    return TRUE;
+
+  glGetShaderiv (shader_id, GL_INFO_LOG_LENGTH, &log_len);
+  buffer = g_malloc0 (log_len + 1);
+  glGetShaderInfoLog (shader_id, log_len, NULL, buffer);
+
+  glGetShaderiv (shader_id, GL_SHADER_SOURCE_LENGTH, &code_len);
+  code = g_malloc0 (code_len + 1);
+  glGetShaderSource (shader_id, code_len, NULL, code);
+
+  s = g_string_new ("");
+  prepend_line_numbers (code, s);
+
+  g_set_error (error,
+               GDK_GL_ERROR,
+               GDK_GL_ERROR_COMPILATION_FAILED,
+               "Compilation failure in shader.\n"
+               "Source Code: %s\n"
+               "\n"
+               "Error Message:\n"
+               "%s\n"
+               "\n",
+               s->str,
+               buffer);
+
+  g_string_free (s, TRUE);
+  g_free (buffer);
+  g_free (code);
+
+  return FALSE;
+}
+
+static void
+print_shader_info (const char *prefix,
+                   int         shader_id,
+                   const char *name)
+{
+  if (GSK_DEBUG_CHECK(SHADERS))
+    {
+      int code_len;
+
+      glGetShaderiv (shader_id, GL_SHADER_SOURCE_LENGTH, &code_len);
+
+      if (code_len > 0)
+        {
+          char *code;
+          GString *s;
+
+          code = g_malloc0 (code_len + 1);
+          glGetShaderSource (shader_id, code_len, NULL, code);
+
+          s = g_string_new (NULL);
+          prepend_line_numbers (code, s);
+
+          g_message ("%s %d, %s:\n%s",
+                     prefix, shader_id,
+                     name ? name : "unnamed",
+                     s->str);
+          g_string_free (s,  TRUE);
+          g_free (code);
+        }
+    }
+}
+
+static const char *
+get_shader_string (GBytes *bytes)
+{
+  /* 0 length bytes will give us NULL back */
+  const char *str = g_bytes_get_data (bytes, NULL);
+  return str ? str : "";
+}
+
+GskGLProgram *
+gsk_gl_compiler_compile (GskGLCompiler  *self,
+                         const char     *name,
+                         GError        **error)
+{
+  char version[32];
+  const char *debug = "";
+  const char *legacy = "";
+  const char *gl3 = "";
+  const char *gles = "";
+  int program_id;
+  int vertex_id;
+  int fragment_id;
+  int status;
+
+  g_return_val_if_fail (GSK_IS_GL_COMPILER (self), NULL);
+  g_return_val_if_fail (self->all_preamble != NULL, NULL);
+  g_return_val_if_fail (self->fragment_preamble != NULL, NULL);
+  g_return_val_if_fail (self->vertex_preamble != NULL, NULL);
+  g_return_val_if_fail (self->fragment_source != NULL, NULL);
+  g_return_val_if_fail (self->vertex_source != NULL, NULL);
+  g_return_val_if_fail (self->driver != NULL, NULL);
+
+  gsk_gl_command_queue_make_current (self->driver->command_queue);
+
+  g_snprintf (version, sizeof version, "#version %d\n", self->glsl_version);
+
+  if (self->debug_shaders)
+    debug = "#define GSK_DEBUG 1\n";
+
+  if (self->legacy)
+    legacy = "#define GSK_LEGACY 1\n";
+
+  if (self->gles)
+    gles = "#define GSK_GLES 1\n";
+
+  if (self->gl3)
+    gl3 = "#define GSK_GL3 1\n";
+
+  vertex_id = glCreateShader (GL_VERTEX_SHADER);
+  glShaderSource (vertex_id,
+                  9,
+                  (const char *[]) {
+                    version, debug, legacy, gl3, gles,
+                    get_shader_string (self->all_preamble),
+                    get_shader_string (self->vertex_preamble),
+                    get_shader_string (self->vertex_source),
+                    get_shader_string (self->vertex_suffix),
+                  },
+                  (int[]) {
+                    strlen (version),
+                    strlen (debug),
+                    strlen (legacy),
+                    strlen (gl3),
+                    strlen (gles),
+                    g_bytes_get_size (self->all_preamble),
+                    g_bytes_get_size (self->vertex_preamble),
+                    g_bytes_get_size (self->vertex_source),
+                    g_bytes_get_size (self->vertex_suffix),
+                  });
+  glCompileShader (vertex_id);
+
+  if (!check_shader_error (vertex_id, error))
+    {
+      glDeleteShader (vertex_id);
+      return NULL;
+    }
+
+  print_shader_info ("Vertex shader", vertex_id, name);
+
+  fragment_id = glCreateShader (GL_FRAGMENT_SHADER);
+  glShaderSource (fragment_id,
+                  9,
+                  (const char *[]) {
+                    version, debug, legacy, gl3, gles,
+                    get_shader_string (self->all_preamble),
+                    get_shader_string (self->fragment_preamble),
+                    get_shader_string (self->fragment_source),
+                    get_shader_string (self->fragment_suffix),
+                  },
+                  (int[]) {
+                    strlen (version),
+                    strlen (debug),
+                    strlen (legacy),
+                    strlen (gl3),
+                    strlen (gles),
+                    g_bytes_get_size (self->all_preamble),
+                    g_bytes_get_size (self->fragment_preamble),
+                    g_bytes_get_size (self->fragment_source),
+                    g_bytes_get_size (self->fragment_suffix),
+                  });
+  glCompileShader (fragment_id);
+
+  if (!check_shader_error (fragment_id, error))
+    {
+      glDeleteShader (vertex_id);
+      glDeleteShader (fragment_id);
+      return NULL;
+    }
+
+  print_shader_info ("Fragment shader", fragment_id, name);
+
+  program_id = glCreateProgram ();
+  glAttachShader (program_id, vertex_id);
+  glAttachShader (program_id, fragment_id);
+
+  for (guint i = 0; i < self->attrib_locations->len; i++)
+    {
+      const GskGLProgramAttrib *attrib;
+
+      attrib = &g_array_index (self->attrib_locations, GskGLProgramAttrib, i);
+      glBindAttribLocation (program_id, attrib->location, attrib->name);
+    }
+
+  glLinkProgram (program_id);
+
+  glGetProgramiv (program_id, GL_LINK_STATUS, &status);
+
+  glDetachShader (program_id, vertex_id);
+  glDeleteShader (vertex_id);
+
+  glDetachShader (program_id, fragment_id);
+  glDeleteShader (fragment_id);
+
+  if (status == GL_FALSE)
+    {
+      char *buffer = NULL;
+      int log_len = 0;
+
+      glGetProgramiv (program_id, GL_INFO_LOG_LENGTH, &log_len);
+
+      if (log_len > 0)
+        {
+          /* log_len includes NULL */
+          buffer = g_malloc0 (log_len);
+          glGetProgramInfoLog (program_id, log_len, NULL, buffer);
+        }
+
+      g_warning ("Linking failure in shader:\n%s",
+                 buffer ? buffer : "");
+
+      g_set_error (error,
+                   GDK_GL_ERROR,
+                   GDK_GL_ERROR_LINK_FAILED,
+                   "Linking failure in shader: %s",
+                   buffer ? buffer : "");
+
+      g_free (buffer);
+
+      glDeleteProgram (program_id);
+
+      return NULL;
+    }
+
+  return gsk_gl_program_new (self->driver, name, program_id);
+}
diff --git a/gsk/next/gskglcompilerprivate.h b/gsk/next/gskglcompilerprivate.h
new file mode 100644
index 0000000000..6f3a2774b3
--- /dev/null
+++ b/gsk/next/gskglcompilerprivate.h
@@ -0,0 +1,69 @@
+/* gskglcompilerprivate.h
+ *
+ * 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
+ */
+
+#ifndef __GSK_GL_COMPILER_PRIVATE_H__
+#define __GSK_GL_COMPILER_PRIVATE_H__
+
+#include "gskgltypesprivate.h"
+
+G_BEGIN_DECLS
+
+typedef enum _GskGLCompilerKind
+{
+  GSK_GL_COMPILER_ALL,
+  GSK_GL_COMPILER_FRAGMENT,
+  GSK_GL_COMPILER_VERTEX,
+} GskGLCompilerKind;
+
+#define GSK_TYPE_GL_COMPILER (gsk_gl_compiler_get_type())
+
+G_DECLARE_FINAL_TYPE (GskGLCompiler, gsk_gl_compiler, GSK, GL_COMPILER, GObject)
+
+GskGLCompiler *gsk_gl_compiler_new                        (GskNextDriver      *driver,
+                                                           gboolean            debug);
+void           gsk_gl_compiler_set_preamble               (GskGLCompiler      *self,
+                                                           GskGLCompilerKind   kind,
+                                                           GBytes             *preamble_bytes);
+void           gsk_gl_compiler_set_preamble_from_resource (GskGLCompiler      *self,
+                                                           GskGLCompilerKind   kind,
+                                                           const char         *resource_path);
+void           gsk_gl_compiler_set_source                 (GskGLCompiler      *self,
+                                                           GskGLCompilerKind   kind,
+                                                           GBytes             *source_bytes);
+void           gsk_gl_compiler_set_source_from_resource   (GskGLCompiler      *self,
+                                                           GskGLCompilerKind   kind,
+                                                           const char         *resource_path);
+void           gsk_gl_compiler_set_suffix                 (GskGLCompiler      *self,
+                                                           GskGLCompilerKind   kind,
+                                                           GBytes             *suffix_bytes);
+void           gsk_gl_compiler_set_suffix_from_resource   (GskGLCompiler      *self,
+                                                           GskGLCompilerKind   kind,
+                                                           const char         *resource_path);
+void           gsk_gl_compiler_bind_attribute             (GskGLCompiler      *self,
+                                                           const char         *name,
+                                                           guint               location);
+void           gsk_gl_compiler_clear_attributes           (GskGLCompiler      *self);
+GskGLProgram  *gsk_gl_compiler_compile                    (GskGLCompiler      *self,
+                                                           const char         *name,
+                                                           GError            **error);
+
+G_END_DECLS
+
+#endif /* __GSK_GL_COMPILER_PRIVATE_H__ */
diff --git a/gsk/next/gskgldriver.c b/gsk/next/gskgldriver.c
new file mode 100644
index 0000000000..03cb15a356
--- /dev/null
+++ b/gsk/next/gskgldriver.c
@@ -0,0 +1,1344 @@
+/* gskgldriver.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/gdktextureprivate.h>
+#include <gsk/gskdebugprivate.h>
+#include <gsk/gskglshaderprivate.h>
+#include <gsk/gskrendererprivate.h>
+
+#include "gskglcommandqueueprivate.h"
+#include "gskglcompilerprivate.h"
+#include "gskgldriverprivate.h"
+#include "gskglglyphlibraryprivate.h"
+#include "gskgliconlibraryprivate.h"
+#include "gskglprogramprivate.h"
+#include "gskglshadowlibraryprivate.h"
+#include "gskgltexturepoolprivate.h"
+
+#define ATLAS_SIZE 512
+
+typedef struct _GskGLTextureState
+{
+  GdkGLContext *context;
+  GLuint        texture_id;
+} GskGLTextureState;
+
+G_DEFINE_TYPE (GskNextDriver, gsk_next_driver, G_TYPE_OBJECT)
+
+static guint
+texture_key_hash (gconstpointer v)
+{
+  const GskTextureKey *k = (const GskTextureKey *)v;
+
+  return GPOINTER_TO_UINT (k->pointer)
+         + (guint)(k->scale_x * 100)
+         + (guint)(k->scale_y * 100)
+         + (guint)k->filter * 2 +
+         + (guint)k->pointer_is_child;
+}
+
+static gboolean
+texture_key_equal (gconstpointer v1, gconstpointer v2)
+{
+  const GskTextureKey *k1 = (const GskTextureKey *)v1;
+  const GskTextureKey *k2 = (const GskTextureKey *)v2;
+
+  return k1->pointer == k2->pointer &&
+         k1->scale_x == k2->scale_x &&
+         k1->scale_y == k2->scale_y &&
+         k1->filter == k2->filter &&
+         k1->pointer_is_child == k2->pointer_is_child &&
+         (!k1->pointer_is_child || memcmp (&k1->parent_rect, &k2->parent_rect, sizeof k1->parent_rect) == 0);
+}
+
+static inline void
+write_atlas_to_png (GskGLTextureAtlas *atlas,
+                    const char        *filename)
+{
+  int stride = cairo_format_stride_for_width (CAIRO_FORMAT_ARGB32, atlas->width);
+  guchar *data = g_malloc (atlas->height * stride);
+  cairo_surface_t *s;
+
+  glBindTexture (GL_TEXTURE_2D, atlas->texture_id);
+  glGetTexImage (GL_TEXTURE_2D, 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, data);
+  s = cairo_image_surface_create_for_data (data, CAIRO_FORMAT_ARGB32, atlas->width, atlas->height, stride);
+  cairo_surface_write_to_png (s, filename);
+
+  cairo_surface_destroy (s);
+  g_free (data);
+}
+
+static void
+remove_texture_key_for_id (GskNextDriver *self,
+                           guint          texture_id)
+{
+  GskTextureKey *key;
+
+  g_assert (GSK_IS_NEXT_DRIVER (self));
+  g_assert (texture_id > 0);
+
+  /* g_hash_table_remove() will cause @key to be freed */
+  if (g_hash_table_steal_extended (self->texture_id_to_key,
+                                   GUINT_TO_POINTER (texture_id),
+                                   NULL,
+                                   (gpointer *)&key))
+    g_hash_table_remove (self->key_to_texture_id, key);
+}
+
+static void
+gsk_gl_texture_destroyed (gpointer data)
+{
+  ((GskGLTexture *)data)->user = NULL;
+}
+
+static guint
+gsk_next_driver_collect_unused_textures (GskNextDriver *self,
+                                         gint64         watermark)
+{
+  GHashTableIter iter;
+  gpointer k, v;
+  guint old_size;
+  guint collected;
+
+  g_assert (GSK_IS_NEXT_DRIVER (self));
+
+  old_size = g_hash_table_size (self->textures);
+
+  g_hash_table_iter_init (&iter, self->textures);
+  while (g_hash_table_iter_next (&iter, &k, &v))
+    {
+      GskGLTexture *t = v;
+
+      if (t->user || t->permanent)
+        continue;
+
+      if (t->last_used_in_frame <= watermark)
+        {
+          g_hash_table_iter_steal (&iter);
+
+          g_assert (t->width_link.prev == NULL);
+          g_assert (t->width_link.next == NULL);
+          g_assert (t->width_link.data == t);
+          g_assert (t->height_link.prev == NULL);
+          g_assert (t->height_link.next == NULL);
+          g_assert (t->height_link.data == t);
+
+          /* Steal this texture and put it back into the pool */
+          remove_texture_key_for_id (self, t->texture_id);
+          gsk_gl_texture_pool_put (&self->texture_pool, t);
+        }
+    }
+
+  collected = old_size - g_hash_table_size (self->textures);
+
+  return collected;
+}
+
+static void
+gsk_gl_texture_atlas_free (GskGLTextureAtlas *atlas)
+{
+  if (atlas->texture_id != 0)
+    {
+      glDeleteTextures (1, &atlas->texture_id);
+      atlas->texture_id = 0;
+    }
+
+  g_clear_pointer (&atlas->nodes, g_free);
+  g_slice_free (GskGLTextureAtlas, atlas);
+}
+
+GskGLTextureAtlas *
+gsk_next_driver_create_atlas (GskNextDriver *self)
+{
+  GskGLTextureAtlas *atlas;
+
+  g_return_val_if_fail (GSK_IS_NEXT_DRIVER (self), NULL);
+
+  atlas = g_slice_new0 (GskGLTextureAtlas);
+  atlas->width = ATLAS_SIZE;
+  atlas->height = ATLAS_SIZE;
+  /* TODO: We might want to change the strategy about the amount of
+   *       nodes here? stb_rect_pack.h says width is optimal. */
+  atlas->nodes = g_malloc0_n (atlas->width, sizeof (struct stbrp_node));
+  stbrp_init_target (&atlas->context, atlas->width, atlas->height, atlas->nodes, atlas->width);
+  atlas->texture_id = gsk_gl_command_queue_create_texture (self->command_queue,
+                                                           atlas->width,
+                                                           atlas->height,
+                                                           GL_LINEAR,
+                                                           GL_LINEAR);
+
+  gdk_gl_context_label_object_printf (gdk_gl_context_get_current (),
+                                      GL_TEXTURE, atlas->texture_id,
+                                      "Texture atlas %d",
+                                      atlas->texture_id);
+
+  g_ptr_array_add (self->atlases, atlas);
+
+  return atlas;
+}
+
+static void
+remove_program (gpointer data)
+{
+  GskGLProgram *program = data;
+
+  g_assert (!program || GSK_IS_GL_PROGRAM (program));
+
+  if (program != NULL)
+    {
+      gsk_gl_program_delete (program);
+      g_object_unref (program);
+    }
+}
+
+static void
+gsk_next_driver_shader_weak_cb (gpointer  data,
+                                GObject  *where_object_was)
+{
+  GskNextDriver *self = data;
+
+  g_assert (GSK_IS_NEXT_DRIVER (self));
+
+  if (self->shader_cache != NULL)
+    g_hash_table_remove (self->shader_cache, where_object_was);
+}
+
+static void
+gsk_next_driver_dispose (GObject *object)
+{
+  GskNextDriver *self = (GskNextDriver *)object;
+
+  g_assert (GSK_IS_NEXT_DRIVER (self));
+  g_assert (self->in_frame == FALSE);
+
+#define GSK_GL_NO_UNIFORMS
+#define GSK_GL_ADD_UNIFORM(pos, KEY, name)
+#define GSK_GL_DEFINE_PROGRAM(name, resource, uniforms) \
+  G_STMT_START {                                        \
+    if (self->name)                                     \
+      gsk_gl_program_delete (self->name);               \
+    g_clear_object (&self->name);                       \
+  } G_STMT_END;
+# include "gskglprograms.defs"
+#undef GSK_GL_NO_UNIFORMS
+#undef GSK_GL_ADD_UNIFORM
+#undef GSK_GL_DEFINE_PROGRAM
+
+  if (self->shader_cache != NULL)
+    {
+      GHashTableIter iter;
+      gpointer k, v;
+
+      g_hash_table_iter_init (&iter, self->shader_cache);
+      while (g_hash_table_iter_next (&iter, &k, &v))
+        {
+          GskGLShader *shader = k;
+          g_object_weak_unref (G_OBJECT (shader),
+                               gsk_next_driver_shader_weak_cb,
+                               self);
+          g_hash_table_iter_remove (&iter);
+        }
+
+      g_clear_pointer (&self->shader_cache, g_hash_table_unref);
+    }
+
+  if (self->command_queue != NULL)
+    {
+      gsk_gl_command_queue_make_current (self->command_queue);
+      gsk_next_driver_collect_unused_textures (self, 0);
+      g_clear_object (&self->command_queue);
+    }
+
+  if (self->autorelease_framebuffers->len > 0)
+    {
+      glDeleteFramebuffers (self->autorelease_framebuffers->len,
+                            (GLuint *)(gpointer)self->autorelease_framebuffers->data);
+      self->autorelease_framebuffers->len = 0;
+    }
+
+  gsk_gl_texture_pool_clear (&self->texture_pool);
+
+  g_assert (!self->textures || g_hash_table_size (self->textures) == 0);
+  g_assert (!self->texture_id_to_key || g_hash_table_size (self->texture_id_to_key) == 0);
+  g_assert (!self->key_to_texture_id|| g_hash_table_size (self->key_to_texture_id) == 0);
+
+  g_clear_object (&self->glyphs);
+  g_clear_object (&self->icons);
+  g_clear_object (&self->shadows);
+
+  g_clear_pointer (&self->atlases, g_ptr_array_unref);
+  g_clear_pointer (&self->autorelease_framebuffers, g_array_unref);
+  g_clear_pointer (&self->key_to_texture_id, g_hash_table_unref);
+  g_clear_pointer (&self->textures, g_hash_table_unref);
+  g_clear_pointer (&self->key_to_texture_id, g_hash_table_unref);
+  g_clear_pointer (&self->texture_id_to_key, g_hash_table_unref);
+  g_clear_pointer (&self->render_targets, g_ptr_array_unref);
+  g_clear_pointer (&self->shader_cache, g_hash_table_unref);
+
+  g_clear_object (&self->command_queue);
+  g_clear_object (&self->shared_command_queue);
+
+  G_OBJECT_CLASS (gsk_next_driver_parent_class)->dispose (object);
+}
+
+static void
+gsk_next_driver_class_init (GskNextDriverClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->dispose = gsk_next_driver_dispose;
+}
+
+static void
+gsk_next_driver_init (GskNextDriver *self)
+{
+  self->autorelease_framebuffers = g_array_new (FALSE, FALSE, sizeof (guint));
+  self->textures = g_hash_table_new_full (NULL, NULL, NULL,
+                                          (GDestroyNotify)gsk_gl_texture_free);
+  self->texture_id_to_key = g_hash_table_new (NULL, NULL);
+  self->key_to_texture_id = g_hash_table_new_full (texture_key_hash,
+                                                   texture_key_equal,
+                                                   g_free,
+                                                   NULL);
+  self->shader_cache = g_hash_table_new_full (NULL, NULL, NULL, remove_program);
+  gsk_gl_texture_pool_init (&self->texture_pool);
+  self->render_targets = g_ptr_array_new ();
+  self->atlases = g_ptr_array_new_with_free_func ((GDestroyNotify)gsk_gl_texture_atlas_free);
+}
+
+static gboolean
+gsk_next_driver_load_programs (GskNextDriver  *self,
+                               GError        **error)
+{
+  GskGLCompiler *compiler;
+  gboolean ret = FALSE;
+
+  g_assert (GSK_IS_NEXT_DRIVER (self));
+  g_assert (GSK_IS_GL_COMMAND_QUEUE (self->command_queue));
+
+  compiler = gsk_gl_compiler_new (self, self->debug);
+
+  /* Setup preambles that are shared by all shaders */
+  gsk_gl_compiler_set_preamble_from_resource (compiler,
+                                              GSK_GL_COMPILER_ALL,
+                                              "/org/gtk/libgsk/glsl/preamble.glsl");
+  gsk_gl_compiler_set_preamble_from_resource (compiler,
+                                              GSK_GL_COMPILER_VERTEX,
+                                              "/org/gtk/libgsk/glsl/preamble.vs.glsl");
+  gsk_gl_compiler_set_preamble_from_resource (compiler,
+                                              GSK_GL_COMPILER_FRAGMENT,
+                                              "/org/gtk/libgsk/glsl/preamble.fs.glsl");
+
+  /* Setup attributes that are provided via VBO */
+  gsk_gl_compiler_bind_attribute (compiler, "aPosition", 0);
+  gsk_gl_compiler_bind_attribute (compiler, "aUv", 1);
+
+  /* Use XMacros to register all of our programs and their uniforms */
+#define GSK_GL_NO_UNIFORMS
+#define GSK_GL_ADD_UNIFORM(pos, KEY, name)                                                      \
+  gsk_gl_program_add_uniform (program, #name, UNIFORM_##KEY);
+#define GSK_GL_DEFINE_PROGRAM(name, resource, uniforms)                                         \
+  G_STMT_START {                                                                                \
+    GskGLProgram *program;                                                                      \
+    gboolean have_alpha;                                                                        \
+    gboolean have_source;                                                                       \
+                                                                                                \
+    gsk_gl_compiler_set_source_from_resource (compiler, GSK_GL_COMPILER_ALL, resource);         \
+                                                                                                \
+    if (!(program = gsk_gl_compiler_compile (compiler, #name, error)))                          \
+      goto failure;                                                                             \
+                                                                                                \
+    have_alpha = gsk_gl_program_add_uniform (program, "u_alpha", UNIFORM_SHARED_ALPHA);         \
+    have_source = gsk_gl_program_add_uniform (program, "u_source", UNIFORM_SHARED_SOURCE);      \
+    gsk_gl_program_add_uniform (program, "u_clip_rect", UNIFORM_SHARED_CLIP_RECT);              \
+    gsk_gl_program_add_uniform (program, "u_viewport", UNIFORM_SHARED_VIEWPORT);                \
+    gsk_gl_program_add_uniform (program, "u_projection", UNIFORM_SHARED_PROJECTION);            \
+    gsk_gl_program_add_uniform (program, "u_modelview", UNIFORM_SHARED_MODELVIEW);              \
+                                                                                                \
+    uniforms                                                                                    \
+                                                                                                \
+    gsk_gl_program_uniforms_added (program, have_source);                                       \
+                                                                                                \
+    if (have_alpha)                                                                             \
+      gsk_gl_program_set_uniform1f (program, UNIFORM_SHARED_ALPHA, 0, 1.0f);                    \
+                                                                                                \
+    *(GskGLProgram **)(((guint8 *)self) + G_STRUCT_OFFSET (GskNextDriver, name)) =              \
+        g_steal_pointer (&program);                                                             \
+  } G_STMT_END;
+# include "gskglprograms.defs"
+#undef GSK_GL_DEFINE_PROGRAM
+#undef GSK_GL_ADD_UNIFORM
+
+  ret = TRUE;
+
+failure:
+  g_clear_object (&compiler);
+
+  return ret;
+}
+
+/**
+ * gsk_next_driver_autorelease_framebuffer:
+ * @self: a #GskNextDriver
+ * @framebuffer_id: the id of the OpenGL framebuffer
+ *
+ * Marks @framebuffer_id to be deleted when the current frame has cmopleted.
+ */
+static void
+gsk_next_driver_autorelease_framebuffer (GskNextDriver *self,
+                                         guint          framebuffer_id)
+{
+  g_assert (GSK_IS_NEXT_DRIVER (self));
+
+  g_array_append_val (self->autorelease_framebuffers, framebuffer_id);
+}
+
+static GskNextDriver *
+gsk_next_driver_new (GskGLCommandQueue  *command_queue,
+                     gboolean            debug_shaders,
+                     GError            **error)
+{
+  GskNextDriver *self;
+  GdkGLContext *context;
+
+  g_return_val_if_fail (GSK_IS_GL_COMMAND_QUEUE (command_queue), NULL);
+
+  context = gsk_gl_command_queue_get_context (command_queue);
+
+  gdk_gl_context_make_current (context);
+
+  self = g_object_new (GSK_TYPE_NEXT_DRIVER, NULL);
+  self->command_queue = g_object_ref (command_queue);
+  self->shared_command_queue = g_object_ref (command_queue);
+  self->debug = !!debug_shaders;
+
+  if (!gsk_next_driver_load_programs (self, error))
+    {
+      g_object_unref (self);
+      return NULL;
+    }
+
+  self->glyphs = gsk_gl_glyph_library_new (self);
+  self->icons = gsk_gl_icon_library_new (self);
+  self->shadows = gsk_gl_shadow_library_new (self);
+
+  return g_steal_pointer (&self);
+}
+
+/**
+ * gsk_next_driver_from_shared_context:
+ * @context: a shared #GdkGLContext retrieved with gdk_gl_context_get_shared_context()
+ * @debug_shaders: if debug information for shaders should be displayed
+ * @error: location for error information
+ *
+ * Retrieves a driver for a shared context. Generally this is shared across all GL
+ * contexts for a display so that fewer programs are necessary for driving output.
+ *
+ * Returns: (transfer full): a #GskNextDriver if successful; otherwise %NULL and
+ *   @error is set.
+ */
+GskNextDriver *
+gsk_next_driver_from_shared_context (GdkGLContext  *context,
+                                     gboolean       debug_shaders,
+                                     GError       **error)
+{
+  GskGLCommandQueue *command_queue = NULL;
+  GskNextDriver *driver;
+
+  g_return_val_if_fail (GDK_IS_GL_CONTEXT (context), NULL);
+
+  if ((driver = g_object_get_data (G_OBJECT (context), "GSK_NEXT_DRIVER")))
+    return g_object_ref (driver);
+
+  gdk_gl_context_make_current (context);
+
+  /* Initially we create a command queue using the shared context. However,
+   * as frames are processed this will be replaced with the command queue
+   * for a given renderer. But since the programs are compiled into the
+   * shared context, all other contexts sharing with it will have access
+   * to those programs.
+   */
+  command_queue = gsk_gl_command_queue_new (context, NULL);
+
+  if (!(driver = gsk_next_driver_new (command_queue, debug_shaders, error)))
+    goto failure;
+
+  g_object_set_data_full (G_OBJECT (context),
+                          "GSK_NEXT_DRIVER",
+                          g_object_ref (driver),
+                          g_object_unref);
+
+failure:
+  g_clear_object (&command_queue);
+
+  return g_steal_pointer (&driver);
+}
+
+/**
+ * gsk_next_driver_begin_frame:
+ * @self: a #GskNextDriver
+ * @command_queue: A #GskGLCommandQueue from the renderer
+ *
+ * Begin a new frame.
+ *
+ * Texture atlases, pools, and other resources will be prepared to draw the
+ * next frame. The command queue should be one that was created for the
+ * target context to be drawn into (the context of the renderer's surface).
+ */
+void
+gsk_next_driver_begin_frame (GskNextDriver     *self,
+                             GskGLCommandQueue *command_queue)
+{
+  gint64 last_frame_id;
+
+  g_return_if_fail (GSK_IS_NEXT_DRIVER (self));
+  g_return_if_fail (GSK_IS_GL_COMMAND_QUEUE (command_queue));
+  g_return_if_fail (self->in_frame == FALSE);
+
+  last_frame_id = self->current_frame_id;
+
+  self->in_frame = TRUE;
+  self->current_frame_id++;
+
+  g_set_object (&self->command_queue, command_queue);
+
+  gsk_gl_command_queue_begin_frame (self->command_queue);
+
+  gsk_gl_texture_library_begin_frame (GSK_GL_TEXTURE_LIBRARY (self->icons));
+  gsk_gl_texture_library_begin_frame (GSK_GL_TEXTURE_LIBRARY (self->glyphs));
+  gsk_gl_shadow_library_begin_frame (self->shadows);
+
+  /* Remove all textures that are from a previous frame or are no
+   * longer used by linked GdkTexture. We do this at the beginning
+   * of the following frame instead of the end so that we reduce chances
+   * we block on any resources while delivering our frames.
+   */
+  gsk_next_driver_collect_unused_textures (self, last_frame_id - 1);
+}
+
+/**
+ * gsk_next_driver_end_frame:
+ * @self: a #GskNextDriver
+ *
+ * Clean up resources from drawing the current frame.
+ *
+ * Temporary resources used while drawing will be released.
+ */
+void
+gsk_next_driver_end_frame (GskNextDriver *self)
+{
+  g_return_if_fail (GSK_IS_NEXT_DRIVER (self));
+  g_return_if_fail (self->in_frame == TRUE);
+
+  gsk_gl_command_queue_make_current (self->command_queue);
+  gsk_gl_command_queue_end_frame (self->command_queue);
+
+  gsk_gl_texture_library_end_frame (GSK_GL_TEXTURE_LIBRARY (self->icons));
+  gsk_gl_texture_library_end_frame (GSK_GL_TEXTURE_LIBRARY (self->glyphs));
+
+#if 0
+  g_print ("End Frame: textures=%u with_key=%u reverse=%u pool=%u:%u atlases=%u\n",
+           g_hash_table_size (self->textures),
+           g_hash_table_size (self->key_to_texture_id),
+           g_hash_table_size (self->texture_id_to_key),
+           self->texture_pool.by_width.length,
+           self->texture_pool.by_height.length,
+           self->atlases->len);
+#endif
+
+  self->in_frame = FALSE;
+}
+
+/**
+ * gsk_next_driver_after_frame:
+ * @self: a #GskNextDriver
+ *
+ * This function does post-frame cleanup operations.
+ *
+ * To reduce the chances of blocking on the driver it is performed
+ * after the frame has swapped buffers.
+ */
+void
+gsk_next_driver_after_frame (GskNextDriver *self)
+{
+  g_return_if_fail (GSK_IS_NEXT_DRIVER (self));
+  g_return_if_fail (self->in_frame == FALSE);
+
+  /* Release any render targets (possibly adding them to
+   * self->autorelease_framebuffers) so we can release the FBOs immediately
+   * afterwards.
+   */
+  while (self->render_targets->len > 0)
+    {
+      GskGLRenderTarget *render_target = g_ptr_array_index (self->render_targets, self->render_targets->len 
- 1);
+
+      gsk_next_driver_autorelease_framebuffer (self, render_target->framebuffer_id);
+      glDeleteTextures (1, &render_target->texture_id);
+      g_slice_free (GskGLRenderTarget, render_target);
+
+      self->render_targets->len--;
+    }
+
+  /* Now that we have collected render targets, release all the FBOs */
+  if (self->autorelease_framebuffers->len > 0)
+    {
+      glDeleteFramebuffers (self->autorelease_framebuffers->len,
+                            (GLuint *)(gpointer)self->autorelease_framebuffers->data);
+      self->autorelease_framebuffers->len = 0;
+    }
+
+  /* Release any cached textures we used during the frame */
+  gsk_gl_texture_pool_clear (&self->texture_pool);
+
+  /* Reset command queue to our shared queue incase we have operations
+   * that need to be processed outside of a frame (such as callbacks
+   * from external systems such as GDK).
+   */
+  g_set_object (&self->command_queue, self->shared_command_queue);
+}
+
+GdkGLContext *
+gsk_next_driver_get_context (GskNextDriver *self)
+{
+  g_return_val_if_fail (GSK_IS_NEXT_DRIVER (self), NULL);
+  g_return_val_if_fail (GSK_IS_GL_COMMAND_QUEUE (self->command_queue), NULL);
+
+  return gsk_gl_command_queue_get_context (self->command_queue);
+}
+
+/**
+ * gsk_next_driver_cache_texture:
+ * @self: a #GskNextDriver
+ * @key: the key for the texture
+ * @texture_id: the id of the texture to be cached
+ *
+ * Inserts @texture_id into the texture cache using @key.
+ *
+ * Textures can be looked up by @key after calling this function using
+ * gsk_next_driver_lookup_texture().
+ *
+ * Textures that have not been used within a number of frames will be
+ * purged from the texture cache automatically.
+ */
+void
+gsk_next_driver_cache_texture (GskNextDriver       *self,
+                               const GskTextureKey *key,
+                               guint                texture_id)
+{
+  GskTextureKey *k;
+
+  g_assert (GSK_IS_NEXT_DRIVER (self));
+  g_assert (key != NULL);
+  g_assert (texture_id > 0);
+  g_assert (g_hash_table_contains (self->textures, GUINT_TO_POINTER (texture_id)));
+
+  k = g_memdup (key, sizeof *key);
+
+  g_hash_table_insert (self->key_to_texture_id, k, GUINT_TO_POINTER (texture_id));
+  g_hash_table_insert (self->texture_id_to_key, GUINT_TO_POINTER (texture_id), k);
+}
+
+/**
+ * gsk_next_driver_load_texture:
+ * @self: a #GdkTexture
+ * @texture: a #GdkTexture
+ * @min_filter: GL_NEAREST or GL_LINEAR
+ * @mag_filter: GL_NEAREST or GL_LINEAR
+ *
+ * Loads a #GdkTexture by uploading the contents to the GPU when
+ * necessary. If @texture is a #GdkGLTexture, it can be used without
+ * uploading contents to the GPU.
+ *
+ * If the texture has already been uploaded and not yet released
+ * from cache, this function returns that texture id without further
+ * work.
+ *
+ * If the texture has not been used for a number of frames, it will
+ * be removed from cache.
+ *
+ * There is no need to release the resulting texture identifier after
+ * using it. It will be released automatically.
+ *
+ * Returns: a texture identifier
+ */
+guint
+gsk_next_driver_load_texture (GskNextDriver   *self,
+                              GdkTexture      *texture,
+                              int              min_filter,
+                              int              mag_filter)
+{
+  GdkGLContext *context;
+  GdkTexture *downloaded_texture = NULL;
+  GdkTexture *source_texture;
+  GskGLTexture *t;
+  guint texture_id;
+  int height;
+  int width;
+
+  g_return_val_if_fail (GSK_IS_NEXT_DRIVER (self), 0);
+  g_return_val_if_fail (GDK_IS_TEXTURE (texture), 0);
+  g_return_val_if_fail (GSK_IS_GL_COMMAND_QUEUE (self->command_queue), 0);
+
+  context = self->command_queue->context;
+
+  if (GDK_IS_GL_TEXTURE (texture))
+    {
+      GdkGLContext *texture_context = gdk_gl_texture_get_context ((GdkGLTexture *)texture);
+      GdkGLContext *shared_context = gdk_gl_context_get_shared_context (context);
+
+      if (texture_context == context ||
+          (shared_context != NULL &&
+           shared_context == gdk_gl_context_get_shared_context (texture_context)))
+
+        {
+          /* A GL texture from the same GL context is a simple task... */
+          return gdk_gl_texture_get_id ((GdkGLTexture *)texture);
+        }
+      else
+        {
+          cairo_surface_t *surface;
+
+          /* In this case, we have to temporarily make the texture's
+           * context the current one, download its data into our context
+           * and then create a texture from it. */
+          if (texture_context != NULL)
+            gdk_gl_context_make_current (texture_context);
+
+          surface = gdk_texture_download_surface (texture);
+          downloaded_texture = gdk_texture_new_for_surface (surface);
+          cairo_surface_destroy (surface);
+
+          gdk_gl_context_make_current (context);
+
+          source_texture = downloaded_texture;
+        }
+    }
+  else
+    {
+      if ((t = gdk_texture_get_render_data (texture, self)))
+        {
+          if (t->min_filter == min_filter && t->mag_filter == mag_filter)
+            return t->texture_id;
+        }
+
+      source_texture = texture;
+    }
+
+  width = gdk_texture_get_width (texture);
+  height = gdk_texture_get_height (texture);
+  texture_id = gsk_gl_command_queue_upload_texture (self->command_queue,
+                                                    source_texture,
+                                                    0,
+                                                    0,
+                                                    width,
+                                                    height,
+                                                    min_filter,
+                                                    mag_filter);
+
+  t = gsk_gl_texture_new (texture_id,
+                          width, height, min_filter, mag_filter,
+                          self->current_frame_id);
+
+  g_hash_table_insert (self->textures, GUINT_TO_POINTER (texture_id), t);
+
+  if (gdk_texture_set_render_data (texture, self, t, gsk_gl_texture_destroyed))
+    t->user = texture;
+
+  gdk_gl_context_label_object_printf (context, GL_TEXTURE, t->texture_id,
+                                      "GdkTexture<%p> %d", texture, t->texture_id);
+
+  g_clear_object (&downloaded_texture);
+
+  return texture_id;
+}
+
+/**
+ * gsk_next_driver_create_texture:
+ * @self: a #GskNextDriver
+ * @width: the width of the texture
+ * @height: the height of the texture
+ * @min_filter: GL_NEAREST or GL_LINEAR
+ * @mag_filter: GL_NEAREST or GL_FILTER
+ *
+ * Creates a new texture immediately that can be used by the caller
+ * to upload data, map to a framebuffer, or other uses which may
+ * modify the texture immediately.
+ *
+ * Use this instead of gsk_next_driver_acquire_texture() when you need
+ * to be able to modify the texture immediately instead of just when the
+ * pipeline is executing. Otherwise, gsk_next_driver_acquire_texture()
+ * provides more chances for re-use of textures, reducing the VRAM overhead
+ * on the GPU.
+ *
+ * Use gsk_next_driver_release_texture() to release this texture back into
+ * the pool so it may be reused later in the pipeline.
+ *
+ * Returns: a #GskGLTexture which can be returned to the pool with
+ *   gsk_next_driver_release_texture().
+ */
+GskGLTexture *
+gsk_next_driver_create_texture (GskNextDriver *self,
+                                float          width,
+                                float          height,
+                                int            min_filter,
+                                int            mag_filter)
+{
+  GskGLTexture *texture;
+
+  g_return_val_if_fail (GSK_IS_NEXT_DRIVER (self), NULL);
+
+  texture = gsk_gl_texture_pool_get (&self->texture_pool,
+                                     width, height,
+                                     min_filter, mag_filter,
+                                     TRUE);
+  g_hash_table_insert (self->textures,
+                       GUINT_TO_POINTER (texture->texture_id),
+                       texture);
+  return texture;
+}
+
+/**
+ * gsk_next_driver_acquire_texture:
+ * @self: a #GskNextDriver
+ * @width: the min width of the texture necessary
+ * @height: the min height of the texture necessary
+ * @min_filter: GL_NEAREST or GL_LINEAR
+ * @mag_filter: GL_NEAREST or GL_LINEAR
+ *
+ * This function acquires a #GskGLTexture from the texture pool. Doing
+ * so increases the chances for reduced VRAM usage in the GPU by having
+ * fewer textures in use at one time. Batches later in the stream can
+ * use the same texture memory of a previous batch.
+ *
+ * Consumers of this function are not allowed to modify @texture
+ * immediately, it must wait until batches are being processed as
+ * the texture may contain contents used earlier in the pipeline.
+ *
+ * Returns: a #GskGLTexture that may be returned to the pool with
+ *   gsk_next_driver_release_texture().
+ */
+GskGLTexture *
+gsk_next_driver_acquire_texture (GskNextDriver *self,
+                                 float          width,
+                                 float          height,
+                                 int            min_filter,
+                                 int            mag_filter)
+{
+  GskGLTexture *texture;
+
+  g_assert (GSK_IS_NEXT_DRIVER (self));
+
+  texture = gsk_gl_texture_pool_get (&self->texture_pool,
+                                     width, height,
+                                     min_filter, mag_filter,
+                                     FALSE);
+  g_hash_table_insert (self->textures,
+                       GUINT_TO_POINTER (texture->texture_id),
+                       texture);
+  return texture;
+}
+
+/**
+ * gsk_next_driver_release_texture:
+ * @self: a #GskNextDriver
+ * @texture: a #GskGLTexture
+ *
+ * Releases @texture back into the pool so that it can be used later
+ * in the command stream by future batches. This helps reduce VRAM
+ * usage on the GPU.
+ *
+ * When the frame has completed, pooled textures will be released
+ * to free additional VRAM back to the system.
+ */
+void
+gsk_next_driver_release_texture (GskNextDriver *self,
+                                 GskGLTexture  *texture)
+{
+  guint texture_id;
+
+  g_assert (GSK_IS_NEXT_DRIVER (self));
+  g_assert (texture != NULL);
+
+  texture_id = texture->texture_id;
+
+  if (texture_id > 0)
+    remove_texture_key_for_id (self, texture_id);
+
+  g_hash_table_steal (self->textures, GUINT_TO_POINTER (texture_id));
+  gsk_gl_texture_pool_put (&self->texture_pool, texture);
+}
+
+/**
+ * gsk_next_driver_create_render_target:
+ * @self: a #GskNextDriver
+ * @width: the width for the render target
+ * @height: the height for the render target
+ * @min_filter: the min filter to use for the texture
+ * @mag_filter: the mag filter to use for the texture
+ * @out_render_target: (out): a location for the render target
+ *
+ * Creates a new render target which contains a framebuffer and a texture
+ * bound to that framebuffer of the size @width x @height and using the
+ * appropriate filters.
+ *
+ * Use gsk_next_driver_release_render_target() when you are finished with
+ * the render target to release it. You may steal the texture from the
+ * render target when releasing it.
+ *
+ * Returns: %TRUE if successful; otherwise %FALSE and @out_fbo_id and
+ *   @out_texture_id are undefined.
+ */
+gboolean
+gsk_next_driver_create_render_target (GskNextDriver      *self,
+                                      int                 width,
+                                      int                 height,
+                                      int                 min_filter,
+                                      int                 mag_filter,
+                                      GskGLRenderTarget **out_render_target)
+{
+  guint framebuffer_id;
+  guint texture_id;
+
+  g_return_val_if_fail (GSK_IS_NEXT_DRIVER (self), FALSE);
+  g_return_val_if_fail (GSK_IS_GL_COMMAND_QUEUE (self->command_queue), FALSE);
+  g_return_val_if_fail (out_render_target != NULL, FALSE);
+
+#if 0
+  if (self->render_targets->len > 0)
+    {
+      for (guint i = self->render_targets->len; i > 0; i--)
+        {
+          GskGLRenderTarget *render_target = g_ptr_array_index (self->render_targets, i-1);
+
+          if (render_target->width == width &&
+              render_target->height == height &&
+              render_target->min_filter == min_filter &&
+              render_target->mag_filter == mag_filter)
+            {
+              *out_render_target = g_ptr_array_steal_index_fast (self->render_targets, i-1);
+              return TRUE;
+            }
+        }
+    }
+#endif
+
+  if (gsk_gl_command_queue_create_render_target (self->command_queue,
+                                                 width, height,
+                                                 min_filter, mag_filter,
+                                                 &framebuffer_id, &texture_id))
+    {
+      GskGLRenderTarget *render_target;
+
+      render_target = g_slice_new0 (GskGLRenderTarget);
+      render_target->min_filter = min_filter;
+      render_target->mag_filter = mag_filter;
+      render_target->width = width;
+      render_target->height = height;
+      render_target->framebuffer_id = framebuffer_id;
+      render_target->texture_id = texture_id;
+
+      *out_render_target = render_target;
+
+      return TRUE;
+    }
+
+  *out_render_target = NULL;
+
+  return FALSE;
+}
+
+/**
+ * gsk_next_driver_release_render_target:
+ * @self: a #GskNextDriver
+ * @render_target: a #GskGLRenderTarget created with
+ *   gsk_next_driver_create_render_target().
+ * @release_texture: if the texture should also be released
+ *
+ * Releases a render target that was previously created. An attempt may
+ * be made to cache the render target so that future creations of render
+ * targets are performed faster.
+ *
+ * If @release_texture is %FALSE, the backing texture id is returned and
+ * the framebuffer is released. Otherwise, both the texture and framebuffer
+ * are released or cached until the end of the frame.
+ *
+ * This may be called when building the render job as the texture or
+ * framebuffer will not be removed immediately.
+ *
+ * Returns: a texture id if @release_texture is %FALSE, otherwise zero.
+ */
+guint
+gsk_next_driver_release_render_target (GskNextDriver     *self,
+                                       GskGLRenderTarget *render_target,
+                                       gboolean           release_texture)
+{
+  guint texture_id;
+
+  g_return_val_if_fail (GSK_IS_NEXT_DRIVER (self), 0);
+  g_return_val_if_fail (render_target != NULL, 0);
+
+  if (release_texture)
+    {
+      texture_id = 0;
+      g_ptr_array_add (self->render_targets, render_target);
+    }
+  else
+    {
+      GskGLTexture *texture;
+
+      texture_id = render_target->texture_id;
+
+      texture = gsk_gl_texture_new (render_target->texture_id,
+                                    render_target->width,
+                                    render_target->height,
+                                    render_target->min_filter,
+                                    render_target->mag_filter,
+                                    self->current_frame_id);
+      g_hash_table_insert (self->textures,
+                           GUINT_TO_POINTER (texture_id),
+                           g_steal_pointer (&texture));
+
+      gsk_next_driver_autorelease_framebuffer (self, render_target->framebuffer_id);
+      g_slice_free (GskGLRenderTarget, render_target);
+
+    }
+
+  return texture_id;
+}
+
+/**
+ * gsk_next_driver_lookup_shader:
+ * @self: a #GskNextDriver
+ * @shader: the shader to lookup or load
+ * @error: a location for a #GError, or %NULL
+ *
+ * Attepts to load @shader from the shader cache.
+ *
+ * If it has not been loaded, then it will compile the shader on demand.
+ *
+ * Returns: (transfer none): a #GskGLShader if successful; otherwise
+ *   %NULL and @error is set.
+ */
+GskGLProgram *
+gsk_next_driver_lookup_shader (GskNextDriver  *self,
+                               GskGLShader    *shader,
+                               GError        **error)
+{
+  GskGLProgram *program;
+
+  g_return_val_if_fail (self != NULL, NULL);
+  g_return_val_if_fail (shader != NULL, NULL);
+
+  program = g_hash_table_lookup (self->shader_cache, shader);
+
+  if (program == NULL)
+    {
+      const GskGLUniform *uniforms;
+      GskGLCompiler *compiler;
+      GBytes *suffix;
+      int n_required_textures;
+      int n_uniforms;
+
+      uniforms = gsk_gl_shader_get_uniforms (shader, &n_uniforms);
+      if (n_uniforms > G_N_ELEMENTS (program->args_locations))
+        {
+          g_set_error (error,
+                       GDK_GL_ERROR,
+                       GDK_GL_ERROR_UNSUPPORTED_FORMAT,
+                       "GLShaderNode supports max %d custom uniforms",
+                       (int)G_N_ELEMENTS (program->args_locations));
+          return NULL;
+        }
+
+      n_required_textures = gsk_gl_shader_get_n_textures (shader);
+      if (n_required_textures > G_N_ELEMENTS (program->texture_locations))
+        {
+          g_set_error (error,
+                       GDK_GL_ERROR,
+                       GDK_GL_ERROR_UNSUPPORTED_FORMAT,
+                       "GLShaderNode supports max %d texture sources",
+                       (int)(G_N_ELEMENTS (program->texture_locations)));
+          return NULL;
+        }
+
+      compiler = gsk_gl_compiler_new (self, FALSE);
+      suffix = gsk_gl_shader_get_source (shader);
+
+      gsk_gl_compiler_set_preamble_from_resource (compiler,
+                                                  GSK_GL_COMPILER_ALL,
+                                                  "/org/gtk/libgsk/glsl/preamble.glsl");
+      gsk_gl_compiler_set_preamble_from_resource (compiler,
+                                                  GSK_GL_COMPILER_VERTEX,
+                                                  "/org/gtk/libgsk/glsl/preamble.vs.glsl");
+      gsk_gl_compiler_set_preamble_from_resource (compiler,
+                                                  GSK_GL_COMPILER_FRAGMENT,
+                                                  "/org/gtk/libgsk/glsl/preamble.fs.glsl");
+      gsk_gl_compiler_set_source_from_resource (compiler,
+                                                GSK_GL_COMPILER_ALL,
+                                                "/org/gtk/libgsk/glsl/custom.glsl");
+      gsk_gl_compiler_set_suffix (compiler, GSK_GL_COMPILER_FRAGMENT, suffix);
+
+      /* Setup attributes that are provided via VBO */
+      gsk_gl_compiler_bind_attribute (compiler, "aPosition", 0);
+      gsk_gl_compiler_bind_attribute (compiler, "aUv", 1);
+
+      if ((program = gsk_gl_compiler_compile (compiler, NULL, error)))
+        {
+          gboolean have_alpha;
+
+          gsk_gl_program_add_uniform (program, "u_source", UNIFORM_SHARED_SOURCE);
+          gsk_gl_program_add_uniform (program, "u_clip_rect", UNIFORM_SHARED_CLIP_RECT);
+          gsk_gl_program_add_uniform (program, "u_viewport", UNIFORM_SHARED_VIEWPORT);
+          gsk_gl_program_add_uniform (program, "u_projection", UNIFORM_SHARED_PROJECTION);
+          gsk_gl_program_add_uniform (program, "u_modelview", UNIFORM_SHARED_MODELVIEW);
+          have_alpha = gsk_gl_program_add_uniform (program, "u_alpha", UNIFORM_SHARED_ALPHA);
+
+          gsk_gl_program_add_uniform (program, "u_size", UNIFORM_CUSTOM_SIZE);
+          gsk_gl_program_add_uniform (program, "u_texture1", UNIFORM_CUSTOM_TEXTURE1);
+          gsk_gl_program_add_uniform (program, "u_texture2", UNIFORM_CUSTOM_TEXTURE2);
+          gsk_gl_program_add_uniform (program, "u_texture3", UNIFORM_CUSTOM_TEXTURE3);
+          gsk_gl_program_add_uniform (program, "u_texture4", UNIFORM_CUSTOM_TEXTURE4);
+          for (guint i = 0; i < n_uniforms; i++)
+            gsk_gl_program_add_uniform (program, uniforms[i].name, UNIFORM_CUSTOM_LAST+i);
+
+          program->size_location = gsk_gl_program_get_uniform_location (program, UNIFORM_CUSTOM_SIZE);
+          program->texture_locations[0] = gsk_gl_program_get_uniform_location (program, 
UNIFORM_CUSTOM_TEXTURE1);
+          program->texture_locations[1] = gsk_gl_program_get_uniform_location (program, 
UNIFORM_CUSTOM_TEXTURE2);
+          program->texture_locations[2] = gsk_gl_program_get_uniform_location (program, 
UNIFORM_CUSTOM_TEXTURE3);
+          program->texture_locations[3] = gsk_gl_program_get_uniform_location (program, 
UNIFORM_CUSTOM_TEXTURE4);
+          for (guint i = 0; i < n_uniforms; i++)
+            program->args_locations[i] = gsk_gl_program_get_uniform_location (program, 
UNIFORM_CUSTOM_LAST+i);
+          for (guint i = n_uniforms; i < G_N_ELEMENTS (program->args_locations); i++)
+            program->args_locations[i] = -1;
+
+          gsk_gl_program_uniforms_added (program, TRUE);
+
+          if (have_alpha)
+            gsk_gl_program_set_uniform1f (program, UNIFORM_SHARED_ALPHA, 0, 1.0f);
+
+          g_hash_table_insert (self->shader_cache, shader, program);
+          g_object_weak_ref (G_OBJECT (shader),
+                             gsk_next_driver_shader_weak_cb,
+                             self);
+        }
+
+      g_object_unref (compiler);
+    }
+
+  return program;
+}
+
+void
+gsk_next_driver_save_atlases_to_png (GskNextDriver *self,
+                                     const char    *directory)
+{
+  g_return_if_fail (GSK_IS_NEXT_DRIVER (self));
+
+  if (directory == NULL)
+    directory = ".";
+
+  for (guint i = 0; i < self->atlases->len; i++)
+    {
+      GskGLTextureAtlas *atlas = g_ptr_array_index (self->atlases, i);
+      char *filename = g_strdup_printf ("%s%sframe-%d-atlas-%d.png",
+                                        directory,
+                                        G_DIR_SEPARATOR_S,
+                                        (int)self->current_frame_id,
+                                        atlas->texture_id);
+      write_atlas_to_png (atlas, filename);
+      g_free (filename);
+    }
+}
+
+GskGLCommandQueue *
+gsk_next_driver_create_command_queue (GskNextDriver *self,
+                                      GdkGLContext  *context)
+{
+  g_return_val_if_fail (GSK_IS_NEXT_DRIVER (self), NULL);
+  g_return_val_if_fail (GDK_IS_GL_CONTEXT (context), NULL);
+
+  return gsk_gl_command_queue_new (context, self->shared_command_queue->uniforms);
+}
+
+void
+gsk_next_driver_add_texture_slices (GskNextDriver      *self,
+                                    GdkTexture         *texture,
+                                    GskGLTextureSlice **out_slices,
+                                    guint              *out_n_slices)
+{
+  int max_texture_size;
+  GskGLTextureSlice *slices;
+  GskGLTexture *t;
+  guint n_slices;
+  guint cols;
+  guint rows;
+  int tex_width;
+  int tex_height;
+  int x = 0, y = 0;
+
+  g_assert (GSK_IS_NEXT_DRIVER (self));
+  g_assert (GDK_IS_TEXTURE (texture));
+  g_assert (out_slices != NULL);
+  g_assert (out_n_slices != NULL);
+
+  /* XXX: Too much? */
+  max_texture_size = self->command_queue->max_texture_size / 4;
+
+  tex_width = texture->width;
+  tex_height = texture->height;
+  cols = (texture->width / max_texture_size) + 1;
+  rows = (texture->height / max_texture_size) + 1;
+
+  if ((t = gdk_texture_get_render_data (texture, self)))
+    {
+      *out_slices = t->slices;
+      *out_n_slices = t->n_slices;
+      return;
+    }
+
+  n_slices = cols * rows;
+  slices = g_new0 (GskGLTextureSlice, n_slices);
+
+  for (guint col = 0; col < cols; col ++)
+    {
+      int slice_width = MIN (max_texture_size, texture->width - x);
+
+      for (guint row = 0; row < rows; row ++)
+        {
+          int slice_height = MIN (max_texture_size, texture->height - y);
+          int slice_index = (col * rows) + row;
+          guint texture_id;
+
+          texture_id = gsk_gl_command_queue_upload_texture (self->command_queue,
+                                                            texture,
+                                                            x, y,
+                                                            slice_width, slice_height,
+                                                            GL_NEAREST, GL_NEAREST);
+
+          slices[slice_index].rect.x = x;
+          slices[slice_index].rect.y = y;
+          slices[slice_index].rect.width = slice_width;
+          slices[slice_index].rect.height = slice_height;
+          slices[slice_index].texture_id = texture_id;
+
+          y += slice_height;
+        }
+
+      y = 0;
+      x += slice_width;
+    }
+
+  /* Allocate one Texture for the entire thing. */
+  t = gsk_gl_texture_new (0,
+                          tex_width, tex_height,
+                          GL_NEAREST, GL_NEAREST,
+                          self->current_frame_id);
+
+  /* Use gsk_gl_texture_free() as destroy notify here since we are
+   * not inserting this GskGLTexture into self->textures!
+   */
+  gdk_texture_set_render_data (texture, self, t,
+                               (GDestroyNotify)gsk_gl_texture_free);
+
+  t->slices = *out_slices = slices;
+  t->n_slices = *out_n_slices = n_slices;
+}
+
+GskGLTexture *
+gsk_next_driver_mark_texture_permanent (GskNextDriver *self,
+                                        guint          texture_id)
+{
+  GskGLTexture *t;
+
+  g_return_val_if_fail (GSK_IS_NEXT_DRIVER (self), NULL);
+  g_return_val_if_fail (texture_id > 0, NULL);
+
+  if ((t = g_hash_table_lookup (self->textures, GUINT_TO_POINTER (texture_id))))
+    t->permanent = TRUE;
+
+  return t;
+}
+
+void
+gsk_next_driver_release_texture_by_id (GskNextDriver *self,
+                                       guint          texture_id)
+{
+  GskGLTexture *texture;
+
+  g_return_if_fail (GSK_IS_NEXT_DRIVER (self));
+  g_return_if_fail (texture_id > 0);
+
+  remove_texture_key_for_id (self, texture_id);
+
+  if ((texture = g_hash_table_lookup (self->textures, GUINT_TO_POINTER (texture_id))))
+    gsk_next_driver_release_texture (self, texture);
+}
+
+
+static void
+create_texture_from_texture_destroy (gpointer data)
+{
+  GskGLTextureState *state = data;
+
+  g_assert (state != NULL);
+  g_assert (GDK_IS_GL_CONTEXT (state->context));
+
+  gdk_gl_context_make_current (state->context);
+  glDeleteTextures (1, &state->texture_id);
+  g_clear_object (&state->context);
+  g_slice_free (GskGLTextureState, state);
+}
+
+GdkTexture *
+gsk_next_driver_create_gdk_texture (GskNextDriver *self,
+                                    guint          texture_id)
+{
+  GskGLTextureState *state;
+  GskGLTexture *texture;
+
+  g_return_val_if_fail (GSK_IS_NEXT_DRIVER (self), NULL);
+  g_return_val_if_fail (self->command_queue != NULL, NULL);
+  g_return_val_if_fail (GDK_IS_GL_CONTEXT (self->command_queue->context), NULL);
+  g_return_val_if_fail (texture_id > 0, NULL);
+  g_return_val_if_fail (!g_hash_table_contains (self->texture_id_to_key, GUINT_TO_POINTER (texture_id)), 
NULL);
+
+  /* We must be tracking this texture_id already to use it */
+  if (!(texture = g_hash_table_lookup (self->textures, GUINT_TO_POINTER (texture_id))))
+    g_return_val_if_reached (NULL);
+
+  state = g_slice_new0 (GskGLTextureState);
+  state->texture_id = texture_id;
+  state->context = g_object_ref (self->command_queue->context);
+
+  g_hash_table_steal (self->textures, GUINT_TO_POINTER (texture_id));
+
+  return gdk_gl_texture_new (self->command_queue->context,
+                             texture_id,
+                             texture->width,
+                             texture->height,
+                             create_texture_from_texture_destroy,
+                             state);
+}
diff --git a/gsk/next/gskgldriverprivate.h b/gsk/next/gskgldriverprivate.h
new file mode 100644
index 0000000000..6531fc9c6c
--- /dev/null
+++ b/gsk/next/gskgldriverprivate.h
@@ -0,0 +1,236 @@
+/* gskgldriverprivate.h
+ *
+ * Copyright 2020 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU Lesser General Public License as published by the Free
+ * Software Foundation; either version 2.1 of the License, or (at your option)
+ * any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#ifndef __GSK_GL_DRIVER_PRIVATE_H__
+#define __GSK_GL_DRIVER_PRIVATE_H__
+
+#include <gdk/gdkgltextureprivate.h>
+
+#include "gskgltypesprivate.h"
+#include "gskgltexturepoolprivate.h"
+
+G_BEGIN_DECLS
+
+enum {
+  UNIFORM_SHARED_ALPHA,
+  UNIFORM_SHARED_SOURCE,
+  UNIFORM_SHARED_CLIP_RECT,
+  UNIFORM_SHARED_VIEWPORT,
+  UNIFORM_SHARED_PROJECTION,
+  UNIFORM_SHARED_MODELVIEW,
+
+  UNIFORM_SHARED_LAST
+};
+
+enum {
+  UNIFORM_CUSTOM_SIZE = UNIFORM_SHARED_LAST,
+  UNIFORM_CUSTOM_TEXTURE1,
+  UNIFORM_CUSTOM_TEXTURE2,
+  UNIFORM_CUSTOM_TEXTURE3,
+  UNIFORM_CUSTOM_TEXTURE4,
+
+  UNIFORM_CUSTOM_LAST
+};
+
+typedef struct {
+  gconstpointer   pointer;
+  float           scale_x;
+  float           scale_y;
+  int             filter;
+  int             pointer_is_child;
+  graphene_rect_t parent_rect; /* Valid when pointer_is_child */
+} GskTextureKey;
+
+#define GSL_GK_NO_UNIFORMS UNIFORM_INVALID_##__COUNTER__
+#define GSK_GL_ADD_UNIFORM(pos, KEY, name) UNIFORM_##KEY = UNIFORM_SHARED_LAST + pos,
+#define GSK_GL_DEFINE_PROGRAM(name, resource, uniforms) enum { uniforms };
+# include "gskglprograms.defs"
+#undef GSK_GL_DEFINE_PROGRAM
+#undef GSK_GL_ADD_UNIFORM
+#undef GSL_GK_NO_UNIFORMS
+
+#define GSK_TYPE_NEXT_DRIVER (gsk_next_driver_get_type())
+
+G_DECLARE_FINAL_TYPE (GskNextDriver, gsk_next_driver, GSK, NEXT_DRIVER, GObject)
+
+struct _GskGLRenderTarget
+{
+  guint framebuffer_id;
+  guint texture_id;
+  int min_filter;
+  int mag_filter;
+  int width;
+  int height;
+};
+
+struct _GskNextDriver
+{
+  GObject parent_instance;
+
+  GskGLCommandQueue *shared_command_queue;
+  GskGLCommandQueue *command_queue;
+
+  GskGLTexturePool texture_pool;
+
+  GskGLGlyphLibrary *glyphs;
+  GskGLIconLibrary *icons;
+  GskGLShadowLibrary *shadows;
+
+  GHashTable *textures;
+  GHashTable *key_to_texture_id;
+  GHashTable *texture_id_to_key;
+
+  GPtrArray *atlases;
+
+  GHashTable *shader_cache;
+
+  GArray *autorelease_framebuffers;
+  GPtrArray *render_targets;
+
+#define GSK_GL_NO_UNIFORMS
+#define GSK_GL_ADD_UNIFORM(pos, KEY, name)
+#define GSK_GL_DEFINE_PROGRAM(name, resource, uniforms) GskGLProgram *name;
+# include "gskglprograms.defs"
+#undef GSK_GL_NO_UNIFORMS
+#undef GSK_GL_ADD_UNIFORM
+#undef GSK_GL_DEFINE_PROGRAM
+
+  gint64 current_frame_id;
+
+  /* Used to reduce number of comparisons */
+  guint stamps[UNIFORM_SHARED_LAST];
+
+  guint debug : 1;
+  guint in_frame : 1;
+};
+
+GskNextDriver     *gsk_next_driver_from_shared_context   (GdkGLContext         *context,
+                                                          gboolean              debug_shaders,
+                                                          GError              **error);
+GskGLCommandQueue *gsk_next_driver_create_command_queue  (GskNextDriver        *self,
+                                                          GdkGLContext         *context);
+GdkGLContext      *gsk_next_driver_get_context           (GskNextDriver        *self);
+gboolean           gsk_next_driver_create_render_target  (GskNextDriver        *self,
+                                                          int                   width,
+                                                          int                   height,
+                                                          int                   min_filter,
+                                                          int                   mag_filter,
+                                                          GskGLRenderTarget   **render_target);
+guint              gsk_next_driver_release_render_target (GskNextDriver        *self,
+                                                          GskGLRenderTarget    *render_target,
+                                                          gboolean              release_texture);
+void               gsk_next_driver_begin_frame           (GskNextDriver        *self,
+                                                          GskGLCommandQueue    *command_queue);
+void               gsk_next_driver_end_frame             (GskNextDriver        *self);
+void               gsk_next_driver_after_frame           (GskNextDriver        *self);
+GdkTexture        *gsk_next_driver_create_gdk_texture    (GskNextDriver        *self,
+                                                          guint                 texture_id);
+void               gsk_next_driver_cache_texture         (GskNextDriver        *self,
+                                                          const GskTextureKey  *key,
+                                                          guint                 texture_id);
+guint              gsk_next_driver_load_texture          (GskNextDriver        *self,
+                                                          GdkTexture           *texture,
+                                                          int                   min_filter,
+                                                          int                   mag_filter);
+GskGLTexture      *gsk_next_driver_create_texture        (GskNextDriver        *self,
+                                                          float                 width,
+                                                          float                 height,
+                                                          int                   min_filter,
+                                                          int                   mag_filter);
+GskGLTexture      *gsk_next_driver_acquire_texture       (GskNextDriver        *self,
+                                                          float                 width,
+                                                          float                 height,
+                                                          int                   min_filter,
+                                                          int                   mag_filter);
+void               gsk_next_driver_release_texture       (GskNextDriver        *self,
+                                                          GskGLTexture         *texture);
+void               gsk_next_driver_release_texture_by_id (GskNextDriver        *self,
+                                                          guint                 texture_id);
+GskGLTexture      *gsk_next_driver_mark_texture_permanent(GskNextDriver        *self,
+                                                          guint                 texture_id);
+void               gsk_next_driver_add_texture_slices    (GskNextDriver        *self,
+                                                          GdkTexture           *texture,
+                                                          GskGLTextureSlice   **out_slices,
+                                                          guint                *out_n_slices);
+GskGLProgram      *gsk_next_driver_lookup_shader         (GskNextDriver        *self,
+                                                          GskGLShader          *shader,
+                                                          GError              **error);
+GskGLTextureAtlas *gsk_next_driver_create_atlas          (GskNextDriver        *self);
+void              gsk_next_driver_save_atlases_to_png    (GskNextDriver        *self,
+                                                          const char           *directory);
+
+static inline GskGLTexture *
+gsk_next_driver_get_texture_by_id (GskNextDriver *self,
+                                   guint          texture_id)
+{
+  return g_hash_table_lookup (self->textures, GUINT_TO_POINTER (texture_id));
+}
+
+/**
+ * gsk_next_driver_lookup_texture:
+ * @self: a #GskNextDriver
+ * @key: the key for the texture
+ *
+ * Looks up a texture in the texture cache by @key.
+ *
+ * If the texture could not be found, then zero is returned.
+ *
+ * Returns: a positive integer if the texture was found; otherwise 0.
+ */
+static inline guint
+gsk_next_driver_lookup_texture (GskNextDriver       *self,
+                                const GskTextureKey *key)
+{
+  gpointer id;
+
+  if (g_hash_table_lookup_extended (self->key_to_texture_id, key, NULL, &id))
+    {
+      GskGLTexture *texture = g_hash_table_lookup (self->textures, id);
+
+      if (texture != NULL)
+        texture->last_used_in_frame = self->current_frame_id;
+
+      return GPOINTER_TO_UINT (id);
+    }
+
+  return 0;
+}
+
+static inline void
+gsk_next_driver_slice_texture (GskNextDriver      *self,
+                               GdkTexture         *texture,
+                               GskGLTextureSlice **out_slices,
+                               guint              *out_n_slices)
+{
+  GskGLTexture *t;
+
+  if ((t = gdk_texture_get_render_data (texture, self)))
+    {
+      *out_slices = t->slices;
+      *out_n_slices = t->n_slices;
+      return;
+    }
+
+  gsk_next_driver_add_texture_slices (self, texture, out_slices, out_n_slices);
+}
+
+G_END_DECLS
+
+#endif /* __GSK_GL_DRIVER_PRIVATE_H__ */
diff --git a/gsk/next/gskglglyphlibrary.c b/gsk/next/gskglglyphlibrary.c
new file mode 100644
index 0000000000..e432aad9cd
--- /dev/null
+++ b/gsk/next/gskglglyphlibrary.c
@@ -0,0 +1,325 @@
+/* gskglglyphlibrary.c
+ *
+ * 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/gdkmemorytextureprivate.h>
+#include <gdk/gdkprofilerprivate.h>
+
+#include "gskglcommandqueueprivate.h"
+#include "gskgldriverprivate.h"
+#include "gskglglyphlibraryprivate.h"
+
+#define MAX_GLYPH_SIZE 128
+
+G_DEFINE_TYPE (GskGLGlyphLibrary, gsk_gl_glyph_library, GSK_TYPE_GL_TEXTURE_LIBRARY)
+
+GskGLGlyphLibrary *
+gsk_gl_glyph_library_new (GskNextDriver *driver)
+{
+  g_return_val_if_fail (GSK_IS_NEXT_DRIVER (driver), NULL);
+
+  return g_object_new (GSK_TYPE_GL_GLYPH_LIBRARY,
+                       "driver", driver,
+                       NULL);
+}
+
+static guint
+gsk_gl_glyph_key_hash (gconstpointer data)
+{
+  const GskGLGlyphKey *key = data;
+
+  /* We do not store the hash within the key because GHashTable will already
+   * store the hash value for us and so this is called only a single time per
+   * cached item. This saves an extra 4 bytes per GskGLGlyphKey which means on
+   * 64-bit, we fit nicely within 2 pointers (the smallest allocation size
+   * for GSlice).
+   */
+
+  return GPOINTER_TO_UINT (key->font) ^
+         key->glyph ^
+         (key->xshift << 24) ^
+         (key->yshift << 26) ^
+         key->scale;
+}
+
+static gboolean
+gsk_gl_glyph_key_equal (gconstpointer v1,
+                        gconstpointer v2)
+{
+  return memcmp (v1, v2, sizeof (GskGLGlyphKey)) == 0;
+}
+
+static void
+gsk_gl_glyph_key_free (gpointer data)
+{
+  GskGLGlyphKey *key = data;
+
+  g_clear_object (&key->font);
+  g_slice_free (GskGLGlyphKey, key);
+}
+
+static void
+gsk_gl_glyph_value_free (gpointer data)
+{
+  g_slice_free (GskGLGlyphValue, data);
+}
+
+static void
+gsk_gl_glyph_library_finalize (GObject *object)
+{
+  GskGLGlyphLibrary *self = (GskGLGlyphLibrary *)object;
+
+  g_clear_pointer (&self->hash_table, g_hash_table_unref);
+  g_clear_pointer (&self->surface_data, g_free);
+
+  G_OBJECT_CLASS (gsk_gl_glyph_library_parent_class)->finalize (object);
+}
+
+static void
+gsk_gl_glyph_library_class_init (GskGLGlyphLibraryClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = gsk_gl_glyph_library_finalize;
+}
+
+static void
+gsk_gl_glyph_library_init (GskGLGlyphLibrary *self)
+{
+  GSK_GL_TEXTURE_LIBRARY (self)->max_entry_size = MAX_GLYPH_SIZE;
+  gsk_gl_texture_library_set_funcs (GSK_GL_TEXTURE_LIBRARY (self),
+                                    gsk_gl_glyph_key_hash,
+                                    gsk_gl_glyph_key_equal,
+                                    gsk_gl_glyph_key_free,
+                                    gsk_gl_glyph_value_free);
+}
+
+static cairo_surface_t *
+gsk_gl_glyph_library_create_surface (GskGLGlyphLibrary *self,
+                                     int                stride,
+                                     int                width,
+                                     int                height,
+                                     double             device_scale)
+{
+  cairo_surface_t *surface;
+  gsize n_bytes;
+
+  g_assert (GSK_IS_GL_GLYPH_LIBRARY (self));
+  g_assert (width > 0);
+  g_assert (height > 0);
+
+  n_bytes = stride * height;
+
+  if G_LIKELY (n_bytes > self->surface_data_len)
+    {
+      self->surface_data = g_realloc (self->surface_data, n_bytes);
+      self->surface_data_len = n_bytes;
+    }
+
+  memset (self->surface_data, 0, n_bytes);
+  surface = cairo_image_surface_create_for_data (self->surface_data,
+                                                 CAIRO_FORMAT_ARGB32,
+                                                 width, height, stride);
+  cairo_surface_set_device_scale (surface, device_scale, device_scale);
+
+  return surface;
+}
+
+static void
+render_glyph (cairo_surface_t           *surface,
+              const cairo_scaled_font_t *scaled_font,
+              const GskGLGlyphKey       *key,
+              const GskGLGlyphValue     *value)
+{
+  cairo_t *cr;
+  PangoGlyphString glyph_string;
+  PangoGlyphInfo glyph_info;
+
+  g_assert (surface != NULL);
+  g_assert (scaled_font != NULL);
+
+  cr = cairo_create (surface);
+  cairo_set_scaled_font (cr, scaled_font);
+  cairo_set_source_rgba (cr, 1, 1, 1, 1);
+
+  glyph_info.glyph = key->glyph;
+  glyph_info.geometry.width = value->ink_rect.width * 1024;
+  if (glyph_info.glyph & PANGO_GLYPH_UNKNOWN_FLAG)
+    glyph_info.geometry.x_offset = 250 * key->xshift;
+  else
+    glyph_info.geometry.x_offset = 250 * key->xshift - value->ink_rect.x * 1024;
+  glyph_info.geometry.y_offset = 250 * key->yshift - value->ink_rect.y * 1024;
+
+  glyph_string.num_glyphs = 1;
+  glyph_string.glyphs = &glyph_info;
+
+  pango_cairo_show_glyph_string (cr, key->font, &glyph_string);
+  cairo_destroy (cr);
+
+  cairo_surface_flush (surface);
+}
+
+static void
+gsk_gl_glyph_library_upload_glyph (GskGLGlyphLibrary     *self,
+                                   const GskGLGlyphKey   *key,
+                                   const GskGLGlyphValue *value,
+                                   int                    width,
+                                   int                    height,
+                                   double                 device_scale)
+{
+  G_GNUC_UNUSED gint64 start_time = GDK_PROFILER_CURRENT_TIME;
+  cairo_scaled_font_t *scaled_font;
+  GskGLTextureAtlas *atlas;
+  cairo_surface_t *surface;
+  guchar *pixel_data;
+  guchar *free_data = NULL;
+  guint gl_format;
+  guint gl_type;
+  guint texture_id;
+  gsize stride;
+  int x, y;
+
+  g_assert (GSK_IS_GL_GLYPH_LIBRARY (self));
+  g_assert (key != NULL);
+  g_assert (value != NULL);
+
+  scaled_font = pango_cairo_font_get_scaled_font ((PangoCairoFont *)key->font);
+  if G_UNLIKELY (scaled_font == NULL ||
+                 cairo_scaled_font_status (scaled_font) != CAIRO_STATUS_SUCCESS)
+    return;
+
+  stride = cairo_format_stride_for_width (CAIRO_FORMAT_ARGB32, width);
+  atlas = value->entry.is_atlased ? value->entry.atlas : NULL;
+
+  gdk_gl_context_push_debug_group_printf (gdk_gl_context_get_current (),
+                                          "Uploading glyph %d",
+                                          key->glyph);
+
+  surface = gsk_gl_glyph_library_create_surface (self, stride, width, height, device_scale);
+  render_glyph (surface, scaled_font, key, value);
+
+  texture_id = GSK_GL_TEXTURE_ATLAS_ENTRY_TEXTURE (value);
+
+  g_assert (texture_id > 0);
+
+  glPixelStorei (GL_UNPACK_ROW_LENGTH, stride / 4);
+  glBindTexture (GL_TEXTURE_2D, texture_id);
+
+  if G_UNLIKELY (gdk_gl_context_get_use_es (gdk_gl_context_get_current ()))
+    {
+      pixel_data = free_data = g_malloc (width * height * 4);
+      gdk_memory_convert (pixel_data,
+                          width * 4,
+                          GDK_MEMORY_R8G8B8A8_PREMULTIPLIED,
+                          cairo_image_surface_get_data (surface),
+                          width * 4,
+                          GDK_MEMORY_DEFAULT,
+                          width, height);
+      gl_format = GL_RGBA;
+      gl_type = GL_UNSIGNED_BYTE;
+    }
+  else
+    {
+      pixel_data = cairo_image_surface_get_data (surface);
+      gl_format = GL_BGRA;
+      gl_type = GL_UNSIGNED_INT_8_8_8_8_REV;
+    }
+
+  if G_LIKELY (atlas != NULL)
+    {
+      x = atlas->width * value->entry.area.x;
+      y = atlas->width * value->entry.area.y;
+    }
+  else
+    {
+      x = 0;
+      y = 0;
+    }
+
+  glTexSubImage2D (GL_TEXTURE_2D, 0, x, y, width, height,
+                   gl_format, gl_type, pixel_data);
+  glPixelStorei (GL_UNPACK_ROW_LENGTH, 0);
+
+  cairo_surface_destroy (surface);
+  g_free (free_data);
+
+  gdk_gl_context_pop_debug_group (gdk_gl_context_get_current ());
+
+  GSK_GL_TEXTURE_LIBRARY (self)->driver->command_queue->n_uploads++;
+
+  if (gdk_profiler_is_running ())
+    {
+      char message[64];
+      g_snprintf (message, sizeof message, "Size %dx%d", width, height);
+      gdk_profiler_add_mark (start_time, GDK_PROFILER_CURRENT_TIME-start_time, "Upload Glyph", message);
+    }
+}
+
+gboolean
+gsk_gl_glyph_library_add (GskGLGlyphLibrary      *self,
+                          GskGLGlyphKey          *key,
+                          const GskGLGlyphValue **out_value)
+{
+  PangoRectangle ink_rect;
+  GskGLGlyphValue *value;
+  int width;
+  int height;
+  guint packed_x;
+  guint packed_y;
+
+  g_assert (GSK_IS_GL_GLYPH_LIBRARY (self));
+  g_assert (key != NULL);
+  g_assert (out_value != NULL);
+
+  pango_font_get_glyph_extents (key->font, key->glyph, &ink_rect, NULL);
+  pango_extents_to_pixels (&ink_rect, NULL);
+
+  if (key->xshift != 0)
+    ink_rect.width++;
+  if (key->yshift != 0)
+    ink_rect.height++;
+
+  width = ink_rect.width * key->scale / 1024;
+  height = ink_rect.height * key->scale / 1024;
+
+  value = gsk_gl_texture_library_pack (GSK_GL_TEXTURE_LIBRARY (self),
+                                       key,
+                                       sizeof *value,
+                                       width,
+                                       height,
+                                       0,
+                                       &packed_x, &packed_y);
+
+  memcpy (&value->ink_rect, &ink_rect, sizeof ink_rect);
+
+  if (key->scale > 0 && width > 0 && height > 0)
+    gsk_gl_glyph_library_upload_glyph (self,
+                                       key,
+                                       value,
+                                       width,
+                                       height,
+                                       key->scale / 1024.0);
+
+  *out_value = value;
+
+  return GSK_GL_TEXTURE_ATLAS_ENTRY_TEXTURE (value) != 0;
+}
diff --git a/gsk/next/gskglglyphlibraryprivate.h b/gsk/next/gskglglyphlibraryprivate.h
new file mode 100644
index 0000000000..78fa7a370b
--- /dev/null
+++ b/gsk/next/gskglglyphlibraryprivate.h
@@ -0,0 +1,119 @@
+/* gskglglyphlibraryprivate.h
+ *
+ * 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
+ */
+
+#ifndef __GSK_GL_GLYPH_LIBRARY_PRIVATE_H__
+#define __GSK_GL_GLYPH_LIBRARY_PRIVATE_H__
+
+#include <pango/pango.h>
+
+#include "gskgltexturelibraryprivate.h"
+
+G_BEGIN_DECLS
+
+#define GSK_TYPE_GL_GLYPH_LIBRARY (gsk_gl_glyph_library_get_type())
+
+typedef struct _GskGLGlyphKey
+{
+  PangoFont *font;
+  PangoGlyph glyph;
+  guint xshift : 3;
+  guint yshift : 3;
+  guint scale  : 26; /* times 1024 */
+} GskGLGlyphKey;
+
+typedef struct _GskGLGlyphValue
+{
+  GskGLTextureAtlasEntry entry;
+  PangoRectangle ink_rect;
+} GskGLGlyphValue;
+
+#if GLIB_SIZEOF_VOID_P == 8
+G_STATIC_ASSERT (sizeof (GskGLGlyphKey) == 16);
+#elif GLIB_SIZEOF_VOID_P == 4
+G_STATIC_ASSERT (sizeof (GskGLGlyphKey) == 12);
+#endif
+
+G_DECLARE_FINAL_TYPE (GskGLGlyphLibrary, gsk_gl_glyph_library, GSK, GL_GLYPH_LIBRARY, GskGLTextureLibrary)
+
+struct _GskGLGlyphLibrary
+{
+  GskGLTextureLibrary  parent_instance;
+  GHashTable          *hash_table;
+  guint8              *surface_data;
+  gsize                surface_data_len;
+  struct {
+    GskGLGlyphKey key;
+    const GskGLGlyphValue *value;
+  } front[256];
+};
+
+GskGLGlyphLibrary *gsk_gl_glyph_library_new (GskNextDriver          *driver);
+gboolean           gsk_gl_glyph_library_add (GskGLGlyphLibrary      *self,
+                                             GskGLGlyphKey          *key,
+                                             const GskGLGlyphValue **out_value);
+
+static inline int
+gsk_gl_glyph_key_phase (float value)
+{
+  return floor (4 * (value + 0.125)) - 4 * floor (value + 0.125);
+}
+
+static inline void
+gsk_gl_glyph_key_set_glyph_and_shift (GskGLGlyphKey *key,
+                                      PangoGlyph     glyph,
+                                      float          x,
+                                      float          y)
+{
+  key->glyph = glyph;
+  key->xshift = gsk_gl_glyph_key_phase (x);
+  key->yshift = gsk_gl_glyph_key_phase (y);
+}
+
+static inline gboolean
+gsk_gl_glyph_library_lookup_or_add (GskGLGlyphLibrary      *self,
+                                    const GskGLGlyphKey    *key,
+                                    const GskGLGlyphValue **out_value)
+{
+  GskGLTextureAtlasEntry *entry;
+  guint front_index = key->glyph & 0xFF;
+
+  if (memcmp (key, &self->front[front_index], sizeof *key) == 0)
+    {
+      *out_value = self->front[front_index].value;
+    }
+  else if (gsk_gl_texture_library_lookup ((GskGLTextureLibrary *)self, key, &entry))
+    {
+      *out_value = (GskGLGlyphValue *)entry;
+      self->front[front_index].key = *key;
+      self->front[front_index].value = *out_value;
+    }
+  else
+    {
+      GskGLGlyphKey *k = g_slice_copy (sizeof *key, key);
+      g_object_ref (k->font);
+      gsk_gl_glyph_library_add (self, k, out_value);
+    }
+
+  return GSK_GL_TEXTURE_ATLAS_ENTRY_TEXTURE (*out_value) != 0;
+}
+
+G_END_DECLS
+
+#endif /* __GSK_GL_GLYPH_LIBRARY_PRIVATE_H__ */
diff --git a/gsk/next/gskgliconlibrary.c b/gsk/next/gskgliconlibrary.c
new file mode 100644
index 0000000000..05afbbe4c7
--- /dev/null
+++ b/gsk/next/gskgliconlibrary.c
@@ -0,0 +1,213 @@
+/* gskgliconlibrary.c
+ *
+ * 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/gdkmemorytextureprivate.h>
+#include <gdk/gdkprofilerprivate.h>
+#include <gdk/gdktextureprivate.h>
+
+#include "gskglcommandqueueprivate.h"
+#include "gskgldriverprivate.h"
+#include "gskgliconlibraryprivate.h"
+
+struct _GskGLIconLibrary
+{
+  GskGLTextureLibrary parent_instance;
+};
+
+G_DEFINE_TYPE (GskGLIconLibrary, gsk_gl_icon_library, GSK_TYPE_GL_TEXTURE_LIBRARY)
+
+GskGLIconLibrary *
+gsk_gl_icon_library_new (GskNextDriver *driver)
+{
+  g_return_val_if_fail (GSK_IS_NEXT_DRIVER (driver), NULL);
+
+  return g_object_new (GSK_TYPE_GL_ICON_LIBRARY,
+                       "driver", driver,
+                       NULL);
+}
+
+static void
+gsk_gl_icon_data_free (gpointer data)
+{
+  GskGLIconData *icon_data = data;
+
+  g_clear_object (&icon_data->source_texture);
+  g_slice_free (GskGLIconData, icon_data);
+}
+
+static void
+gsk_gl_icon_library_class_init (GskGLIconLibraryClass *klass)
+{
+}
+
+static void
+gsk_gl_icon_library_init (GskGLIconLibrary *self)
+{
+  GSK_GL_TEXTURE_LIBRARY (self)->max_entry_size = 128;
+  gsk_gl_texture_library_set_funcs (GSK_GL_TEXTURE_LIBRARY (self),
+                                    NULL, NULL, NULL,
+                                    gsk_gl_icon_data_free);
+}
+
+void
+gsk_gl_icon_library_add (GskGLIconLibrary     *self,
+                         GdkTexture           *key,
+                         const GskGLIconData **out_value)
+{
+  G_GNUC_UNUSED gint64 start_time = GDK_PROFILER_CURRENT_TIME;
+  cairo_surface_t *surface;
+  GskGLIconData *icon_data;
+  guint8 *pixel_data;
+  guint8 *surface_data;
+  guint8 *free_data = NULL;
+  guint gl_format;
+  guint gl_type;
+  guint packed_x;
+  guint packed_y;
+  int width;
+  int height;
+  guint texture_id;
+
+  g_assert (GSK_IS_GL_ICON_LIBRARY (self));
+  g_assert (GDK_IS_TEXTURE (key));
+  g_assert (out_value != NULL);
+
+  width = key->width;
+  height = key->height;
+
+  icon_data = gsk_gl_texture_library_pack (GSK_GL_TEXTURE_LIBRARY (self),
+                                           key,
+                                           sizeof (GskGLIconData),
+                                           width, height, 1,
+                                           &packed_x, &packed_y);
+  icon_data->source_texture = g_object_ref (key);
+
+  /* actually upload the texture */
+  surface = gdk_texture_download_surface (key);
+  surface_data = cairo_image_surface_get_data (surface);
+  gdk_gl_context_push_debug_group_printf (gdk_gl_context_get_current (),
+                                          "Uploading texture");
+
+  if (gdk_gl_context_get_use_es (gdk_gl_context_get_current ()))
+    {
+      pixel_data = free_data = g_malloc (width * height * 4);
+      gdk_memory_convert (pixel_data, width * 4,
+                          GDK_MEMORY_R8G8B8A8_PREMULTIPLIED,
+                          surface_data, cairo_image_surface_get_stride (surface),
+                          GDK_MEMORY_DEFAULT, width, height);
+      gl_format = GL_RGBA;
+      gl_type = GL_UNSIGNED_BYTE;
+    }
+  else
+    {
+      pixel_data = surface_data;
+      gl_format = GL_BGRA;
+      gl_type = GL_UNSIGNED_INT_8_8_8_8_REV;
+    }
+
+  texture_id = GSK_GL_TEXTURE_ATLAS_ENTRY_TEXTURE (icon_data);
+
+  glBindTexture (GL_TEXTURE_2D, texture_id);
+
+  glTexSubImage2D (GL_TEXTURE_2D, 0,
+                   packed_x + 1, packed_y + 1,
+                   width, height,
+                   gl_format, gl_type,
+                   pixel_data);
+  /* Padding top */
+  glTexSubImage2D (GL_TEXTURE_2D, 0,
+                   packed_x + 1, packed_y,
+                   width, 1,
+                   gl_format, gl_type,
+                   pixel_data);
+  /* Padding left */
+  glTexSubImage2D (GL_TEXTURE_2D, 0,
+                   packed_x, packed_y + 1,
+                   1, height,
+                   gl_format, gl_type,
+                   pixel_data);
+  /* Padding top left */
+  glTexSubImage2D (GL_TEXTURE_2D, 0,
+                   packed_x, packed_y,
+                   1, 1,
+                   gl_format, gl_type,
+                   pixel_data);
+
+  /* Padding right */
+  glPixelStorei (GL_UNPACK_ROW_LENGTH, width);
+  glPixelStorei (GL_UNPACK_SKIP_PIXELS, width - 1);
+  glTexSubImage2D (GL_TEXTURE_2D, 0,
+                   packed_x + width + 1, packed_y + 1,
+                   1, height,
+                   gl_format, gl_type,
+                   pixel_data);
+  /* Padding top right */
+  glTexSubImage2D (GL_TEXTURE_2D, 0,
+                   packed_x + width + 1, packed_y,
+                   1, 1,
+                   gl_format, gl_type,
+                   pixel_data);
+  /* Padding bottom */
+  glPixelStorei (GL_UNPACK_SKIP_PIXELS, 0);
+  glPixelStorei (GL_UNPACK_ROW_LENGTH, 0);
+  glPixelStorei (GL_UNPACK_SKIP_ROWS, height - 1);
+  glTexSubImage2D (GL_TEXTURE_2D, 0,
+                   packed_x + 1, packed_y + 1 + height,
+                   width, 1,
+                   gl_format, gl_type,
+                   pixel_data);
+  /* Padding bottom left */
+  glTexSubImage2D (GL_TEXTURE_2D, 0,
+                   packed_x, packed_y + 1 + height,
+                   1, 1,
+                   gl_format, gl_type,
+                   pixel_data);
+  /* Padding bottom right */
+  glPixelStorei (GL_UNPACK_ROW_LENGTH, width);
+  glPixelStorei (GL_UNPACK_SKIP_PIXELS, width - 1);
+  glTexSubImage2D (GL_TEXTURE_2D, 0,
+                   packed_x + 1 + width, packed_y + 1 + height,
+                   1, 1,
+                   gl_format, gl_type,
+                   pixel_data);
+  /* Reset this */
+  glPixelStorei (GL_UNPACK_SKIP_PIXELS, 0);
+  glPixelStorei (GL_UNPACK_ROW_LENGTH, 0);
+  glPixelStorei (GL_UNPACK_SKIP_ROWS, 0);
+
+  gdk_gl_context_pop_debug_group (gdk_gl_context_get_current ());
+
+  *out_value = icon_data;
+
+  cairo_surface_destroy (surface);
+  g_free (free_data);
+
+  GSK_GL_TEXTURE_LIBRARY (self)->driver->command_queue->n_uploads++;
+
+  if (gdk_profiler_is_running ())
+    {
+      char message[64];
+      g_snprintf (message, sizeof message, "Size %dx%d", width, height);
+      gdk_profiler_add_mark (start_time, GDK_PROFILER_CURRENT_TIME-start_time, "Upload Icon", message);
+    }
+}
diff --git a/gsk/next/gskgliconlibraryprivate.h b/gsk/next/gskgliconlibraryprivate.h
new file mode 100644
index 0000000000..130c49d2bb
--- /dev/null
+++ b/gsk/next/gskgliconlibraryprivate.h
@@ -0,0 +1,60 @@
+/* gskgliconlibraryprivate.h
+ *
+ * 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
+ */
+
+#ifndef __GSK_GL_ICON_LIBRARY_PRIVATE_H__
+#define __GSK_GL_ICON_LIBRARY_PRIVATE_H__
+
+#include <pango/pango.h>
+
+#include "gskgltexturelibraryprivate.h"
+
+G_BEGIN_DECLS
+
+#define GSK_TYPE_GL_ICON_LIBRARY (gsk_gl_icon_library_get_type())
+
+typedef struct _GskGLIconData
+{
+  GskGLTextureAtlasEntry entry;
+  GdkTexture *source_texture;
+} GskGLIconData;
+
+G_DECLARE_FINAL_TYPE (GskGLIconLibrary, gsk_gl_icon_library, GSK, GL_ICON_LIBRARY, GskGLTextureLibrary)
+
+GskGLIconLibrary *gsk_gl_icon_library_new (GskNextDriver        *driver);
+void              gsk_gl_icon_library_add (GskGLIconLibrary     *self,
+                                           GdkTexture           *key,
+                                           const GskGLIconData **out_value);
+
+static inline void
+gsk_gl_icon_library_lookup_or_add (GskGLIconLibrary     *self,
+                                   GdkTexture           *key,
+                                   const GskGLIconData **out_value)
+{
+  GskGLTextureAtlasEntry *entry;
+
+  if G_LIKELY (gsk_gl_texture_library_lookup ((GskGLTextureLibrary *)self, key, &entry))
+    *out_value = (GskGLIconData *)entry;
+  else
+    gsk_gl_icon_library_add (self, key, out_value);
+}
+
+G_END_DECLS
+
+#endif /* __GSK_GL_ICON_LIBRARY_PRIVATE_H__ */
diff --git a/gsk/next/gskglprogram.c b/gsk/next/gskglprogram.c
new file mode 100644
index 0000000000..679a704f7a
--- /dev/null
+++ b/gsk/next/gskglprogram.c
@@ -0,0 +1,176 @@
+/* gskglprogram.c
+ *
+ * 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 "gskglcommandqueueprivate.h"
+#include "gskglprogramprivate.h"
+#include "gskgluniformstateprivate.h"
+
+G_DEFINE_TYPE (GskGLProgram, gsk_gl_program, G_TYPE_OBJECT)
+
+GskGLProgram *
+gsk_gl_program_new (GskNextDriver *driver,
+                    const char    *name,
+                    int            program_id)
+{
+  GskGLProgram *self;
+
+  g_return_val_if_fail (GSK_IS_NEXT_DRIVER (driver), NULL);
+  g_return_val_if_fail (program_id >= -1, NULL);
+
+  self = g_object_new (GSK_TYPE_GL_PROGRAM, NULL);
+  self->id = program_id;
+  self->name = g_strdup (name);
+  self->driver = g_object_ref (driver);
+  self->n_uniforms = 0;
+
+  return self;
+}
+
+static void
+gsk_gl_program_finalize (GObject *object)
+{
+  GskGLProgram *self = (GskGLProgram *)object;
+
+  if (self->id >= 0)
+    g_warning ("Leaking GLSL program %d (%s)",
+               self->id,
+               self->name ? self->name : "");
+
+  g_clear_pointer (&self->name, g_free);
+  g_clear_object (&self->driver);
+
+  G_OBJECT_CLASS (gsk_gl_program_parent_class)->finalize (object);
+}
+
+static void
+gsk_gl_program_class_init (GskGLProgramClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = gsk_gl_program_finalize;
+}
+
+static void
+gsk_gl_program_init (GskGLProgram *self)
+{
+  self->id = -1;
+
+  for (guint i = 0; i < G_N_ELEMENTS (self->uniform_locations); i++)
+    self->uniform_locations[i] = -1;
+}
+
+/**
+ * gsk_gl_program_add_uniform:
+ * @self: a #GskGLProgram
+ * @name: the name of the uniform such as "u_source"
+ * @key: the identifier to use for the uniform
+ *
+ * This method will create a mapping between @key and the location
+ * of the uniform on the GPU. This simplifies calling code to not
+ * need to know where the uniform location is and only register it
+ * when creating the program.
+ *
+ * You might use this with an enum of all your uniforms for the
+ * program and then register each of them like:
+ *
+ * ```
+ * gsk_gl_program_add_uniform (program, "u_source", UNIFORM_SOURCE);
+ * ```
+ *
+ * That allows you to set values for the program with something
+ * like the following:
+ *
+ * ```
+ * gsk_gl_program_set_uniform1i (program, UNIFORM_SOURCE, 1);
+ * ```
+ *
+ * Returns: %TRUE if the uniform was found; otherwise %FALSE
+ */
+gboolean
+gsk_gl_program_add_uniform (GskGLProgram *self,
+                            const char   *name,
+                            guint         key)
+{
+  GLint location;
+
+  g_return_val_if_fail (GSK_IS_GL_PROGRAM (self), FALSE);
+  g_return_val_if_fail (name != NULL, FALSE);
+  g_return_val_if_fail (key < 1024, FALSE);
+
+  if (-1 == (location = glGetUniformLocation (self->id, name)))
+    return FALSE;
+
+  self->uniform_locations[key] = location;
+
+  if (location >= self->n_uniforms)
+    self->n_uniforms = location + 1;
+
+#if 0
+  g_print ("program [%d] %s uniform %s at location %d.\n",
+           self->id, self->name, name, location);
+#endif
+
+  return TRUE;
+}
+
+/**
+ * gsk_gl_program_delete:
+ * @self: a #GskGLProgram
+ *
+ * Deletes the GLSL program.
+ *
+ * You must call gsk_gl_program_use() before and
+ * gsk_gl_program_unuse() after this function.
+ */
+void
+gsk_gl_program_delete (GskGLProgram *self)
+{
+  g_return_if_fail (GSK_IS_GL_PROGRAM (self));
+  g_return_if_fail (self->driver->command_queue != NULL);
+
+  gsk_gl_command_queue_delete_program (self->driver->command_queue, self->id);
+  self->id = -1;
+}
+
+/**
+ * gsk_gl_program_uniforms_added:
+ * @self: a #GskGLProgram
+ * @has_attachments: if any uniform is for a bind/texture attachment
+ *
+ * This function should be called after all of the uniforms ahve
+ * been added with gsk_gl_program_add_uniform().
+ *
+ * This function will setup the uniform state so that the program
+ * has fast access to the data buffers without as many lookups at
+ * runtime for comparison data.
+ */
+void
+gsk_gl_program_uniforms_added (GskGLProgram *self,
+                               gboolean      has_attachments)
+{
+  g_return_if_fail (GSK_IS_GL_PROGRAM (self));
+  g_return_if_fail (self->uniforms == NULL);
+
+  self->uniforms = self->driver->command_queue->uniforms;
+  self->program_info = gsk_gl_uniform_state_get_program (self->uniforms, self->id, self->n_uniforms);
+  self->program_info->has_attachments = has_attachments;
+}
diff --git a/gsk/next/gskglprogramprivate.h b/gsk/next/gskglprogramprivate.h
new file mode 100644
index 0000000000..beadf48758
--- /dev/null
+++ b/gsk/next/gskglprogramprivate.h
@@ -0,0 +1,273 @@
+/* gskglprogramprivate.h
+ *
+ * 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
+ */
+
+#ifndef __GSK_GL_PROGRAM_PRIVATE_H__
+#define __GSK_GL_PROGRAM_PRIVATE_H__
+
+#include "gskgltypesprivate.h"
+
+#include "gskglcommandqueueprivate.h"
+#include "gskgldriverprivate.h"
+
+G_BEGIN_DECLS
+
+#define GSK_TYPE_GL_PROGRAM (gsk_gl_program_get_type())
+
+G_DECLARE_FINAL_TYPE (GskGLProgram, gsk_gl_program, GSK, GL_PROGRAM, GObject)
+
+struct _GskGLProgram
+{
+  GObject parent_instance;
+
+  int id;
+  char *name;
+  GskNextDriver *driver;
+
+  /* In reality, this is the largest uniform position
+   * as returned after linking so that we can use direct
+   * indexes based on location.
+   */
+  guint n_uniforms;
+
+  /* Cached pointer to avoid lots of pointer chasing/lookups */
+  GskGLUniformState *uniforms;
+  GskGLUniformProgram *program_info;
+
+  /* For custom programs */
+  int texture_locations[4];
+  int args_locations[8];
+  int size_location;
+
+  /* Static array for key->location transforms */
+  int uniform_locations[32];
+};
+
+GskGLProgram *gsk_gl_program_new            (GskNextDriver           *driver,
+                                             const char              *name,
+                                             int                      program_id);
+gboolean      gsk_gl_program_add_uniform    (GskGLProgram            *self,
+                                             const char              *name,
+                                             guint                    key);
+void          gsk_gl_program_uniforms_added (GskGLProgram            *self,
+                                             gboolean                 has_attachments);
+void          gsk_gl_program_delete         (GskGLProgram            *self);
+
+#define gsk_gl_program_get_uniform_location(s,k) ((s)->uniform_locations[(k)])
+
+static inline void
+gsk_gl_program_set_uniform1fv (GskGLProgram *self,
+                               guint         key,
+                               guint         stamp,
+                               guint         count,
+                               const float  *values)
+{
+  gsk_gl_uniform_state_set1fv (self->uniforms, self->program_info,
+                               gsk_gl_program_get_uniform_location (self, key),
+                               stamp, count, values);
+}
+
+static inline void
+gsk_gl_program_set_uniform2fv (GskGLProgram *self,
+                               guint         key,
+                               guint         stamp,
+                               guint         count,
+                               const float  *values)
+{
+  gsk_gl_uniform_state_set2fv (self->uniforms, self->program_info,
+                               gsk_gl_program_get_uniform_location (self, key),
+                               stamp, count, values);
+}
+
+static inline void
+gsk_gl_program_set_uniform4fv (GskGLProgram *self,
+                               guint         key,
+                               guint         stamp,
+                               guint         count,
+                               const float  *values)
+{
+  gsk_gl_uniform_state_set4fv (self->uniforms, self->program_info,
+                               gsk_gl_program_get_uniform_location (self, key),
+                               stamp, count, values);
+}
+
+static inline void
+gsk_gl_program_set_uniform_rounded_rect (GskGLProgram         *self,
+                                         guint                 key,
+                                         guint                 stamp,
+                                         const GskRoundedRect *rounded_rect)
+{
+  gsk_gl_uniform_state_set_rounded_rect (self->uniforms, self->program_info,
+                                         gsk_gl_program_get_uniform_location (self, key),
+                                         stamp, rounded_rect);
+}
+
+static inline void
+gsk_gl_program_set_uniform1i (GskGLProgram *self,
+                              guint         key,
+                              guint         stamp,
+                              int           value0)
+{
+  gsk_gl_uniform_state_set1i (self->uniforms,
+                              self->program_info,
+                              gsk_gl_program_get_uniform_location (self, key),
+                              stamp, value0);
+}
+
+static inline void
+gsk_gl_program_set_uniform2i (GskGLProgram *self,
+                              guint         key,
+                              guint         stamp,
+                              int           value0,
+                              int           value1)
+{
+  gsk_gl_uniform_state_set2i (self->uniforms,
+                              self->program_info,
+                              gsk_gl_program_get_uniform_location (self, key),
+                              stamp, value0, value1);
+}
+
+static inline void
+gsk_gl_program_set_uniform3i (GskGLProgram *self,
+                              guint         key,
+                              guint         stamp,
+                              int           value0,
+                              int           value1,
+                              int           value2)
+{
+  gsk_gl_uniform_state_set3i (self->uniforms,
+                              self->program_info,
+                              gsk_gl_program_get_uniform_location (self, key),
+                              stamp, value0, value1, value2);
+}
+
+static inline void
+gsk_gl_program_set_uniform4i (GskGLProgram *self,
+                              guint         key,
+                              guint         stamp,
+                              int           value0,
+                              int           value1,
+                              int           value2,
+                              int           value3)
+{
+  gsk_gl_uniform_state_set4i (self->uniforms,
+                              self->program_info,
+                              gsk_gl_program_get_uniform_location (self, key),
+                              stamp, value0, value1, value2, value3);
+}
+
+static inline void
+gsk_gl_program_set_uniform1f (GskGLProgram *self,
+                              guint         key,
+                              guint         stamp,
+                              float         value0)
+{
+  gsk_gl_uniform_state_set1f (self->uniforms,
+                              self->program_info,
+                              gsk_gl_program_get_uniform_location (self, key),
+                              stamp, value0);
+}
+
+static inline void
+gsk_gl_program_set_uniform2f (GskGLProgram *self,
+                              guint         key,
+                              guint         stamp,
+                              float         value0,
+                              float         value1)
+{
+  gsk_gl_uniform_state_set2f (self->uniforms,
+                              self->program_info,
+                              gsk_gl_program_get_uniform_location (self, key),
+                              stamp, value0, value1);
+}
+
+static inline void
+gsk_gl_program_set_uniform3f (GskGLProgram *self,
+                              guint         key,
+                              guint         stamp,
+                              float         value0,
+                              float         value1,
+                              float         value2)
+{
+  gsk_gl_uniform_state_set3f (self->uniforms,
+                              self->program_info,
+                              gsk_gl_program_get_uniform_location (self, key),
+                              stamp, value0, value1, value2);
+}
+
+static inline void
+gsk_gl_program_set_uniform4f (GskGLProgram *self,
+                              guint         key,
+                              guint         stamp,
+                              float         value0,
+                              float         value1,
+                              float         value2,
+                              float         value3)
+{
+  gsk_gl_uniform_state_set4f (self->uniforms,
+                              self->program_info,
+                              gsk_gl_program_get_uniform_location (self, key),
+                              stamp, value0, value1, value2, value3);
+}
+
+static inline void
+gsk_gl_program_set_uniform_color (GskGLProgram  *self,
+                                  guint          key,
+                                  guint          stamp,
+                                  const GdkRGBA *color)
+{
+  gsk_gl_uniform_state_set_color (self->uniforms,
+                                  self->program_info,
+                                  gsk_gl_program_get_uniform_location (self, key),
+                                  stamp, color);
+}
+
+static inline void
+gsk_gl_program_set_uniform_texture (GskGLProgram *self,
+                                    guint         key,
+                                    guint         stamp,
+                                    GLenum        texture_target,
+                                    GLenum        texture_slot,
+                                    guint         texture_id)
+{
+  gsk_gl_attachment_state_bind_texture (self->driver->command_queue->attachments,
+                                        texture_target,
+                                        texture_slot,
+                                        texture_id);
+  gsk_gl_uniform_state_set_texture (self->uniforms,
+                                    self->program_info,
+                                    gsk_gl_program_get_uniform_location (self, key),
+                                    stamp, texture_slot);
+}
+
+static inline void
+gsk_gl_program_set_uniform_matrix (GskGLProgram            *self,
+                                   guint                    key,
+                                   guint                    stamp,
+                                   const graphene_matrix_t *matrix)
+{
+  gsk_gl_uniform_state_set_matrix (self->uniforms,
+                                   self->program_info,
+                                   gsk_gl_program_get_uniform_location (self, key),
+                                   stamp, matrix);
+}
+
+G_END_DECLS
+
+#endif /* __GSK_GL_PROGRAM_PRIVATE_H__ */
diff --git a/gsk/next/gskglprograms.defs b/gsk/next/gskglprograms.defs
new file mode 100644
index 0000000000..a6f745e071
--- /dev/null
+++ b/gsk/next/gskglprograms.defs
@@ -0,0 +1,83 @@
+GSK_GL_DEFINE_PROGRAM (blend,
+                       "/org/gtk/libgsk/glsl/blend.glsl",
+                       GSK_GL_ADD_UNIFORM (1, BLEND_SOURCE2, u_source2)
+                       GSK_GL_ADD_UNIFORM (2, BLEND_MODE, u_mode))
+
+GSK_GL_DEFINE_PROGRAM (blit,
+                       "/org/gtk/libgsk/glsl/blit.glsl",
+                       GSK_GL_NO_UNIFORMS)
+
+GSK_GL_DEFINE_PROGRAM (blur,
+                       "/org/gtk/libgsk/glsl/blur.glsl",
+                       GSK_GL_ADD_UNIFORM (1, BLUR_RADIUS, u_blur_radius)
+                       GSK_GL_ADD_UNIFORM (2, BLUR_SIZE, u_blur_size)
+                       GSK_GL_ADD_UNIFORM (3, BLUR_DIR, u_blur_dir))
+
+GSK_GL_DEFINE_PROGRAM (border,
+                       "/org/gtk/libgsk/glsl/border.glsl",
+                       GSK_GL_ADD_UNIFORM (1, BORDER_COLOR, u_color)
+                       GSK_GL_ADD_UNIFORM (2, BORDER_WIDTHS, u_widths)
+                       GSK_GL_ADD_UNIFORM (3, BORDER_OUTLINE_RECT, u_outline_rect))
+
+GSK_GL_DEFINE_PROGRAM (color,
+                       "/org/gtk/libgsk/glsl/color.glsl",
+                       GSK_GL_ADD_UNIFORM (1, COLOR_COLOR, u_color))
+
+GSK_GL_DEFINE_PROGRAM (coloring,
+                       "/org/gtk/libgsk/glsl/coloring.glsl",
+                       GSK_GL_ADD_UNIFORM (1, COLORING_COLOR, u_color))
+
+GSK_GL_DEFINE_PROGRAM (color_matrix,
+                       "/org/gtk/libgsk/glsl/color_matrix.glsl",
+                       GSK_GL_ADD_UNIFORM (1, COLOR_MATRIX_COLOR_MATRIX, u_color_matrix)
+                       GSK_GL_ADD_UNIFORM (2, COLOR_MATRIX_COLOR_OFFSET, u_color_offset))
+
+GSK_GL_DEFINE_PROGRAM (conic_gradient,
+                       "/org/gtk/libgsk/glsl/conic_gradient.glsl",
+                       GSK_GL_ADD_UNIFORM (1, CONIC_GRADIENT_COLOR_STOPS, u_color_stops)
+                       GSK_GL_ADD_UNIFORM (2, CONIC_GRADIENT_NUM_COLOR_STOPS, u_num_color_stops)
+                       GSK_GL_ADD_UNIFORM (3, CONIC_GRADIENT_GEOMETRY, u_geometry))
+
+GSK_GL_DEFINE_PROGRAM (cross_fade,
+                       "/org/gtk/libgsk/glsl/cross_fade.glsl",
+                       GSK_GL_ADD_UNIFORM (1, CROSS_FADE_PROGRESS, u_progress)
+                       GSK_GL_ADD_UNIFORM (2, CROSS_FADE_SOURCE2, u_source2))
+
+GSK_GL_DEFINE_PROGRAM (inset_shadow,
+                       "/org/gtk/libgsk/glsl/inset_shadow.glsl",
+                       GSK_GL_ADD_UNIFORM (1, INSET_SHADOW_COLOR, u_color)
+                       GSK_GL_ADD_UNIFORM (2, INSET_SHADOW_SPREAD, u_spread)
+                       GSK_GL_ADD_UNIFORM (3, INSET_SHADOW_OFFSET, u_offset)
+                       GSK_GL_ADD_UNIFORM (4, INSET_SHADOW_OUTLINE_RECT, u_outline_rect))
+
+GSK_GL_DEFINE_PROGRAM (linear_gradient,
+                       "/org/gtk/libgsk/glsl/linear_gradient.glsl",
+                       GSK_GL_ADD_UNIFORM (1, LINEAR_GRADIENT_COLOR_STOPS, u_color_stops)
+                       GSK_GL_ADD_UNIFORM (2, LINEAR_GRADIENT_NUM_COLOR_STOPS, u_num_color_stops)
+                       GSK_GL_ADD_UNIFORM (3, LINEAR_GRADIENT_POINTS, u_points)
+                       GSK_GL_ADD_UNIFORM (4, LINEAR_GRADIENT_REPEAT, u_repeat))
+
+GSK_GL_DEFINE_PROGRAM (outset_shadow,
+                       "/org/gtk/libgsk/glsl/outset_shadow.glsl",
+                       GSK_GL_ADD_UNIFORM (1, OUTSET_SHADOW_COLOR, u_color)
+                       GSK_GL_ADD_UNIFORM (2, OUTSET_SHADOW_OUTLINE_RECT, u_outline_rect))
+
+GSK_GL_DEFINE_PROGRAM (radial_gradient,
+                       "/org/gtk/libgsk/glsl/radial_gradient.glsl",
+                       GSK_GL_ADD_UNIFORM (1, RADIAL_GRADIENT_COLOR_STOPS, u_color_stops)
+                       GSK_GL_ADD_UNIFORM (2, RADIAL_GRADIENT_NUM_COLOR_STOPS, u_num_color_stops)
+                       GSK_GL_ADD_UNIFORM (3, RADIAL_GRADIENT_REPEAT, u_repeat)
+                       GSK_GL_ADD_UNIFORM (4, RADIAL_GRADIENT_RANGE, u_range)
+                       GSK_GL_ADD_UNIFORM (5, RADIAL_GRADIENT_GEOMETRY, u_geometry))
+
+GSK_GL_DEFINE_PROGRAM (repeat,
+                       "/org/gtk/libgsk/glsl/repeat.glsl",
+                       GSK_GL_ADD_UNIFORM (1, REPEAT_CHILD_BOUNDS, u_child_bounds)
+                       GSK_GL_ADD_UNIFORM (2, REPEAT_TEXTURE_RECT, u_texture_rect))
+
+GSK_GL_DEFINE_PROGRAM (unblurred_outset_shadow,
+                       "/org/gtk/libgsk/glsl/unblurred_outset_shadow.glsl",
+                       GSK_GL_ADD_UNIFORM (1, UNBLURRED_OUTSET_SHADOW_COLOR, u_color)
+                       GSK_GL_ADD_UNIFORM (2, UNBLURRED_OUTSET_SHADOW_SPREAD, u_spread)
+                       GSK_GL_ADD_UNIFORM (3, UNBLURRED_OUTSET_SHADOW_OFFSET, u_offset)
+                       GSK_GL_ADD_UNIFORM (4, UNBLURRED_OUTSET_SHADOW_OUTLINE_RECT, u_outline_rect))
diff --git a/gsk/next/gskglrenderer.c b/gsk/next/gskglrenderer.c
new file mode 100644
index 0000000000..b49e3c75f7
--- /dev/null
+++ b/gsk/next/gskglrenderer.c
@@ -0,0 +1,312 @@
+/* gskglrenderer.c
+ *
+ * Copyright 2020 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU Lesser General Public License as published by the Free
+ * Software Foundation; either version 2.1 of the License, or (at your option)
+ * any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "config.h"
+
+#include <gdk/gdkprofilerprivate.h>
+#include <gdk/gdksurfaceprivate.h>
+#include <gsk/gskdebugprivate.h>
+#include <gsk/gskrendererprivate.h>
+
+#include "gskglcommandqueueprivate.h"
+#include "gskgldriverprivate.h"
+#include "gskglprogramprivate.h"
+#include "gskglrenderjobprivate.h"
+#include "gskglrendererprivate.h"
+
+struct _GskNextRendererClass
+{
+  GskRendererClass parent_class;
+};
+
+struct _GskNextRenderer
+{
+  GskRenderer parent_instance;
+
+  /* This context is used to swap buffers when we are rendering directly
+   * to a GDK surface. It is also used to locate the shared driver for
+   * the display that we use to drive the command queue.
+   */
+  GdkGLContext *context;
+
+  /* Our command queue is private to this renderer and talks to the GL
+   * context for our target surface. This ensure that framebuffer 0 matches
+   * the surface we care about. Since the context is shared with other
+   * contexts from other renderers on the display, texture atlases,
+   * programs, and other objects are available to them all.
+   */
+  GskGLCommandQueue *command_queue;
+
+  /* The driver manages our program state and command queues. It also
+   * deals with caching textures, shaders, shadows, glyph, and icon
+   * caches through various helpers.
+   */
+  GskNextDriver *driver;
+};
+
+G_DEFINE_TYPE (GskNextRenderer, gsk_next_renderer, GSK_TYPE_RENDERER)
+
+GskRenderer *
+gsk_next_renderer_new (void)
+{
+  return g_object_new (GSK_TYPE_NEXT_RENDERER, NULL);
+}
+
+static gboolean
+gsk_next_renderer_realize (GskRenderer  *renderer,
+                           GdkSurface   *surface,
+                           GError      **error)
+{
+  G_GNUC_UNUSED gint64 start_time = GDK_PROFILER_CURRENT_TIME;
+  GskNextRenderer *self = (GskNextRenderer *)renderer;
+  GdkGLContext *context = NULL;
+  GdkGLContext *shared_context;
+  GskNextDriver *driver = NULL;
+  gboolean ret = FALSE;
+  gboolean debug_shaders = FALSE;
+
+  g_assert (GSK_IS_NEXT_RENDERER (self));
+  g_assert (GDK_IS_SURFACE (surface));
+
+  if (self->context != NULL)
+    return TRUE;
+
+  g_assert (self->driver == NULL);
+  g_assert (self->context == NULL);
+  g_assert (self->command_queue == NULL);
+
+  if (!(context = gdk_surface_create_gl_context (surface, error)) ||
+      !gdk_gl_context_realize (context, error))
+    goto failure;
+
+  if (!(shared_context = gdk_surface_get_shared_data_gl_context (surface)))
+    {
+      g_set_error (error,
+                   GDK_GL_ERROR,
+                   GDK_GL_ERROR_NOT_AVAILABLE,
+                   "Failed to locate shared GL context for driver");
+      goto failure;
+    }
+
+#ifdef G_ENABLE_DEBUG
+  if (GSK_RENDERER_DEBUG_CHECK (GSK_RENDERER (self), SHADERS))
+    debug_shaders = TRUE;
+#endif
+
+  if (!(driver = gsk_next_driver_from_shared_context (shared_context, debug_shaders, error)))
+    goto failure;
+
+  self->command_queue = gsk_next_driver_create_command_queue (driver, context);
+  self->context = g_steal_pointer (&context);
+  self->driver = g_steal_pointer (&driver);
+
+  gsk_gl_command_queue_set_profiler (self->command_queue,
+                                     gsk_renderer_get_profiler (renderer));
+
+  ret = TRUE;
+
+failure:
+  g_clear_object (&driver);
+  g_clear_object (&context);
+
+  gdk_profiler_end_mark (start_time, "GskNextRenderer realize", NULL);
+
+  return ret;
+}
+
+static void
+gsk_next_renderer_unrealize (GskRenderer *renderer)
+{
+  GskNextRenderer *self = (GskNextRenderer *)renderer;
+
+  g_assert (GSK_IS_NEXT_RENDERER (renderer));
+
+  g_clear_object (&self->driver);
+  g_clear_object (&self->context);
+  g_clear_object (&self->command_queue);
+}
+
+static cairo_region_t *
+get_render_region (GdkSurface   *surface,
+                   GdkGLContext *context)
+{
+  const cairo_region_t *damage;
+  GdkRectangle whole_surface;
+  GdkRectangle extents;
+
+  g_assert (GDK_IS_SURFACE (surface));
+  g_assert (GDK_IS_GL_CONTEXT (context));
+
+  whole_surface.x = 0;
+  whole_surface.y = 0;
+  whole_surface.width = gdk_surface_get_width (surface);
+  whole_surface.height = gdk_surface_get_height (surface);
+
+  /* Damage does not have scale factor applied. so we can compare
+   * it to whole surface which also doesn'th have scale factor applied.
+   */
+  damage = gdk_draw_context_get_frame_region (GDK_DRAW_CONTEXT (context));
+
+  if (cairo_region_contains_rectangle (damage, &whole_surface) == CAIRO_REGION_OVERLAP_IN)
+    return NULL;
+
+  /* If the extents match the full-scene, do the same as above */
+  cairo_region_get_extents (damage, &extents);
+  if (gdk_rectangle_equal (&extents, &whole_surface))
+    return NULL;
+
+  /* Draw clipped to the bounding-box of the region. */
+  return cairo_region_create_rectangle (&extents);
+}
+
+static void
+gsk_next_renderer_render (GskRenderer          *renderer,
+                          GskRenderNode        *root,
+                          const cairo_region_t *update_area)
+{
+  GskNextRenderer *self = (GskNextRenderer *)renderer;
+  cairo_region_t *render_region;
+  graphene_rect_t viewport;
+  GskGLRenderJob *job;
+  GdkSurface *surface;
+  float scale_factor;
+
+  g_assert (GSK_IS_NEXT_RENDERER (renderer));
+  g_assert (root != NULL);
+
+  surface = gdk_draw_context_get_surface (GDK_DRAW_CONTEXT (self->context));
+  scale_factor = gdk_surface_get_scale_factor (surface);
+
+  viewport.origin.x = 0;
+  viewport.origin.y = 0;
+  viewport.size.width = gdk_surface_get_width (surface) * scale_factor;
+  viewport.size.height = gdk_surface_get_height (surface) * scale_factor;
+
+  gdk_gl_context_make_current (self->context);
+  gdk_draw_context_begin_frame (GDK_DRAW_CONTEXT (self->context), update_area);
+
+  /* Must be called *AFTER* gdk_draw_context_begin_frame() */
+  render_region = get_render_region (surface, self->context);
+
+  gsk_next_driver_begin_frame (self->driver, self->command_queue);
+  job = gsk_gl_render_job_new (self->driver, &viewport, scale_factor, render_region, 0);
+#ifdef G_ENABLE_DEBUG
+  if (GSK_RENDERER_DEBUG_CHECK (GSK_RENDERER (self), FALLBACK))
+    gsk_gl_render_job_set_debug_fallback (job, TRUE);
+#endif
+  gsk_gl_render_job_render (job, root);
+  gsk_next_driver_end_frame (self->driver);
+  gsk_gl_render_job_free (job);
+
+  gdk_gl_context_make_current (self->context);
+  gdk_draw_context_end_frame (GDK_DRAW_CONTEXT (self->context));
+
+  gsk_next_driver_after_frame (self->driver);
+
+  cairo_region_destroy (render_region);
+}
+
+static GdkTexture *
+gsk_next_renderer_render_texture (GskRenderer           *renderer,
+                                  GskRenderNode         *root,
+                                  const graphene_rect_t *viewport)
+{
+  GskNextRenderer *self = (GskNextRenderer *)renderer;
+  GskGLRenderTarget *render_target;
+  GskGLRenderJob *job;
+  GdkTexture *texture = NULL;
+  guint texture_id;
+  int width;
+  int height;
+
+  g_assert (GSK_IS_NEXT_RENDERER (renderer));
+  g_assert (root != NULL);
+
+  width = ceilf (viewport->size.width);
+  height = ceilf (viewport->size.height);
+
+  if (gsk_next_driver_create_render_target (self->driver,
+                                            width, height,
+                                            GL_NEAREST, GL_NEAREST,
+                                            &render_target))
+    {
+      gsk_next_driver_begin_frame (self->driver, self->command_queue);
+      job = gsk_gl_render_job_new (self->driver, viewport, 1, NULL, render_target->framebuffer_id);
+#ifdef G_ENABLE_DEBUG
+      if (GSK_RENDERER_DEBUG_CHECK (GSK_RENDERER (self), FALLBACK))
+        gsk_gl_render_job_set_debug_fallback (job, TRUE);
+#endif
+      gsk_gl_render_job_render_flipped (job, root);
+      texture_id = gsk_next_driver_release_render_target (self->driver, render_target, FALSE);
+      texture = gsk_next_driver_create_gdk_texture (self->driver, texture_id);
+      gsk_next_driver_end_frame (self->driver);
+      gsk_gl_render_job_free (job);
+
+      gsk_next_driver_after_frame (self->driver);
+    }
+
+  return g_steal_pointer (&texture);
+}
+
+static void
+gsk_next_renderer_dispose (GObject *object)
+{
+#ifdef G_ENABLE_DEBUG
+  GskNextRenderer *self = (GskNextRenderer *)object;
+
+  g_assert (self->driver == NULL);
+#endif
+
+  G_OBJECT_CLASS (gsk_next_renderer_parent_class)->dispose (object);
+}
+
+static void
+gsk_next_renderer_class_init (GskNextRendererClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GskRendererClass *renderer_class = GSK_RENDERER_CLASS (klass);
+
+  object_class->dispose = gsk_next_renderer_dispose;
+
+  renderer_class->realize = gsk_next_renderer_realize;
+  renderer_class->unrealize = gsk_next_renderer_unrealize;
+  renderer_class->render = gsk_next_renderer_render;
+  renderer_class->render_texture = gsk_next_renderer_render_texture;
+}
+
+static void
+gsk_next_renderer_init (GskNextRenderer *self)
+{
+}
+
+gboolean
+gsk_next_renderer_try_compile_gl_shader (GskNextRenderer  *renderer,
+                                         GskGLShader      *shader,
+                                         GError          **error)
+{
+  GskGLProgram *program;
+
+  g_return_val_if_fail (GSK_IS_NEXT_RENDERER (renderer), FALSE);
+  g_return_val_if_fail (shader != NULL, FALSE);
+
+  program = gsk_next_driver_lookup_shader (renderer->driver, shader, error);
+
+  return program != NULL;
+}
diff --git a/gsk/next/gskglrenderer.h b/gsk/next/gskglrenderer.h
new file mode 100644
index 0000000000..b8a09c1b86
--- /dev/null
+++ b/gsk/next/gskglrenderer.h
@@ -0,0 +1,47 @@
+/* gskglrenderer.h
+ *
+ * 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
+ */
+
+#ifndef __GSK_NEXT_RENDERER_H__
+#define __GSK_NEXT_RENDERER_H__
+
+#include <gsk/gskrenderer.h>
+
+G_BEGIN_DECLS
+
+#define GSK_TYPE_NEXT_RENDERER (gsk_next_renderer_get_type ())
+
+#define GSK_NEXT_RENDERER(obj)                    (G_TYPE_CHECK_INSTANCE_CAST ((obj), 
GSK_TYPE_NEXT_RENDERER, GskNextRenderer))
+#define GSK_IS_NEXT_RENDERER(obj)                 (G_TYPE_CHECK_INSTANCE_TYPE ((obj), 
GSK_TYPE_NEXT_RENDERER))
+#define GSK_NEXT_RENDERER_CLASS(klass)            (G_TYPE_CHECK_CLASS_CAST ((klass), GSK_TYPE_NEXT_RENDERER, 
GskNextRendererClass))
+#define GSK_IS_NEXT_RENDERER_CLASS(klass)         (G_TYPE_CHECK_CLASS_TYPE ((klass), GSK_TYPE_NEXT_RENDERER))
+#define GSK_NEXT_RENDERER_GET_CLASS(obj)          (G_TYPE_INSTANCE_GET_CLASS ((obj), GSK_TYPE_NEXT_RENDERER, 
GskNextRendererClass))
+
+typedef struct _GskNextRenderer                   GskNextRenderer;
+typedef struct _GskNextRendererClass              GskNextRendererClass;
+
+GDK_AVAILABLE_IN_ALL
+GType                   gsk_next_renderer_get_type                (void) G_GNUC_CONST;
+
+GDK_AVAILABLE_IN_ALL
+GskRenderer *           gsk_next_renderer_new                     (void);
+
+G_END_DECLS
+
+#endif /* __GSK_NEXT_RENDERER__ */
diff --git a/gsk/next/gskglrendererprivate.h b/gsk/next/gskglrendererprivate.h
new file mode 100644
index 0000000000..fc8ad12236
--- /dev/null
+++ b/gsk/next/gskglrendererprivate.h
@@ -0,0 +1,34 @@
+/* gskglrendererprivate.h
+ *
+ * Copyright 2021 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
+ */
+
+#ifndef __GSK_NEXT_RENDERER_PRIVATE_H__
+#define __GSK_NEXT_RENDERER_PRIVATE_H__
+
+#include "gskglrenderer.h"
+
+G_BEGIN_DECLS
+
+gboolean gsk_next_renderer_try_compile_gl_shader (GskNextRenderer  *renderer,
+                                                  GskGLShader      *shader,
+                                                  GError          **error);
+
+G_END_DECLS
+
+#endif /* __GSK_NEXT_RENDERER_PRIVATE_H__ */
diff --git a/gsk/next/gskglrenderjob.c b/gsk/next/gskglrenderjob.c
new file mode 100644
index 0000000000..4c713687c2
--- /dev/null
+++ b/gsk/next/gskglrenderjob.c
@@ -0,0 +1,3756 @@
+/* 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"
+
+#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;
+} GskGLRenderClip;
+
+typedef struct _GskGLRenderModelview
+{
+  GskTransform *transform;
+  float scale_x;
+  float scale_y;
+  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.
+   */
+  GskNextDriver *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;
+
+  /* If we should be rendering red zones over fallback nodes */
+  guint debug_fallback : 1;
+};
+
+typedef struct _GskGLRenderOffscreen
+{
+  const graphene_rect_t *bounds;
+  struct {
+    float x;
+    float y;
+    float x2;
+    float y2;
+  } area;
+  guint texture_id;
+  guint force_offscreen : 1;
+  guint reset_clip : 1;
+  guint do_not_cache : 1;
+  guint linear_filter : 1;
+  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 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 int
+_isnan_f (float x)
+{
+  return x != x;
+}
+
+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 ||
+         _isnan_f (node->bounds.size.width) ||
+         _isnan_f (node->bounds.size.height);
+}
+
+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_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));
+  /* top right? */
+  corners[1] = rounded_rect_has_corner (rounded, 1) &&
+               rect_intersects (non_rounded,
+                                &rounded_rect_corner (rounded, 1));
+  /* bottom right? */
+  corners[2] = rounded_rect_has_corner (rounded, 2) &&
+               rect_intersects (non_rounded,
+                                &rounded_rect_corner (rounded, 2));
+  /* bottom left */
+  corners[3] = rounded_rect_has_corner (rounded, 3) &&
+               rect_intersects (non_rounded,
+                                &rounded_rect_corner (rounded, 3));
+
+  if (corners[0] && !rect_contains_rect (non_rounded, &rounded_rect_corner (rounded, 0)))
+    return FALSE;
+  if (corners[1] && !rect_contains_rect (non_rounded, &rounded_rect_corner (rounded, 1)))
+    return FALSE;
+  if (corners[2] && !rect_contains_rect (non_rounded, &rounded_rect_corner (rounded, 2)))
+    return FALSE;
+  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)
+{
+  float dummy;
+  graphene_matrix_t m;
+
+  gsk_transform_to_matrix (modelview->transform, &modelview->matrix);
+
+  switch (gsk_transform_get_category (modelview->transform))
+    {
+    case GSK_TRANSFORM_CATEGORY_IDENTITY:
+    case GSK_TRANSFORM_CATEGORY_2D_TRANSLATE:
+      modelview->scale_x = 1;
+      modelview->scale_y = 1;
+      break;
+
+    case GSK_TRANSFORM_CATEGORY_2D_AFFINE:
+      gsk_transform_to_affine (modelview->transform,
+                               &modelview->scale_x, &modelview->scale_y,
+                               &dummy, &dummy);
+      break;
+
+    case GSK_TRANSFORM_CATEGORY_UNKNOWN:
+    case GSK_TRANSFORM_CATEGORY_ANY:
+    case GSK_TRANSFORM_CATEGORY_3D:
+    case GSK_TRANSFORM_CATEGORY_2D:
+      {
+        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 (&m, 0, 0),
+                            graphene_matrix_get_value (&m, 1, 0),
+                            graphene_matrix_get_value (&m, 2, 0));
+
+        graphene_vec3_init (&col2,
+                            graphene_matrix_get_value (&m, 0, 1),
+                            graphene_matrix_get_value (&m, 1, 1),
+                            graphene_matrix_get_value (&m, 2, 1));
+
+        modelview->scale_x = graphene_vec3_length (&col1);
+        modelview->scale_y = graphene_vec3_length (&col2);
+      }
+      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 previews 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);
+
+  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 dx, dy, scale_x, scale_y;
+
+      gsk_transform_to_affine (transform, &scale_x, &scale_y, &dx, &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;
+
+      /* Normaize 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 gboolean
+gsk_gl_render_job_node_overlaps_clip (GskGLRenderJob      *job,
+                                      const GskRenderNode *node)
+{
+  graphene_rect_t transformed_bounds;
+  gsk_gl_render_job_transform_bounds (job, &node->bounds, &transformed_bounds);
+  return rect_intersects (&job->current_clip->rect.bounds, &transformed_bounds);
+}
+
+/* load_vertex_data_with_region */
+static inline void
+gsk_gl_render_job_load_vertices_from_offscreen (GskGLRenderJob             *job,
+                                                const graphene_rect_t      *bounds,
+                                                const GskGLRenderOffscreen *offscreen)
+{
+  GskGLDrawVertex *vertices = gsk_gl_command_queue_add_vertices (job->command_queue);
+  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;
+
+  vertices[0].position[0] = min_x;
+  vertices[0].position[1] = min_y;
+  vertices[0].uv[0] = offscreen->area.x;
+  vertices[0].uv[1] = y1;
+
+  vertices[1].position[0] = min_x;
+  vertices[1].position[1] = max_y;
+  vertices[1].uv[0] = offscreen->area.x;
+  vertices[1].uv[1] = y2;
+
+  vertices[2].position[0] = max_x;
+  vertices[2].position[1] = min_y;
+  vertices[2].uv[0] = offscreen->area.x2;
+  vertices[2].uv[1] = y1;
+
+  vertices[3].position[0] = max_x;
+  vertices[3].position[1] = max_y;
+  vertices[3].uv[0] = offscreen->area.x2;
+  vertices[3].uv[1] = y2;
+
+  vertices[4].position[0] = min_x;
+  vertices[4].position[1] = max_y;
+  vertices[4].uv[0] = offscreen->area.x;
+  vertices[4].uv[1] = y2;
+
+  vertices[5].position[0] = max_x;
+  vertices[5].position[1] = min_y;
+  vertices[5].uv[0] = offscreen->area.x2;
+  vertices[5].uv[1] = y1;
+}
+
+/* load_float_vertex_data */
+static inline void
+gsk_gl_render_job_draw (GskGLRenderJob *job,
+                        float           x,
+                        float           y,
+                        float           width,
+                        float           height)
+{
+  GskGLDrawVertex *vertices = gsk_gl_command_queue_add_vertices (job->command_queue);
+  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;
+
+  vertices[0].position[0] = min_x;
+  vertices[0].position[1] = min_y;
+  vertices[0].uv[0] = 0;
+  vertices[0].uv[1] = 0;
+
+  vertices[1].position[0] = min_x;
+  vertices[1].position[1] = max_y;
+  vertices[1].uv[0] = 0;
+  vertices[1].uv[1] = 1;
+
+  vertices[2].position[0] = max_x;
+  vertices[2].position[1] = min_y;
+  vertices[2].uv[0] = 1;
+  vertices[2].uv[1] = 0;
+
+  vertices[3].position[0] = max_x;
+  vertices[3].position[1] = max_y;
+  vertices[3].uv[0] = 1;
+  vertices[3].uv[1] = 1;
+
+  vertices[4].position[0] = min_x;
+  vertices[4].position[1] = max_y;
+  vertices[4].uv[0] = 0;
+  vertices[4].uv[1] = 1;
+
+  vertices[5].position[0] = max_x;
+  vertices[5].position[1] = min_y;
+  vertices[5].uv[0] = 1;
+  vertices[5].uv[1] = 0;
+}
+
+/* load_vertex_data */
+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);
+}
+
+/* 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)
+{
+  GskGLDrawVertex *vertices = gsk_gl_command_queue_add_vertices (job->command_queue);
+
+  vertices[0].position[0] = min_x;
+  vertices[0].position[1] = min_y;
+  vertices[0].uv[0] = 0;
+  vertices[0].uv[1] = 1;
+
+  vertices[1].position[0] = min_x;
+  vertices[1].position[1] = max_y;
+  vertices[1].uv[0] = 0;
+  vertices[1].uv[1] = 0;
+
+  vertices[2].position[0] = max_x;
+  vertices[2].position[1] = min_y;
+  vertices[2].uv[0] = 1;
+  vertices[2].uv[1] = 1;
+
+  vertices[3].position[0] = max_x;
+  vertices[3].position[1] = max_y;
+  vertices[3].uv[0] = 1;
+  vertices[3].uv[1] = 0;
+
+  vertices[4].position[0] = min_x;
+  vertices[4].position[1] = max_y;
+  vertices[4].uv[0] = 0;
+  vertices[4].uv[1] = 0;
+
+  vertices[5].position[0] = max_x;
+  vertices[5].position[1] = min_y;
+  vertices[5].uv[0] = 1;
+  vertices[5].uv[1] = 1;
+}
+
+/* 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;
+
+  gsk_gl_render_job_draw_coords (job, min_x, min_y, max_x, max_y);
+}
+
+static inline void
+gsk_gl_render_job_begin_draw (GskGLRenderJob *job,
+                              GskGLProgram   *program)
+{
+  gsk_gl_command_queue_begin_draw (job->command_queue,
+                                   program->program_info,
+                                   &job->viewport);
+
+  if (program->uniform_locations[UNIFORM_SHARED_VIEWPORT] > -1)
+    gsk_gl_uniform_state_set4fv (program->uniforms,
+                                 program->program_info,
+                                 program->uniform_locations[UNIFORM_SHARED_VIEWPORT],
+                                 job->driver->stamps[UNIFORM_SHARED_VIEWPORT],
+                                 1,
+                                 (const float *)&job->viewport);
+
+  if (program->uniform_locations[UNIFORM_SHARED_MODELVIEW] > -1)
+    gsk_gl_uniform_state_set_matrix (program->uniforms,
+                                     program->program_info,
+                                     program->uniform_locations[UNIFORM_SHARED_MODELVIEW],
+                                     job->driver->stamps[UNIFORM_SHARED_MODELVIEW],
+                                     &job->current_modelview->matrix);
+
+  if (program->uniform_locations[UNIFORM_SHARED_PROJECTION] > -1)
+    gsk_gl_uniform_state_set_matrix (program->uniforms,
+                                     program->program_info,
+                                     program->uniform_locations[UNIFORM_SHARED_PROJECTION],
+                                     job->driver->stamps[UNIFORM_SHARED_PROJECTION],
+                                     &job->projection);
+
+  if (program->uniform_locations[UNIFORM_SHARED_CLIP_RECT] > -1)
+    gsk_gl_uniform_state_set_rounded_rect (program->uniforms,
+                                           program->program_info,
+                                           program->uniform_locations[UNIFORM_SHARED_CLIP_RECT],
+                                           job->driver->stamps[UNIFORM_SHARED_CLIP_RECT],
+                                           &job->current_clip->rect);
+
+  if (program->uniform_locations[UNIFORM_SHARED_ALPHA] > -1)
+    gsk_gl_uniform_state_set1f (program->uniforms,
+                                program->program_info,
+                                program->uniform_locations[UNIFORM_SHARED_ALPHA],
+                                job->driver->stamps[UNIFORM_SHARED_ALPHA],
+                                job->alpha);
+}
+
+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);
+}
+
+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_next_driver_lookup_texture (job->driver, &key);
+
+  if (cached_id != 0)
+    {
+      gsk_gl_render_job_begin_draw (job, job->driver->blit);
+      gsk_gl_program_set_uniform_texture (job->driver->blit,
+                                          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_next_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_next_driver_cache_texture (job->driver, &key, texture_id);
+
+  gsk_gl_render_job_begin_draw (job, job->driver->blit);
+  gsk_gl_program_set_uniform_texture (job->driver->blit,
+                                      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_next_driver_create_render_target (job->driver,
+                                             MAX (texture_to_blur_width, 1),
+                                             MAX (texture_to_blur_height, 1),
+                                             GL_NEAREST, GL_NEAREST,
+                                             &pass1))
+    return 0;
+
+  if (texture_to_blur_width <= 0 || texture_to_blur_height <= 0)
+    return gsk_next_driver_release_render_target (job->driver, pass1, FALSE);
+
+  if (!gsk_next_driver_create_render_target (job->driver,
+                                             texture_to_blur_width,
+                                             texture_to_blur_height,
+                                             GL_NEAREST, GL_NEAREST,
+                                             &pass2))
+    return gsk_next_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, job->driver->blur);
+  gsk_gl_program_set_uniform_texture (job->driver->blur,
+                                      UNIFORM_SHARED_SOURCE, 0,
+                                      GL_TEXTURE_2D,
+                                      GL_TEXTURE0,
+                                      offscreen->texture_id);
+  gsk_gl_program_set_uniform1f (job->driver->blur,
+                                UNIFORM_BLUR_RADIUS, 0,
+                                blur_radius_x);
+  gsk_gl_program_set_uniform2f (job->driver->blur,
+                                UNIFORM_BLUR_SIZE, 0,
+                                texture_to_blur_width,
+                                texture_to_blur_height);
+  gsk_gl_program_set_uniform2f (job->driver->blur,
+                                UNIFORM_BLUR_DIR, 0,
+                                1, 0);
+  gsk_gl_render_job_draw_coords (job, 0, 0, texture_to_blur_width, texture_to_blur_height);
+  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, job->driver->blur);
+  gsk_gl_program_set_uniform_texture (job->driver->blur,
+                                      UNIFORM_SHARED_SOURCE, 0,
+                                      GL_TEXTURE_2D,
+                                      GL_TEXTURE0,
+                                      pass1->texture_id);
+  gsk_gl_program_set_uniform1f (job->driver->blur,
+                                UNIFORM_BLUR_RADIUS, 0,
+                                blur_radius_y);
+  gsk_gl_program_set_uniform2f (job->driver->blur,
+                                UNIFORM_BLUR_SIZE, 0,
+                                texture_to_blur_width,
+                                texture_to_blur_height);
+  gsk_gl_program_set_uniform2f (job->driver->blur,
+                                UNIFORM_BLUR_DIR, 0,
+                                0, 1);
+  gsk_gl_render_job_draw_coords (job, 0, 0, texture_to_blur_width, texture_to_blur_height);
+  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_next_driver_release_render_target (job->driver, pass1, TRUE);
+
+  return gsk_next_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;
+}
+
+static inline void
+gsk_gl_render_job_visit_color_node (GskGLRenderJob      *job,
+                                    const GskRenderNode *node)
+{
+  gsk_gl_render_job_begin_draw (job, job->driver->color);
+  gsk_gl_program_set_uniform_color (job->driver->color,
+                                    UNIFORM_COLOR_COLOR, 0,
+                                    gsk_color_node_get_color (node));
+  gsk_gl_render_job_draw_rect (job, &node->bounds);
+  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, job->driver->linear_gradient);
+  gsk_gl_program_set_uniform1i (job->driver->linear_gradient,
+                                UNIFORM_LINEAR_GRADIENT_NUM_COLOR_STOPS, 0,
+                                n_color_stops);
+  gsk_gl_program_set_uniform1fv (job->driver->linear_gradient,
+                                 UNIFORM_LINEAR_GRADIENT_COLOR_STOPS, 0,
+                                 n_color_stops * 5,
+                                 (const float *)stops);
+  gsk_gl_program_set_uniform4f (job->driver->linear_gradient,
+                                UNIFORM_LINEAR_GRADIENT_POINTS, 0,
+                                x1, y1, x2 - x1, y2 - y1);
+  gsk_gl_program_set_uniform1i (job->driver->linear_gradient,
+                                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, job->driver->conic_gradient);
+  gsk_gl_program_set_uniform1i (job->driver->conic_gradient,
+                                UNIFORM_CONIC_GRADIENT_NUM_COLOR_STOPS, 0,
+                                n_color_stops);
+  gsk_gl_program_set_uniform1fv (job->driver->conic_gradient,
+                                 UNIFORM_CONIC_GRADIENT_COLOR_STOPS, 0,
+                                 n_color_stops * 5,
+                                 (const float *)stops);
+  gsk_gl_program_set_uniform4f (job->driver->conic_gradient,
+                                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, job->driver->radial_gradient);
+  gsk_gl_program_set_uniform1i (job->driver->radial_gradient,
+                                UNIFORM_RADIAL_GRADIENT_NUM_COLOR_STOPS, 0,
+                                n_color_stops);
+  gsk_gl_program_set_uniform1fv (job->driver->radial_gradient,
+                                 UNIFORM_RADIAL_GRADIENT_COLOR_STOPS, 0,
+                                 n_color_stops * 5,
+                                 (const float *)stops);
+  gsk_gl_program_set_uniform1i (job->driver->radial_gradient,
+                                UNIFORM_RADIAL_GRADIENT_REPEAT, 0,
+                                repeat);
+  gsk_gl_program_set_uniform2f (job->driver->radial_gradient,
+                                UNIFORM_RADIAL_GRADIENT_RANGE, 0,
+                                scale, bias);
+  gsk_gl_program_set_uniform4f (job->driver->radial_gradient,
+                                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
+    {
+      GskRoundedRect scaled_clip;
+      GskGLRenderOffscreen offscreen = {0};
+
+      offscreen.bounds = &child->bounds;
+      offscreen.force_offscreen = TRUE;
+
+      scaled_clip = GSK_ROUNDED_RECT_INIT ((job->offset_x + clip->origin.x) * job->scale_x,
+                                           (job->offset_y + clip->origin.y) * job->scale_y,
+                                           clip->size.width * job->scale_x,
+                                           clip->size.height * job->scale_y);
+
+      gsk_gl_render_job_push_clip (job, &scaled_clip);
+      gsk_gl_render_job_visit_node_with_offscreen (job, child, &offscreen);
+      gsk_gl_render_job_pop_clip (job);
+
+      g_assert (offscreen.texture_id);
+
+      gsk_gl_render_job_begin_draw (job, job->driver->blit);
+      gsk_gl_program_set_uniform_texture (job->driver->blit,
+                                          UNIFORM_SHARED_SOURCE, 0,
+                                          GL_TEXTURE_2D,
+                                          GL_TEXTURE0,
+                                          offscreen.texture_id);
+      gsk_gl_render_job_draw_offscreen_rect (job, &child->bounds);
+      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;
+
+      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, job->driver->blit);
+      gsk_gl_program_set_uniform_texture (job->driver->blit,
+                                          UNIFORM_SHARED_SOURCE, 0,
+                                          GL_TEXTURE_2D,
+                                          GL_TEXTURE0,
+                                          offscreen.texture_id);
+      gsk_gl_render_job_load_vertices_from_offscreen (job, &node->bounds, &offscreen);
+      gsk_gl_render_job_end_draw (job);
+    }
+}
+
+static inline void
+sort_border_sides (const GdkRGBA *colors,
+                   int           *indices)
+{
+  gboolean done[4] = {0, 0, 0, 0};
+  guint cur = 0;
+
+  for (guint i = 0; i < 3; i++)
+    {
+      if (done[i])
+        continue;
+
+      indices[cur] = i;
+      done[i] = TRUE;
+      cur++;
+
+      for (guint k = i + 1; k < 4; k ++)
+        {
+          if (memcmp (&colors[k], &colors[i], sizeof (GdkRGBA)) == 0)
+            {
+              indices[cur] = k;
+              done[k] = TRUE;
+              cur++;
+            }
+        }
+
+      if (cur >= 4)
+        break;
+    }
+}
+
+static inline void
+gsk_gl_render_job_visit_uniform_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);
+  GskRoundedRect outline;
+
+  gsk_gl_render_job_transform_rounded_rect (job, rounded_outline, &outline);
+
+  gsk_gl_render_job_begin_draw (job, job->driver->inset_shadow);
+  gsk_gl_program_set_uniform_rounded_rect (job->driver->inset_shadow,
+                                           UNIFORM_INSET_SHADOW_OUTLINE_RECT, 0,
+                                           &outline);
+  gsk_gl_program_set_uniform_color (job->driver->inset_shadow,
+                                    UNIFORM_INSET_SHADOW_COLOR, 0,
+                                    &colors[0]);
+  gsk_gl_program_set_uniform1f (job->driver->inset_shadow,
+                                UNIFORM_INSET_SHADOW_SPREAD, 0,
+                                widths[0]);
+  gsk_gl_program_set_uniform2f (job->driver->inset_shadow,
+                                UNIFORM_INSET_SHADOW_OFFSET, 0,
+                                0, 0);
+  gsk_gl_render_job_draw_rect (job, &node->bounds);
+  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];
+
+  /* Top left */
+  if (widths[3] > 0)
+    sizes[0].w = MAX (widths[3], rounded_outline->corner[0].width);
+  else
+    sizes[0].w = 0;
+
+  if (widths[0] > 0)
+    sizes[0].h = MAX (widths[0], rounded_outline->corner[0].height);
+  else
+    sizes[0].h = 0;
+
+  /* Top right */
+  if (widths[1] > 0)
+    sizes[1].w = MAX (widths[1], rounded_outline->corner[1].width);
+  else
+    sizes[1].w = 0;
+
+  if (widths[0] > 0)
+    sizes[1].h = MAX (widths[0], rounded_outline->corner[1].height);
+  else
+    sizes[1].h = 0;
+
+  /* Bottom right */
+  if (widths[1] > 0)
+    sizes[2].w = MAX (widths[1], rounded_outline->corner[2].width);
+  else
+    sizes[2].w = 0;
+
+  if (widths[2] > 0)
+    sizes[2].h = MAX (widths[2], rounded_outline->corner[2].height);
+  else
+    sizes[2].h = 0;
+
+
+  /* Bottom left */
+  if (widths[3] > 0)
+    sizes[3].w = MAX (widths[3], rounded_outline->corner[3].width);
+  else
+    sizes[3].w = 0;
+
+  if (widths[2] > 0)
+    sizes[3].h = MAX (widths[2], rounded_outline->corner[3].height);
+  else
+    sizes[3].h = 0;
+
+  {
+    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;
+    const GskGLDrawVertex side_data[4][6] = {
+      /* Top */
+      {
+        { { min_x,              min_y              }, { 0, 1 }, }, /* Upper left */
+        { { min_x + sizes[0].w, min_y + sizes[0].h }, { 0, 0 }, }, /* Lower left */
+        { { max_x,              min_y              }, { 1, 1 }, }, /* Upper right */
+
+        { { max_x - sizes[1].w, min_y + sizes[1].h }, { 1, 0 }, }, /* Lower right */
+        { { min_x + sizes[0].w, min_y + sizes[0].h }, { 0, 0 }, }, /* Lower left */
+        { { max_x,              min_y              }, { 1, 1 }, }, /* Upper right */
+      },
+      /* Right */
+      {
+        { { max_x - sizes[1].w, min_y + sizes[1].h }, { 0, 1 }, }, /* Upper left */
+        { { max_x - sizes[2].w, max_y - sizes[2].h }, { 0, 0 }, }, /* Lower left */
+        { { max_x,              min_y              }, { 1, 1 }, }, /* Upper right */
+
+        { { max_x,              max_y              }, { 1, 0 }, }, /* Lower right */
+        { { max_x - sizes[2].w, max_y - sizes[2].h }, { 0, 0 }, }, /* Lower left */
+        { { max_x,              min_y              }, { 1, 1 }, }, /* Upper right */
+      },
+      /* Bottom */
+      {
+        { { min_x + sizes[3].w, max_y - sizes[3].h }, { 0, 1 }, }, /* Upper left */
+        { { min_x,              max_y              }, { 0, 0 }, }, /* Lower left */
+        { { max_x - sizes[2].w, max_y - sizes[2].h }, { 1, 1 }, }, /* Upper right */
+
+        { { max_x,              max_y              }, { 1, 0 }, }, /* Lower right */
+        { { min_x            ,  max_y              }, { 0, 0 }, }, /* Lower left */
+        { { max_x - sizes[2].w, max_y - sizes[2].h }, { 1, 1 }, }, /* Upper right */
+      },
+      /* Left */
+      {
+        { { min_x,              min_y              }, { 0, 1 }, }, /* Upper left */
+        { { min_x,              max_y              }, { 0, 0 }, }, /* Lower left */
+        { { min_x + sizes[0].w, min_y + sizes[0].h }, { 1, 1 }, }, /* Upper right */
+
+        { { min_x + sizes[3].w, max_y - sizes[3].h }, { 1, 0 }, }, /* Lower right */
+        { { min_x,              max_y              }, { 0, 0 }, }, /* Lower left */
+        { { min_x + sizes[0].w, min_y + sizes[0].h }, { 1, 1 }, }, /* Upper right */
+      }
+    };
+    int indices[4] = { 0, 1, 2, 3 };
+    GskRoundedRect outline;
+
+    /* We sort them by color */
+    sort_border_sides (colors, indices);
+
+    /* Prepare outline */
+    gsk_gl_render_job_transform_rounded_rect (job, rounded_outline, &outline);
+
+    gsk_gl_program_set_uniform4fv (job->driver->border,
+                                   UNIFORM_BORDER_WIDTHS, 0,
+                                   1,
+                                   widths);
+    gsk_gl_program_set_uniform_rounded_rect (job->driver->border,
+                                             UNIFORM_BORDER_OUTLINE_RECT, 0,
+                                             &outline);
+
+    for (guint i = 0; i < 4; i++)
+      {
+        GskGLDrawVertex *vertices;
+
+        if (widths[indices[i]] <= 0)
+          continue;
+
+        gsk_gl_render_job_begin_draw (job, job->driver->border);
+        gsk_gl_program_set_uniform4fv (job->driver->border,
+                                       UNIFORM_BORDER_COLOR, 0,
+                                       1,
+                                       (const float *)&colors[indices[i]]);
+        vertices = gsk_gl_command_queue_add_vertices (job->command_queue);
+        memcpy (vertices, side_data[indices[i]], sizeof (GskGLDrawVertex) * GSK_GL_N_VERTICES);
+        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_to_translate (transform, &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};
+
+          offscreen.bounds = &child->bounds;
+          offscreen.reset_clip = TRUE;
+
+          if (!result_is_axis_aligned (transform, &child->bounds))
+            offscreen.linear_filter = TRUE;
+
+          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. */
+              /* TODO: We should compute a modelview containing only the "non-trivial"
+               *       part (e.g. the rotation) and use that. We want to keep the scale
+               *       for the texture.
+               */
+              gsk_gl_render_job_push_modelview (job, transform);
+
+              gsk_gl_render_job_begin_draw (job, job->driver->blit);
+              gsk_gl_program_set_uniform_texture (job->driver->blit,
+                                                  UNIFORM_SHARED_SOURCE, 0,
+                                                  GL_TEXTURE_2D,
+                                                  GL_TEXTURE0,
+                                                  offscreen.texture_id);
+              gsk_gl_render_job_load_vertices_from_offscreen (job, &child->bounds, &offscreen);
+              gsk_gl_render_job_end_draw (job);
+
+              gsk_gl_render_job_pop_modelview (job);
+            }
+        }
+    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;
+
+  gsk_gl_render_job_transform_rounded_rect (job, outline, &transformed_outline);
+
+  gsk_gl_render_job_begin_draw (job, job->driver->inset_shadow);
+  gsk_gl_program_set_uniform_rounded_rect (job->driver->inset_shadow,
+                                           UNIFORM_INSET_SHADOW_OUTLINE_RECT, 0,
+                                           &transformed_outline);
+  gsk_gl_program_set_uniform_color (job->driver->inset_shadow,
+                                    UNIFORM_INSET_SHADOW_COLOR, 0,
+                                    gsk_inset_shadow_node_get_color (node));
+  gsk_gl_program_set_uniform1f (job->driver->inset_shadow,
+                                UNIFORM_INSET_SHADOW_SPREAD, 0,
+                                gsk_inset_shadow_node_get_spread (node));
+  gsk_gl_program_set_uniform2f (job->driver->inset_shadow,
+                                UNIFORM_INSET_SHADOW_OFFSET, 0,
+                                gsk_inset_shadow_node_get_dx (node),
+                                gsk_inset_shadow_node_get_dy (node));
+  gsk_gl_render_job_draw_rect (job, &node->bounds);
+  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};
+
+  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_next_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_next_driver_create_render_target (job->driver,
+                                                 texture_width, texture_height,
+                                                 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, job->driver->inset_shadow);
+      gsk_gl_program_set_uniform_rounded_rect (job->driver->inset_shadow,
+                                               UNIFORM_INSET_SHADOW_OUTLINE_RECT, 0,
+                                               &transformed_outline);
+      gsk_gl_program_set_uniform_color (job->driver->inset_shadow,
+                                        UNIFORM_INSET_SHADOW_COLOR, 0,
+                                        gsk_inset_shadow_node_get_color (node));
+      gsk_gl_program_set_uniform1f (job->driver->inset_shadow,
+                                    UNIFORM_INSET_SHADOW_SPREAD, 0,
+                                    spread * MAX (scale_x, scale_y));
+      gsk_gl_program_set_uniform2f (job->driver->inset_shadow,
+                                    UNIFORM_INSET_SHADOW_OFFSET, 0,
+                                    offset_x * scale_x,
+                                    offset_y * scale_y);
+      gsk_gl_render_job_draw (job, 0, 0, texture_width, texture_height);
+      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_next_driver_release_render_target (job->driver, render_target, TRUE);
+    }
+
+  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;
+
+    gsk_next_driver_cache_texture (job->driver, &key, blurred_texture_id);
+
+    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, job->driver->blit);
+    gsk_gl_program_set_uniform_texture (job->driver->blit,
+                                        UNIFORM_SHARED_SOURCE, 0,
+                                        GL_TEXTURE_2D,
+                                        GL_TEXTURE0,
+                                        blurred_texture_id);
+    gsk_gl_render_job_load_vertices_from_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);
+  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 },
+  };
+
+  gsk_gl_render_job_transform_rounded_rect (job, outline, &transformed_outline);
+
+  gsk_gl_render_job_begin_draw (job, job->driver->unblurred_outset_shadow);
+  gsk_gl_program_set_uniform_rounded_rect (job->driver->unblurred_outset_shadow,
+                                           UNIFORM_UNBLURRED_OUTSET_SHADOW_OUTLINE_RECT, 0,
+                                           &transformed_outline);
+  gsk_gl_program_set_uniform_color (job->driver->unblurred_outset_shadow,
+                                    UNIFORM_UNBLURRED_OUTSET_SHADOW_COLOR, 0,
+                                    gsk_outset_shadow_node_get_color (node));
+  gsk_gl_program_set_uniform1f (job->driver->unblurred_outset_shadow,
+                                UNIFORM_UNBLURRED_OUTSET_SHADOW_SPREAD, 0,
+                                spread);
+  gsk_gl_program_set_uniform2f (job->driver->unblurred_outset_shadow,
+                                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 (job,
+                            x, y,
+                            corner_sizes[0][0], corner_sizes[0][1]);
+  if (corner_sizes[1][0] > 0 && corner_sizes[1][1] > 0) /* Top right */
+    gsk_gl_render_job_draw (job,
+                            x + w - corner_sizes[1][0], y,
+                            corner_sizes[1][0], corner_sizes[1][1]);
+  if (corner_sizes[2][0] > 0 && corner_sizes[2][1] > 0) /* Bottom right */
+    gsk_gl_render_job_draw (job,
+                            x + w - corner_sizes[2][0], y + h - corner_sizes[2][1],
+                            corner_sizes[2][0], corner_sizes[2][1]);
+  if (corner_sizes[3][0] > 0 && corner_sizes[3][1] > 0) /* Bottom left */
+    gsk_gl_render_job_draw (job,
+                            x, y + h - corner_sizes[3][1],
+                            corner_sizes[3][0], corner_sizes[3][1]);
+  /* Edges... */;
+  if (edge_sizes[0] > 0) /* Top */
+    gsk_gl_render_job_draw (job,
+                            x + corner_sizes[0][0], y,
+                            w - corner_sizes[0][0] - corner_sizes[1][0], edge_sizes[0]);
+  if (edge_sizes[1] > 0) /* Right */
+    gsk_gl_render_job_draw (job,
+                            x + w - edge_sizes[1], y + corner_sizes[1][1],
+                            edge_sizes[1], h - corner_sizes[1][1] - corner_sizes[2][1]);
+  if (edge_sizes[2] > 0) /* Bottom */
+    gsk_gl_render_job_draw (job,
+                            x + corner_sizes[3][0], y + h - edge_sizes[2],
+                            w - corner_sizes[3][0] - corner_sizes[2][0], edge_sizes[2]);
+  if (edge_sizes[3] > 0) /* Left */
+    gsk_gl_render_job_draw (job,
+                            x, y + corner_sizes[0][1],
+                            edge_sizes[3], h - corner_sizes[0][1] - corner_sizes[3][1]);
+
+  gsk_gl_render_job_end_draw (job);
+}
+
+static inline void
+gsk_gl_render_job_visit_blurred_outset_shadow_node (GskGLRenderJob      *job,
+                                                    const GskRenderNode *node)
+{
+  static const GdkRGBA white = { 1, 1, 1, 1 };
+
+  const GskRoundedRect *outline = gsk_outset_shadow_node_get_outline (node);
+  const GdkRGBA *color = gsk_outset_shadow_node_get_color (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 = ceilf (half_blur_extra * scale_x);
+  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;
+
+  /* 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)
+    {
+      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;
+  scaled_outline.bounds.origin.y = extra_blur_pixels;
+  scaled_outline.bounds.size.width = texture_width - (extra_blur_pixels * 2);
+  scaled_outline.bounds.size.height = texture_height - (extra_blur_pixels * 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_next_driver_create_render_target (job->driver,
+                                            texture_width, texture_height,
+                                            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, job->driver->color);
+      gsk_gl_program_set_uniform_color (job->driver->color,
+                                        UNIFORM_COLOR_COLOR, 0,
+                                        &white);
+      gsk_gl_render_job_draw (job, 0, 0, texture_width, texture_height);
+      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_next_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 = FALSE;
+      offscreen.texture_id = blurred_texture_id;
+      init_full_texture_region (&offscreen);
+
+      gsk_gl_render_job_begin_draw (job, job->driver->outset_shadow);
+      gsk_gl_program_set_uniform_color (job->driver->outset_shadow,
+                                        UNIFORM_OUTSET_SHADOW_COLOR, 0,
+                                        color);
+      gsk_gl_program_set_uniform_texture (job->driver->outset_shadow,
+                                          UNIFORM_SHARED_SOURCE, 0,
+                                          GL_TEXTURE_2D,
+                                          GL_TEXTURE0,
+                                          blurred_texture_id);
+      gsk_gl_program_set_uniform_rounded_rect (job->driver->outset_shadow,
+                                               UNIFORM_OUTSET_SHADOW_OUTLINE_RECT, 0,
+                                               &transformed_outline);
+      gsk_gl_render_job_load_vertices_from_offscreen (job,
+                                                      &GRAPHENE_RECT_INIT (min_x,
+                                                                           min_y,
+                                                                           texture_width / scale_x,
+                                                                           texture_height / scale_y),
+                                                      &offscreen);
+      gsk_gl_render_job_end_draw (job);
+
+      return;
+    }
+
+  gsk_gl_render_job_begin_draw (job, job->driver->outset_shadow);
+  gsk_gl_program_set_uniform_color (job->driver->outset_shadow,
+                                    UNIFORM_OUTSET_SHADOW_COLOR, 0,
+                                    color);
+  gsk_gl_program_set_uniform_texture (job->driver->outset_shadow,
+                                      UNIFORM_SHARED_SOURCE, 0,
+                                      GL_TEXTURE_2D,
+                                      GL_TEXTURE0,
+                                      blurred_texture_id);
+  gsk_gl_program_set_uniform_rounded_rect (job->driver->outset_shadow,
+                                           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;
+    GskGLTexture *texture;
+
+    texture = gsk_next_driver_get_texture_by_id (job->driver, blurred_texture_id);
+    slices = gsk_gl_texture_get_nine_slice (texture, &scaled_outline, extra_blur_pixels);
+
+    offscreen.was_offscreen = TRUE;
+
+    /* Our texture coordinates MUST be scaled, while the actual vertex coords
+     * MUST NOT be scaled. */
+
+    /* 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_load_vertices_from_offscreen (job,
+                                                        &GRAPHENE_RECT_INIT (min_x, min_y,
+                                                                             
slices[NINE_SLICE_TOP_LEFT].rect.width / scale_x,
+                                                                             
slices[NINE_SLICE_TOP_LEFT].rect.height / scale_y),
+                                                        &offscreen);
+      }
+
+    /* Top center */
+    if (nine_slice_is_visible (&slices[NINE_SLICE_TOP_CENTER]))
+      {
+        memcpy (&offscreen.area, &slices[NINE_SLICE_TOP_CENTER].area, sizeof offscreen.area);
+        float width = (max_x - min_x) - (slices[NINE_SLICE_TOP_LEFT].rect.width / scale_x +
+                                         slices[NINE_SLICE_TOP_RIGHT].rect.width / scale_x);
+        gsk_gl_render_job_load_vertices_from_offscreen (job,
+                                                        &GRAPHENE_RECT_INIT (min_x + 
(slices[NINE_SLICE_TOP_LEFT].rect.width / scale_x),
+                                                                             min_y,
+                                                                             width,
+                                                                             
slices[NINE_SLICE_TOP_CENTER].rect.height / scale_y),
+                                                        &offscreen);
+      }
+
+    /* 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_load_vertices_from_offscreen (job,
+                                                        &GRAPHENE_RECT_INIT (max_x - 
(slices[NINE_SLICE_TOP_RIGHT].rect.width / scale_x),
+                                                                             min_y,
+                                                                             
slices[NINE_SLICE_TOP_RIGHT].rect.width / scale_x,
+                                                                             
slices[NINE_SLICE_TOP_RIGHT].rect.height / scale_y),
+                                                        &offscreen);
+      }
+
+    /* 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_load_vertices_from_offscreen (job,
+                                                        &GRAPHENE_RECT_INIT (max_x - 
(slices[NINE_SLICE_BOTTOM_RIGHT].rect.width / scale_x),
+                                                                             max_y - 
(slices[NINE_SLICE_BOTTOM_RIGHT].rect.height / scale_y),
+                                                                             
slices[NINE_SLICE_BOTTOM_RIGHT].rect.width / scale_x,
+                                                                             
slices[NINE_SLICE_BOTTOM_RIGHT].rect.height / scale_y),
+                                                        &offscreen);
+      }
+
+    /* 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_load_vertices_from_offscreen (job,
+                                                        &GRAPHENE_RECT_INIT (min_x,
+                                                                             max_y - 
(slices[NINE_SLICE_BOTTOM_LEFT].rect.height / scale_y),
+                                                                             
slices[NINE_SLICE_BOTTOM_LEFT].rect.width / scale_x,
+                                                                             
slices[NINE_SLICE_BOTTOM_LEFT].rect.height / scale_y),
+                                                        &offscreen);
+      }
+
+    /* Left side */
+    if (nine_slice_is_visible (&slices[NINE_SLICE_LEFT_CENTER]))
+      {
+        memcpy (&offscreen.area, &slices[NINE_SLICE_LEFT_CENTER].area, sizeof offscreen.area);
+        float height = (max_y - min_y) - (slices[NINE_SLICE_TOP_LEFT].rect.height / scale_y +
+                                                slices[NINE_SLICE_BOTTOM_LEFT].rect.height / scale_y);
+        gsk_gl_render_job_load_vertices_from_offscreen (job,
+                                                        &GRAPHENE_RECT_INIT (min_x,
+                                                                             min_y + 
(slices[NINE_SLICE_TOP_LEFT].rect.height / scale_y),
+                                                                             
slices[NINE_SLICE_LEFT_CENTER].rect.width / scale_x,
+                                                                             height),
+                                                        &offscreen);
+      }
+
+    /* Right side */
+    if (nine_slice_is_visible (&slices[NINE_SLICE_RIGHT_CENTER]))
+      {
+        memcpy (&offscreen.area, &slices[NINE_SLICE_RIGHT_CENTER].area, sizeof offscreen.area);
+        float height = (max_y - min_y) - (slices[NINE_SLICE_TOP_RIGHT].rect.height / scale_y +
+                                          slices[NINE_SLICE_BOTTOM_RIGHT].rect.height / scale_y);
+        gsk_gl_render_job_load_vertices_from_offscreen (job,
+                                                        &GRAPHENE_RECT_INIT (max_x - 
(slices[NINE_SLICE_RIGHT_CENTER].rect.width / scale_x),
+                                                                             min_y + 
(slices[NINE_SLICE_TOP_LEFT].rect.height / scale_y),
+                                                                             
slices[NINE_SLICE_RIGHT_CENTER].rect.width / scale_x,
+                                                                             height),
+                                                        &offscreen);
+      }
+
+    /* Bottom side */
+    if (nine_slice_is_visible (&slices[NINE_SLICE_BOTTOM_CENTER]))
+      {
+        memcpy (&offscreen.area, &slices[NINE_SLICE_BOTTOM_CENTER].area, sizeof offscreen.area);
+        float width = (max_x - min_x) - (slices[NINE_SLICE_BOTTOM_LEFT].rect.width / scale_x +
+                                         slices[NINE_SLICE_BOTTOM_RIGHT].rect.width / scale_x);
+        gsk_gl_render_job_load_vertices_from_offscreen (job,
+                                                        &GRAPHENE_RECT_INIT (min_x + 
(slices[NINE_SLICE_BOTTOM_LEFT].rect.width / scale_x),
+                                                                             max_y - 
(slices[NINE_SLICE_BOTTOM_CENTER].rect.height / scale_y),
+                                                                             width,
+                                                                             
slices[NINE_SLICE_BOTTOM_CENTER].rect.height / scale_y),
+                                                        &offscreen);
+      }
+
+    /* Middle */
+    if (nine_slice_is_visible (&slices[NINE_SLICE_CENTER]))
+      {
+        memcpy (&offscreen.area, &slices[NINE_SLICE_CENTER].area, sizeof offscreen.area);
+        float width = (max_x - min_x) - (slices[NINE_SLICE_LEFT_CENTER].rect.width / scale_x +
+                                         slices[NINE_SLICE_RIGHT_CENTER].rect.width / scale_x);
+        float height = (max_y - min_y) - (slices[NINE_SLICE_TOP_CENTER].rect.height / scale_y +
+                                          slices[NINE_SLICE_BOTTOM_CENTER].rect.height / scale_y);
+        gsk_gl_render_job_load_vertices_from_offscreen (job,
+                                                        &GRAPHENE_RECT_INIT (min_x + 
(slices[NINE_SLICE_LEFT_CENTER].rect.width / scale_x),
+                                                                             min_y + 
(slices[NINE_SLICE_TOP_CENTER].rect.height / scale_y),
+                                                                             width, height),
+                                                        &offscreen);
+      }
+  }
+
+  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, job->driver->cross_fade);
+  gsk_gl_program_set_uniform_texture (job->driver->cross_fade,
+                                      UNIFORM_SHARED_SOURCE, 0,
+                                      GL_TEXTURE_2D,
+                                      GL_TEXTURE0,
+                                      offscreen_start.texture_id);
+  gsk_gl_program_set_uniform_texture (job->driver->cross_fade,
+                                      UNIFORM_CROSS_FADE_SOURCE2, 0,
+                                      GL_TEXTURE_2D,
+                                      GL_TEXTURE1,
+                                      offscreen_end.texture_id);
+  gsk_gl_program_set_uniform1f (job->driver->cross_fade,
+                                UNIFORM_CROSS_FADE_PROGRESS, 0,
+                                progress);
+  gsk_gl_render_job_load_vertices_from_offscreen (job, &node->bounds, &offscreen_end);
+  gsk_gl_render_job_end_draw (job);
+}
+
+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);
+
+      if (gsk_render_node_get_node_type (child) == GSK_CONTAINER_NODE)
+        {
+          GskGLRenderOffscreen offscreen = {0};
+
+          offscreen.bounds = &child->bounds;
+          offscreen.force_offscreen = TRUE;
+          offscreen.reset_clip = TRUE;
+
+          /* The semantics of an opacity node mandate that when, e.g., two
+           * color nodes overlap, there may not be any blending between them.
+           */
+          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, job->driver->blit);
+          gsk_gl_program_set_uniform_texture (job->driver->blit,
+                                              UNIFORM_SHARED_SOURCE, 0,
+                                              GL_TEXTURE_2D,
+                                              GL_TEXTURE0,
+                                              offscreen.texture_id);
+          gsk_gl_render_job_load_vertices_from_offscreen (job, &node->bounds, &offscreen);
+          gsk_gl_render_job_end_draw (job);
+        }
+      else
+        {
+          gsk_gl_render_job_visit_node (job, child);
+        }
+
+      gsk_gl_render_job_set_alpha (job, prev_alpha);
+    }
+}
+
+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;
+  GskGLProgram *program;
+  int x_position = 0;
+  GskGLGlyphKey lookup;
+  guint last_texture = 0;
+  GskGLDrawVertex *vertices;
+  guint used = 0;
+
+  if (num_glyphs == 0)
+    return;
+
+  /* If the font has color glyphs, we don't need to recolor anything */
+  if (!force_color && gsk_text_node_has_color_glyphs (node))
+    {
+      program = job->driver->blit;
+    }
+  else
+    {
+      program = job->driver->coloring;
+      gsk_gl_program_set_uniform_color (program, UNIFORM_COLORING_COLOR, 0, color);
+    }
+
+  lookup.font = (PangoFont *)font;
+  lookup.scale = (guint) (text_scale * 1024);
+
+  gsk_gl_render_job_begin_draw (job, program);
+
+  vertices = gsk_gl_command_queue_add_n_vertices (job->command_queue, num_glyphs);
+
+  /* We use one quad per character */
+  for (guint i = 0; i < num_glyphs; i++)
+    {
+      const PangoGlyphInfo *gi = &glyphs[i];
+      const GskGLGlyphValue *glyph;
+      float glyph_x, glyph_y, glyph_x2, glyph_y2;
+      float tx, ty, tx2, ty2;
+      float cx;
+      float cy;
+      guint texture_id;
+      guint base;
+
+      if (gi->glyph == PANGO_GLYPH_EMPTY)
+        continue;
+
+      cx = (float)(x_position + gi->geometry.x_offset) / PANGO_SCALE;
+      cy = (float)(gi->geometry.y_offset) / PANGO_SCALE;
+
+      gsk_gl_glyph_key_set_glyph_and_shift (&lookup, gi->glyph, x + cx, y + cy);
+
+      if (!gsk_gl_glyph_library_lookup_or_add (library, &lookup, &glyph))
+        goto next;
+
+      base = used * GSK_GL_N_VERTICES;
+
+      texture_id = GSK_GL_TEXTURE_ATLAS_ENTRY_TEXTURE (glyph);
+
+      g_assert (texture_id > 0);
+
+      if G_UNLIKELY (last_texture != texture_id)
+        {
+          if G_LIKELY (last_texture != 0)
+            gsk_gl_render_job_split_draw (job);
+          gsk_gl_program_set_uniform_texture (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 = floorf (x + cx + 0.125) + glyph->ink_rect.x;
+      glyph_y = floorf (y + cy + 0.125) + glyph->ink_rect.y;
+      glyph_x2 = glyph_x + glyph->ink_rect.width;
+      glyph_y2 = glyph_y + glyph->ink_rect.height;
+
+      vertices[base+0].position[0] = glyph_x;
+      vertices[base+0].position[1] = glyph_y;
+      vertices[base+0].uv[0] = tx;
+      vertices[base+0].uv[1] = ty;
+
+      vertices[base+1].position[0] = glyph_x;
+      vertices[base+1].position[1] = glyph_y2;
+      vertices[base+1].uv[0] = tx;
+      vertices[base+1].uv[1] = ty2;
+
+      vertices[base+2].position[0] = glyph_x2;
+      vertices[base+2].position[1] = glyph_y;
+      vertices[base+2].uv[0] = tx2;
+      vertices[base+2].uv[1] = ty;
+
+      vertices[base+3].position[0] = glyph_x2;
+      vertices[base+3].position[1] = glyph_y2;
+      vertices[base+3].uv[0] = tx2;
+      vertices[base+3].uv[1] = ty2;
+
+      vertices[base+4].position[0] = glyph_x;
+      vertices[base+4].position[1] = glyph_y2;
+      vertices[base+4].uv[0] = tx;
+      vertices[base+4].uv[1] = ty2;
+
+      vertices[base+5].position[0] = glyph_x2;
+      vertices[base+5].position[1] = glyph_y;
+      vertices[base+5].uv[0] = tx2;
+      vertices[base+5].uv[1] = ty;
+
+      gsk_gl_command_queue_get_batch (job->command_queue)->draw.vbo_count += GSK_GL_N_VERTICES;
+      used++;
+
+next:
+      x_position += gi->geometry.width;
+    }
+
+  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;
+
+      if (shadow->radius == 0 &&
+          gsk_render_node_get_node_type (shadow_child) == GSK_TEXT_NODE)
+        {
+          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 (RGBA_IS_CLEAR (&shadow->color))
+        continue;
+
+      if (node_is_invisible (shadow_child))
+        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, job->driver->coloring);
+      gsk_gl_program_set_uniform_texture (job->driver->coloring,
+                                          UNIFORM_SHARED_SOURCE, 0,
+                                          GL_TEXTURE_2D,
+                                          GL_TEXTURE0,
+                                          offscreen.texture_id);
+      gsk_gl_program_set_uniform_color (job->driver->coloring,
+                                        UNIFORM_COLORING_COLOR, 0,
+                                        &shadow->color);
+      gsk_gl_render_job_load_vertices_from_offscreen (job, &bounds, &offscreen);
+      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_next_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_next_driver_cache_texture (job->driver, &key, offscreen.texture_id);
+
+  gsk_gl_render_job_begin_draw (job, job->driver->blit);
+  gsk_gl_program_set_uniform_texture (job->driver->blit,
+                                      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);
+  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, job->driver->blit);
+      gsk_gl_program_set_uniform_texture (job->driver->blit,
+                                          UNIFORM_SHARED_SOURCE, 0,
+                                          GL_TEXTURE_2D,
+                                          GL_TEXTURE0,
+                                          bottom_offscreen.texture_id);
+      gsk_gl_render_job_load_vertices_from_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, job->driver->blend);
+  gsk_gl_program_set_uniform_texture (job->driver->blend,
+                                      UNIFORM_SHARED_SOURCE, 0,
+                                      GL_TEXTURE_2D,
+                                      GL_TEXTURE0,
+                                      bottom_offscreen.texture_id);
+  gsk_gl_program_set_uniform_texture (job->driver->blend,
+                                      UNIFORM_BLEND_SOURCE2, 0,
+                                      GL_TEXTURE_2D,
+                                      GL_TEXTURE1,
+                                      top_offscreen.texture_id);
+  gsk_gl_program_set_uniform1i (job->driver->blend,
+                                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, job->driver->color_matrix);
+  gsk_gl_program_set_uniform_texture (job->driver->color_matrix,
+                                      UNIFORM_SHARED_SOURCE, 0,
+                                      GL_TEXTURE_2D,
+                                      GL_TEXTURE0,
+                                      offscreen.texture_id);
+  gsk_gl_program_set_uniform_matrix (job->driver->color_matrix,
+                                     UNIFORM_COLOR_MATRIX_COLOR_MATRIX, 0,
+                                     gsk_color_matrix_node_get_color_matrix (node));
+  gsk_gl_program_set_uniform4fv (job->driver->color_matrix,
+                                 UNIFORM_COLOR_MATRIX_COLOR_OFFSET, 0,
+                                 1,
+                                 offset);
+  gsk_gl_render_job_load_vertices_from_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)
+{
+  static const GdkRGBA pink = { 255 / 255., 105 / 255., 180 / 255., 1.0 };
+
+  gsk_gl_render_job_begin_draw (job, job->driver->color);
+  gsk_gl_program_set_uniform_color (job->driver->color,
+                                    UNIFORM_COLOR_COLOR, 0,
+                                    &pink);
+  gsk_gl_render_job_draw_rect (job, &node->bounds);
+  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_next_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;
+
+          /* Ignore unused uniforms */
+          if (program->args_locations[i] == -1)
+            continue;
+
+          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,
+                                           program->args_locations[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,
+                                          program->args_locations[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,
+                                           program->args_locations[i],
+                                           0, *(const guint32 *)data);
+              break;
+            case GSK_GL_UNIFORM_TYPE_VEC2:
+              gsk_gl_uniform_state_set2fv (job->command_queue->uniforms,
+                                           program->program_info,
+                                           program->args_locations[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,
+                                           program->args_locations[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,
+                                           program->args_locations[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 (GSK_GL_TEXTURE_LIBRARY (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_next_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, job->driver->blit);
+      gsk_gl_program_set_uniform_texture (job->driver->blit,
+                                          UNIFORM_SHARED_SOURCE, 0,
+                                          GL_TEXTURE_2D,
+                                          GL_TEXTURE0,
+                                          offscreen.texture_id);
+      gsk_gl_render_job_load_vertices_from_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_next_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, job->driver->blit);
+
+      for (guint i = 0; i < n_slices; i ++)
+        {
+          GskGLDrawVertex *vertices;
+          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->driver->blit,
+                                              UNIFORM_SHARED_SOURCE, 0,
+                                              GL_TEXTURE_2D,
+                                              GL_TEXTURE0,
+                                              slice->texture_id);
+          vertices = gsk_gl_command_queue_add_vertices (job->command_queue);
+
+          vertices[0].position[0] = x1;
+          vertices[0].position[1] = y1;
+          vertices[0].uv[0] = 0;
+          vertices[0].uv[1] = 0;
+
+          vertices[1].position[0] = x1;
+          vertices[1].position[1] = y2;
+          vertices[1].uv[0] = 0;
+          vertices[1].uv[1] = 1;
+
+          vertices[2].position[0] = x2;
+          vertices[2].position[1] = y1;
+          vertices[2].uv[0] = 1;
+          vertices[2].uv[1] = 0;
+
+          vertices[3].position[0] = x2;
+          vertices[3].position[1] = y2;
+          vertices[3].uv[0] = 1;
+          vertices[3].uv[1] = 1;
+
+          vertices[4].position[0] = x1;
+          vertices[4].position[1] = y2;
+          vertices[4].uv[0] = 0;
+          vertices[4].uv[1] = 1;
+
+          vertices[5].position[0] = x2;
+          vertices[5].position[1] = y1;
+          vertices[5].uv[0] = 1;
+          vertices[5].uv[1] = 0;
+        }
+
+      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, job->driver->repeat);
+  gsk_gl_program_set_uniform_texture (job->driver->repeat,
+                                      UNIFORM_SHARED_SOURCE, 0,
+                                      GL_TEXTURE_2D,
+                                      GL_TEXTURE0,
+                                      offscreen.texture_id);
+  gsk_gl_program_set_uniform4f (job->driver->repeat,
+                                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->driver->repeat,
+                                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_load_vertices_from_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)
+{
+  g_assert (job != NULL);
+  g_assert (node != NULL);
+  g_assert (GSK_IS_NEXT_DRIVER (job->driver));
+  g_assert (GSK_IS_GL_COMMAND_QUEUE (job->command_queue));
+
+  if (node_is_invisible (node) ||
+      !gsk_gl_render_job_node_overlaps_clip (job, node))
+    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 (node))
+        gsk_gl_render_job_visit_uniform_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:
+      {
+        guint n_children = gsk_container_node_get_n_children (node);
+
+        for (guint i = 0; i < n_children; i++)
+          {
+            const GskRenderNode *child = gsk_container_node_get_child (node, i);
+            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:
+      gsk_gl_render_job_visit_radial_gradient_node (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;
+    }
+}
+
+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_next_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 scale_x = job->scale_x;
+  float scale_y = job->scale_y;
+
+  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 * scale_x);
+    if (scaled_width > max_texture_size)
+      {
+        scale_x *= (float)max_texture_size / scaled_width;
+        scaled_width = max_texture_size;
+      }
+
+    scaled_height = ceilf (offscreen->bounds->size.height * scale_y);
+    if (scaled_height > max_texture_size)
+      {
+        scale_y *= (float)max_texture_size / scaled_height;
+        scaled_height = max_texture_size;
+      }
+  }
+
+  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_next_driver_create_render_target (job->driver,
+                                             scaled_width, scaled_height,
+                                             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);
+    }
+
+  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);
+  gsk_gl_render_job_set_modelview (job, gsk_transform_scale (NULL, scale_x, scale_y));
+  prev_alpha = gsk_gl_render_job_set_alpha (job, 1.0f);
+  job->offset_x = offset_x;
+  job->offset_y = offset_y;
+
+  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);
+
+  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_next_driver_release_render_target (job->driver,
+                                                                 render_target,
+                                                                 FALSE);
+
+  init_full_texture_region (offscreen);
+
+  if (!offscreen->do_not_cache)
+    gsk_next_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_NEXT_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),
+                                                  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, job->driver->blit);
+  gsk_gl_program_set_uniform_texture (job->driver->blit,
+                                      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_NEXT_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_next_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;
+}
+
+GskGLRenderJob *
+gsk_gl_render_job_new (GskNextDriver         *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_NEXT_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;
+
+  gsk_gl_render_job_set_alpha (job, 1.0);
+  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);
+}
diff --git a/gsk/next/gskglrenderjobprivate.h b/gsk/next/gskglrenderjobprivate.h
new file mode 100644
index 0000000000..2a8c01fa71
--- /dev/null
+++ b/gsk/next/gskglrenderjobprivate.h
@@ -0,0 +1,39 @@
+/* gskglrenderjobprivate.h
+ *
+ * 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
+ */
+
+#ifndef __GSK_GL_RENDER_JOB_H__
+#define __GSK_GL_RENDER_JOB_H__
+
+#include "gskgltypesprivate.h"
+
+GskGLRenderJob *gsk_gl_render_job_new                 (GskNextDriver         *driver,
+                                                       const graphene_rect_t *viewport,
+                                                       float                  scale_factor,
+                                                       const cairo_region_t  *region,
+                                                       guint                  framebuffer);
+void            gsk_gl_render_job_free                (GskGLRenderJob        *job);
+void            gsk_gl_render_job_render              (GskGLRenderJob        *job,
+                                                       GskRenderNode         *root);
+void            gsk_gl_render_job_render_flipped      (GskGLRenderJob        *job,
+                                                       GskRenderNode         *root);
+void            gsk_gl_render_job_set_debug_fallback  (GskGLRenderJob        *job,
+                                                       gboolean               debug_fallback);
+
+#endif /* __GSK_GL_RENDER_JOB_H__ */
diff --git a/gsk/next/gskglshadowlibrary.c b/gsk/next/gskglshadowlibrary.c
new file mode 100644
index 0000000000..24a9481fc2
--- /dev/null
+++ b/gsk/next/gskglshadowlibrary.c
@@ -0,0 +1,228 @@
+/* gskglshadowlibrary.c
+ *
+ * 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 "gskgldriverprivate.h"
+#include "gskglshadowlibraryprivate.h"
+
+#define MAX_UNUSED_FRAMES (16 * 5)
+
+struct _GskGLShadowLibrary
+{
+  GObject        parent_instance;
+  GskNextDriver *driver;
+  GArray        *shadows;
+};
+
+typedef struct _Shadow
+{
+  GskRoundedRect outline;
+  float          blur_radius;
+  guint          texture_id;
+  gint64         last_used_in_frame;
+} Shadow;
+
+G_DEFINE_TYPE (GskGLShadowLibrary, gsk_gl_shadow_library, G_TYPE_OBJECT)
+
+enum {
+  PROP_0,
+  PROP_DRIVER,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+GskGLShadowLibrary *
+gsk_gl_shadow_library_new (GskNextDriver *driver)
+{
+  g_return_val_if_fail (GSK_IS_NEXT_DRIVER (driver), NULL);
+
+  return g_object_new (GSK_TYPE_GL_SHADOW_LIBRARY,
+                       "driver", driver,
+                       NULL);
+}
+
+static void
+gsk_gl_shadow_library_dispose (GObject *object)
+{
+  GskGLShadowLibrary *self = (GskGLShadowLibrary *)object;
+
+  for (guint i = 0; i < self->shadows->len; i++)
+    {
+      const Shadow *shadow = &g_array_index (self->shadows, Shadow, i);
+      gsk_next_driver_release_texture_by_id (self->driver, shadow->texture_id);
+    }
+
+  g_clear_pointer (&self->shadows, g_array_unref);
+  g_clear_object (&self->driver);
+
+  G_OBJECT_CLASS (gsk_gl_shadow_library_parent_class)->dispose (object);
+}
+
+static void
+gsk_gl_shadow_library_get_property (GObject    *object,
+                                    guint       prop_id,
+                                    GValue     *value,
+                                    GParamSpec *pspec)
+{
+  GskGLShadowLibrary *self = GSK_GL_SHADOW_LIBRARY (object);
+
+  switch (prop_id)
+    {
+    case PROP_DRIVER:
+      g_value_set_object (value, self->driver);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gsk_gl_shadow_library_set_property (GObject      *object,
+                                    guint         prop_id,
+                                    const GValue *value,
+                                    GParamSpec   *pspec)
+{
+  GskGLShadowLibrary *self = GSK_GL_SHADOW_LIBRARY (object);
+
+  switch (prop_id)
+    {
+    case PROP_DRIVER:
+      self->driver = g_value_dup_object (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gsk_gl_shadow_library_class_init (GskGLShadowLibraryClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->dispose = gsk_gl_shadow_library_dispose;
+  object_class->get_property = gsk_gl_shadow_library_get_property;
+  object_class->set_property = gsk_gl_shadow_library_set_property;
+
+  properties [PROP_DRIVER] =
+    g_param_spec_object ("driver",
+                         "Driver",
+                         "Driver",
+                         GSK_TYPE_NEXT_DRIVER,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+gsk_gl_shadow_library_init (GskGLShadowLibrary *self)
+{
+  self->shadows = g_array_new (FALSE, FALSE, sizeof (Shadow));
+}
+
+void
+gsk_gl_shadow_library_insert (GskGLShadowLibrary   *self,
+                              const GskRoundedRect *outline,
+                              float                 blur_radius,
+                              guint                 texture_id)
+{
+  Shadow *shadow;
+
+  g_assert (GSK_IS_GL_SHADOW_LIBRARY (self));
+  g_assert (outline != NULL);
+  g_assert (texture_id != 0);
+
+  gsk_next_driver_mark_texture_permanent (self->driver, texture_id);
+
+  g_array_set_size (self->shadows, self->shadows->len + 1);
+
+  shadow = &g_array_index (self->shadows, Shadow, self->shadows->len - 1);
+  shadow->outline = *outline;
+  shadow->blur_radius = blur_radius;
+  shadow->texture_id = texture_id;
+  shadow->last_used_in_frame = self->driver->current_frame_id;
+}
+
+guint
+gsk_gl_shadow_library_lookup (GskGLShadowLibrary   *self,
+                              const GskRoundedRect *outline,
+                              float                 blur_radius)
+{
+  Shadow *ret = NULL;
+
+  g_assert (GSK_IS_GL_SHADOW_LIBRARY (self));
+  g_assert (outline != NULL);
+
+  /* Ensure GskRoundedRect  is 12 packed floats without padding
+   * so that we can use memcmp instead of float comparisons.
+   */
+  G_STATIC_ASSERT (sizeof *outline == (sizeof (float) * 12));
+
+  for (guint i = 0; i < self->shadows->len; i++)
+    {
+      Shadow *shadow = &g_array_index (self->shadows, Shadow, i);
+
+      if (blur_radius == shadow->blur_radius &&
+          memcmp (outline, &shadow->outline, sizeof *outline) == 0)
+        {
+          ret = shadow;
+          break;
+        }
+    }
+
+  if (ret == NULL)
+    return 0;
+
+  g_assert (ret->texture_id != 0);
+
+  ret->last_used_in_frame = self->driver->current_frame_id;
+
+  return ret->texture_id;
+}
+
+void
+gsk_gl_shadow_library_begin_frame (GskGLShadowLibrary *self)
+{
+  gint64 watermark;
+  int i;
+  int p;
+
+  g_return_if_fail (GSK_IS_GL_SHADOW_LIBRARY (self));
+
+  watermark = self->driver->current_frame_id - MAX_UNUSED_FRAMES;
+
+  for (i = 0, p = self->shadows->len; i < p; i++)
+    {
+      const Shadow *shadow = &g_array_index (self->shadows, Shadow, i);
+
+      if (shadow->last_used_in_frame < watermark)
+        {
+          gsk_next_driver_release_texture_by_id (self->driver, shadow->texture_id);
+          g_array_remove_index_fast (self->shadows, i);
+          p--;
+          i--;
+        }
+    }
+}
diff --git a/gsk/next/gskglshadowlibraryprivate.h b/gsk/next/gskglshadowlibraryprivate.h
new file mode 100644
index 0000000000..437ad6d39d
--- /dev/null
+++ b/gsk/next/gskglshadowlibraryprivate.h
@@ -0,0 +1,44 @@
+/* gskglshadowlibraryprivate.h
+ *
+ * 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
+ */
+
+#ifndef __GSK_GL_SHADOW_LIBRARY_PRIVATE_H__
+#define __GSK_GL_SHADOW_LIBRARY_PRIVATE_H__
+
+#include "gskgltexturelibraryprivate.h"
+
+G_BEGIN_DECLS
+
+#define GSK_TYPE_GL_SHADOW_LIBRARY (gsk_gl_shadow_library_get_type())
+
+G_DECLARE_FINAL_TYPE (GskGLShadowLibrary, gsk_gl_shadow_library, GSK, GL_SHADOW_LIBRARY, GObject)
+
+GskGLShadowLibrary *gsk_gl_shadow_library_new         (GskNextDriver        *driver);
+void                gsk_gl_shadow_library_begin_frame (GskGLShadowLibrary   *self);
+guint               gsk_gl_shadow_library_lookup      (GskGLShadowLibrary   *self,
+                                                       const GskRoundedRect *outline,
+                                                       float                 blur_radius);
+void                gsk_gl_shadow_library_insert      (GskGLShadowLibrary   *self,
+                                                       const GskRoundedRect *outline,
+                                                       float                 blur_radius,
+                                                       guint                 texture_id);
+
+G_END_DECLS
+
+#endif /* __GSK_GL_SHADOW_LIBRARY_PRIVATE_H__ */
diff --git a/gsk/next/gskgltexturelibrary.c b/gsk/next/gskgltexturelibrary.c
new file mode 100644
index 0000000000..b6185a0302
--- /dev/null
+++ b/gsk/next/gskgltexturelibrary.c
@@ -0,0 +1,312 @@
+/* gskgltexturelibrary.c
+ *
+ * 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 "gskglcommandqueueprivate.h"
+#include "gskgldriverprivate.h"
+#include "gskgltexturelibraryprivate.h"
+
+G_DEFINE_ABSTRACT_TYPE (GskGLTextureLibrary, gsk_gl_texture_library, G_TYPE_OBJECT)
+
+enum {
+  PROP_0,
+  PROP_DRIVER,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+gsk_gl_texture_library_constructed (GObject *object)
+{
+  G_OBJECT_CLASS (gsk_gl_texture_library_parent_class)->constructed (object);
+
+  g_assert (GSK_GL_TEXTURE_LIBRARY (object)->hash_table != NULL);
+}
+
+static void
+gsk_gl_texture_library_dispose (GObject *object)
+{
+  GskGLTextureLibrary *self = (GskGLTextureLibrary *)object;
+
+  g_clear_object (&self->driver);
+
+  G_OBJECT_CLASS (gsk_gl_texture_library_parent_class)->dispose (object);
+}
+
+static void
+gsk_gl_texture_library_get_property (GObject    *object,
+                                     guint       prop_id,
+                                     GValue     *value,
+                                     GParamSpec *pspec)
+{
+  GskGLTextureLibrary *self = GSK_GL_TEXTURE_LIBRARY (object);
+
+  switch (prop_id)
+    {
+    case PROP_DRIVER:
+      g_value_set_object (value, self->driver);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gsk_gl_texture_library_set_property (GObject      *object,
+                                     guint         prop_id,
+                                     const GValue *value,
+                                     GParamSpec   *pspec)
+{
+  GskGLTextureLibrary *self = GSK_GL_TEXTURE_LIBRARY (object);
+
+  switch (prop_id)
+    {
+    case PROP_DRIVER:
+      self->driver = g_value_dup_object (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gsk_gl_texture_library_class_init (GskGLTextureLibraryClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->constructed = gsk_gl_texture_library_constructed;
+  object_class->dispose = gsk_gl_texture_library_dispose;
+  object_class->get_property = gsk_gl_texture_library_get_property;
+  object_class->set_property = gsk_gl_texture_library_set_property;
+
+  properties [PROP_DRIVER] =
+    g_param_spec_object ("driver",
+                         "Driver",
+                         "Driver",
+                         GSK_TYPE_NEXT_DRIVER,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+gsk_gl_texture_library_init (GskGLTextureLibrary *self)
+{
+}
+
+void
+gsk_gl_texture_library_set_funcs (GskGLTextureLibrary *self,
+                                  GHashFunc            hash_func,
+                                  GEqualFunc           equal_func,
+                                  GDestroyNotify       key_destroy,
+                                  GDestroyNotify       value_destroy)
+{
+  g_return_if_fail (GSK_IS_GL_TEXTURE_LIBRARY (self));
+  g_return_if_fail (self->hash_table == NULL);
+
+  self->hash_table = g_hash_table_new_full (hash_func, equal_func,
+                                            key_destroy, value_destroy);
+}
+
+void
+gsk_gl_texture_library_begin_frame (GskGLTextureLibrary *self)
+{
+  g_return_if_fail (GSK_IS_GL_TEXTURE_LIBRARY (self));
+
+  if (GSK_GL_TEXTURE_LIBRARY_GET_CLASS (self)->begin_frame)
+    GSK_GL_TEXTURE_LIBRARY_GET_CLASS (self)->begin_frame (self);
+}
+
+void
+gsk_gl_texture_library_end_frame (GskGLTextureLibrary *self)
+{
+  g_return_if_fail (GSK_IS_GL_TEXTURE_LIBRARY (self));
+
+  if (GSK_GL_TEXTURE_LIBRARY_GET_CLASS (self)->end_frame)
+    GSK_GL_TEXTURE_LIBRARY_GET_CLASS (self)->end_frame (self);
+}
+
+static GskGLTexture *
+gsk_gl_texture_library_pack_one (GskGLTextureLibrary *self,
+                                 guint                width,
+                                 guint                height)
+{
+  GskGLTexture *texture;
+
+  g_assert (GSK_IS_GL_TEXTURE_LIBRARY (self));
+
+  if (width > self->driver->command_queue->max_texture_size ||
+      height > self->driver->command_queue->max_texture_size)
+    {
+      g_warning ("Clipping requested texture of size %ux%u to maximum allowable size %u.",
+                 width, height, self->driver->command_queue->max_texture_size);
+      width = MIN (width, self->driver->command_queue->max_texture_size);
+      height = MIN (height, self->driver->command_queue->max_texture_size);
+    }
+
+  texture = gsk_next_driver_create_texture (self->driver, width, height, GL_LINEAR, GL_LINEAR);
+  texture->permanent = TRUE;
+
+  return texture;
+}
+
+static inline gboolean
+gsk_gl_texture_atlas_pack (GskGLTextureAtlas *self,
+                           int                width,
+                           int                height,
+                           int               *out_x,
+                           int               *out_y)
+{
+  stbrp_rect rect;
+
+  rect.w = width;
+  rect.h = height;
+
+  stbrp_pack_rects (&self->context, &rect, 1);
+
+  if (rect.was_packed)
+    {
+      *out_x = rect.x;
+      *out_y = rect.y;
+    }
+
+  return rect.was_packed;
+}
+
+static void
+gsk_gl_texture_atlases_pack (GskNextDriver      *driver,
+                             int                 width,
+                             int                 height,
+                             GskGLTextureAtlas **out_atlas,
+                             int                *out_x,
+                             int                *out_y)
+{
+  GskGLTextureAtlas *atlas = NULL;
+  int x, y;
+
+  for (guint i = 0; i < driver->atlases->len; i++)
+    {
+      atlas = g_ptr_array_index (driver->atlases, i);
+
+      if (gsk_gl_texture_atlas_pack (atlas, width, height, &x, &y))
+        break;
+
+      atlas = NULL;
+    }
+
+  if (atlas == NULL)
+    {
+      /* No atlas has enough space, so create a new one... */
+      atlas = gsk_next_driver_create_atlas (driver);
+
+      /* Pack it onto that one, which surely has enough space... */
+      if (!gsk_gl_texture_atlas_pack (atlas, width, height, &x, &y))
+        g_assert_not_reached ();
+    }
+
+  *out_atlas = atlas;
+  *out_x = x;
+  *out_y = y;
+}
+
+gpointer
+gsk_gl_texture_library_pack (GskGLTextureLibrary *self,
+                             gpointer             key,
+                             gsize                valuelen,
+                             guint                width,
+                             guint                height,
+                             int                  padding,
+                             guint               *out_packed_x,
+                             guint               *out_packed_y)
+{
+  GskGLTextureAtlasEntry *entry;
+  GskGLTextureAtlas *atlas = NULL;
+
+  g_assert (GSK_IS_GL_TEXTURE_LIBRARY (self));
+  g_assert (key != NULL);
+  g_assert (valuelen > sizeof (GskGLTextureAtlasEntry));
+  g_assert (out_packed_x != NULL);
+  g_assert (out_packed_y != NULL);
+
+  entry = g_slice_alloc0 (valuelen);
+  entry->n_pixels = width * height;
+  entry->accessed = TRUE;
+
+  /* If our size is invisible then we just want an entry in the
+   * cache for faster lookups, but do not actually spend any texture
+   * allocations on this entry.
+   */
+  if (width <= 0 && height <= 0)
+    {
+      entry->is_atlased = FALSE;
+      entry->texture = NULL;
+      entry->area.x = 0.0f;
+      entry->area.y = 0.0f;
+      entry->area.x2 = 0.0f;
+      entry->area.y2 = 0.0f;
+
+      *out_packed_x = 0;
+      *out_packed_y = 0;
+    }
+  else if (width <= self->max_entry_size && height <= self->max_entry_size)
+    {
+      int packed_x;
+      int packed_y;
+
+      gsk_gl_texture_atlases_pack (self->driver,
+                                   width + padding,
+                                   height + padding,
+                                   &atlas,
+                                   &packed_x,
+                                   &packed_y);
+
+      entry->atlas = atlas;
+      entry->is_atlased = TRUE;
+      entry->area.x = (float)(packed_x + padding) / atlas->width;
+      entry->area.y = (float)(packed_y + padding) / atlas->height;
+      entry->area.x2 = entry->area.x + (float)width / atlas->width;
+      entry->area.y2 = entry->area.y + (float)height / atlas->height;
+
+      *out_packed_x = packed_x;
+      *out_packed_y = packed_y;
+    }
+  else
+    {
+      GskGLTexture *texture = gsk_gl_texture_library_pack_one (self, width + 2, height + 2);
+
+      entry->texture = texture;
+      entry->is_atlased = FALSE;
+      entry->area.x = 0.0f;
+      entry->area.y = 0.0f;
+      entry->area.x2 = 1.0f;
+      entry->area.y2 = 1.0f;
+
+      *out_packed_x = 0;
+      *out_packed_y = 0;
+    }
+
+  g_hash_table_insert (self->hash_table, key, entry);
+
+  return entry;
+}
diff --git a/gsk/next/gskgltexturelibraryprivate.h b/gsk/next/gskgltexturelibraryprivate.h
new file mode 100644
index 0000000000..3a63ebbc07
--- /dev/null
+++ b/gsk/next/gskgltexturelibraryprivate.h
@@ -0,0 +1,201 @@
+/* gskgltexturelibraryprivate.h
+ *
+ * Copyright 2020 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU Lesser General Public License as published by the Free
+ * Software Foundation; either version 2.1 of the License, or (at your option)
+ * any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#ifndef __GSK_GL_TEXTURE_LIBRARY_PRIVATE_H__
+#define __GSK_GL_TEXTURE_LIBRARY_PRIVATE_H__
+
+#include "gskgltypesprivate.h"
+#include "gskgltexturepoolprivate.h"
+#include "../gl/stb_rect_pack.h"
+
+G_BEGIN_DECLS
+
+#define GSK_TYPE_GL_TEXTURE_LIBRARY            (gsk_gl_texture_library_get_type ())
+#define GSK_GL_TEXTURE_LIBRARY(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), 
GSK_TYPE_GL_TEXTURE_LIBRARY, GskGLTextureLibrary))
+#define GSK_IS_GL_TEXTURE_LIBRARY(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), 
GSK_TYPE_GL_TEXTURE_LIBRARY))
+#define GSK_GL_TEXTURE_LIBRARY_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass), 
GSK_TYPE_GL_TEXTURE_LIBRARY, GskGLTextureLibraryClass))
+#define GSK_IS_GL_TEXTURE_LIBRARY_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), 
GSK_TYPE_GL_TEXTURE_LIBRARY))
+#define GSK_GL_TEXTURE_LIBRARY_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj), 
GSK_TYPE_GL_TEXTURE_LIBRARY, GskGLTextureLibraryClass))
+
+typedef struct _GskGLTextureAtlas
+{
+  struct stbrp_context context;
+  struct stbrp_node *nodes;
+
+  int width;
+  int height;
+
+  guint texture_id;
+
+  /* Pixels of rects that have been used at some point,
+   * But are now unused.
+   */
+  int unused_pixels;
+
+  void *user_data;
+} GskGLTextureAtlas;
+
+typedef struct _GskGLTextureAtlasEntry
+{
+  /* A backreference to either the atlas or texture containing
+   * the contents of the atlas entry. For larger items, no atlas
+   * is used and instead a direct texture.
+   */
+  union {
+    GskGLTextureAtlas *atlas;
+    GskGLTexture *texture;
+  };
+
+  /* The area within the atlas translated to 0..1 bounds */
+  struct {
+    float x;
+    float y;
+    float x2;
+    float y2;
+  } area;
+
+  /* Number of pixels in the entry, used to calculate usage
+   * of an atlas while processing.
+   */
+  guint n_pixels : 29;
+
+  /* If entry has marked pixels as used in the atlas this frame */
+  guint used : 1;
+
+  /* If entry was accessed this frame */
+  guint accessed : 1;
+
+  /* When true, backref is an atlas, otherwise texture */
+  guint is_atlased : 1;
+
+  /* Suffix data that is per-library specific. gpointer used to
+   * guarantee the alignment for the entries using this.
+   */
+  gpointer data[0];
+} GskGLTextureAtlasEntry;
+
+typedef struct _GskGLTextureLibrary
+{
+  GObject        parent_instance;
+  GskNextDriver *driver;
+  GHashTable    *hash_table;
+  guint          max_entry_size;
+} GskGLTextureLibrary;
+
+typedef struct _GskGLTextureLibraryClass
+{
+  GObjectClass parent_class;
+
+  void (*begin_frame) (GskGLTextureLibrary *library);
+  void (*end_frame)   (GskGLTextureLibrary *library);
+} GskGLTextureLibraryClass;
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (GskGLTextureLibrary, g_object_unref)
+
+GType    gsk_gl_texture_library_get_type    (void) G_GNUC_CONST;
+void     gsk_gl_texture_library_set_funcs   (GskGLTextureLibrary    *self,
+                                             GHashFunc               hash_func,
+                                             GEqualFunc              equal_func,
+                                             GDestroyNotify          key_destroy,
+                                             GDestroyNotify          value_destroy);
+void     gsk_gl_texture_library_begin_frame (GskGLTextureLibrary    *self);
+void     gsk_gl_texture_library_end_frame   (GskGLTextureLibrary    *self);
+gpointer gsk_gl_texture_library_pack        (GskGLTextureLibrary    *self,
+                                             gpointer                key,
+                                             gsize                   valuelen,
+                                             guint                   width,
+                                             guint                   height,
+                                             int                     padding,
+                                             guint                  *out_packed_x,
+                                             guint                  *out_packed_y);
+
+static inline void
+gsk_gl_texture_atlas_mark_unused (GskGLTextureAtlas *self,
+                                  int                n_pixels)
+{
+  self->unused_pixels += n_pixels;
+}
+
+static inline void
+gsk_gl_texture_atlas_mark_used (GskGLTextureAtlas *self,
+                                int                n_pixels)
+{
+  self->unused_pixels -= n_pixels;
+}
+
+static inline gboolean
+gsk_gl_texture_library_lookup (GskGLTextureLibrary     *self,
+                               gconstpointer            key,
+                               GskGLTextureAtlasEntry **out_entry)
+{
+  GskGLTextureAtlasEntry *entry = g_hash_table_lookup (self->hash_table, key);
+
+  if G_LIKELY (entry != NULL && entry->accessed && entry->used)
+    {
+      *out_entry = entry;
+      return TRUE;
+    }
+
+  if (entry != NULL)
+    {
+      if (!entry->used && entry->is_atlased)
+        {
+          g_assert (entry->atlas != NULL);
+          gsk_gl_texture_atlas_mark_used (entry->atlas, entry->n_pixels);
+          entry->used = TRUE;
+        }
+
+      entry->accessed = TRUE;
+      *out_entry = entry;
+      return TRUE;
+    }
+
+  return FALSE;
+}
+
+static inline guint
+GSK_GL_TEXTURE_ATLAS_ENTRY_TEXTURE (gconstpointer d)
+{
+  const GskGLTextureAtlasEntry *e = d;
+
+  return e->is_atlased ? e->atlas->texture_id
+                       : e->texture ? e->texture->texture_id : 0;
+}
+
+static inline double
+gsk_gl_texture_atlas_get_unused_ratio (const GskGLTextureAtlas *self)
+{
+  if (self->unused_pixels > 0)
+    return (double)(self->unused_pixels) / (double)(self->width * self->height);
+  return 0.0;
+}
+
+static inline gboolean
+gsk_gl_texture_library_can_cache (GskGLTextureLibrary *self,
+                                  int                  width,
+                                  int                  height)
+{
+  g_assert (self->max_entry_size > 0);
+  return width <= self->max_entry_size && height <= self->max_entry_size;
+}
+
+G_END_DECLS
+
+#endif /* __GSK_GL_TEXTURE_LIBRARY_PRIVATE_H__ */
diff --git a/gsk/next/gskgltexturepool.c b/gsk/next/gskgltexturepool.c
new file mode 100644
index 0000000000..6c8b26669b
--- /dev/null
+++ b/gsk/next/gskgltexturepool.c
@@ -0,0 +1,273 @@
+/* gskgltexturepool.c
+ *
+ * Copyright 2020 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU Lesser General Public License as published by the Free
+ * Software Foundation; either version 2.1 of the License, or (at your option)
+ * any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "config.h"
+
+#include <string.h>
+
+#include <gdk/gdktextureprivate.h>
+
+#include "gskgltexturepoolprivate.h"
+#include "ninesliceprivate.h"
+
+void
+gsk_gl_texture_free (GskGLTexture *texture)
+{
+  if (texture != NULL)
+    {
+      g_assert (texture->width_link.prev == NULL);
+      g_assert (texture->width_link.next == NULL);
+      g_assert (texture->height_link.prev == NULL);
+      g_assert (texture->height_link.next == NULL);
+
+      if (texture->user)
+        g_clear_pointer (&texture->user, gdk_texture_clear_render_data);
+
+      if (texture->texture_id != 0)
+        {
+          glDeleteTextures (1, &texture->texture_id);
+          texture->texture_id = 0;
+        }
+
+      for (guint i = 0; i < texture->n_slices; i++)
+        {
+          glDeleteTextures (1, &texture->slices[i].texture_id);
+          texture->slices[i].texture_id = 0;
+        }
+
+      g_clear_pointer (&texture->slices, g_free);
+      g_clear_pointer (&texture->nine_slice, g_free);
+
+      g_slice_free (GskGLTexture, texture);
+    }
+}
+
+void
+gsk_gl_texture_pool_init (GskGLTexturePool *self)
+{
+  memset (self, 0, sizeof *self);
+}
+
+void
+gsk_gl_texture_pool_clear (GskGLTexturePool *self)
+{
+  guint *texture_ids = g_newa (guint, self->by_width.length);
+  guint i = 0;
+
+  while (self->by_width.length > 0)
+    {
+      GskGLTexture *head = g_queue_peek_head (&self->by_width);
+
+      g_queue_unlink (&self->by_width, &head->width_link);
+      g_queue_unlink (&self->by_height, &head->height_link);
+
+      texture_ids[i++] = head->texture_id;
+      head->texture_id = 0;
+
+      gsk_gl_texture_free (head);
+    }
+
+  g_assert (self->by_width.length == 0);
+  g_assert (self->by_height.length == 0);
+
+  if (i > 0)
+    glDeleteTextures (i, texture_ids);
+}
+
+void
+gsk_gl_texture_pool_put (GskGLTexturePool *self,
+                         GskGLTexture     *texture)
+{
+  GList *sibling;
+
+  g_assert (self != NULL);
+  g_assert (texture != NULL);
+  g_assert (texture->user == NULL);
+  g_assert (texture->width_link.prev == NULL);
+  g_assert (texture->width_link.next == NULL);
+  g_assert (texture->width_link.data == texture);
+  g_assert (texture->height_link.prev == NULL);
+  g_assert (texture->height_link.next == NULL);
+  g_assert (texture->height_link.data == texture);
+
+  if (texture->permanent)
+    {
+      gsk_gl_texture_free (texture);
+      return;
+    }
+
+  sibling = NULL;
+  for (GList *iter = self->by_width.head;
+       iter != NULL;
+       iter = iter->next)
+    {
+      GskGLTexture *other = iter->data;
+
+      if (other->width > texture->width ||
+          (other->width == texture->width &&
+           other->height > texture->height))
+        break;
+
+      sibling = iter;
+    }
+
+  g_queue_insert_after_link (&self->by_width, sibling, &texture->width_link);
+
+  sibling = NULL;
+  for (GList *iter = self->by_height.head;
+       iter != NULL;
+       iter = iter->next)
+    {
+      GskGLTexture *other = iter->data;
+
+      if (other->height > texture->height ||
+          (other->height == texture->height &&
+           other->width > texture->width))
+        break;
+
+      sibling = iter;
+    }
+
+  g_queue_insert_after_link (&self->by_height, sibling, &texture->height_link);
+}
+
+GskGLTexture *
+gsk_gl_texture_pool_get (GskGLTexturePool *self,
+                         int               width,
+                         int               height,
+                         int               min_filter,
+                         int               mag_filter,
+                         gboolean          always_create)
+{
+  GskGLTexture *texture;
+
+  if (always_create)
+    goto create_texture;
+
+  if (width >= height)
+    {
+      for (GList *iter = self->by_width.head;
+           iter != NULL;
+           iter = iter->next)
+        {
+          texture = iter->data;
+
+          if (texture->width >= width &&
+              texture->height >= height &&
+              texture->min_filter == min_filter &&
+              texture->mag_filter == mag_filter)
+            {
+              g_queue_unlink (&self->by_width, &texture->width_link);
+              g_queue_unlink (&self->by_height, &texture->height_link);
+
+              return texture;
+            }
+        }
+    }
+  else
+    {
+      for (GList *iter = self->by_height.head;
+           iter != NULL;
+           iter = iter->next)
+        {
+          texture = iter->data;
+
+          if (texture->width >= width &&
+              texture->height >= height &&
+              texture->min_filter == min_filter &&
+              texture->mag_filter == mag_filter)
+            {
+              g_queue_unlink (&self->by_width, &texture->width_link);
+              g_queue_unlink (&self->by_height, &texture->height_link);
+
+              return texture;
+            }
+        }
+    }
+
+create_texture:
+
+  texture = g_slice_new0 (GskGLTexture);
+  texture->width_link.data = texture;
+  texture->height_link.data = texture;
+  texture->min_filter = min_filter;
+  texture->mag_filter = mag_filter;
+
+  glGenTextures (1, &texture->texture_id);
+
+  glActiveTexture (GL_TEXTURE0);
+  glBindTexture (GL_TEXTURE_2D, texture->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);
+
+  if (gdk_gl_context_get_use_es (gdk_gl_context_get_current ()))
+    glTexImage2D (GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
+  else
+    glTexImage2D (GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_BGRA, GL_UNSIGNED_BYTE, NULL);
+
+  glBindTexture (GL_TEXTURE_2D, 0);
+
+  return texture;
+}
+
+GskGLTexture *
+gsk_gl_texture_new (guint  texture_id,
+                    int    width,
+                    int    height,
+                    int    min_filter,
+                    int    mag_filter,
+                    gint64 frame_id)
+{
+  GskGLTexture *texture;
+
+  texture = g_slice_new0 (GskGLTexture);
+  texture->texture_id = texture_id;
+  texture->width_link.data = texture;
+  texture->height_link.data = texture;
+  texture->min_filter = min_filter;
+  texture->mag_filter = mag_filter;
+  texture->width = width;
+  texture->height = height;
+  texture->last_used_in_frame = frame_id;
+
+  return texture;
+}
+
+const GskGLTextureNineSlice *
+gsk_gl_texture_get_nine_slice (GskGLTexture         *texture,
+                               const GskRoundedRect *outline,
+                               float                 extra_pixels)
+{
+  g_assert (texture != NULL);
+  g_assert (outline != NULL);
+
+  if G_UNLIKELY (texture->nine_slice == NULL)
+    {
+      texture->nine_slice = g_new0 (GskGLTextureNineSlice, 9);
+
+      nine_slice_rounded_rect (texture->nine_slice, outline);
+      nine_slice_grow (texture->nine_slice, extra_pixels);
+      nine_slice_to_texture_coords (texture->nine_slice, texture->width, texture->height);
+    }
+
+  return texture->nine_slice;
+}
diff --git a/gsk/next/gskgltexturepoolprivate.h b/gsk/next/gskgltexturepoolprivate.h
new file mode 100644
index 0000000000..1410fa30d6
--- /dev/null
+++ b/gsk/next/gskgltexturepoolprivate.h
@@ -0,0 +1,105 @@
+/* gskgltexturepoolprivate.h
+ *
+ * Copyright 2020 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU Lesser General Public License as published by the Free
+ * Software Foundation; either version 2.1 of the License, or (at your option)
+ * any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#ifndef _GSK_GL_TEXTURE_POOL_PRIVATE_H__
+#define _GSK_GL_TEXTURE_POOL_PRIVATE_H__
+
+#include "gskgltypesprivate.h"
+
+G_BEGIN_DECLS
+
+typedef struct _GskGLTexturePool
+{
+  GQueue by_width;
+  GQueue by_height;
+} GskGLTexturePool;
+
+struct _GskGLTextureSlice
+{
+  cairo_rectangle_int_t rect;
+  guint texture_id;
+};
+
+struct _GskGLTextureNineSlice
+{
+  cairo_rectangle_int_t rect;
+  struct {
+    float x;
+    float y;
+    float x2;
+    float y2;
+  } area;
+};
+
+struct _GskGLTexture
+{
+  /* Used to sort by width/height in pool */
+  GList width_link;
+  GList height_link;
+
+  /* Identifier of the frame that created it */
+  gint64 last_used_in_frame;
+
+  /* Backpointer to texture (can be cleared asynchronously) */
+  GdkTexture *user;
+
+  /* Only used by sliced textures */
+  GskGLTextureSlice *slices;
+  guint n_slices;
+
+  /* Only used by nine-slice textures */
+  GskGLTextureNineSlice *nine_slice;
+
+  /* The actual GL texture identifier in some shared context */
+  guint texture_id;
+
+  int width;
+  int height;
+  int min_filter;
+  int mag_filter;
+
+  /* Set when used by an atlas so we don't drop the texture */
+  guint              permanent : 1;
+};
+
+void                         gsk_gl_texture_pool_init      (GskGLTexturePool *self);
+void                         gsk_gl_texture_pool_clear     (GskGLTexturePool *self);
+GskGLTexture                *gsk_gl_texture_pool_get       (GskGLTexturePool *self,
+                                                            int               width,
+                                                            int               height,
+                                                            int               min_filter,
+                                                            int               mag_filter,
+                                                            gboolean          always_create);
+void                         gsk_gl_texture_pool_put       (GskGLTexturePool *self,
+                                                            GskGLTexture     *texture);
+GskGLTexture                *gsk_gl_texture_new            (guint             texture_id,
+                                                            int               width,
+                                                            int               height,
+                                                            int               min_filter,
+                                                            int               mag_filter,
+                                                            gint64            frame_id);
+const GskGLTextureNineSlice *gsk_gl_texture_get_nine_slice (GskGLTexture         *texture,
+                                                            const GskRoundedRect *outline,
+                                                            float                 extra_pixels);
+void                         gsk_gl_texture_free           (GskGLTexture     *texture);
+
+G_END_DECLS
+
+#endif /* _GSK_GL_TEXTURE_POOL_PRIVATE_H__ */
diff --git a/gsk/next/gskgltypesprivate.h b/gsk/next/gskgltypesprivate.h
new file mode 100644
index 0000000000..044f350850
--- /dev/null
+++ b/gsk/next/gskgltypesprivate.h
@@ -0,0 +1,62 @@
+/* gskgltypesprivate.h
+ *
+ * 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
+ */
+
+#ifndef __GSK_GL_TYPES_PRIVATE_H__
+#define __GSK_GL_TYPES_PRIVATE_H__
+
+#include <epoxy/gl.h>
+#include <graphene.h>
+#include <gdk/gdk.h>
+#include <gsk/gsk.h>
+
+G_BEGIN_DECLS
+
+#define GSK_GL_N_VERTICES 6
+
+typedef struct _GskGLAttachmentState GskGLAttachmentState;
+typedef struct _GskGLBuffer GskGLBuffer;
+typedef struct _GskGLCommandQueue GskGLCommandQueue;
+typedef struct _GskGLCompiler GskGLCompiler;
+typedef struct _GskGLDrawVertex GskGLDrawVertex;
+typedef struct _GskGLRenderTarget GskGLRenderTarget;
+typedef struct _GskGLGlyphLibrary GskGLGlyphLibrary;
+typedef struct _GskGLIconLibrary GskGLIconLibrary;
+typedef struct _GskGLProgram GskGLProgram;
+typedef struct _GskGLRenderJob GskGLRenderJob;
+typedef struct _GskGLShadowLibrary GskGLShadowLibrary;
+typedef struct _GskGLTexture GskGLTexture;
+typedef struct _GskGLTextureSlice GskGLTextureSlice;
+typedef struct _GskGLTextureAtlas GskGLTextureAtlas;
+typedef struct _GskGLTextureLibrary GskGLTextureLibrary;
+typedef struct _GskGLTextureNineSlice GskGLTextureNineSlice;
+typedef struct _GskGLUniformInfo GskGLUniformInfo;
+typedef struct _GskGLUniformProgram GskGLUniformProgram;
+typedef struct _GskGLUniformState GskGLUniformState;
+typedef struct _GskNextDriver GskNextDriver;
+
+struct _GskGLDrawVertex
+{
+  float position[2];
+  float uv[2];
+};
+
+G_END_DECLS
+
+#endif /* __GSK_GL_TYPES_PRIVATE_H__ */
diff --git a/gsk/next/gskgluniformstate.c b/gsk/next/gskgluniformstate.c
new file mode 100644
index 0000000000..fae38f91bd
--- /dev/null
+++ b/gsk/next/gskgluniformstate.c
@@ -0,0 +1,270 @@
+/* gskgluniformstate.c
+ *
+ * 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 <gsk/gskroundedrectprivate.h>
+#include <string.h>
+
+#include "gskgluniformstateprivate.h"
+
+static const guint8 uniform_sizes[] = {
+  0,
+
+  sizeof (Uniform1f),
+  sizeof (Uniform2f),
+  sizeof (Uniform3f),
+  sizeof (Uniform4f),
+
+  sizeof (Uniform1f),
+  sizeof (Uniform2f),
+  sizeof (Uniform3f),
+  sizeof (Uniform4f),
+
+  sizeof (Uniform1i),
+  sizeof (Uniform2i),
+  sizeof (Uniform3i),
+  sizeof (Uniform4i),
+
+  sizeof (Uniform1ui),
+
+  sizeof (guint),
+
+  sizeof (graphene_matrix_t),
+  sizeof (GskRoundedRect),
+  sizeof (GdkRGBA),
+
+  0,
+};
+
+GskGLUniformState *
+gsk_gl_uniform_state_new (void)
+{
+  GskGLUniformState *state;
+
+  state = g_atomic_rc_box_new0 (GskGLUniformState);
+  state->programs = g_hash_table_new_full (NULL, NULL, NULL, g_free);
+  state->values_len = 4096;
+  state->values_pos = 0;
+  state->values_buf = g_malloc (4096);
+
+  return g_steal_pointer (&state);
+}
+
+GskGLUniformState *
+gsk_gl_uniform_state_ref (GskGLUniformState *state)
+{
+  return g_atomic_rc_box_acquire (state);
+}
+
+static void
+gsk_gl_uniform_state_finalize (gpointer data)
+{
+  GskGLUniformState *state = data;
+
+  g_clear_pointer (&state->programs, g_hash_table_unref);
+  g_clear_pointer (&state->values_buf, g_free);
+}
+
+void
+gsk_gl_uniform_state_unref (GskGLUniformState *state)
+{
+  g_atomic_rc_box_release_full (state, gsk_gl_uniform_state_finalize);
+}
+
+gpointer
+gsk_gl_uniform_state_init_value (GskGLUniformState        *state,
+                                 GskGLUniformProgram      *program,
+                                 GskGLUniformFormat        format,
+                                 guint                     array_count,
+                                 guint                     location,
+                                 GskGLUniformInfoElement **infoptr)
+{
+  GskGLUniformInfoElement *info;
+  guint offset;
+
+  g_assert (state != NULL);
+  g_assert (array_count < 32);
+  g_assert ((int)format >= 0 && format < GSK_GL_UNIFORM_FORMAT_LAST);
+  g_assert (format > 0);
+  g_assert (program != NULL);
+  g_assert (program->sparse != NULL);
+  g_assert (program->n_sparse <= program->n_uniforms);
+  g_assert (location < GL_MAX_UNIFORM_LOCATIONS || location == (guint)-1);
+  g_assert (location < program->n_uniforms);
+
+  /* Handle unused uniforms gracefully */
+  if G_UNLIKELY (location == (guint)-1)
+    return NULL;
+
+  info = &program->uniforms[location];
+
+  if G_LIKELY (format == info->info.format)
+    {
+      if G_LIKELY (array_count <= info->info.array_count)
+        {
+          *infoptr = info;
+          return GSK_GL_UNIFORM_VALUE (state->values_buf, info->info.offset);
+        }
+
+      /* We found the uniform, but there is not enough space for the
+       * amount that was requested. Instead, allocate new space and
+       * set the value to "initial" so that the caller just writes
+       * over the previous value.
+       *
+       * This can happen when using dynamic array lengths like the
+       * "n_color_stops" in gradient shaders.
+       */
+      goto setup_info;
+    }
+  else if (info->info.format == 0)
+    {
+      goto setup_info;
+    }
+  else
+    {
+      g_critical ("Attempt to access uniform with different type of value "
+                  "than it was initialized with. Program %u Location %u. "
+                  "Was %d now %d (array length %d now %d).",
+                  program->program_id, location, info->info.format, format,
+                  info->info.array_count, array_count);
+      *infoptr = NULL;
+      return NULL;
+    }
+
+setup_info:
+
+  gsk_gl_uniform_state_realloc (state,
+                                uniform_sizes[format] * MAX (1, array_count),
+                                &offset);
+
+  /* we have 21 bits for offset */
+  g_assert (offset < (1 << GSK_GL_UNIFORM_OFFSET_BITS));
+
+  /* We could once again be setting up this info if the array size grew.
+   * So make sure that we have space in our space array for the value.
+   */
+  g_assert (info->info.format != 0 || program->n_sparse < program->n_uniforms);
+  if (info->info.format == 0)
+    program->sparse[program->n_sparse++] = location;
+
+  info->info.format = format;
+  info->info.offset = offset;
+  info->info.array_count = array_count;
+  info->info.initial = TRUE;
+  info->stamp = 0;
+
+  *infoptr = info;
+
+  return GSK_GL_UNIFORM_VALUE (state->values_buf, info->info.offset);
+}
+
+void
+gsk_gl_uniform_state_end_frame (GskGLUniformState *state)
+{
+  GHashTableIter iter;
+  GskGLUniformProgram *program;
+  guint allocator = 0;
+
+  g_return_if_fail (state != NULL);
+
+  /* After a frame finishes, we want to remove all our copies of uniform
+   * data that isn't needed any longer. Since we treat it as uninitialized
+   * after this frame (to reset it on first use next frame) we can just
+   * discard it but keep an allocation around to reuse.
+   */
+
+  g_hash_table_iter_init (&iter, state->programs);
+  while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&program))
+    {
+      for (guint j = 0; j < program->n_sparse; j++)
+        {
+          guint location = program->sparse[j];
+          GskGLUniformInfoElement *info = &program->uniforms[location];
+          guint size;
+
+          g_assert (info->info.format > 0);
+
+          /* Calculate how much size is needed for the uniform, including arrays */
+          size = uniform_sizes[info->info.format] * MAX (1, info->info.array_count);
+
+          /* Adjust alignment for value */
+          allocator += gsk_gl_uniform_state_align (allocator, size);
+
+          /* Offset is in slots of 4 bytes */
+          info->info.offset = allocator / 4;
+          info->info.initial = TRUE;
+          info->stamp = 0;
+
+          /* Now advance for this items data */
+          allocator += size;
+        }
+    }
+
+  state->values_pos = allocator;
+
+  g_assert (allocator <= state->values_len);
+}
+
+gsize
+gsk_gl_uniform_format_size (GskGLUniformFormat format)
+{
+  g_assert (format > 0);
+  g_assert (format < GSK_GL_UNIFORM_FORMAT_LAST);
+
+  return uniform_sizes[format];
+}
+
+GskGLUniformProgram *
+gsk_gl_uniform_state_get_program (GskGLUniformState *state,
+                                  guint              program,
+                                  guint              n_uniforms)
+{
+  GskGLUniformProgram *ret;
+
+  g_return_val_if_fail (state != NULL, NULL);
+  g_return_val_if_fail (program > 0, NULL);
+  g_return_val_if_fail (program < G_MAXUINT, NULL);
+
+  ret = g_hash_table_lookup (state->programs, GUINT_TO_POINTER (program));
+
+  if (ret == NULL)
+    {
+      gsize uniform_size = n_uniforms * sizeof (GskGLUniformInfoElement);
+      gsize sparse_size = n_uniforms * sizeof (guint);
+      gsize size = sizeof (GskGLUniformProgram) + uniform_size + sparse_size;
+
+      /* Must be multiple of 4 for space pointer to align */
+      G_STATIC_ASSERT (sizeof (GskGLUniformInfoElement) == 8);
+
+      ret = g_malloc0 (size);
+      ret->program_id = program;
+      ret->n_uniforms = n_uniforms;
+      ret->n_sparse = 0;
+      ret->sparse = (guint *)&ret->uniforms[n_uniforms];
+
+      for (guint i = 0; i < n_uniforms; i++)
+        ret->uniforms[i].info.initial = TRUE;
+
+      g_hash_table_insert (state->programs, GUINT_TO_POINTER (program), ret);
+    }
+
+  return ret;
+}
diff --git a/gsk/next/gskgluniformstateprivate.h b/gsk/next/gskgluniformstateprivate.h
new file mode 100644
index 0000000000..d084fa27a7
--- /dev/null
+++ b/gsk/next/gskgluniformstateprivate.h
@@ -0,0 +1,685 @@
+/* gskgluniformstateprivate.h
+ *
+ * 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
+ */
+
+#ifndef GSK_GL_UNIFORM_STATE_PRIVATE_H
+#define GSK_GL_UNIFORM_STATE_PRIVATE_H
+
+#include "gskgltypesprivate.h"
+
+G_BEGIN_DECLS
+
+typedef struct { float v0; } Uniform1f;
+typedef struct { float v0; float v1; } Uniform2f;
+typedef struct { float v0; float v1; float v2; } Uniform3f;
+typedef struct { float v0; float v1; float v2; float v3; } Uniform4f;
+
+typedef struct { int v0; } Uniform1i;
+typedef struct { int v0; int v1; } Uniform2i;
+typedef struct { int v0; int v1; int v2; } Uniform3i;
+typedef struct { int v0; int v1; int v2; int v3; } Uniform4i;
+
+typedef struct { guint v0; } Uniform1ui;
+
+#define GSK_GL_UNIFORM_ARRAY_BITS  5
+#define GSK_GL_UNIFORM_FORMAT_BITS 5
+#define GSK_GL_UNIFORM_OFFSET_BITS 21
+
+typedef struct _GskGLUniformInfo
+{
+  guint initial     : 1;
+  guint format      : GSK_GL_UNIFORM_FORMAT_BITS;
+  guint array_count : GSK_GL_UNIFORM_ARRAY_BITS;
+  guint offset      : GSK_GL_UNIFORM_OFFSET_BITS;
+} GskGLUniformInfo;
+
+G_STATIC_ASSERT (sizeof (GskGLUniformInfo) == 4);
+
+typedef struct _GskGLUniformInfoElement
+{
+  GskGLUniformInfo info;
+  guint stamp;
+} GskGLUniformInfoElement;
+
+G_STATIC_ASSERT (sizeof (GskGLUniformInfoElement) == 8);
+
+typedef struct _GskGLUniformProgram
+{
+  guint program_id;
+  guint n_uniforms : 12;
+  guint has_attachments : 1;
+
+  /* To avoid walking our 1:1 array of location->uniform slots, we have
+   * a sparse index that allows us to skip the empty zones.
+   */
+  guint *sparse;
+  guint n_sparse;
+
+  /* Uniforms are provided inline at the end of structure to avoid
+   * an extra dereference.
+   */
+  GskGLUniformInfoElement uniforms[0];
+} GskGLUniformProgram;
+
+typedef struct _GskGLUniformState
+{
+  GHashTable *programs;
+  guint8 *values_buf;
+  guint values_pos;
+  guint values_len;
+} GskGLUniformState;
+
+/**
+ * GskGLUniformStateCallback:
+ * @info: a pointer to the information about the uniform
+ * @location: the location of the uniform within the GPU program.
+ * @user_data: closure data for the callback
+ *
+ * This callback can be used to snapshot state of a program which
+ * is useful when batching commands so that the state may be compared
+ * with future evocations of the program.
+ */
+typedef void (*GskGLUniformStateCallback) (const GskGLUniformInfo *info,
+                                           guint                   location,
+                                           gpointer                user_data);
+
+typedef enum _GskGLUniformKind
+{
+  GSK_GL_UNIFORM_FORMAT_1F = 1,
+  GSK_GL_UNIFORM_FORMAT_2F,
+  GSK_GL_UNIFORM_FORMAT_3F,
+  GSK_GL_UNIFORM_FORMAT_4F,
+
+  GSK_GL_UNIFORM_FORMAT_1FV,
+  GSK_GL_UNIFORM_FORMAT_2FV,
+  GSK_GL_UNIFORM_FORMAT_3FV,
+  GSK_GL_UNIFORM_FORMAT_4FV,
+
+  GSK_GL_UNIFORM_FORMAT_1I,
+  GSK_GL_UNIFORM_FORMAT_2I,
+  GSK_GL_UNIFORM_FORMAT_3I,
+  GSK_GL_UNIFORM_FORMAT_4I,
+
+  GSK_GL_UNIFORM_FORMAT_1UI,
+
+  GSK_GL_UNIFORM_FORMAT_TEXTURE,
+
+  GSK_GL_UNIFORM_FORMAT_MATRIX,
+  GSK_GL_UNIFORM_FORMAT_ROUNDED_RECT,
+  GSK_GL_UNIFORM_FORMAT_COLOR,
+
+  GSK_GL_UNIFORM_FORMAT_LAST
+} GskGLUniformFormat;
+
+G_STATIC_ASSERT (GSK_GL_UNIFORM_FORMAT_LAST < (1 << GSK_GL_UNIFORM_FORMAT_BITS));
+
+GskGLUniformState   *gsk_gl_uniform_state_new         (void);
+GskGLUniformState   *gsk_gl_uniform_state_ref         (GskGLUniformState         *state);
+void                 gsk_gl_uniform_state_unref       (GskGLUniformState         *state);
+GskGLUniformProgram *gsk_gl_uniform_state_get_program (GskGLUniformState         *state,
+                                                       guint                      program,
+                                                       guint                      n_uniforms);
+void                 gsk_gl_uniform_state_end_frame   (GskGLUniformState         *state);
+gsize                gsk_gl_uniform_format_size       (GskGLUniformFormat         format);
+gpointer             gsk_gl_uniform_state_init_value  (GskGLUniformState         *state,
+                                                       GskGLUniformProgram       *program,
+                                                       GskGLUniformFormat         format,
+                                                       guint                      array_count,
+                                                       guint                      location,
+                                                       GskGLUniformInfoElement  **infoptr);
+
+#define GSK_GL_UNIFORM_VALUE(base, offset) ((gpointer)((base) + ((offset) * 4)))
+#define gsk_gl_uniform_state_get_uniform_data(state,offset) GSK_GL_UNIFORM_VALUE((state)->values_buf, offset)
+#define gsk_gl_uniform_state_snapshot(state, program_info, callback, user_data) \
+  G_STMT_START {                                                                \
+    for (guint z = 0; z < program_info->n_sparse; z++)                          \
+      {                                                                         \
+        guint location = program_info->sparse[z];                               \
+        GskGLUniformInfoElement *info = &program_info->uniforms[location];      \
+                                                                                \
+        g_assert (location < GL_MAX_UNIFORM_LOCATIONS);                         \
+        g_assert (location < program_info->n_uniforms);                         \
+                                                                                \
+        if (info->info.format > 0)                                              \
+          callback (&info->info, location, user_data);                          \
+      }                                                                         \
+  } G_STMT_END
+
+static inline gpointer
+gsk_gl_uniform_state_get_value (GskGLUniformState        *state,
+                                GskGLUniformProgram      *program,
+                                GskGLUniformFormat        format,
+                                guint                     array_count,
+                                guint                     location,
+                                guint                     stamp,
+                                GskGLUniformInfoElement **infoptr)
+{
+  GskGLUniformInfoElement *info;
+
+  if (location == (guint)-1)
+    return NULL;
+
+  /* If the stamp is the same, then we can ignore the request
+   * and short-circuit as early as possible. This requires the
+   * caller to increment their private stamp when they change
+   * internal state.
+   *
+   * This is generally used for the shared uniforms like projection,
+   * modelview, clip, etc to avoid so many comparisons which cost
+   * considerable CPU.
+   */
+  info = &program->uniforms[location];
+  if (stamp != 0 && stamp == info->stamp)
+    return NULL;
+
+  if G_LIKELY (format == info->info.format && array_count <= info->info.array_count)
+    {
+      *infoptr = info;
+      return GSK_GL_UNIFORM_VALUE (state->values_buf, info->info.offset);
+    }
+
+  return gsk_gl_uniform_state_init_value (state, program, format, array_count, location, infoptr);
+}
+
+static inline guint
+gsk_gl_uniform_state_align (guint current_pos,
+                            guint size)
+{
+  guint align = size > 8 ? 16 : (size > 4 ? 8 : 4);
+  guint masked = current_pos & (align - 1);
+
+  g_assert (size > 0);
+  g_assert (align == 4 || align == 8 || align == 16);
+  g_assert (masked < align);
+
+  return align - masked;
+}
+
+static inline gpointer
+gsk_gl_uniform_state_realloc (GskGLUniformState *state,
+                              guint              size,
+                              guint             *offset)
+{
+  guint padding = gsk_gl_uniform_state_align (state->values_pos, size);
+
+  if G_UNLIKELY (state->values_len - padding - size < state->values_pos)
+    {
+      state->values_len *= 2;
+      state->values_buf = g_realloc (state->values_buf, state->values_len);
+    }
+
+  /* offsets are in slots of 4 to use fewer bits */
+  g_assert ((state->values_pos + padding) % 4 == 0);
+  *offset = (state->values_pos + padding) / 4;
+  state->values_pos += padding + size;
+
+  return GSK_GL_UNIFORM_VALUE (state->values_buf, *offset);
+}
+
+#define GSK_GL_UNIFORM_STATE_REPLACE(info, u, type, count)                                \
+  G_STMT_START {                                                                          \
+    if ((info)->info.initial && count == (info)->info.array_count)                        \
+      {                                                                                   \
+        u = GSK_GL_UNIFORM_VALUE (state->values_buf, (info)->info.offset);                \
+      }                                                                                   \
+    else                                                                                  \
+      {                                                                                   \
+        guint offset;                                                                     \
+        u = gsk_gl_uniform_state_realloc (state, sizeof(type) * MAX (1, count), &offset); \
+        g_assert (offset < (1 << GSK_GL_UNIFORM_OFFSET_BITS));                            \
+        (info)->info.offset = offset;                                                     \
+        /* We might have increased array length */                                        \
+        (info)->info.array_count = count;                                                 \
+      }                                                                                   \
+  } G_STMT_END
+
+static inline void
+gsk_gl_uniform_info_changed (GskGLUniformInfoElement *info,
+                             guint                    location,
+                             guint                    stamp)
+{
+  info->stamp = stamp;
+  info->info.initial = FALSE;
+}
+
+static inline void
+gsk_gl_uniform_state_set1f (GskGLUniformState   *state,
+                            GskGLUniformProgram *program,
+                            guint                location,
+                            guint                stamp,
+                            float                value0)
+{
+  Uniform1f *u;
+  GskGLUniformInfoElement *info;
+
+  g_assert (state != NULL);
+  g_assert (program != 0);
+
+  if ((u = gsk_gl_uniform_state_get_value (state, program, GSK_GL_UNIFORM_FORMAT_1F, 1, location, stamp, 
&info)))
+    {
+      GSK_GL_UNIFORM_STATE_REPLACE (info, u, Uniform1f , 1);
+      u->v0 = value0;
+      gsk_gl_uniform_info_changed (info, location, stamp);
+    }
+}
+
+static inline void
+gsk_gl_uniform_state_set2f (GskGLUniformState   *state,
+                            GskGLUniformProgram *program,
+                            guint                location,
+                            guint                stamp,
+                            float                value0,
+                            float                value1)
+{
+  Uniform2f *u;
+  GskGLUniformInfoElement *info;
+
+  g_assert (state != NULL);
+  g_assert (program != NULL);
+
+  if ((u = gsk_gl_uniform_state_get_value (state, program, GSK_GL_UNIFORM_FORMAT_2F, 1, location, stamp, 
&info)))
+    {
+      GSK_GL_UNIFORM_STATE_REPLACE (info, u, Uniform2f, 1);
+      u->v0 = value0;
+      u->v1 = value1;
+      gsk_gl_uniform_info_changed (info, location, stamp);
+    }
+}
+
+static inline void
+gsk_gl_uniform_state_set3f (GskGLUniformState   *state,
+                            GskGLUniformProgram *program,
+                            guint                location,
+                            guint                stamp,
+                            float                value0,
+                            float                value1,
+                            float                value2)
+{
+  Uniform3f *u;
+  GskGLUniformInfoElement *info;
+
+  g_assert (state != NULL);
+  g_assert (program != NULL);
+
+  if ((u = gsk_gl_uniform_state_get_value (state, program, GSK_GL_UNIFORM_FORMAT_3F, 1, location, stamp, 
&info)))
+    {
+      GSK_GL_UNIFORM_STATE_REPLACE (info, u, Uniform3f, 1);
+      u->v0 = value0;
+      u->v1 = value1;
+      u->v2 = value2;
+      gsk_gl_uniform_info_changed (info, location, stamp);
+    }
+}
+
+static inline void
+gsk_gl_uniform_state_set4f (GskGLUniformState   *state,
+                            GskGLUniformProgram *program,
+                            guint                location,
+                            guint                stamp,
+                            float                value0,
+                            float                value1,
+                            float                value2,
+                            float                value3)
+{
+  Uniform4f *u;
+  GskGLUniformInfoElement *info;
+
+  g_assert (state != NULL);
+  g_assert (program != NULL);
+
+  if ((u = gsk_gl_uniform_state_get_value (state, program, GSK_GL_UNIFORM_FORMAT_4F, 1, location, stamp, 
&info)))
+    {
+      GSK_GL_UNIFORM_STATE_REPLACE (info, u, Uniform4f, 1);
+      u->v0 = value0;
+      u->v1 = value1;
+      u->v2 = value2;
+      u->v3 = value3;
+      gsk_gl_uniform_info_changed (info, location, stamp);
+    }
+}
+
+static inline void
+gsk_gl_uniform_state_set1ui (GskGLUniformState   *state,
+                             GskGLUniformProgram *program,
+                             guint                location,
+                             guint                stamp,
+                             guint                value0)
+{
+  Uniform1ui *u;
+  GskGLUniformInfoElement *info;
+
+  g_assert (state != NULL);
+  g_assert (program != NULL);
+
+  if ((u = gsk_gl_uniform_state_get_value (state, program, GSK_GL_UNIFORM_FORMAT_1UI, 1, location, stamp, 
&info)))
+    {
+      GSK_GL_UNIFORM_STATE_REPLACE (info, u, Uniform1ui, 1);
+      u->v0 = value0;
+      gsk_gl_uniform_info_changed (info, location, stamp);
+    }
+}
+
+static inline void
+gsk_gl_uniform_state_set1i (GskGLUniformState   *state,
+                            GskGLUniformProgram *program,
+                            guint                location,
+                            guint                stamp,
+                            int                  value0)
+{
+  Uniform1i *u;
+  GskGLUniformInfoElement *info;
+
+  g_assert (state != NULL);
+  g_assert (program != NULL);
+
+  if ((u = gsk_gl_uniform_state_get_value (state, program, GSK_GL_UNIFORM_FORMAT_1I, 1, location, stamp, 
&info)))
+    {
+      GSK_GL_UNIFORM_STATE_REPLACE (info, u, Uniform1i, 1);
+      u->v0 = value0;
+      gsk_gl_uniform_info_changed (info, location, stamp);
+    }
+}
+
+static inline void
+gsk_gl_uniform_state_set2i (GskGLUniformState   *state,
+                            GskGLUniformProgram *program,
+                            guint                location,
+                            guint                stamp,
+                            int                  value0,
+                            int                  value1)
+{
+  Uniform2i *u;
+  GskGLUniformInfoElement *info;
+
+  g_assert (state != NULL);
+  g_assert (program != NULL);
+
+  if ((u = gsk_gl_uniform_state_get_value (state, program, GSK_GL_UNIFORM_FORMAT_2I, 1, location, stamp, 
&info)))
+    {
+      GSK_GL_UNIFORM_STATE_REPLACE (info, u, Uniform2i, 1);
+      u->v0 = value0;
+      u->v1 = value1;
+      gsk_gl_uniform_info_changed (info, location, stamp);
+    }
+}
+
+static inline void
+gsk_gl_uniform_state_set3i (GskGLUniformState   *state,
+                            GskGLUniformProgram *program,
+                            guint                location,
+                            guint                stamp,
+                            int                  value0,
+                            int                  value1,
+                            int                  value2)
+{
+  Uniform3i *u;
+  GskGLUniformInfoElement *info;
+
+  g_assert (state != NULL);
+  g_assert (program != NULL);
+
+  if ((u = gsk_gl_uniform_state_get_value (state, program, GSK_GL_UNIFORM_FORMAT_3I, 1, location, stamp, 
&info)))
+    {
+      GSK_GL_UNIFORM_STATE_REPLACE (info, u, Uniform3i, 1);
+      u->v0 = value0;
+      u->v1 = value1;
+      u->v2 = value2;
+      gsk_gl_uniform_info_changed (info, location, stamp);
+    }
+}
+
+static inline void
+gsk_gl_uniform_state_set4i (GskGLUniformState   *state,
+                            GskGLUniformProgram *program,
+                            guint                location,
+                            guint                stamp,
+                            int                  value0,
+                            int                  value1,
+                            int                  value2,
+                            int                  value3)
+{
+  Uniform4i *u;
+  GskGLUniformInfoElement *info;
+
+  g_assert (state != NULL);
+  g_assert (program != NULL);
+
+  if ((u = gsk_gl_uniform_state_get_value (state, program, GSK_GL_UNIFORM_FORMAT_4I, 1, location, stamp, 
&info)))
+    {
+      GSK_GL_UNIFORM_STATE_REPLACE (info, u, Uniform4i, 1);
+      u->v0 = value0;
+      u->v1 = value1;
+      u->v2 = value2;
+      u->v3 = value3;
+      gsk_gl_uniform_info_changed (info, location, stamp);
+    }
+}
+
+static inline void
+gsk_gl_uniform_state_set_rounded_rect (GskGLUniformState    *state,
+                                       GskGLUniformProgram  *program,
+                                       guint                 location,
+                                       guint                 stamp,
+                                       const GskRoundedRect *rounded_rect)
+{
+  GskRoundedRect *u;
+  GskGLUniformInfoElement *info;
+
+  g_assert (state != NULL);
+  g_assert (program != NULL);
+  g_assert (rounded_rect != NULL);
+
+  if ((u = gsk_gl_uniform_state_get_value (state, program, GSK_GL_UNIFORM_FORMAT_ROUNDED_RECT, 1, location, 
stamp, &info)))
+    {
+      GSK_GL_UNIFORM_STATE_REPLACE (info, u, GskRoundedRect, 1);
+      memcpy (u, rounded_rect, sizeof *rounded_rect);
+      gsk_gl_uniform_info_changed (info, location, stamp);
+    }
+}
+
+static inline void
+gsk_gl_uniform_state_set_matrix (GskGLUniformState       *state,
+                                 GskGLUniformProgram     *program,
+                                 guint                    location,
+                                 guint                    stamp,
+                                 const graphene_matrix_t *matrix)
+{
+  graphene_matrix_t *u;
+  GskGLUniformInfoElement *info;
+
+  g_assert (state != NULL);
+  g_assert (program != NULL);
+  g_assert (matrix != NULL);
+
+  if ((u = gsk_gl_uniform_state_get_value (state, program, GSK_GL_UNIFORM_FORMAT_MATRIX, 1, location, stamp, 
&info)))
+    {
+      GSK_GL_UNIFORM_STATE_REPLACE (info, u, graphene_matrix_t, 1);
+      memcpy (u, matrix, sizeof *matrix);
+      gsk_gl_uniform_info_changed (info, location, stamp);
+    }
+}
+
+/**
+ * gsk_gl_uniform_state_set_texture:
+ * @state: a #GskGLUniformState
+ * @program: the program id
+ * @location: the location of the texture
+ * @texture_slot: a texturing slot such as GL_TEXTURE0
+ *
+ * Sets the uniform expecting a texture to @texture_slot. This API
+ * expects a texture slot such as GL_TEXTURE0 to reduce chances of
+ * miss-use by the caller.
+ *
+ * The value stored to the uniform is in the form of 0 for GL_TEXTURE0,
+ * 1 for GL_TEXTURE1, and so on.
+ */
+static inline void
+gsk_gl_uniform_state_set_texture (GskGLUniformState   *state,
+                                  GskGLUniformProgram *program,
+                                  guint                location,
+                                  guint                stamp,
+                                  guint                texture_slot)
+{
+  GskGLUniformInfoElement *info;
+  guint *u;
+
+  g_assert (texture_slot >= GL_TEXTURE0);
+  g_assert (texture_slot < GL_TEXTURE16);
+
+  texture_slot -= GL_TEXTURE0;
+
+  if ((u = gsk_gl_uniform_state_get_value (state, program, GSK_GL_UNIFORM_FORMAT_TEXTURE, 1, location, 
stamp, &info)))
+    {
+      GSK_GL_UNIFORM_STATE_REPLACE (info, u, guint, 1);
+      *u = texture_slot;
+      gsk_gl_uniform_info_changed (info, location, stamp);
+    }
+}
+
+/**
+ * gsk_gl_uniform_state_set_color:
+ * @state: a #GskGLUniformState
+ * @program: a program id > 0
+ * @location: the uniform location
+ * @color: a color to set or %NULL for transparent
+ *
+ * Sets a uniform to the color described by @color. This is a convenience
+ * function to allow callers to avoid having to translate colors to floats
+ * in other portions of the renderer.
+ */
+static inline void
+gsk_gl_uniform_state_set_color (GskGLUniformState   *state,
+                                GskGLUniformProgram *program,
+                                guint                location,
+                                guint                stamp,
+                                const GdkRGBA       *color)
+{
+  static const GdkRGBA transparent = {0};
+  GskGLUniformInfoElement *info;
+  GdkRGBA *u;
+
+  g_assert (state != NULL);
+  g_assert (program != NULL);
+
+  if ((u = gsk_gl_uniform_state_get_value (state, program, GSK_GL_UNIFORM_FORMAT_COLOR, 1, location, stamp, 
&info)))
+    {
+      if (color == NULL)
+        color = &transparent;
+
+      GSK_GL_UNIFORM_STATE_REPLACE (info, u, GdkRGBA, 1);
+      memcpy (u, color, sizeof *color);
+      gsk_gl_uniform_info_changed (info, location, stamp);
+    }
+}
+
+static inline void
+gsk_gl_uniform_state_set1fv (GskGLUniformState   *state,
+                             GskGLUniformProgram *program,
+                             guint                location,
+                             guint                stamp,
+                             guint                count,
+                             const float         *value)
+{
+  Uniform1f *u;
+  GskGLUniformInfoElement *info;
+
+  g_assert (state != NULL);
+  g_assert (program != NULL);
+  g_assert (count > 0);
+
+  if ((u = gsk_gl_uniform_state_get_value (state, program, GSK_GL_UNIFORM_FORMAT_1FV, count, location, 
stamp, &info)))
+    {
+      GSK_GL_UNIFORM_STATE_REPLACE (info, u, Uniform1f, count);
+      memcpy (u, value, sizeof (Uniform1f) * count);
+      gsk_gl_uniform_info_changed (info, location, stamp);
+    }
+}
+
+static inline void
+gsk_gl_uniform_state_set2fv (GskGLUniformState   *state,
+                             GskGLUniformProgram *program,
+                             guint                location,
+                             guint                stamp,
+                             guint                count,
+                             const float         *value)
+{
+  Uniform2f *u;
+  GskGLUniformInfoElement *info;
+
+  g_assert (state != NULL);
+  g_assert (program != NULL);
+  g_assert (count > 0);
+
+  if ((u = gsk_gl_uniform_state_get_value (state, program, GSK_GL_UNIFORM_FORMAT_2FV, count, location, 
stamp, &info)))
+    {
+      GSK_GL_UNIFORM_STATE_REPLACE (info, u, Uniform2f, count);
+      memcpy (u, value, sizeof (Uniform2f) * count);
+      gsk_gl_uniform_info_changed (info, location, stamp);
+    }
+}
+
+static inline void
+gsk_gl_uniform_state_set3fv (GskGLUniformState   *state,
+                             GskGLUniformProgram *program,
+                             guint                location,
+                             guint                stamp,
+                             guint                count,
+                             const float         *value)
+{
+  Uniform3f *u;
+  GskGLUniformInfoElement *info;
+
+  g_assert (state != NULL);
+  g_assert (program != NULL);
+  g_assert (count > 0);
+
+  if ((u = gsk_gl_uniform_state_get_value (state, program, GSK_GL_UNIFORM_FORMAT_3FV, count, location, 
stamp, &info)))
+    {
+      GSK_GL_UNIFORM_STATE_REPLACE (info, u, Uniform3f, count);
+      memcpy (u, value, sizeof (Uniform3f) * count);
+      gsk_gl_uniform_info_changed (info, location, stamp);
+    }
+}
+
+static inline void
+gsk_gl_uniform_state_set4fv (GskGLUniformState   *state,
+                             GskGLUniformProgram *program,
+                             guint                location,
+                             guint                stamp,
+                             guint                count,
+                             const float         *value)
+{
+  Uniform4f *u;
+  GskGLUniformInfoElement *info;
+
+  g_assert (state != NULL);
+  g_assert (program != NULL);
+  g_assert (count > 0);
+
+  if ((u = gsk_gl_uniform_state_get_value (state, program, GSK_GL_UNIFORM_FORMAT_4FV, count, location, 
stamp, &info)))
+    {
+      GSK_GL_UNIFORM_STATE_REPLACE (info, u, Uniform4f, count);
+      memcpy (u, value, sizeof (Uniform4f) * count);
+      gsk_gl_uniform_info_changed (info, location, stamp);
+    }
+}
+
+G_END_DECLS
+
+#endif /* GSK_GL_UNIFORM_STATE_PRIVATE_H */
diff --git a/gsk/next/inlinearray.h b/gsk/next/inlinearray.h
new file mode 100644
index 0000000000..98a9255c68
--- /dev/null
+++ b/gsk/next/inlinearray.h
@@ -0,0 +1,77 @@
+#ifndef __INLINE_ARRAY_H__
+#define __INLINE_ARRAY_H__
+
+#define DEFINE_INLINE_ARRAY(Type, prefix, ElementType)              \
+  typedef struct _##Type {                                          \
+    gsize len;                                                      \
+    gsize allocated;                                                \
+    ElementType *items;                                             \
+  } Type;                                                           \
+                                                                    \
+  static inline void                                                \
+  prefix##_init (Type  *ar,                                         \
+                 gsize  initial_size)                               \
+  {                                                                 \
+    ar->len = 0;                                                    \
+    ar->allocated = initial_size ? initial_size : 16;               \
+    ar->items = g_new0 (ElementType, ar->allocated);                \
+  }                                                                 \
+                                                                    \
+  static inline void                                                \
+  prefix##_clear (Type *ar)                                         \
+  {                                                                 \
+    ar->len = 0;                                                    \
+    ar->allocated = 0;                                              \
+    g_clear_pointer (&ar->items, g_free);                           \
+  }                                                                 \
+                                                                    \
+  static inline ElementType *                                       \
+  prefix##_head (Type *ar)                                          \
+  {                                                                 \
+    return &ar->items[0];                                           \
+  }                                                                 \
+                                                                    \
+  static inline ElementType *                                       \
+  prefix##_tail (Type *ar)                                          \
+  {                                                                 \
+    return &ar->items[ar->len-1];                                   \
+  }                                                                 \
+                                                                    \
+  static inline ElementType *                                       \
+  prefix##_append (Type *ar)                                        \
+  {                                                                 \
+    if G_UNLIKELY (ar->len == ar->allocated)                        \
+      {                                                             \
+        ar->allocated *= 2;                                         \
+        ar->items = g_renew (ElementType, ar->items, ar->allocated);\
+      }                                                             \
+                                                                    \
+    ar->len++;                                                      \
+                                                                    \
+    return prefix##_tail (ar);                                      \
+  }                                                                 \
+                                                                    \
+  static inline ElementType *                                       \
+  prefix##_append_n (Type  *ar,                                     \
+                     gsize  n)                                      \
+  {                                                                 \
+    if G_UNLIKELY ((ar->len + n) > ar->allocated)                   \
+      {                                                             \
+        while ((ar->len + n) > ar->allocated)                       \
+          ar->allocated *= 2;                                       \
+        ar->items = g_renew (ElementType, ar->items, ar->allocated);\
+      }                                                             \
+                                                                    \
+    ar->len += n;                                                   \
+                                                                    \
+    return &ar->items[ar->len-n];                                   \
+  }                                                                 \
+                                                                    \
+  static inline gsize                                               \
+  prefix##_index_of (Type              *ar,                         \
+                     const ElementType *element)                    \
+  {                                                                 \
+    return element - &ar->items[0];                                 \
+  }
+
+#endif /* __INLINE_ARRAY_H__ */
diff --git a/gsk/next/ninesliceprivate.h b/gsk/next/ninesliceprivate.h
new file mode 100644
index 0000000000..46677b437f
--- /dev/null
+++ b/gsk/next/ninesliceprivate.h
@@ -0,0 +1,309 @@
+/* ninesliceprivate.h
+ *
+ * Copyright 2017 Timm Bäder <mail baedert org>
+ * Copyright 2021 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
+ */
+
+#ifndef __NINE_SLICE_PRIVATE_H__
+#define __NINE_SLICE_PRIVATE_H__
+
+#include "gskgltexturepoolprivate.h"
+
+#if 0
+# define DEBUG_NINE_SLICE
+#endif
+
+G_BEGIN_DECLS
+
+enum {
+  NINE_SLICE_TOP_LEFT      = 0,
+  NINE_SLICE_TOP_CENTER    = 1,
+  NINE_SLICE_TOP_RIGHT     = 2,
+  NINE_SLICE_LEFT_CENTER   = 3,
+  NINE_SLICE_CENTER        = 4,
+  NINE_SLICE_RIGHT_CENTER  = 5,
+  NINE_SLICE_BOTTOM_LEFT   = 6,
+  NINE_SLICE_BOTTOM_CENTER = 7,
+  NINE_SLICE_BOTTOM_RIGHT  = 8,
+};
+
+static inline bool G_GNUC_PURE
+nine_slice_is_visible (const GskGLTextureNineSlice *slice)
+{
+  return slice->rect.width > 0 && slice->rect.height > 0;
+}
+
+static inline void
+nine_slice_rounded_rect (GskGLTextureNineSlice *slices,
+                         const GskRoundedRect  *rect)
+{
+  const graphene_point_t *origin = &rect->bounds.origin;
+  const graphene_size_t *size = &rect->bounds.size;
+  int top_height = ceilf (MAX (rect->corner[GSK_CORNER_TOP_LEFT].height,
+                               rect->corner[GSK_CORNER_TOP_RIGHT].height));
+  int bottom_height = ceilf (MAX (rect->corner[GSK_CORNER_BOTTOM_LEFT].height,
+                                  rect->corner[GSK_CORNER_BOTTOM_RIGHT].height));
+  int right_width = ceilf (MAX (rect->corner[GSK_CORNER_TOP_RIGHT].width,
+                                rect->corner[GSK_CORNER_BOTTOM_RIGHT].width));
+  int left_width = ceilf (MAX (rect->corner[GSK_CORNER_TOP_LEFT].width,
+                               rect->corner[GSK_CORNER_BOTTOM_LEFT].width));
+
+  /* Top left */
+  slices[0].rect.x = origin->x;
+  slices[0].rect.y = origin->y;
+  slices[0].rect.width = left_width;
+  slices[0].rect.height = top_height;
+
+  /* Top center */
+  slices[1].rect.x = origin->x + size->width / 2.0 - 0.5;
+  slices[1].rect.y = origin->y;
+  slices[1].rect.width = 1;
+  slices[1].rect.height = top_height;
+
+  /* Top right */
+  slices[2].rect.x = origin->x + size->width - right_width;
+  slices[2].rect.y = origin->y;
+  slices[2].rect.width = right_width;
+  slices[2].rect.height = top_height;
+
+  /* Left center */
+  slices[3].rect.x = origin->x;
+  slices[3].rect.y = origin->y + size->height / 2;
+  slices[3].rect.width = left_width;
+  slices[3].rect.height = 1;
+
+  /* center */
+  slices[4].rect.x = origin->x + size->width / 2.0 - 0.5;
+  slices[4].rect.y = origin->y + size->height / 2.0 - 0.5;
+  slices[4].rect.width = 1;
+  slices[4].rect.height = 1;
+
+  /* Right center */
+  slices[5].rect.x = origin->x + size->width - right_width;
+  slices[5].rect.y = origin->y + (size->height / 2.0) - 0.5;
+  slices[5].rect.width = right_width;
+  slices[5].rect.height = 1;
+
+  /* Bottom Left */
+  slices[6].rect.x = origin->x;
+  slices[6].rect.y = origin->y + size->height - bottom_height;
+  slices[6].rect.width = left_width;
+  slices[6].rect.height = bottom_height;
+
+  /* Bottom center */
+  slices[7].rect.x = origin->x + (size->width / 2.0) - 0.5;
+  slices[7].rect.y = origin->y + size->height - bottom_height;
+  slices[7].rect.width = 1;
+  slices[7].rect.height = bottom_height;
+
+  /* Bottom right */
+  slices[8].rect.x = origin->x + size->width - right_width;
+  slices[8].rect.y = origin->y + size->height - bottom_height;
+  slices[8].rect.width = right_width;
+  slices[8].rect.height = bottom_height;
+
+#ifdef DEBUG_NINE_SLICE
+  /* These only hold true when the values from ceilf() above
+   * are greater than one. Otherwise they fail, like will happen
+   * with the node editor viewing the textures zoomed out.
+   */
+  if (size->width > 1)
+    g_assert_cmpfloat (size->width, >=, left_width + right_width);
+  if (size->height > 1)
+  g_assert_cmpfloat (size->height, >=, top_height + bottom_height);
+#endif
+}
+
+static inline void
+nine_slice_to_texture_coords (GskGLTextureNineSlice *slices,
+                              int                    texture_width,
+                              int                    texture_height)
+{
+  float fw = texture_width;
+  float fh = texture_height;
+
+  for (guint i = 0; i < 9; i++)
+    {
+      GskGLTextureNineSlice *slice = &slices[i];
+
+      slice->area.x = slice->rect.x / fw;
+      slice->area.y = 1.0 - ((slice->rect.y + slice->rect.height) / fh);
+      slice->area.x2 = ((slice->rect.x + slice->rect.width) / fw);
+      slice->area.y2 = (1.0 - (slice->rect.y / fh));
+
+#ifdef DEBUG_NINE_SLICE
+      g_assert_cmpfloat (slice->area.x, >=, 0);
+      g_assert_cmpfloat (slice->area.x, <=, 1);
+      g_assert_cmpfloat (slice->area.y, >=, 0);
+      g_assert_cmpfloat (slice->area.y, <=, 1);
+      g_assert_cmpfloat (slice->area.x2, >, slice->area.x);
+      g_assert_cmpfloat (slice->area.y2, >, slice->area.y);
+#endif
+    }
+}
+
+static inline void
+nine_slice_grow (GskGLTextureNineSlice *slices,
+                 int                    amount)
+{
+  if (amount == 0)
+    return;
+
+  /* top left */
+  slices[0].rect.x -= amount;
+  slices[0].rect.y -= amount;
+  if (amount > slices[0].rect.width)
+    slices[0].rect.width += amount * 2;
+  else
+    slices[0].rect.width += amount;
+
+  if (amount > slices[0].rect.height)
+    slices[0].rect.height += amount * 2;
+  else
+    slices[0].rect.height += amount;
+
+
+  /* Top center */
+  slices[1].rect.y -= amount;
+  if (amount > slices[1].rect.height)
+    slices[1].rect.height += amount * 2;
+  else
+    slices[1].rect.height += amount;
+
+  /* top right */
+  slices[2].rect.y -= amount;
+  if (amount > slices[2].rect.width)
+    {
+      slices[2].rect.x -= amount;
+      slices[2].rect.width += amount * 2;
+    }
+  else
+    {
+     slices[2].rect.width += amount;
+    }
+
+  if (amount > slices[2].rect.height)
+    slices[2].rect.height += amount * 2;
+  else
+    slices[2].rect.height += amount;
+
+
+
+  slices[3].rect.x -= amount;
+  if (amount > slices[3].rect.width)
+    slices[3].rect.width += amount * 2;
+  else
+    slices[3].rect.width += amount;
+
+  /* Leave center alone */
+
+  if (amount > slices[5].rect.width)
+    {
+      slices[5].rect.x -= amount;
+      slices[5].rect.width += amount * 2;
+    }
+  else
+    {
+      slices[5].rect.width += amount;
+    }
+
+
+  /* Bottom left */
+  slices[6].rect.x -= amount;
+  if (amount > slices[6].rect.width)
+    {
+      slices[6].rect.width += amount * 2;
+    }
+  else
+    {
+      slices[6].rect.width += amount;
+    }
+
+  if (amount > slices[6].rect.height)
+    {
+      slices[6].rect.y -= amount;
+      slices[6].rect.height += amount * 2;
+    }
+  else
+    {
+      slices[6].rect.height += amount;
+    }
+
+
+  /* Bottom center */
+  if (amount > slices[7].rect.height)
+    {
+      slices[7].rect.y -= amount;
+      slices[7].rect.height += amount * 2;
+    }
+  else
+    {
+      slices[7].rect.height += amount;
+    }
+
+  if (amount > slices[8].rect.width)
+    {
+      slices[8].rect.x -= amount;
+      slices[8].rect.width += amount * 2;
+    }
+  else
+    {
+      slices[8].rect.width += amount;
+    }
+
+  if (amount > slices[8].rect.height)
+    {
+      slices[8].rect.y -= amount;
+      slices[8].rect.height += amount * 2;
+    }
+  else
+    {
+      slices[8].rect.height += amount;
+    }
+
+#ifdef DEBUG_NINE_SLICE
+  /* These cannot be relied on in all cases right now, specifically
+   * when viewing data zoomed out.
+   */
+  for (guint i = 0; i < 9; i ++)
+    {
+      g_assert_cmpint (slices[i].rect.x, >=, 0);
+      g_assert_cmpint (slices[i].rect.y, >=, 0);
+      g_assert_cmpint (slices[i].rect.width, >=, 0);
+      g_assert_cmpint (slices[i].rect.height, >=, 0);
+    }
+
+  /* Rows don't overlap */
+  for (guint i = 0; i < 3; i++)
+    {
+      int lhs = slices[i * 3 + 0].rect.x + slices[i * 3 + 0].rect.width;
+      int rhs = slices[i * 3 + 1].rect.x;
+
+      /* Ignore the case where we are scaled out and the
+       * positioning is degenerate, such as from node-editor.
+       */
+      if (rhs > 1)
+        g_assert_cmpint (lhs, <, rhs);
+    }
+#endif
+
+}
+
+G_END_DECLS
+
+#endif /* __NINE_SLICE_PRIVATE_H__ */
diff --git a/gtk/gtktestutils.c b/gtk/gtktestutils.c
index 690931a153..64d4e5a4a1 100644
--- a/gtk/gtktestutils.c
+++ b/gtk/gtktestutils.c
@@ -41,6 +41,7 @@
 #define GTK_COMPILATION
 
 #include <gsk/gl/gskglrenderer.h>
+#include <gsk/next/gskglrenderer.h>
 
 #ifdef GDK_WINDOWING_BROADWAY
 #include <gsk/broadway/gskbroadwayrenderer.h>
diff --git a/testsuite/gsk/meson.build b/testsuite/gsk/meson.build
index 844e76c475..f78a71fc39 100644
--- a/testsuite/gsk/meson.build
+++ b/testsuite/gsk/meson.build
@@ -90,6 +90,7 @@ informative_render_tests = [
 renderers = [
   # name      exclude term
   [ 'opengl', ''    ],
+  [ 'next', ''    ],
   [ 'broadway',  '-3d' ],
   [ 'cairo',  '-3d' ],
 ]


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]