From 2c30dda4d8c106e377833019ee42fdfef101d7af Mon Sep 17 00:00:00 2001
From: Sergey Fedorov <vital.had@gmail.com>
Date: Tue, 5 May 2026 08:29:05 +0800
Subject: [PATCH] Fix gl 2.0

---
 gdk/gdk.c                                     |    1 +
 gdk/gdkdebugprivate.h                         |    1 +
 gdk/gdkglcontext.c                            |  104 +-
 gdk/gdkglcontextprivate.h                     |    5 +-
 gdk/gdkglversionprivate.h                     |   10 +-
 gdk/x11/gdkglcontext-glx.c                    |    3 +-
 gsk/gen-gsk-gresources-xml.py                 |   11 +-
 gsk/gl/gskglattachmentstate.c                 |  116 +
 gsk/gl/gskglattachmentstateprivate.h          |  102 +
 gsk/gl/gskglbuffer.c                          |   69 +
 gsk/gl/gskglbufferprivate.h                   |   80 +
 gsk/gl/gskglcommandqueue.c                    | 1904 +++++++
 gsk/gl/gskglcommandqueueprivate.h             |  474 ++
 gsk/gl/gskglcompiler.c                        |  713 +++
 gsk/gl/gskglcompilerprivate.h                 |   68 +
 gsk/gl/gskgldriver.c                          | 1868 +++++++
 gsk/gl/gskgldriverprivate.h                   |  261 +
 gsk/gl/gskglglyphlibrary.c                    |  437 ++
 gsk/gl/gskglglyphlibraryprivate.h             |  103 +
 gsk/gl/gskgliconlibrary.c                     |  212 +
 gsk/gl/gskgliconlibraryprivate.h              |   58 +
 gsk/gl/gskglprofiler.c                        |  179 +
 gsk/gl/gskglprofilerprivate.h                 |   16 +
 gsk/gl/gskglprogram.c                         |  173 +
 gsk/gl/gskglprogramprivate.h                  |  318 ++
 gsk/gl/gskglprograms.defs                     |  104 +
 gsk/gl/gskglrenderer.c                        |  497 ++
 gsk/gl/gskglrenderer.h                        |   41 +-
 gsk/gl/gskglrendererprivate.h                 |   38 +
 gsk/gl/gskglrenderjob.c                       | 4749 +++++++++++++++++
 gsk/gl/gskglrenderjobprivate.h                |   36 +
 gsk/gl/gskglshadowlibrary.c                   |  256 +
 gsk/gl/gskglshadowlibraryprivate.h            |   42 +
 gsk/gl/gskgltexture.c                         |   95 +
 gsk/gl/gskgltexturelibrary.c                  |  583 ++
 gsk/gl/gskgltexturelibraryprivate.h           |  232 +
 gsk/gl/gskgltextureprivate.h                  |   93 +
 gsk/gl/gskgltypesprivate.h                    |   84 +
 gsk/gl/gskgluniformstate.c                    |  269 +
 gsk/gl/gskgluniformstateprivate.h             |  834 +++
 gsk/gl/inlinearray.h                          |   75 +
 gsk/gl/ninesliceprivate.h                     |  308 ++
 gsk/gl/resources/blend.glsl                   |  314 ++
 gsk/gl/resources/blit.glsl                    |   17 +
 gsk/gl/resources/blur.glsl                    |   59 +
 gsk/gl/resources/border.glsl                  |   53 +
 gsk/gl/resources/color.glsl                   |   20 +
 gsk/gl/resources/color_matrix.glsl            |   27 +
 gsk/gl/resources/coloring.glsl                |   33 +
 gsk/gl/resources/conic_gradient.glsl          |   85 +
 gsk/gl/resources/cross_fade.glsl              |   24 +
 gsk/gl/resources/custom.glsl                  |   25 +
 gsk/gl/resources/external.glsl                |   34 +
 gsk/gl/resources/filled_border.glsl           |   58 +
 gsk/gl/resources/inset_shadow.glsl            |   51 +
 gsk/gl/resources/linear_gradient.glsl         |  107 +
 gsk/gl/resources/mask.glsl                    |   39 +
 gsk/gl/resources/outset_shadow.glsl           |   39 +
 gsk/gl/resources/preamble.fs.glsl             |  173 +
 gsk/gl/resources/preamble.glsl                |  133 +
 gsk/gl/resources/preamble.vs.glsl             |   75 +
 gsk/gl/resources/premultiply.glsl             |   19 +
 gsk/gl/resources/radial_gradient.glsl         |   87 +
 gsk/gl/resources/repeat.glsl                  |   44 +
 gsk/gl/resources/unblurred_outset_shadow.glsl |   47 +
 gsk/gl/stb_rect_pack.c                        |  427 ++
 gsk/gl/stb_rect_pack.h                        |  197 +
 gsk/gpu/gskglframe.c                          |    3 +-
 gsk/gpu/gskgpudownloadop.c                    |    5 +-
 gsk/gsk.h                                     |    1 +
 gsk/gskcairorenderer.c                        |   31 +-
 gsk/gskprofiler.c                             |  525 ++
 gsk/gskprofilerprivate.h                      |   53 +
 gsk/gskrenderer.c                             |   46 +-
 gsk/gskrendererprivate.h                      |    3 +
 gsk/meson.build                               |   44 +
 gtk/gtkglarea.c                               |    7 +-
 gtk/inspector/eventrecording.h                |    1 +
 gtk/inspector/recorder.c                      |    1 +
 gtk/inspector/renderrecording.c               |   21 +
 gtk/inspector/renderrecording.h               |    6 +
 tools/gtk-rendernode-tool-benchmark.c         |    2 +-
 82 files changed, 18532 insertions(+), 26 deletions(-)
 create mode 100644 gsk/gl/gskglattachmentstate.c
 create mode 100644 gsk/gl/gskglattachmentstateprivate.h
 create mode 100644 gsk/gl/gskglbuffer.c
 create mode 100644 gsk/gl/gskglbufferprivate.h
 create mode 100644 gsk/gl/gskglcommandqueue.c
 create mode 100644 gsk/gl/gskglcommandqueueprivate.h
 create mode 100644 gsk/gl/gskglcompiler.c
 create mode 100644 gsk/gl/gskglcompilerprivate.h
 create mode 100644 gsk/gl/gskgldriver.c
 create mode 100644 gsk/gl/gskgldriverprivate.h
 create mode 100644 gsk/gl/gskglglyphlibrary.c
 create mode 100644 gsk/gl/gskglglyphlibraryprivate.h
 create mode 100644 gsk/gl/gskgliconlibrary.c
 create mode 100644 gsk/gl/gskgliconlibraryprivate.h
 create mode 100644 gsk/gl/gskglprofiler.c
 create mode 100644 gsk/gl/gskglprofilerprivate.h
 create mode 100644 gsk/gl/gskglprogram.c
 create mode 100644 gsk/gl/gskglprogramprivate.h
 create mode 100644 gsk/gl/gskglprograms.defs
 create mode 100644 gsk/gl/gskglrenderer.c
 create mode 100644 gsk/gl/gskglrendererprivate.h
 create mode 100644 gsk/gl/gskglrenderjob.c
 create mode 100644 gsk/gl/gskglrenderjobprivate.h
 create mode 100644 gsk/gl/gskglshadowlibrary.c
 create mode 100644 gsk/gl/gskglshadowlibraryprivate.h
 create mode 100644 gsk/gl/gskgltexture.c
 create mode 100644 gsk/gl/gskgltexturelibrary.c
 create mode 100644 gsk/gl/gskgltexturelibraryprivate.h
 create mode 100644 gsk/gl/gskgltextureprivate.h
 create mode 100644 gsk/gl/gskgltypesprivate.h
 create mode 100644 gsk/gl/gskgluniformstate.c
 create mode 100644 gsk/gl/gskgluniformstateprivate.h
 create mode 100644 gsk/gl/inlinearray.h
 create mode 100644 gsk/gl/ninesliceprivate.h
 create mode 100644 gsk/gl/resources/blend.glsl
 create mode 100644 gsk/gl/resources/blit.glsl
 create mode 100644 gsk/gl/resources/blur.glsl
 create mode 100644 gsk/gl/resources/border.glsl
 create mode 100644 gsk/gl/resources/color.glsl
 create mode 100644 gsk/gl/resources/color_matrix.glsl
 create mode 100644 gsk/gl/resources/coloring.glsl
 create mode 100644 gsk/gl/resources/conic_gradient.glsl
 create mode 100644 gsk/gl/resources/cross_fade.glsl
 create mode 100644 gsk/gl/resources/custom.glsl
 create mode 100644 gsk/gl/resources/external.glsl
 create mode 100644 gsk/gl/resources/filled_border.glsl
 create mode 100644 gsk/gl/resources/inset_shadow.glsl
 create mode 100644 gsk/gl/resources/linear_gradient.glsl
 create mode 100644 gsk/gl/resources/mask.glsl
 create mode 100644 gsk/gl/resources/outset_shadow.glsl
 create mode 100644 gsk/gl/resources/preamble.fs.glsl
 create mode 100644 gsk/gl/resources/preamble.glsl
 create mode 100644 gsk/gl/resources/preamble.vs.glsl
 create mode 100644 gsk/gl/resources/premultiply.glsl
 create mode 100644 gsk/gl/resources/radial_gradient.glsl
 create mode 100644 gsk/gl/resources/repeat.glsl
 create mode 100644 gsk/gl/resources/unblurred_outset_shadow.glsl
 create mode 100644 gsk/gl/stb_rect_pack.c
 create mode 100644 gsk/gl/stb_rect_pack.h
 create mode 100644 gsk/gskprofiler.c
 create mode 100644 gsk/gskprofilerprivate.h

diff --git gdk/gdk.c gdk/gdk.c
index c84edac346..2c1b68893a 100644
--- gdk/gdk.c
+++ gdk/gdk.c
@@ -148,6 +148,7 @@ static const GdkDebugKey gdk_debug_keys[] = {
   { "no-vsync",        GDK_DEBUG_NO_VSYNC, "Repaint instantly (uses 100% CPU with animations)" },
   { "color-mgmt",      GDK_DEBUG_COLOR_MANAGEMENT, "Enable color management" },
   { "dcomp",           GDK_DEBUG_DCOMP, "Enable Direct Composition (Windows)" },
+  { "gl-no-fractional",GDK_DEBUG_GL_NO_FRACTIONAL, "Disable fractional scaling for OpenGL" },
 };
 
 static const GdkDebugKey gdk_feature_keys[] = {
diff --git gdk/gdkdebugprivate.h gdk/gdkdebugprivate.h
index eb211d5aec..4bc36459c3 100644
--- gdk/gdkdebugprivate.h
+++ gdk/gdkdebugprivate.h
@@ -51,6 +51,7 @@ typedef enum {
   GDK_DEBUG_NO_VSYNC           = 1 << 23,
   GDK_DEBUG_COLOR_MANAGEMENT   = 1 << 24,
   GDK_DEBUG_DCOMP              = 1 << 25,
+  GDK_DEBUG_GL_NO_FRACTIONAL   = 1 << 26,
 } GdkDebugFlags;
 
 typedef enum {
diff --git gdk/gdkglcontext.c gdk/gdkglcontext.c
index ae06a06978..f55f52e44a 100644
--- gdk/gdkglcontext.c
+++ gdk/gdkglcontext.c
@@ -111,6 +111,7 @@ const GdkDebugKey gdk_gl_feature_keys[] = {
   { "external-objects", GDK_GL_FEATURE_EXTERNAL_OBJECTS, "GL_EXT_memory_object and GL_EXT_semaphore"},
   { "external-objects-win32", GDK_GL_FEATURE_EXTERNAL_OBJECTS_WIN32, "GL_EXT_memory_object_win32 and GL_EXT_semaphore_win32" },
   { "blend-func-extended", GDK_GL_FEATURE_BLEND_FUNC_EXTENDED, "GL_EXT_blend_func_extended" },
+  { "sync", GDK_GL_FEATURE_SYNC, "GL_ARB_sync" },
 };
 
 typedef struct _GdkGLContextPrivate GdkGLContextPrivate;
@@ -595,6 +596,23 @@ gdk_gl_context_real_make_current (GdkGLContext *context,
 #endif
 }
 
+double
+gdk_gl_context_get_scale (GdkGLContext *self)
+{
+  GdkDisplay *display;
+  GdkSurface *surface;
+  double scale;
+
+  surface = gdk_draw_context_get_surface (GDK_DRAW_CONTEXT (self));
+  scale = gdk_surface_get_scale (surface);
+
+  display = gdk_gl_context_get_display (self);
+  if (gdk_display_get_debug_flags (display) & GDK_DEBUG_GL_NO_FRACTIONAL)
+    scale = ceil (scale);
+
+  return scale;
+}
+
 #ifdef HAVE_EGL
 void
 gdk_gl_context_set_egl_native_window (GdkGLContext *self,
@@ -1681,10 +1699,78 @@ gdk_gl_context_init_memory_flags (GdkGLContext *self)
 
   if (!gdk_gl_context_get_use_es (self))
     {
+      gboolean has_rg, has_float, has_half_float, has_swizzle;
+
       for (i = 0; i < G_N_ELEMENTS (priv->memory_flags); i++)
         {
           priv->memory_flags[i] = GDK_GL_FORMAT_USABLE | GDK_GL_FORMAT_RENDERABLE | GDK_GL_FORMAT_FILTERABLE;
         }
+
+      /* Everything the formats below need is core in GL 3.3, which used to be
+       * the minimum version GTK would accept. Only the older contexts we now
+       * allow have to have their formats restricted.
+       */
+      has_rg = gdk_gl_version_greater_equal (&priv->gl_version, &GDK_GL_VERSION_INIT (3, 0)) ||
+               epoxy_has_gl_extension ("GL_ARB_texture_rg");
+      has_float = gdk_gl_version_greater_equal (&priv->gl_version, &GDK_GL_VERSION_INIT (3, 0)) ||
+                  epoxy_has_gl_extension ("GL_ARB_texture_float");
+      has_half_float = has_float &&
+                       (gdk_gl_version_greater_equal (&priv->gl_version, &GDK_GL_VERSION_INIT (3, 0)) ||
+                        epoxy_has_gl_extension ("GL_ARB_half_float_pixel"));
+      has_swizzle = gdk_gl_version_greater_equal (&priv->gl_version, &GDK_GL_VERSION_INIT (3, 3)) ||
+                    epoxy_has_gl_extension ("GL_ARB_texture_swizzle");
+
+      if (has_rg && has_float && has_half_float && has_swizzle)
+        return;
+
+      for (i = 0; i < G_N_ELEMENTS (priv->memory_flags); i++)
+        {
+          GLint internal_format, internal_srgb_format;
+          GLenum format, type;
+          GdkSwizzle swizzle;
+          gboolean supported;
+
+          if (!gdk_memory_format_gl_format ((GdkMemoryFormat) i, 0, FALSE,
+                                            &internal_format, &internal_srgb_format,
+                                            &format, &type, &swizzle))
+            continue;
+
+          switch (internal_format)
+            {
+            case GL_R8:
+            case GL_RG8:
+            case GL_R16:
+            case GL_RG16:
+              supported = has_rg;
+              break;
+
+            case GL_R16F:
+            case GL_RGB16F:
+            case GL_RGBA16F:
+              supported = has_half_float;
+              break;
+
+            case GL_R32F:
+            case GL_RGB32F:
+            case GL_RGBA32F:
+              supported = has_float;
+              break;
+
+            default:
+              supported = TRUE;
+              break;
+            }
+
+          /* Without GL_ARB_texture_swizzle we cannot make the channels end up
+           * where the shaders expect them, so those formats are unusable.
+           */
+          if (!gdk_swizzle_is_identity (swizzle) && !has_swizzle)
+            supported = FALSE;
+
+          if (!supported)
+            priv->memory_flags[i] = 0;
+        }
+
       return;
     }
 
@@ -1700,6 +1786,9 @@ gdk_gl_context_init_memory_flags (GdkGLContext *self)
   priv->memory_flags[GDK_MEMORY_G8A8] = GDK_GL_FORMAT_USABLE | GDK_GL_FORMAT_FILTERABLE;
 #endif
 
+  if (!gdk_gl_version_greater_equal (&priv->gl_version, &GDK_GL_VERSION_INIT (3, 0)))
+    return;
+
   /* GLES 3.0.6 spec, table 3.13 */
   priv->memory_flags[GDK_MEMORY_G8] |= GDK_GL_FORMAT_USABLE | GDK_GL_FORMAT_RENDERABLE | GDK_GL_FORMAT_FILTERABLE;
   priv->memory_flags[GDK_MEMORY_A8] |= GDK_GL_FORMAT_USABLE | GDK_GL_FORMAT_RENDERABLE | GDK_GL_FORMAT_FILTERABLE;
@@ -1832,6 +1921,11 @@ gdk_gl_context_check_features (GdkGLContext *context)
       epoxy_has_gl_extension ("GL_EXT_blend_func_extended"))
     features |= GDK_GL_FEATURE_BLEND_FUNC_EXTENDED;
 
+  if (gdk_gl_context_check_version (context, "3.2", "3.0") ||
+      epoxy_has_gl_extension ("GL_ARB_sync") ||
+      epoxy_has_gl_extension ("GL_APPLE_sync"))
+    features |= GDK_GL_FEATURE_SYNC;
+
   return features;
 }
 
@@ -2082,8 +2176,12 @@ gdk_gl_context_get_glsl_version_string (GdkGLContext *self)
         return "#version 150";
       else if (gdk_gl_version_greater_equal (&priv->gl_version, &GDK_GL_VERSION_INIT (3, 1)))
         return "#version 140";
-      else
+      else if (gdk_gl_version_greater_equal (&priv->gl_version, &GDK_GL_VERSION_INIT (3, 0)))
         return "#version 130";
+      else if (gdk_gl_version_greater_equal (&priv->gl_version, &GDK_GL_VERSION_INIT (2, 1)))
+        return "#version 120";
+      else
+        return "#version 110";
     }
   else if (priv->api == GDK_GL_API_GLES)
     {
@@ -2208,7 +2306,9 @@ gdk_gl_context_has_vertex_arrays (GdkGLContext *self)
   switch (priv->api)
     {
     case GDK_GL_API_GL:
-      return TRUE;
+      return gdk_gl_version_greater_equal (&priv->gl_version, &GDK_GL_VERSION_INIT (3, 0)) ||
+             epoxy_has_gl_extension ("GL_ARB_vertex_array_object") ||
+             epoxy_has_gl_extension ("GL_APPLE_vertex_array_object");
 
     case GDK_GL_API_GLES:
       return gdk_gl_version_get_major (&priv->gl_version) >= 3;
diff --git gdk/gdkglcontextprivate.h gdk/gdkglcontextprivate.h
index 5411579006..7d42f5377d 100644
--- gdk/gdkglcontextprivate.h
+++ gdk/gdkglcontextprivate.h
@@ -35,9 +35,10 @@ typedef enum {
   GDK_GL_FEATURE_EXTERNAL_OBJECTS           = 1 << 3,
   GDK_GL_FEATURE_EXTERNAL_OBJECTS_WIN32     = 1 << 4,
   GDK_GL_FEATURE_BLEND_FUNC_EXTENDED        = 1 << 5,
+  GDK_GL_FEATURE_SYNC                       = 1 << 6,
 } GdkGLFeatures;
 
-#define GDK_GL_N_FEATURES 3
+#define GDK_GL_N_FEATURES 7
 
 extern const GdkDebugKey gdk_gl_feature_keys[];
 
@@ -183,6 +184,8 @@ gboolean                gdk_gl_context_use_es_bgra              (GdkGLContext
 
 gboolean                gdk_gl_context_has_vertex_arrays        (GdkGLContext    *self) G_GNUC_PURE;
 
+double                  gdk_gl_context_get_scale                (GdkGLContext    *self);
+
 void                    gdk_gl_context_download                 (GdkGLContext    *self,
                                                                  GLuint           tex_id,
                                                                  GdkMemoryFormat  tex_format,
diff --git gdk/gdkglversionprivate.h gdk/gdkglversionprivate.h
index d9475082ae..c0c39400a3 100644
--- gdk/gdkglversionprivate.h
+++ gdk/gdkglversionprivate.h
@@ -34,8 +34,8 @@ G_BEGIN_DECLS
 /* Minimum OpenGL versions supported by GTK.
  * Backends should make sure to never create a context of a previous version.
  */
-#define GDK_GL_MIN_GL_VERSION GDK_GL_VERSION_INIT (3, 3)
-#define GDK_GL_MIN_GLES_VERSION GDK_GL_VERSION_INIT (3, 0)
+#define GDK_GL_MIN_GL_VERSION GDK_GL_VERSION_INIT (2, 0)
+#define GDK_GL_MIN_GLES_VERSION GDK_GL_VERSION_INIT (2, 0)
 
 typedef struct _GdkGLVersion GdkGLVersion;
 
@@ -56,6 +56,11 @@ static const GdkGLVersion supported_gl_versions[] = {
   GDK_GL_VERSION_INIT (4, 1),
   GDK_GL_VERSION_INIT (4, 0),
   GDK_GL_VERSION_INIT (3, 3),
+  GDK_GL_VERSION_INIT (3, 2),
+  GDK_GL_VERSION_INIT (3, 1),
+  GDK_GL_VERSION_INIT (3, 0),
+  GDK_GL_VERSION_INIT (2, 1),
+  GDK_GL_VERSION_INIT (2, 0),
 
   GDK_GL_VERSION_INIT (0, 0)
 };
@@ -64,6 +69,7 @@ static const GdkGLVersion supported_gles_versions[] = {
   GDK_GL_VERSION_INIT (3, 2),
   GDK_GL_VERSION_INIT (3, 1),
   GDK_GL_VERSION_INIT (3, 0),
+  GDK_GL_VERSION_INIT (2, 0),
 
   GDK_GL_VERSION_INIT (0, 0)
 };
diff --git gdk/x11/gdkglcontext-glx.c gdk/x11/gdkglcontext-glx.c
index f7a028bf50..e6cb06dead 100644
--- gdk/x11/gdkglcontext-glx.c
+++ gdk/x11/gdkglcontext-glx.c
@@ -187,7 +187,8 @@ gdk_x11_gl_context_glx_end_frame (GdkDrawContext *draw_context,
   gdk_x11_surface_pre_damage (surface);
 
 #ifdef HAVE_XDAMAGE
-  if (self->xdamage != 0 && _gdk_x11_surface_syncs_frames (surface))
+  if (self->xdamage != 0 && _gdk_x11_surface_syncs_frames (surface) &&
+      gdk_gl_context_has_feature (context, GDK_GL_FEATURE_SYNC))
     {
       g_assert (self->frame_fence == 0);
 
diff --git gsk/gen-gsk-gresources-xml.py gsk/gen-gsk-gresources-xml.py
index 8a18126283..7c17ac249b 100644
--- gsk/gen-gsk-gresources-xml.py
+++ gsk/gen-gsk-gresources-xml.py
@@ -21,11 +21,15 @@ def replace_if_changed(new, old):
     os.remove(new)
 
 gl_source_shaders = []
+gl2_source_shaders = []
 vulkan_compiled_shaders = []
 
 for f in sys.argv[2:]:
   if f.endswith('.glsl'):
-    gl_source_shaders.append(f)
+    if f.find('gsk/gpu') > -1:
+      gl_source_shaders.append(f)
+    else:
+      gl2_source_shaders.append(f)
   elif f.endswith('.spv'):
     vulkan_compiled_shaders.append(f)
   else:
@@ -37,6 +41,11 @@ xml = '''<?xml version='1.0' encoding='UTF-8'?>
 
 '''
 
+for f in gl2_source_shaders:
+  xml += '    <file alias=\'gl/{0}\'>gl/resources/{0}</file>\n'.format(os.path.basename(f))
+
+xml += '\n'
+
 for f in gl_source_shaders:
   xml += '    <file alias=\'shaders/gl/{0}\'>gpu/shaders/{0}</file>\n'.format(os.path.basename(f))
 
diff --git gsk/gl/gskglattachmentstate.c gsk/gl/gskglattachmentstate.c
new file mode 100644
index 0000000000..3765e95f79
--- /dev/null
+++ gsk/gl/gskglattachmentstate.c
@@ -0,0 +1,116 @@
+/* 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;
+      self->textures[i].sampler = sampler_index (GL_LINEAR, GL_LINEAR);
+    }
+
+  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,
+                                      GLint                 min_filter,
+                                      GLint                 mag_filter)
+{
+  GskGLBindTexture *attach;
+  unsigned int sampler;
+
+  g_assert (self != NULL);
+  g_assert (target == GL_TEXTURE_1D ||
+            target == GL_TEXTURE_2D ||
+            target == GL_TEXTURE_3D ||
+            target == GL_TEXTURE_EXTERNAL_OES);
+  g_assert (texture >= GL_TEXTURE0 && texture <= GL_TEXTURE16);
+  g_assert (texture - GL_TEXTURE0 < G_N_ELEMENTS (self->textures));
+
+  attach = &self->textures[texture - GL_TEXTURE0];
+
+  sampler = sampler_index (min_filter, mag_filter);
+
+  if (attach->target != target || attach->texture != texture || attach->id != id ||
+      attach->sampler != sampler)
+    {
+      attach->target = target;
+      attach->texture = texture;
+      attach->id = id;
+      attach->initial = FALSE;
+      attach->sampler = sampler;
+
+      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 gsk/gl/gskglattachmentstateprivate.h gsk/gl/gskglattachmentstateprivate.h
new file mode 100644
index 0000000000..e2ecf1a539
--- /dev/null
+++ gsk/gl/gskglattachmentstateprivate.h
@@ -0,0 +1,102 @@
+/* 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
+ */
+
+#pragma once
+
+#include "gskgltypesprivate.h"
+
+G_BEGIN_DECLS
+
+typedef struct _GskGLAttachmentState GskGLAttachmentState;
+typedef struct _GskGLBindFramebuffer GskGLBindFramebuffer;
+typedef struct _GskGLBindTexture     GskGLBindTexture;
+
+#define GSK_GL_N_FILTERS 3
+#define SAMPLER_EXTERNAL 9
+
+G_STATIC_ASSERT (SAMPLER_EXTERNAL >= GSK_GL_N_FILTERS * GSK_GL_N_FILTERS);
+
+static inline guint
+filter_index (GLint filter)
+{
+  switch (filter)
+  {
+    case GL_LINEAR:
+      return 0;
+    case GL_NEAREST:
+      return 1;
+    case GL_LINEAR_MIPMAP_LINEAR:
+      return 2;
+    default:
+      g_assert_not_reached ();
+  }
+}
+
+static inline guint
+sampler_index (GLint min_filter,
+               GLint mag_filter)
+{
+  return filter_index (min_filter) * GSK_GL_N_FILTERS + filter_index (mag_filter);
+}
+
+struct _GskGLBindTexture
+{
+  guint changed : 1;
+  guint initial : 1;
+  GLenum target : 26;
+  guint sampler : 4;
+  GLenum texture;
+  guint id;
+};
+
+G_STATIC_ASSERT (sizeof (GskGLBindTexture) == 12);
+
+struct _GskGLBindFramebuffer
+{
+  guint changed : 1;
+  guint id : 31;
+};
+
+G_STATIC_ASSERT (sizeof (GskGLBindFramebuffer) == 4);
+
+/* Increase if shaders add more textures */
+#define GSK_GL_MAX_TEXTURES_PER_PROGRAM 4
+
+struct _GskGLAttachmentState
+{
+  GskGLBindFramebuffer fbo;
+  GskGLBindTexture textures[GSK_GL_MAX_TEXTURES_PER_PROGRAM];
+  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,
+                                                                GLint                  min_filter,
+                                                                GLint                  mag_filter);
+void                  gsk_gl_attachment_state_bind_framebuffer (GskGLAttachmentState *self,
+                                                                guint                  id);
+
+G_END_DECLS
+
diff --git gsk/gl/gskglbuffer.c gsk/gl/gskglbuffer.c
new file mode 100644
index 0000000000..63aabd1dc3
--- /dev/null
+++ gsk/gl/gskglbuffer.c
@@ -0,0 +1,69 @@
+/* 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 <string.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);
+
+  /* Default to 2 pages, power-of-two growth from there */
+  self->buffer_len = 4096 * 2;
+  self->buffer = g_malloc (self->buffer_len);
+  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 gsk/gl/gskglbufferprivate.h gsk/gl/gskglbufferprivate.h
new file mode 100644
index 0000000000..fa4e65cc37
--- /dev/null
+++ gsk/gl/gskglbufferprivate.h
@@ -0,0 +1,80 @@
+/* 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
+ */
+
+#pragma once
+
+#include "gskgltypesprivate.h"
+
+G_BEGIN_DECLS
+
+typedef struct _GskGLBuffer
+{
+  guint8 *buffer;
+  gsize   buffer_pos;
+  gsize   buffer_len;
+  guint   count;
+  GLenum  target;
+  gsize   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)
+    {
+      while (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
+
diff --git gsk/gl/gskglcommandqueue.c gsk/gl/gskglcommandqueue.c
new file mode 100644
index 0000000000..f96fe6108d
--- /dev/null
+++ gsk/gl/gskglcommandqueue.c
@@ -0,0 +1,1904 @@
+/* gskglcommandqueue.c
+ *
+ * Copyright 2017 Timm Bäder <mail@baedert.org>
+ * Copyright 2018 Matthias Clasen <mclasen@redhat.com>
+ * Copyright 2018 Alexander Larsson <alexl@redhat.com>
+ * Copyright 2020 Christian Hergert <chergert@redhat.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "config.h"
+
+#include <string.h>
+
+#include <gdk/gdkglcontextprivate.h>
+#include <gdk/gdkmemoryformatprivate.h>
+#include <gdk/gdkprofilerprivate.h>
+#include <gdk/gdktexturedownloaderprivate.h>
+#include <gsk/gskdebugprivate.h>
+#include <gsk/gskroundedrectprivate.h>
+
+#include "fp16private.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)
+
+#if 0
+G_GNUC_UNUSED static inline void
+print_uniform (GskGLUniformFormat format,
+               guint              array_count,
+               gconstpointer      valueptr)
+{
+  const union {
+    graphene_matrix_t matrix[0];
+    GskRoundedRect rounded_rect[0];
+    float fval[0];
+    int ival[0];
+    guint uval[0];
+  } *data = valueptr;
+
+  switch (format)
+    {
+    case GSK_GL_UNIFORM_FORMAT_1F:
+      g_printerr ("1f<%f>", data->fval[0]);
+      break;
+
+    case GSK_GL_UNIFORM_FORMAT_2F:
+      g_printerr ("2f<%f,%f>", data->fval[0], data->fval[1]);
+      break;
+
+    case GSK_GL_UNIFORM_FORMAT_3F:
+      g_printerr ("3f<%f,%f,%f>", data->fval[0], data->fval[1], data->fval[2]);
+      break;
+
+    case GSK_GL_UNIFORM_FORMAT_4F:
+      g_printerr ("4f<%f,%f,%f,%f>", data->fval[0], data->fval[1], data->fval[2], data->fval[3]);
+      break;
+
+    case GSK_GL_UNIFORM_FORMAT_1I:
+    case GSK_GL_UNIFORM_FORMAT_TEXTURE:
+      g_printerr ("1i<%d>", data->ival[0]);
+      break;
+
+    case GSK_GL_UNIFORM_FORMAT_1UI:
+      g_printerr ("1ui<%u>", data->uval[0]);
+      break;
+
+    case GSK_GL_UNIFORM_FORMAT_COLOR: {
+      char *str = gdk_rgba_to_string (valueptr);
+      g_printerr ("%s", str);
+      g_free (str);
+      break;
+    }
+
+    case GSK_GL_UNIFORM_FORMAT_ROUNDED_RECT: {
+      char *str = gsk_rounded_rect_to_string (valueptr);
+      g_printerr ("%s", str);
+      g_free (str);
+      break;
+    }
+
+    case GSK_GL_UNIFORM_FORMAT_MATRIX: {
+      float mat[16];
+      graphene_matrix_to_float (&data->matrix[0], mat);
+      g_printerr ("matrix<");
+      for (guint i = 0; i < G_N_ELEMENTS (mat)-1; i++)
+        g_printerr ("%f,", mat[i]);
+      g_printerr ("%f>", mat[G_N_ELEMENTS (mat)-1]);
+      break;
+    }
+
+    case GSK_GL_UNIFORM_FORMAT_1FV:
+    case GSK_GL_UNIFORM_FORMAT_2FV:
+    case GSK_GL_UNIFORM_FORMAT_3FV:
+    case GSK_GL_UNIFORM_FORMAT_4FV:
+      /* non-V variants are -4 from V variants */
+      format -= 4;
+      g_printerr ("[");
+      for (guint i = 0; i < array_count; i++)
+        {
+          print_uniform (format, 0, valueptr);
+          if (i + 1 != array_count)
+            g_printerr (",");
+          valueptr = ((guint8*)valueptr + gsk_gl_uniform_format_size (format));
+        }
+      g_printerr ("]");
+      break;
+
+    case GSK_GL_UNIFORM_FORMAT_2I:
+      g_printerr ("2i<%d,%d>", data->ival[0], data->ival[1]);
+      break;
+
+    case GSK_GL_UNIFORM_FORMAT_3I:
+      g_printerr ("3i<%d,%d,%d>", data->ival[0], data->ival[1], data->ival[2]);
+      break;
+
+    case GSK_GL_UNIFORM_FORMAT_4I:
+      g_printerr ("3i<%d,%d,%d,%d>", data->ival[0], data->ival[1], data->ival[2], data->ival[3]);
+      break;
+
+    case GSK_GL_UNIFORM_FORMAT_LAST:
+    default:
+      g_assert_not_reached ();
+    }
+}
+
+G_GNUC_UNUSED static inline void
+gsk_gl_command_queue_print_batch (GskGLCommandQueue       *self,
+                                  const GskGLCommandBatch *batch)
+{
+  static const char *command_kinds[] = { "Clear", "Draw", };
+  guint framebuffer_id;
+
+  g_assert (GSK_IS_GL_COMMAND_QUEUE (self));
+  g_assert (batch != NULL);
+
+  if (batch->any.kind == GSK_GL_COMMAND_KIND_CLEAR)
+    framebuffer_id = batch->clear.framebuffer;
+  else if (batch->any.kind == GSK_GL_COMMAND_KIND_DRAW)
+    framebuffer_id = batch->draw.framebuffer;
+  else
+    return;
+
+  g_printerr ("Batch {\n");
+  g_printerr ("         Kind: %s\n", command_kinds[batch->any.kind]);
+  g_printerr ("     Viewport: %dx%d\n", batch->any.viewport.width, batch->any.viewport.height);
+  g_printerr ("  Framebuffer: %d\n", framebuffer_id);
+
+  if (batch->any.kind == GSK_GL_COMMAND_KIND_DRAW)
+    {
+      g_printerr ("      Program: %d\n", batch->any.program);
+      g_printerr ("     Vertices: %d\n", batch->draw.vbo_count);
+
+      for (guint i = 0; i < batch->draw.bind_count; i++)
+        {
+          const GskGLCommandBind *bind = &self->batch_binds.items[batch->draw.bind_offset + i];
+          g_printerr ("      Bind[%d]: %u\n", bind->texture, bind->id);
+        }
+
+      for (guint i = 0; i < batch->draw.uniform_count; i++)
+        {
+          const GskGLCommandUniform *uniform = &self->batch_uniforms.items[batch->draw.uniform_offset + i];
+          g_printerr ("  Uniform[%02d]: ", uniform->location);
+          print_uniform (uniform->info.format,
+                         uniform->info.array_count,
+                         gsk_gl_uniform_state_get_uniform_data (self->uniforms, uniform->info.offset));
+          g_printerr ("\n");
+        }
+    }
+  else if (batch->any.kind == GSK_GL_COMMAND_KIND_CLEAR)
+    {
+      g_printerr ("         Bits: 0x%x\n", batch->clear.bits);
+    }
+
+  g_printerr ("}\n");
+}
+
+G_GNUC_UNUSED static inline void
+gsk_gl_command_queue_capture_png (GskGLCommandQueue *self,
+                                  const char        *filename,
+                                  guint              width,
+                                  guint              height,
+                                  gboolean           flip_y)
+{
+  guint stride;
+  guint8 *data;
+  GBytes *bytes;
+  GdkTexture *texture;
+
+  g_assert (GSK_IS_GL_COMMAND_QUEUE (self));
+  g_assert (filename != NULL);
+
+  stride = cairo_format_stride_for_width (CAIRO_FORMAT_ARGB32, width);
+  data = g_malloc_n (height, stride);
+
+  /* GL_UNSIGNED_INT_8_8_8_8_REV, not GL_UNSIGNED_BYTE: cairo's ARGB32 is
+   * native endian, so a byte-order-dependent read gets it wrong on big endian.
+   */
+  glReadPixels (0, 0, width, height, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, data);
+
+  if (flip_y)
+    {
+      guint8 *flipped = g_malloc_n (height, stride);
+
+      for (guint i = 0; i < height; i++)
+        memcpy (flipped + (height * stride) - ((i + 1) * stride),
+                data + (stride * i),
+                stride);
+
+      g_free (data);
+      data = flipped;
+    }
+
+  bytes = g_bytes_new_take (data, height * stride);
+  texture = gdk_memory_texture_new (width, height, GDK_MEMORY_DEFAULT, bytes, stride);
+  g_bytes_unref (bytes);
+
+  gdk_texture_save_to_png (texture, filename);
+  g_object_unref (texture);
+}
+#endif
+
+static inline gboolean
+will_ignore_batch (GskGLCommandQueue *self)
+{
+  if G_LIKELY (self->batches.len < G_MAXINT16)
+    return FALSE;
+
+  if (!self->have_truncated)
+    {
+      self->have_truncated = TRUE;
+      g_critical ("GL command queue too large, truncating further batches.");
+    }
+
+  return TRUE;
+}
+
+static inline GLint
+min_filter_from_index (guint index)
+{
+  GLint filters[3] = { GL_LINEAR, GL_NEAREST, GL_LINEAR_MIPMAP_LINEAR };
+
+  g_assert (index < GSK_GL_N_FILTERS);
+
+  return filters[index];
+}
+
+static inline GLint
+mag_filter_from_index (guint index)
+{
+  GLint filters[3] = { GL_LINEAR, GL_NEAREST, GL_LINEAR };
+
+  g_assert (index < GSK_GL_N_FILTERS);
+
+  return filters[index];
+}
+
+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;
+          if (state->textures[i].target == GL_TEXTURE_EXTERNAL_OES)
+            bind[count].sampler = SAMPLER_EXTERNAL;
+          else
+            bind[count].sampler = state->textures[i].sampler;
+          count++;
+        }
+    }
+
+  if (count != G_N_ELEMENTS (state->textures))
+    array->len -= G_N_ELEMENTS (state->textures) - count;
+
+  return count;
+}
+
+static inline guint
+snapshot_uniforms (GskGLUniformState    *state,
+                   GskGLUniformProgram  *program,
+                   GskGLCommandUniforms *array)
+{
+  GskGLCommandUniform *uniform = gsk_gl_command_uniforms_append_n (array, program->n_mappings);
+  guint count = 0;
+
+  for (guint i = 0; i < program->n_mappings; i++)
+    {
+      const GskGLUniformMapping *mapping = &program->mappings[i];
+
+      if (!mapping->info.initial && mapping->info.format && mapping->location > -1)
+        {
+          uniform[count].location = mapping->location;
+          uniform[count].info = mapping->info;
+          count++;
+        }
+    }
+
+  if (count != program->n_mappings)
+    array->len -= program->n_mappings - count;
+
+  return count;
+}
+
+static inline gboolean
+snapshots_equal (GskGLCommandQueue *self,
+                 GskGLCommandBatch *first,
+                 GskGLCommandBatch *second)
+{
+  if (first->draw.bind_count != second->draw.bind_count ||
+      first->draw.uniform_count != second->draw.uniform_count)
+    return FALSE;
+
+  for (guint i = 0; i < first->draw.bind_count; i++)
+    {
+      const GskGLCommandBind *fb = &self->batch_binds.items[first->draw.bind_offset+i];
+      const GskGLCommandBind *sb = &self->batch_binds.items[second->draw.bind_offset+i];
+
+      if (fb->id != sb->id || fb->texture != sb->texture)
+        return FALSE;
+    }
+
+  for (guint i = 0; i < first->draw.uniform_count; i++)
+    {
+      const GskGLCommandUniform *fu = &self->batch_uniforms.items[first->draw.uniform_offset+i];
+      const GskGLCommandUniform *su = &self->batch_uniforms.items[second->draw.uniform_offset+i];
+      gconstpointer fdata;
+      gconstpointer sdata;
+      gsize len;
+
+      /* Short circuit if we'd end up with the same memory */
+      if (fu->info.offset == su->info.offset)
+        continue;
+
+      if (fu->info.format != su->info.format ||
+          fu->info.array_count != su->info.array_count)
+        return FALSE;
+
+      fdata = gsk_gl_uniform_state_get_uniform_data (self->uniforms, fu->info.offset);
+      sdata = gsk_gl_uniform_state_get_uniform_data (self->uniforms, su->info.offset);
+
+      switch (fu->info.format)
+        {
+        case GSK_GL_UNIFORM_FORMAT_1F:
+        case GSK_GL_UNIFORM_FORMAT_1FV:
+        case GSK_GL_UNIFORM_FORMAT_1I:
+        case GSK_GL_UNIFORM_FORMAT_TEXTURE:
+        case GSK_GL_UNIFORM_FORMAT_1UI:
+          len = 4;
+          break;
+
+        case GSK_GL_UNIFORM_FORMAT_2F:
+        case GSK_GL_UNIFORM_FORMAT_2FV:
+        case GSK_GL_UNIFORM_FORMAT_2I:
+          len = 8;
+          break;
+
+        case GSK_GL_UNIFORM_FORMAT_3F:
+        case GSK_GL_UNIFORM_FORMAT_3FV:
+        case GSK_GL_UNIFORM_FORMAT_3I:
+          len = 12;
+          break;
+
+        case GSK_GL_UNIFORM_FORMAT_4F:
+        case GSK_GL_UNIFORM_FORMAT_4FV:
+        case GSK_GL_UNIFORM_FORMAT_4I:
+          len = 16;
+          break;
+
+
+        case GSK_GL_UNIFORM_FORMAT_MATRIX:
+          len = sizeof (float) * 16;
+          break;
+
+        case GSK_GL_UNIFORM_FORMAT_ROUNDED_RECT:
+          len = sizeof (float) * 12;
+          break;
+
+        case GSK_GL_UNIFORM_FORMAT_COLOR:
+          len = sizeof (float) * 4;
+          break;
+
+        default:
+          g_assert_not_reached ();
+        }
+
+      len *= fu->info.array_count;
+
+      if (memcmp (fdata, sdata, len) != 0)
+        return FALSE;
+    }
+
+  return TRUE;
+}
+
+static void
+gsk_gl_command_queue_dispose (GObject *object)
+{
+  GskGLCommandQueue *self = (GskGLCommandQueue *)object;
+
+  g_assert (GSK_IS_GL_COMMAND_QUEUE (self));
+
+  g_clear_object (&self->profiler);
+  g_clear_object (&self->gl_profiler);
+  g_clear_object (&self->context);
+  g_clear_pointer (&self->attachments, gsk_gl_attachment_state_unref);
+  g_clear_pointer (&self->uniforms, gsk_gl_uniform_state_unref);
+
+  if (self->has_samplers)
+    glDeleteSamplers (G_N_ELEMENTS (self->samplers), self->samplers);
+
+  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_syncs_clear (&self->syncs);
+
+  gsk_gl_buffer_destroy (&self->vertices);
+
+  g_clear_pointer (&self->float_vertices, g_free);
+  self->float_vertices_len = 0;
+
+  G_OBJECT_CLASS (gsk_gl_command_queue_parent_class)->dispose (object);
+}
+
+static void
+gsk_gl_command_queue_class_init (GskGLCommandQueueClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->dispose = gsk_gl_command_queue_dispose;
+}
+
+static void
+gsk_gl_command_queue_init (GskGLCommandQueue *self)
+{
+  self->max_texture_size = -1;
+
+  gsk_gl_command_batches_init (&self->batches, 128);
+  gsk_gl_command_binds_init (&self->batch_binds, 1024);
+  gsk_gl_command_uniforms_init (&self->batch_uniforms, 2048);
+  gsk_gl_syncs_init (&self->syncs, 10);
+
+  gsk_gl_buffer_init (&self->vertices, GL_ARRAY_BUFFER, sizeof (GskGLDrawVertex));
+}
+
+GskGLCommandQueue *
+gsk_gl_command_queue_new (GdkGLContext      *context,
+                          GskGLUniformState *uniforms)
+{
+  GskGLCommandQueue *self;
+  guint i;
+
+  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);
+  if (g_getenv ("GSK_MAX_TEXTURE_SIZE"))
+    {
+      int max_texture_size = atoi (g_getenv ("GSK_MAX_TEXTURE_SIZE"));
+      if (max_texture_size == 0)
+        {
+          g_warning ("Failed to parse %s", "GSK_MAX_TEXTURE_SIZE");
+        }
+      else
+        {
+          max_texture_size = MAX (max_texture_size, 512);
+          GSK_DEBUG (RENDERER,
+                     "Limiting texture size in the GL renderer to %d (via %s)",
+                     max_texture_size, "GSK_MAX_TEXTURE_SIZE");
+          self->max_texture_size = MIN (self->max_texture_size, max_texture_size);
+        }
+    }
+
+  /* Sampler objects are core in GLES 3.0. On desktop they are core in GL 3.3,
+   * but only trust that if the driver also does GLSL 1.30, since genuine GL
+   * 3.3 requires both: drivers that claim 3.3 (or the extension) but top out
+   * at GLSL 1.20 don't actually export glGenSamplers.
+   */
+  if (gdk_gl_context_get_use_es (context))
+    self->has_samplers = gdk_gl_context_check_version (context, NULL, "3.0");
+  else
+    self->has_samplers = gsk_gl_has_glsl_version (1, 30) &&
+                         (gdk_gl_context_check_version (context, "3.3", NULL) ||
+                          epoxy_has_gl_extension ("GL_ARB_sampler_objects"));
+
+  /* Texture swizzling is GL 3.3 and GLES 3.0, not GL 3.0 */
+  self->can_swizzle = gdk_gl_context_check_version (context, "3.3", "3.0") ||
+                      epoxy_has_gl_extension ("GL_ARB_texture_swizzle");
+
+  /* Half float vertex attributes are GL 3.0 and GLES 3.0. GL_HALF_FLOAT and
+   * GL_HALF_FLOAT_NV share a token value, so the NV extension works as-is;
+   * GL_HALF_FLOAT_OES does not, so GLES 2 always takes the fallback.
+   */
+  self->has_half_float_vertex = gdk_gl_context_check_version (context, "3.0", "3.0") ||
+                                epoxy_has_gl_extension ("GL_ARB_half_float_vertex") ||
+                                epoxy_has_gl_extension ("GL_NV_half_float");
+
+  self->has_sync = gdk_gl_context_has_feature (context, GDK_GL_FEATURE_SYNC);
+
+  if (!self->has_half_float_vertex)
+    GSK_DEBUG (RENDERER,
+               "No half float vertex support, expanding vertices on the CPU");
+
+  /* create the samplers */
+  if (self->has_samplers)
+    {
+      glGenSamplers (G_N_ELEMENTS (self->samplers), self->samplers);
+      for (i = 0; i < G_N_ELEMENTS (self->samplers); i++)
+        {
+          glSamplerParameteri (self->samplers[i], GL_TEXTURE_MIN_FILTER, min_filter_from_index (i / GSK_GL_N_FILTERS));
+          glSamplerParameteri (self->samplers[i], GL_TEXTURE_MAG_FILTER, mag_filter_from_index (i % GSK_GL_N_FILTERS));
+          glSamplerParameteri (self->samplers[i], GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+          glSamplerParameteri (self->samplers[i], GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+        }
+    }
+
+  return g_steal_pointer (&self);
+}
+
+/* Uploads the vertex buffer and points the vertex attributes at it.
+ *
+ * Drivers without half float vertex support get the vertices expanded into
+ * GskGLDrawVertexFloat first; that costs a pass over the vertices and twice
+ * the buffer memory, but keeps the fast path untouched.
+ */
+static GLuint
+gsk_gl_command_queue_submit_vertices (GskGLCommandQueue *self)
+{
+  GskGLBuffer *buffer = &self->vertices;
+  const GskGLDrawVertex *src;
+  gsize n_vertices;
+  GLuint id;
+
+  g_assert (GSK_IS_GL_COMMAND_QUEUE (self));
+
+  if G_LIKELY (self->has_half_float_vertex)
+    {
+      id = gsk_gl_buffer_submit (buffer);
+
+      /* 0 = position location */
+      glEnableVertexAttribArray (0);
+      glVertexAttribPointer (0, 2, GL_FLOAT, GL_FALSE,
+                             sizeof (GskGLDrawVertex),
+                             GSIZE_TO_POINTER ((gsize) G_STRUCT_OFFSET (GskGLDrawVertex, position)));
+
+      /* 1 = texture coord location */
+      glEnableVertexAttribArray (1);
+      glVertexAttribPointer (1, 2, GL_FLOAT, GL_FALSE,
+                             sizeof (GskGLDrawVertex),
+                             GSIZE_TO_POINTER ((gsize) G_STRUCT_OFFSET (GskGLDrawVertex, uv)));
+
+      /* 2 = color location */
+      glEnableVertexAttribArray (2);
+      glVertexAttribPointer (2, 4, GL_HALF_FLOAT, GL_FALSE,
+                             sizeof (GskGLDrawVertex),
+                             GSIZE_TO_POINTER ((gsize) G_STRUCT_OFFSET (GskGLDrawVertex, color)));
+
+      /* 3 = color2 location */
+      glEnableVertexAttribArray (3);
+      glVertexAttribPointer (3, 4, GL_HALF_FLOAT, GL_FALSE,
+                             sizeof (GskGLDrawVertex),
+                             GSIZE_TO_POINTER ((gsize) G_STRUCT_OFFSET (GskGLDrawVertex, color2)));
+
+      return id;
+    }
+
+  n_vertices = buffer->buffer_pos / sizeof (GskGLDrawVertex);
+
+  if (self->float_vertices_len < n_vertices)
+    {
+      self->float_vertices_len = n_vertices;
+      self->float_vertices = g_realloc_n (self->float_vertices,
+                                          self->float_vertices_len,
+                                          sizeof (GskGLDrawVertexFloat));
+    }
+
+  src = (const GskGLDrawVertex *) buffer->buffer;
+
+  for (gsize i = 0; i < n_vertices; i++)
+    {
+      GskGLDrawVertexFloat *dst = &self->float_vertices[i];
+
+      dst->position[0] = src[i].position[0];
+      dst->position[1] = src[i].position[1];
+      dst->uv[0] = src[i].uv[0];
+      dst->uv[1] = src[i].uv[1];
+      half_to_float4 (src[i].color, dst->color);
+      half_to_float4 (src[i].color2, dst->color2);
+    }
+
+  glGenBuffers (1, &id);
+  glBindBuffer (GL_ARRAY_BUFFER, id);
+  glBufferData (GL_ARRAY_BUFFER,
+                n_vertices * sizeof (GskGLDrawVertexFloat),
+                self->float_vertices,
+                GL_STATIC_DRAW);
+
+  buffer->buffer_pos = 0;
+  buffer->count = 0;
+
+  glEnableVertexAttribArray (0);
+  glVertexAttribPointer (0, 2, GL_FLOAT, GL_FALSE,
+                         sizeof (GskGLDrawVertexFloat),
+                         GSIZE_TO_POINTER ((gsize) G_STRUCT_OFFSET (GskGLDrawVertexFloat, position)));
+
+  glEnableVertexAttribArray (1);
+  glVertexAttribPointer (1, 2, GL_FLOAT, GL_FALSE,
+                         sizeof (GskGLDrawVertexFloat),
+                         GSIZE_TO_POINTER ((gsize) G_STRUCT_OFFSET (GskGLDrawVertexFloat, uv)));
+
+  glEnableVertexAttribArray (2);
+  glVertexAttribPointer (2, 4, GL_FLOAT, GL_FALSE,
+                         sizeof (GskGLDrawVertexFloat),
+                         GSIZE_TO_POINTER ((gsize) G_STRUCT_OFFSET (GskGLDrawVertexFloat, color)));
+
+  glEnableVertexAttribArray (3);
+  glVertexAttribPointer (3, 4, GL_FLOAT, GL_FALSE,
+                         sizeof (GskGLDrawVertexFloat),
+                         GSIZE_TO_POINTER ((gsize) G_STRUCT_OFFSET (GskGLDrawVertexFloat, color2)));
+
+  return id;
+}
+
+static inline GskGLCommandBatch *
+begin_next_batch (GskGLCommandQueue *self)
+{
+  GskGLCommandBatch *batch;
+
+  g_assert (GSK_IS_GL_COMMAND_QUEUE (self));
+
+  /* GskGLCommandBatch contains an embedded linked list using integers into the
+   * self->batches array. We can't use pointer because the batches could be
+   * realloc()'d at runtime.
+   *
+   * Before we execute the command queue, we sort the batches by framebuffer but
+   * leave the batches in place as we can just tweak the links via prev/next.
+   *
+   * Generally we only traverse forwards, so we could ignore the previous field.
+   * But to optimize the reordering of batches by framebuffer we walk backwards
+   * so we sort by most-recently-seen framebuffer to ensure draws happen in the
+   * proper order.
+   */
+
+  batch = gsk_gl_command_batches_append (&self->batches);
+  batch->any.next_batch_index = -1;
+  batch->any.prev_batch_index = self->tail_batch_index;
+
+  return batch;
+}
+
+static void
+enqueue_batch (GskGLCommandQueue *self)
+{
+  guint index;
+
+  g_assert (GSK_IS_GL_COMMAND_QUEUE (self));
+  g_assert (self->batches.len > 0);
+
+  /* Batches are linked lists but using indexes into the batches array instead
+   * of pointers. This is for two main reasons. First, 16-bit indexes allow us
+   * to store the information in 4 bytes, where as two pointers would take 16
+   * bytes.  Furthermore, we have an array here so pointers would get
+   * invalidated if we realloc()'d (and that can happen from time to time).
+   */
+
+  index = self->batches.len - 1;
+
+  if (self->head_batch_index == -1)
+    self->head_batch_index = index;
+
+  if (self->tail_batch_index != -1)
+    {
+      GskGLCommandBatch *prev = &self->batches.items[self->tail_batch_index];
+
+      prev->any.next_batch_index = index;
+    }
+
+  self->tail_batch_index = index;
+}
+
+static void
+discard_batch (GskGLCommandQueue *self)
+{
+  g_assert (GSK_IS_GL_COMMAND_QUEUE (self));
+  g_assert (self->batches.len > 0);
+
+  self->batches.len--;
+}
+
+gboolean
+gsk_gl_command_queue_begin_draw (GskGLCommandQueue   *self,
+                                 GskGLUniformProgram *program,
+                                 guint                width,
+                                 guint                height)
+{
+  GskGLCommandBatch *batch;
+
+  g_assert (GSK_IS_GL_COMMAND_QUEUE (self));
+  g_assert (self->in_draw == FALSE);
+  g_assert (width <= G_MAXUINT16);
+  g_assert (height <= G_MAXUINT16);
+
+  /* Our internal links use 16-bits, so that is our max number
+   * of batches we can have in one frame.
+   */
+  if (will_ignore_batch (self))
+    return FALSE;
+
+  self->program_info = program;
+
+  batch = begin_next_batch (self);
+  batch->any.kind = GSK_GL_COMMAND_KIND_DRAW;
+  batch->any.program = program->program_id;
+  batch->any.next_batch_index = -1;
+  batch->any.viewport.width = width;
+  batch->any.viewport.height = height;
+  batch->draw.blend = 1;
+  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;
+
+  return TRUE;
+}
+
+void
+gsk_gl_command_queue_end_draw (GskGLCommandQueue *self)
+{
+  GskGLCommandBatch *last_batch;
+  GskGLCommandBatch *batch;
+
+  g_assert (GSK_IS_GL_COMMAND_QUEUE (self));
+  g_assert (self->batches.len > 0);
+
+  if (will_ignore_batch (self))
+    {
+      self->in_draw = FALSE;
+      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.blend == batch->draw.blend &&
+      last_batch->draw.framebuffer == batch->draw.framebuffer &&
+      last_batch->draw.vbo_offset + last_batch->draw.vbo_count == batch->draw.vbo_offset &&
+      last_batch->draw.vbo_count + batch->draw.vbo_count <= 0xffff &&
+      snapshots_equal (self, last_batch, batch))
+    {
+      last_batch->draw.vbo_count += batch->draw.vbo_count;
+      discard_batch (self);
+    }
+  else
+    {
+      enqueue_batch (self);
+    }
+
+  self->in_draw = FALSE;
+  self->program_info = NULL;
+}
+
+/**
+ * gsk_gl_command_queue_split_draw:
+ * @self a `GskGLCommandQueue`
+ *
+ * This function is like calling gsk_gl_command_queue_end_draw() followed by
+ * a gsk_gl_command_queue_begin_draw() with the same parameters as a
+ * previous begin draw (if shared uniforms where not changed further).
+ *
+ * This is useful to avoid comparisons inside of loops where we know shared
+ * uniforms are not changing.
+ *
+ * This generally should just be called from gsk_gl_program_split_draw()
+ * as that is where the begin/end flow happens from the render job.
+ */
+void
+gsk_gl_command_queue_split_draw (GskGLCommandQueue *self)
+{
+  GskGLCommandBatch *batch;
+  GskGLUniformProgram *program;
+  guint width;
+  guint height;
+
+  g_assert (GSK_IS_GL_COMMAND_QUEUE (self));
+  g_assert (self->batches.len > 0);
+
+  if (will_ignore_batch (self))
+    return;
+
+  g_assert (self->in_draw == TRUE);
+
+  program = self->program_info;
+
+  batch = gsk_gl_command_batches_tail (&self->batches);
+
+  g_assert (batch->any.kind == GSK_GL_COMMAND_KIND_DRAW);
+
+  width = batch->any.viewport.width;
+  height = batch->any.viewport.height;
+
+  gsk_gl_command_queue_end_draw (self);
+  gsk_gl_command_queue_begin_draw (self, program, width, height);
+}
+
+void
+gsk_gl_command_queue_clear (GskGLCommandQueue     *self,
+                            guint                  clear_bits,
+                            const graphene_rect_t *viewport)
+{
+  GskGLCommandBatch *batch;
+
+  g_assert (GSK_IS_GL_COMMAND_QUEUE (self));
+  g_assert (self->in_draw == FALSE);
+
+  if (will_ignore_batch (self))
+    return;
+
+  if (clear_bits == 0)
+    clear_bits = GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT;
+
+  batch = begin_next_batch (self);
+  batch->any.kind = GSK_GL_COMMAND_KIND_CLEAR;
+  batch->any.viewport.width = viewport->size.width;
+  batch->any.viewport.height = viewport->size.height;
+  batch->clear.bits = clear_bits;
+  batch->clear.framebuffer = self->attachments->fbo.id;
+  batch->any.next_batch_index = -1;
+  batch->any.program = 0;
+
+  self->fbo_max = MAX (self->fbo_max, batch->clear.framebuffer);
+
+  enqueue_batch (self);
+
+  self->attachments->fbo.changed = FALSE;
+}
+
+GdkGLContext *
+gsk_gl_command_queue_get_context (GskGLCommandQueue *self)
+{
+  g_return_val_if_fail (GSK_IS_GL_COMMAND_QUEUE (self), NULL);
+
+  return self->context;
+}
+
+void
+gsk_gl_command_queue_make_current (GskGLCommandQueue *self)
+{
+  g_assert (GSK_IS_GL_COMMAND_QUEUE (self));
+  g_assert (GDK_IS_GL_CONTEXT (self->context));
+
+  gdk_gl_context_make_current (self->context);
+}
+
+void
+gsk_gl_command_queue_delete_program (GskGLCommandQueue *self,
+                                     guint              program)
+{
+  g_assert (GSK_IS_GL_COMMAND_QUEUE (self));
+
+  glDeleteProgram (program);
+}
+
+static inline void
+apply_viewport (guint *current_width,
+                guint *current_height,
+                guint  width,
+                guint  height)
+{
+  if G_UNLIKELY (*current_width != width || *current_height != height)
+    {
+      *current_width = width;
+      *current_height = height;
+      glViewport (0, 0, width, height);
+    }
+}
+
+static inline void
+apply_scissor (gboolean              *state,
+               guint                  framebuffer,
+               const graphene_rect_t *scissor,
+               gboolean               has_scissor,
+               guint                  default_framebuffer)
+{
+  g_assert (framebuffer != (guint)-1);
+
+  if (framebuffer != default_framebuffer || !has_scissor)
+    {
+      if (*state != FALSE)
+        {
+          glDisable (GL_SCISSOR_TEST);
+          *state = FALSE;
+        }
+    }
+  else
+    {
+      if (*state != TRUE)
+        {
+          glEnable (GL_SCISSOR_TEST);
+          glScissor (scissor->origin.x,
+                     scissor->origin.y,
+                     scissor->size.width,
+                     scissor->size.height);
+          *state = TRUE;
+        }
+    }
+}
+
+static inline gboolean
+apply_framebuffer (int   *framebuffer,
+                   guint  new_framebuffer)
+{
+  if G_UNLIKELY (new_framebuffer != *framebuffer)
+    {
+      *framebuffer = new_framebuffer;
+      glBindFramebuffer (GL_FRAMEBUFFER, new_framebuffer);
+      return TRUE;
+    }
+
+  return FALSE;
+}
+
+static inline void
+gsk_gl_command_queue_unlink (GskGLCommandQueue *self,
+                             GskGLCommandBatch *batch)
+{
+  if (batch->any.prev_batch_index == -1)
+    self->head_batch_index = batch->any.next_batch_index;
+  else
+    self->batches.items[batch->any.prev_batch_index].any.next_batch_index = batch->any.next_batch_index;
+
+  if (batch->any.next_batch_index == -1)
+    self->tail_batch_index = batch->any.prev_batch_index;
+  else
+    self->batches.items[batch->any.next_batch_index].any.prev_batch_index = batch->any.prev_batch_index;
+
+  batch->any.prev_batch_index = -1;
+  batch->any.next_batch_index = -1;
+}
+
+static inline void
+gsk_gl_command_queue_insert_before (GskGLCommandQueue *self,
+                                    GskGLCommandBatch *batch,
+                                    GskGLCommandBatch *sibling)
+{
+  int sibling_index;
+  int index;
+
+  g_assert (batch >= self->batches.items);
+  g_assert (batch < &self->batches.items[self->batches.len]);
+  g_assert (sibling >= self->batches.items);
+  g_assert (sibling < &self->batches.items[self->batches.len]);
+
+  index = gsk_gl_command_batches_index_of (&self->batches, batch);
+  sibling_index = gsk_gl_command_batches_index_of (&self->batches, sibling);
+
+  batch->any.next_batch_index = sibling_index;
+  batch->any.prev_batch_index = sibling->any.prev_batch_index;
+
+  if (batch->any.prev_batch_index > -1)
+    self->batches.items[batch->any.prev_batch_index].any.next_batch_index = index;
+
+  sibling->any.prev_batch_index = index;
+
+  if (batch->any.prev_batch_index == -1)
+    self->head_batch_index = index;
+}
+
+static void
+gsk_gl_command_queue_sort_batches (GskGLCommandQueue *self)
+{
+  int *seen;
+  int *seen_free = NULL;
+  int index;
+
+  g_assert (GSK_IS_GL_COMMAND_QUEUE (self));
+  g_assert (self->tail_batch_index >= 0);
+  g_assert (self->fbo_max >= 0);
+
+  /* Create our seen list with most recent index set to -1,
+   * meaning we haven't yet seen that framebuffer.
+   */
+  if (self->fbo_max < 1024)
+    seen = g_alloca (sizeof (int) * (self->fbo_max + 1));
+  else
+    seen = seen_free = g_new0 (int, (self->fbo_max + 1));
+  for (int i = 0; i <= self->fbo_max; i++)
+    seen[i] = -1;
+
+  /* Walk in reverse, and if we've seen that framebuffer before, we want to
+   * delay this operation until right before the last batch we saw for that
+   * framebuffer.
+   *
+   * We can do this because we don't use a framebuffer's texture until it has
+   * been completely drawn.
+   */
+  index = self->tail_batch_index;
+
+  while (index >= 0)
+    {
+      GskGLCommandBatch *batch = &self->batches.items[index];
+      int cur_index = index;
+      int fbo = -1;
+
+      g_assert (index > -1);
+      g_assert (index < self->batches.len);
+
+      switch (batch->any.kind)
+        {
+        case GSK_GL_COMMAND_KIND_DRAW:
+          fbo = batch->draw.framebuffer;
+          break;
+
+        case GSK_GL_COMMAND_KIND_CLEAR:
+          fbo = batch->clear.framebuffer;
+          break;
+
+        default:
+          g_assert_not_reached ();
+        }
+
+      index = batch->any.prev_batch_index;
+
+      g_assert (index >= -1);
+      g_assert (index < (int)self->batches.len);
+      g_assert (fbo >= -1);
+
+      if (fbo == -1)
+        continue;
+
+      g_assert (fbo <= self->fbo_max);
+      g_assert (seen[fbo] >= -1);
+      g_assert (seen[fbo] < (int)self->batches.len);
+
+      if (seen[fbo] != -1 && seen[fbo] != batch->any.next_batch_index)
+        {
+          int mru_index = seen[fbo];
+          GskGLCommandBatch *mru = &self->batches.items[mru_index];
+
+          g_assert (mru_index > -1);
+
+          gsk_gl_command_queue_unlink (self, batch);
+
+          g_assert (batch->any.prev_batch_index == -1);
+          g_assert (batch->any.next_batch_index == -1);
+
+          gsk_gl_command_queue_insert_before (self, batch, mru);
+
+          g_assert (batch->any.prev_batch_index > -1 ||
+                    self->head_batch_index == cur_index);
+          g_assert (batch->any.next_batch_index == seen[fbo]);
+        }
+
+      g_assert (cur_index > -1);
+      g_assert (seen[fbo] >= -1);
+
+      seen[fbo] = cur_index;
+    }
+
+  g_free (seen_free);
+}
+
+/**
+ * gsk_gl_command_queue_execute:
+ * @self: a `GskGLCommandQueue`
+ * @surface_height: the height of the backing surface
+ * @scale: the scale of the backing surface
+ * @scissor: (nullable): the scissor clip if any
+ * @default_framebuffer: the default framebuffer id if not zero
+ *
+ * Executes all of the batches in the command queue.
+ *
+ * Typically, the scissor rect is only applied when rendering to the default
+ * framebuffer (zero in most cases). However, if @default_framebuffer is not
+ * zero, it will be checked to see if the rendering target matches so that
+ * the scissor rect is applied. This should be used in cases where rendering
+ * to the backbuffer for display is not the default GL framebuffer of zero.
+ * Currently, this happens when rendering on macOS using IOSurface.
+ */
+void
+gsk_gl_command_queue_execute (GskGLCommandQueue    *self,
+                              guint                 surface_height,
+                              float                 scale,
+                              const cairo_region_t *scissor,
+                              guint                 default_framebuffer)
+{
+  G_GNUC_UNUSED guint count = 0;
+  graphene_rect_t scissor_test = GRAPHENE_RECT_INIT (0, 0, 0, 0);
+  gboolean has_scissor = scissor != NULL;
+  gboolean scissor_state = -1;
+  guint program = 0;
+  guint width = 0;
+  guint height = 0;
+  G_GNUC_UNUSED unsigned int n_binds = 0;
+  G_GNUC_UNUSED unsigned int n_fbos = 0;
+  G_GNUC_UNUSED unsigned int n_uniforms = 0;
+  G_GNUC_UNUSED unsigned int n_programs = 0;
+  guint vao_id;
+  guint vbo_id;
+  int textures[GSK_GL_MAX_TEXTURES_PER_PROGRAM];
+  int samplers[GSK_GL_MAX_TEXTURES_PER_PROGRAM];
+  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;
+  for (guint i = 0; i < G_N_ELEMENTS (samplers); i++)
+    samplers[i] = -1;
+
+  gsk_gl_command_queue_sort_batches (self);
+
+  gsk_gl_command_queue_make_current (self);
+
+  gsk_gl_profiler_begin_gpu_region (self->gl_profiler);
+  gsk_profiler_timer_begin (self->profiler, self->metrics.cpu_time);
+
+  glDisable (GL_DEPTH_TEST);
+
+  /* Pre-multiplied alpha */
+  glEnable (GL_BLEND);
+  glBlendFunc (GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
+  glBlendEquation (GL_FUNC_ADD);
+
+  if (gdk_gl_context_has_vertex_arrays (self->context))
+    {
+      glGenVertexArrays (1, &vao_id);
+      glBindVertexArray (vao_id);
+    }
+
+  vbo_id = gsk_gl_command_queue_submit_vertices (self);
+
+  /* Setup initial scissor clip */
+  if (scissor != NULL && cairo_region_num_rectangles (scissor) > 0)
+    {
+      cairo_rectangle_int_t r;
+
+      g_assert (cairo_region_num_rectangles (scissor) == 1);
+      cairo_region_get_rectangle (scissor, 0, &r);
+
+      scissor_test.origin.x = (int) floor (r.x * scale);
+      scissor_test.origin.y = (int) floor (surface_height - (r.height * scale) - (r.y * scale));
+      scissor_test.size.width = (int) ceil (r.width * scale);
+      scissor_test.size.height = (int) ceil (r.height * scale);
+    }
+
+  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, default_framebuffer);
+              n_fbos++;
+            }
+
+          apply_viewport (&width,
+                          &height,
+                          batch->any.viewport.width,
+                          batch->any.viewport.height);
+
+          glClearColor (0, 0, 0, 0);
+          glClear (batch->clear.bits);
+        break;
+
+        case GSK_GL_COMMAND_KIND_DRAW:
+          if (batch->any.program != program)
+            {
+              program = batch->any.program;
+              glUseProgram (program);
+
+              n_programs++;
+            }
+
+          if (apply_framebuffer (&framebuffer, batch->draw.framebuffer))
+            {
+              apply_scissor (&scissor_state, framebuffer, &scissor_test, has_scissor, default_framebuffer);
+              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];
+
+              g_assert (bind->texture < G_N_ELEMENTS (textures));
+              for (guint i = 0; i < batch->draw.bind_count; i++)
+                {
+                  if (textures[bind->texture] != bind->id)
+                    {
+                      GskGLSync *s;
+
+                      if (active != bind->texture)
+                        {
+                          active = bind->texture;
+                          glActiveTexture (GL_TEXTURE0 + bind->texture);
+                        }
+
+                      s = gsk_gl_syncs_get_sync (&self->syncs, bind->id);
+                      if (s && s->sync)
+                        {
+                          if (self->has_sync)
+                            glWaitSync ((GLsync) s->sync, 0, GL_TIMEOUT_IGNORED);
+                          s->sync = NULL;
+                        }
+
+                      if (bind->sampler == SAMPLER_EXTERNAL)
+                        glBindTexture (GL_TEXTURE_EXTERNAL_OES, bind->id);
+                      else
+                        glBindTexture (GL_TEXTURE_2D, bind->id);
+                      textures[bind->texture] = bind->id;
+                      if (!self->has_samplers)
+                        {
+                          if (bind->sampler == SAMPLER_EXTERNAL)
+                            {
+                              glTexParameteri (GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+                              glTexParameteri (GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+                            }
+                          else
+                            {
+                              glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, min_filter_from_index (bind->sampler / GSK_GL_N_FILTERS));
+                              glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, mag_filter_from_index (bind->sampler % GSK_GL_N_FILTERS));
+                            }
+                        }
+                    }
+
+                  if (samplers[bind->texture] != bind->sampler)
+                    {
+                      if (self->has_samplers)
+                        glBindSampler (bind->texture, self->samplers[bind->sampler]);
+                      else
+                        {
+                          if (bind->sampler == SAMPLER_EXTERNAL)
+                            {
+                              glTexParameteri (GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+                              glTexParameteri (GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+                            }
+                          else
+                            {
+                              glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, min_filter_from_index (bind->sampler / GSK_GL_N_FILTERS));
+                              glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, mag_filter_from_index (bind->sampler % GSK_GL_N_FILTERS));
+                            }
+                        }
+                      samplers[bind->texture] = bind->sampler;
+                    }
+
+                  bind++;
+                }
+
+              n_binds += batch->draw.bind_count;
+            }
+
+          if (batch->draw.uniform_count > 0)
+            {
+              const GskGLCommandUniform *u = &self->batch_uniforms.items[batch->draw.uniform_offset];
+
+              for (guint i = 0; i < batch->draw.uniform_count; i++, u++)
+                gsk_gl_uniform_state_apply (self->uniforms, program, u->location, u->info);
+
+              n_uniforms += batch->draw.uniform_count;
+            }
+
+          if (batch->draw.blend == 0)
+            glDisable (GL_BLEND);
+
+          glDrawArrays (GL_TRIANGLES, batch->draw.vbo_offset, batch->draw.vbo_count);
+
+          if (batch->draw.blend == 0)
+            glEnable (GL_BLEND);
+        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);
+  if (gdk_gl_context_has_vertex_arrays (self->context))
+    glDeleteVertexArrays (1, &vao_id);
+
+  gdk_profiler_set_int_counter (self->metrics.n_binds, n_binds);
+  gdk_profiler_set_int_counter (self->metrics.n_uniforms, n_uniforms);
+  gdk_profiler_set_int_counter (self->metrics.n_fbos, n_fbos);
+  gdk_profiler_set_int_counter (self->metrics.n_programs, n_programs);
+  gdk_profiler_set_int_counter (self->metrics.n_uploads, self->n_uploads);
+  gdk_profiler_set_int_counter (self->metrics.queue_depth, self->batches.len);
+
+  {
+    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);
+  }
+}
+
+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 (self->attachments->textures[i].target, 0);
+
+          self->attachments->textures[i].id = 0;
+          self->attachments->textures[i].changed = FALSE;
+          self->attachments->textures[i].initial = TRUE;
+        }
+    }
+
+  self->batches.len = 0;
+  self->batch_binds.len = 0;
+  self->batch_uniforms.len = 0;
+  self->syncs.len = 0;
+  self->n_uploads = 0;
+  self->tail_batch_index = -1;
+  self->in_frame = FALSE;
+}
+
+gboolean
+gsk_gl_command_queue_create_render_target (GskGLCommandQueue *self,
+                                           int                width,
+                                           int                height,
+                                           int                format,
+                                           guint             *out_fbo_id,
+                                           guint             *out_texture_id)
+{
+  GLuint fbo_id = 0;
+  GLint texture_id;
+
+  g_assert (GSK_IS_GL_COMMAND_QUEUE (self));
+  g_assert (width > 0);
+  g_assert (height > 0);
+  g_assert (out_fbo_id != NULL);
+  g_assert (out_texture_id != NULL);
+
+  texture_id = gsk_gl_command_queue_create_texture (self,
+                                                    width, height,
+                                                    format);
+
+  if (texture_id == -1)
+    {
+      *out_fbo_id = 0;
+      *out_texture_id = 0;
+      return FALSE;
+    }
+
+  fbo_id = gsk_gl_command_queue_create_framebuffer (self);
+
+  glBindFramebuffer (GL_FRAMEBUFFER, fbo_id);
+  glFramebufferTexture2D (GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture_id, 0);
+  g_assert_cmphex (glCheckFramebufferStatus (GL_FRAMEBUFFER), ==, GL_FRAMEBUFFER_COMPLETE);
+
+  *out_fbo_id = fbo_id;
+  *out_texture_id = texture_id;
+
+  return TRUE;
+}
+
+int
+gsk_gl_command_queue_create_texture (GskGLCommandQueue *self,
+                                     int                width,
+                                     int                height,
+                                     int                format)
+{
+  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, GL_LINEAR);
+  glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+  glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+  glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+
+  switch (format)
+  {
+    case 0:
+      break;
+    case GL_RGBA8:
+      glTexImage2D (GL_TEXTURE_2D, 0, format, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
+      break;
+    case GL_RGBA16F:
+      glTexImage2D (GL_TEXTURE_2D, 0, format, width, height, 0, GL_RGBA, GL_HALF_FLOAT, NULL);
+      break;
+    case GL_RGBA32F:
+      glTexImage2D (GL_TEXTURE_2D, 0, format, width, height, 0, GL_RGBA, GL_FLOAT, NULL);
+      break;
+    default:
+      /* If you add new formats, make sure to set the correct format and type here
+       * so that GLES doesn't barf invalid operations at you.
+       * Because it is very important that these 3 values match when data is set to
+       * NULL, do you hear me?
+       */
+      g_assert_not_reached ();
+      break;
+  }
+
+  /* Restore the previous texture if it was set */
+  if (self->attachments->textures[0].id != 0 &&
+      self->attachments->textures[0].target == GL_TEXTURE_2D)
+    glBindTexture (self->attachments->textures[0].target, 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;
+}
+
+static GdkMemoryFormat
+memory_format_gl_format (GskGLCommandQueue *self,
+                         GdkMemoryFormat    data_format,
+                         gboolean           ensure_mipmap,
+                         gboolean          *out_can_mipmap,
+                         GLint             *gl_internalformat,
+                         GLint             *gl_internalsrgbformat,
+                         GLenum            *gl_format,
+                         GLenum            *gl_type,
+                         GLint              gl_swizzle[4])
+{
+  GdkGLMemoryFlags flags, required_flags;
+  GdkMemoryFormat alt_format;
+  const GdkMemoryFormat *fallbacks;
+  GdkSwizzle gdk_sw;
+  gsize i;
+
+  /* No support for straight formats yet */
+  if (gdk_memory_format_alpha (data_format) == GDK_MEMORY_ALPHA_STRAIGHT)
+    data_format = gdk_memory_format_get_premultiplied (data_format);
+
+  required_flags = GDK_GL_FORMAT_USABLE | GDK_GL_FORMAT_FILTERABLE;
+  if (ensure_mipmap)
+    required_flags |= GDK_GL_FORMAT_RENDERABLE;
+
+  /* First, try the format itself */
+  flags = gdk_gl_context_get_format_flags (self->context, data_format);
+  if ((flags & required_flags) == required_flags)
+    {
+      gdk_memory_format_gl_format (data_format,
+                                   0,   /* plane */
+                                   gdk_gl_context_get_use_es (self->context),
+                                   gl_internalformat,
+                                   gl_internalsrgbformat,
+                                   gl_format,
+                                   gl_type,
+                                   &gdk_sw);
+      gdk_swizzle_to_gl (gdk_sw, gl_swizzle);
+      *out_can_mipmap = (flags & GDK_GL_FORMAT_RENDERABLE) ? TRUE : FALSE;
+      return data_format;
+    }
+
+  /* Second, try the potential RGBA format */
+  {
+    GdkSwizzle alt_swizzle;
+    if (gdk_memory_format_get_rgba_format (data_format, &alt_format, &alt_swizzle))
+      {
+        flags = gdk_gl_context_get_format_flags (self->context, alt_format);
+        if ((flags & required_flags) == required_flags)
+          {
+            *out_can_mipmap = (flags & GDK_GL_FORMAT_RENDERABLE) ? TRUE : FALSE;
+
+            gdk_memory_format_gl_format (alt_format,
+                                         0,   /* plane */
+                                         gdk_gl_context_get_use_es (self->context),
+                                         gl_internalformat,
+                                         gl_internalsrgbformat,
+                                         gl_format,
+                                         gl_type,
+                                         &gdk_sw);
+
+            if (self->can_swizzle)
+              {
+                /* Upload as alt_format, but swizzle to display as data_format */
+                gdk_swizzle_to_gl (alt_swizzle, gl_swizzle);
+                return data_format;
+              }
+
+            gdk_swizzle_to_gl (gdk_sw, gl_swizzle);
+            return alt_format;
+          }
+      }
+  }
+
+  /* Next, try the fallbacks */
+  fallbacks = gdk_memory_format_get_fallbacks (data_format);
+  for (i = 0; fallbacks[i] != -1; i++)
+    {
+      flags = gdk_gl_context_get_format_flags (self->context, fallbacks[i]);
+      if (((flags & required_flags) == required_flags))
+        {
+          gdk_memory_format_gl_format (fallbacks[i],
+                                       0,   /* plane */
+                                       gdk_gl_context_get_use_es (self->context),
+                                       gl_internalformat,
+                                       gl_internalsrgbformat,
+                                       gl_format,
+                                       gl_type,
+                                       &gdk_sw);
+          gdk_swizzle_to_gl (gdk_sw, gl_swizzle);
+
+          *out_can_mipmap = (flags & GDK_GL_FORMAT_RENDERABLE) ? TRUE : FALSE;
+          return fallbacks[i];
+        }
+    }
+
+  g_assert_not_reached ();
+  return GDK_MEMORY_R8G8B8A8_PREMULTIPLIED;
+}
+
+static void
+gsk_gl_command_queue_do_upload_texture_chunk (GskGLCommandQueue *self,
+                                              GdkTexture        *texture,
+                                              int                x,
+                                              int                y,
+                                              GdkMemoryFormat    data_format,
+                                              GLenum             gl_format,
+                                              GLenum             gl_type,
+                                              GLint              gl_swizzle[4])
+
+{
+  G_GNUC_UNUSED gint64 start_time = GDK_PROFILER_CURRENT_TIME;
+  const guchar *data;
+  gsize stride;
+  GBytes *bytes;
+  GdkTextureDownloader downloader;
+  int width, height;
+  gsize bpp;
+
+  width = gdk_texture_get_width (texture);
+  height = gdk_texture_get_height (texture);
+
+  if (GSK_DEBUG_CHECK (FALLBACK) &&
+      data_format != gdk_texture_get_format (texture))
+    {
+      GEnumClass *enum_class = g_type_class_ref (GDK_TYPE_MEMORY_FORMAT);
+
+      gdk_debug_message ("Unsupported format %s, converting on CPU to %s",
+                         g_enum_get_value (enum_class, gdk_texture_get_format (texture))->value_nick,
+                         g_enum_get_value (enum_class, data_format)->value_nick);
+
+      g_type_class_unref (enum_class);
+    }
+
+  gdk_texture_downloader_init (&downloader, texture);
+  gdk_texture_downloader_set_format (&downloader, data_format);
+  bytes = gdk_texture_downloader_download_bytes (&downloader, &stride);
+  gdk_texture_downloader_finish (&downloader);
+  data = g_bytes_get_data (bytes, NULL);
+  bpp = gdk_memory_format_get_plane_block_bytes (data_format, 0);
+
+  if (gdk_profiler_is_running ())
+    {
+      gdk_profiler_end_markf (start_time,
+                              "Download texture chunk",
+                              "Tile %dx%d Size %dx%d", x, y, width, height);
+      start_time = GDK_PROFILER_CURRENT_TIME;
+    }
+
+  glPixelStorei (GL_UNPACK_ALIGNMENT, gdk_memory_format_alignment (data_format));
+
+  /* GL_UNPACK_ROW_LENGTH is available on desktop GL, OpenGL ES >= 3.0, or if
+   * the GL_EXT_unpack_subimage extension for OpenGL ES 2.0 is available
+   */
+  if (stride == width * bpp)
+    {
+      glTexSubImage2D (GL_TEXTURE_2D, 0, x, y, width, height, gl_format, gl_type, data);
+    }
+  else if (stride % bpp == 0)
+    {
+      glPixelStorei (GL_UNPACK_ROW_LENGTH, stride / bpp);
+
+      glTexSubImage2D (GL_TEXTURE_2D, 0, x, y, width, height, gl_format, gl_type, data);
+
+      glPixelStorei (GL_UNPACK_ROW_LENGTH, 0);
+    }
+  else
+    {
+      for (int i = 0; i < height; i++)
+        glTexSubImage2D (GL_TEXTURE_2D, 0, x, y + i, width, 1, gl_format, gl_type, data + (i * stride));
+    }
+
+  glPixelStorei (GL_UNPACK_ALIGNMENT, 4);
+
+  /* Only apply swizzle if really needed, might not even be
+   * supported if default values are set
+   */
+  if (self->can_swizzle &&
+      (gl_swizzle[0] != GL_RED || gl_swizzle[1] != GL_GREEN || gl_swizzle[2] != GL_BLUE || gl_swizzle[3] != GL_ALPHA))
+    {
+      /* Set each channel independently since GLES 3.0 doesn't support the iv method */
+      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, gl_swizzle[0]);
+      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_G, gl_swizzle[1]);
+      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, gl_swizzle[2]);
+      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_A, gl_swizzle[3]);
+    }
+
+  g_bytes_unref (bytes);
+
+  if (gdk_profiler_is_running ())
+    gdk_profiler_end_markf (start_time,
+                            "Upload texture chunk",
+                            "Tile %dx%d Size %dx%d", x, y, width, height);
+}
+
+int
+gsk_gl_command_queue_upload_texture_chunks (GskGLCommandQueue    *self,
+                                            gboolean              ensure_mipmap,
+                                            unsigned int          n_chunks,
+                                            GskGLTextureChunk    *chunks,
+                                            gboolean             *out_can_mipmap)
+{
+  G_GNUC_UNUSED gint64 start_time = GDK_PROFILER_CURRENT_TIME;
+  int width, height;
+  GdkMemoryFormat data_format;
+  GLint gl_internalformat, gl_internalsrgbformat;
+  GLenum gl_format;
+  GLenum gl_type;
+  GLint gl_swizzle[4];
+  int texture_id;
+
+  g_assert (GSK_IS_GL_COMMAND_QUEUE (self));
+
+  width = height = 0;
+  for (unsigned int i = 0; i < n_chunks; i++)
+    {
+      GskGLTextureChunk *c = &chunks[i];
+
+      width = MAX (width, c->x + gdk_texture_get_width (c->texture));
+      height = MAX (height, c->y + gdk_texture_get_height (c->texture));
+    }
+
+  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 = MIN (width, self->max_texture_size);
+      height = MIN (height, self->max_texture_size);
+    }
+
+  texture_id = gsk_gl_command_queue_create_texture (self, width, height, 0);
+  if (texture_id == -1)
+    return texture_id;
+
+  self->n_uploads++;
+
+  /* Switch to texture0 as 2D. We'll restore it later. */
+  glActiveTexture (GL_TEXTURE0);
+  glBindTexture (GL_TEXTURE_2D, texture_id);
+
+  /* Initialize the texture */
+  data_format = gdk_texture_get_format (chunks[0].texture);
+  data_format = memory_format_gl_format (self,
+                                         data_format,
+                                         ensure_mipmap,
+                                         out_can_mipmap,
+                                         &gl_internalformat,
+                                         &gl_internalsrgbformat,
+                                         &gl_format,
+                                         &gl_type,
+                                         gl_swizzle);
+
+  glTexImage2D (GL_TEXTURE_2D, 0, gl_internalformat, width, height, 0, gl_format, gl_type, NULL);
+
+  for (unsigned int i = 0; i < n_chunks; i++)
+    {
+      GskGLTextureChunk *c = &chunks[i];
+      gsk_gl_command_queue_do_upload_texture_chunk (self, c->texture, c->x, c->y, data_format, gl_format, gl_type, gl_swizzle);
+    }
+
+  /* Restore previous texture state if any */
+  if (self->attachments->textures[0].id > 0)
+    glBindTexture (self->attachments->textures[0].target,
+                   self->attachments->textures[0].id);
+
+  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;
+}
+
+int
+gsk_gl_command_queue_upload_texture (GskGLCommandQueue *self,
+                                     GdkTexture        *texture,
+                                     gboolean           ensure_mipmap,
+                                     gboolean          *out_can_mipmap)
+{
+  return gsk_gl_command_queue_upload_texture_chunks (self,
+                                                     ensure_mipmap,
+                                                     1,
+                                                     &(GskGLTextureChunk){ texture, 0, 0},
+                                                     out_can_mipmap);
+}
+
+void
+gsk_gl_command_queue_set_profiler (GskGLCommandQueue *self,
+                                   GskProfiler       *profiler)
+{
+  g_assert (GSK_IS_GL_COMMAND_QUEUE (self));
+  g_assert (GSK_IS_PROFILER (profiler));
+
+  if (g_set_object (&self->profiler, profiler))
+    {
+      self->gl_profiler = gsk_gl_profiler_new (self->context);
+
+      self->metrics.n_frames = gsk_profiler_add_counter (profiler, "frames", "Frames", FALSE);
+      self->metrics.cpu_time = gsk_profiler_add_timer (profiler, "cpu-time", "CPU Time", FALSE, TRUE);
+      self->metrics.gpu_time = gsk_profiler_add_timer (profiler, "gpu-time", "GPU Time", FALSE, TRUE);
+
+      self->metrics.n_binds = gdk_profiler_define_int_counter ("attachments", "Number of texture attachments");
+      self->metrics.n_fbos = gdk_profiler_define_int_counter ("fbos", "Number of framebuffers attached");
+      self->metrics.n_uniforms = gdk_profiler_define_int_counter ("uniforms", "Number of uniforms changed");
+      self->metrics.n_uploads = gdk_profiler_define_int_counter ("uploads", "Number of texture uploads");
+      self->metrics.n_programs = gdk_profiler_define_int_counter ("programs", "Number of program changes");
+      self->metrics.queue_depth = gdk_profiler_define_int_counter ("gl-queue-depth", "Depth of GL command batches");
+    }
+}
diff --git gsk/gl/gskglcommandqueueprivate.h gsk/gl/gskglcommandqueueprivate.h
new file mode 100644
index 0000000000..aa848e689a
--- /dev/null
+++ gsk/gl/gskglcommandqueueprivate.h
@@ -0,0 +1,474 @@
+/* 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
+ */
+
+#pragma once
+
+#include <gsk/gskprofilerprivate.h>
+
+#include "gskgltypesprivate.h"
+#include "gskglbufferprivate.h"
+#include "gskglattachmentstateprivate.h"
+#include "gskgluniformstateprivate.h"
+
+#include "inlinearray.h"
+
+#include "gskglprofilerprivate.h"
+
+#include <stdio.h>
+
+G_BEGIN_DECLS
+
+/* The GL version a driver reports and the GLSL version it can actually
+ * compile do not always agree, so ask the driver directly. Requires the
+ * context to be current.
+ */
+static inline gboolean
+gsk_gl_has_glsl_version (int major,
+                         int minor)
+{
+  const char *str = (const char *) glGetString (GL_SHADING_LANGUAGE_VERSION);
+  int maj = 1;
+  int min = 0;
+
+  /* "OpenGL ES GLSL ES 3.20" and friends have a prefix, skip to the digits */
+  if (str != NULL)
+    {
+      while (*str != '\0' && !g_ascii_isdigit (*str))
+        str++;
+
+      if (sscanf (str, "%d.%d", &maj, &min) != 2)
+        {
+          maj = 1;
+          min = 0;
+        }
+    }
+
+  /* GLSL writes 1.10 as "1.10", ie the minor version is two digits */
+  return maj > major || (maj == major && min >= minor);
+}
+
+#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 : 4;
+
+  /* the sampler to use. We set sampler to 15 to indicate external textures */
+  guint sampler : 4;
+
+  /* The identifier for the texture created with glGenTextures(). */
+  guint id: 24;
+} 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;
+
+  guint blend : 1;
+
+  /* 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 : 31;
+
+  /* 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);
+
+typedef struct _GskGLSync {
+  guint id;
+  gpointer sync;
+} GskGLSync;
+
+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)
+DEFINE_INLINE_ARRAY (GskGLSyncs, gsk_gl_syncs, GskGLSync)
+
+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;
+
+  /* Array of samplers that we use for mag/min filter handling. It is indexed
+   * by the sampler_index() function.
+   *
+   * Note that when samplers are not supported (hello GLES), we fall back to
+   * setting the texture filter, but that needs to be done for every texture.
+   *
+   * Also note that we don't use all of these samplers since some combinations
+   * are invalid. An index of SAMPLER_EXTERNAL is used to indicate an external
+   * texture, which needs special sampler treatment.
+   */
+  GLuint samplers[GSK_GL_N_FILTERS * GSK_GL_N_FILTERS];
+
+  /* Array of sync objects to wait on.
+   */
+  GskGLSyncs syncs;
+
+  /* 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;
+    guint n_programs;
+    guint queue_depth;
+  } metrics;
+
+  /* Counter for uploads on the frame */
+  guint n_uploads;
+
+  /* Scratch space to expand vertices into when the driver cannot source
+   * vertex attributes from half floats. Only used if !has_half_float_vertex.
+   */
+  GskGLDrawVertexFloat *float_vertices;
+  gsize float_vertices_len;
+
+  /* If the GL context is new enough for sampler support */
+  guint has_samplers : 1;
+
+  /* If the GL context is new enough to support swizzling (ie is not GLES2) */
+  guint can_swizzle : 1;
+
+  /* If the driver can source vertex attributes from half floats */
+  guint has_half_float_vertex : 1;
+
+  /* If the driver supports GL_ARB_sync style fences */
+  guint has_sync : 1;
+
+  /* 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;
+
+  /* If we've warned about truncating batches */
+  guint have_truncated : 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,
+                                                               float                 scale,
+                                                               const cairo_region_t *scissor,
+                                                               guint                 default_framebuffer);
+int                 gsk_gl_command_queue_upload_texture       (GskGLCommandQueue    *self,
+                                                               GdkTexture           *texture,
+                                                               gboolean              ensure_mipmap,
+                                                               gboolean             *out_can_mipmap);
+int                 gsk_gl_command_queue_create_texture       (GskGLCommandQueue    *self,
+                                                               int                   width,
+                                                               int                   height,
+                                                               int                   format);
+
+
+typedef struct {
+  GdkTexture *texture;
+  int x;
+  int y;
+} GskGLTextureChunk;
+
+int                 gsk_gl_command_queue_upload_texture_chunks(GskGLCommandQueue    *self,
+                                                               gboolean              ensure_mipmap,
+                                                               unsigned int          n_chunks,
+                                                               GskGLTextureChunk    *chunks,
+                                                               gboolean             *out_can_mipmap);
+
+guint               gsk_gl_command_queue_create_framebuffer   (GskGLCommandQueue    *self);
+gboolean            gsk_gl_command_queue_create_render_target (GskGLCommandQueue    *self,
+                                                               int                   width,
+                                                               int                   height,
+                                                               int                   format,
+                                                               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);
+gboolean            gsk_gl_command_queue_begin_draw           (GskGLCommandQueue    *self,
+                                                               GskGLUniformProgram  *program_info,
+                                                               guint                 width,
+                                                               guint                 height);
+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;
+}
+
+static inline GskGLSync *
+gsk_gl_syncs_get_sync (GskGLSyncs *syncs,
+                       guint       id)
+{
+  for (unsigned int i = 0; i < syncs->len; i++)
+    {
+      GskGLSync *sync = &syncs->items[i];
+      if (sync->id == id)
+        return sync;
+    }
+  return NULL;
+}
+
+static inline void
+gsk_gl_syncs_add_sync (GskGLSyncs *syncs,
+                       guint       id,
+                       gpointer    sync)
+{
+  GskGLSync *s;
+
+  s = gsk_gl_syncs_get_sync (syncs, id);
+  if (s)
+    g_assert (s->sync == sync);
+  else
+    {
+      s = gsk_gl_syncs_append (syncs);
+      s->id = id;
+      s->sync = sync;
+    }
+}
+
+G_END_DECLS
+
diff --git gsk/gl/gskglcompiler.c gsk/gl/gskglcompiler.c
new file mode 100644
index 0000000000..0f6e734412
--- /dev/null
+++ gsk/gl/gskglcompiler.c
@@ -0,0 +1,713 @@
+/* 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_GLES3      "300 es"
+#define SHADER_VERSION_GL2_LEGACY "110"
+#define SHADER_VERSION_GL21_LEGACY "120"
+#define SHADER_VERSION_GL3_LEGACY "130"
+#define SHADER_VERSION_GL3        "150"
+
+struct _GskGLCompiler
+{
+  GObject parent_instance;
+
+  GskGLDriver *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;
+
+  const char *glsl_version;
+
+  guint gl3 : 1;
+  guint gles : 1;
+  guint gles3 : 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 (GskGLDriver *driver,
+                     gboolean     debug_shaders)
+{
+  GskGLCompiler *self;
+  GdkGLContext *context;
+
+  g_return_val_if_fail (GSK_IS_GL_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_driver_get_context (self->driver);
+
+  if (gdk_gl_context_get_use_es (context))
+    {
+      int maj, min;
+
+      /* for OpenGL/ES 3.0+, use "300 es" as our shader version */
+      gdk_gl_context_get_version (context, &maj, &min);
+
+      if (maj >= 3)
+        {
+          self->glsl_version = SHADER_VERSION_GLES3;
+          self->gles3 = TRUE;
+        }
+      else
+        {
+          self->glsl_version = SHADER_VERSION_GLES;
+          self->gles = TRUE;
+        }
+    }
+  else if (gdk_gl_context_is_legacy (context))
+    {
+      /* Take the GLSL version from the driver rather than deriving it from
+       * the GL version: the two do not have to match. Drivers reporting GL
+       * 3.x whose GLSL tops out at 1.20 exist (older macOS, some X11
+       * drivers), and so do GL 2.0 drivers that already do GLSL 1.20.
+       *
+       * We never need more than 1.30 here, the legacy shaders use
+       * attribute/varying throughout.
+       *
+       * Make the context current first so glGetString() is valid.
+       */
+      gsk_gl_command_queue_make_current (driver->shared_command_queue);
+
+      if (gsk_gl_has_glsl_version (1, 30))
+        self->glsl_version = SHADER_VERSION_GL3_LEGACY;
+      else if (gsk_gl_has_glsl_version (1, 20))
+        self->glsl_version = SHADER_VERSION_GL21_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,
+                         const char     *clip,
+                         GError        **error)
+{
+  char version[32];
+  const char *debug = "";
+  const char *legacy = "";
+  const char *gl3 = "";
+  const char *gles = "";
+  const char *gles3 = "";
+  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 %s\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->gles3)
+    gles3 = "#define GSK_GLES3 1\n";
+
+  if (self->gl3)
+    gl3 = "#define GSK_GL3 1\n";
+
+  vertex_id = glCreateShader (GL_VERTEX_SHADER);
+  glShaderSource (vertex_id,
+                  11,
+                  (const char *[]) {
+                    version, debug, legacy, gl3, gles, gles3, clip,
+                    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),
+                    strlen (gles3),
+                    strlen (clip),
+                    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,
+                  11,
+                  (const char *[]) {
+                    version, debug, legacy, gl3, gles, gles3, clip,
+                    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),
+                    strlen (gles3),
+                    strlen (clip),
+                    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 gsk/gl/gskglcompilerprivate.h gsk/gl/gskglcompilerprivate.h
new file mode 100644
index 0000000000..82a2dda81a
--- /dev/null
+++ gsk/gl/gskglcompilerprivate.h
@@ -0,0 +1,68 @@
+/* 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
+ */
+
+#pragma once
+
+#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                        (GskGLDriver        *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,
+                                                            const char         *clip,
+                                                            GError            **error);
+
+G_END_DECLS
+
diff --git gsk/gl/gskgldriver.c gsk/gl/gskgldriver.c
new file mode 100644
index 0000000000..7e98349de9
--- /dev/null
+++ gsk/gl/gskgldriver.c
@@ -0,0 +1,1868 @@
+/* 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 "gskgldriverprivate.h"
+
+#include <gsk/gskdebugprivate.h>
+#include <gsk/gskglshaderprivate.h>
+#include <gsk/gskrendererprivate.h>
+
+#include "gskglcommandqueueprivate.h"
+#include "gskglcompilerprivate.h"
+#include "gskglglyphlibraryprivate.h"
+#include "gskgliconlibraryprivate.h"
+#include "gskglprogramprivate.h"
+#include "gskglshadowlibraryprivate.h"
+#include "fp16private.h"
+
+#include <gdk/gdkglcontextprivate.h>
+#include <gdk/gdkdisplayprivate.h>
+#include <gdk/gdkmemorytextureprivate.h>
+#include <gdk/gdkmemoryformatprivate.h>
+#include <gdk/gdkprofilerprivate.h>
+#include <gdk/gdktextureprivate.h>
+
+#include <gdk/gdkmemoryformatprivate.h>
+#include <gdk/gdkdmabuftextureprivate.h>
+
+
+G_DEFINE_TYPE (GskGLDriver, gsk_gl_driver, G_TYPE_OBJECT)
+
+static guint
+texture_key_hash (gconstpointer v)
+{
+  const GskTextureKey *k = (const GskTextureKey *)v;
+
+  /* Optimize for 0..3 where 0 is the scaled out case. Usually
+   * we'll be squarely on 1 or 2 for standard vs HiDPI. When rendering
+   * to a texture scaled out like in node-editor, we might be < 1.
+   */
+  guint scale_x = floorf (k->scale_x);
+  guint scale_y = floorf (k->scale_y);
+
+  return GPOINTER_TO_SIZE (k->pointer) ^
+    ((scale_x << 8) |
+     (scale_y << 4) |
+     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->pointer_is_child == k2->pointer_is_child &&
+         (!k1->pointer_is_child || memcmp (&k1->parent_rect, &k2->parent_rect, sizeof k1->parent_rect) == 0);
+}
+
+static void
+remove_texture_key_for_id (GskGLDriver *self,
+                           guint        texture_id)
+{
+  GskTextureKey *key;
+
+  g_assert (GSK_IS_GL_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 void
+gsk_gl_driver_autorelease_texture (GskGLDriver *self,
+                                   guint        texture_id)
+{
+  g_assert (GSK_IS_GL_DRIVER (self));
+
+  g_array_append_val (self->texture_pool, texture_id);
+}
+
+static guint
+gsk_gl_driver_collect_unused_textures (GskGLDriver *self,
+                                       gint64       watermark)
+{
+  GHashTableIter iter;
+  gpointer k, v;
+  guint old_size;
+  guint collected;
+
+  g_assert (GSK_IS_GL_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->link.prev == NULL);
+          g_assert (t->link.next == NULL);
+          g_assert (t->link.data == t);
+
+          remove_texture_key_for_id (self, t->texture_id);
+          gsk_gl_driver_autorelease_texture (self, t->texture_id);
+          t->texture_id = 0;
+          gsk_gl_texture_free (t);
+        }
+    }
+
+  collected = old_size - g_hash_table_size (self->textures);
+
+  return collected;
+}
+
+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_gl_driver_shader_weak_cb (gpointer  data,
+                              GObject  *where_object_was)
+{
+  GskGLDriver *self = data;
+
+  g_assert (GSK_IS_GL_DRIVER (self));
+
+  if (self->shader_cache != NULL)
+    {
+      if (self->command_queue != NULL)
+        gsk_gl_command_queue_make_current (self->command_queue);
+
+      g_hash_table_remove (self->shader_cache, where_object_was);
+    }
+}
+
+G_GNUC_UNUSED G_GNUC_NULL_TERMINATED static inline GBytes *
+join_sources (GBytes *first_bytes,
+              ...)
+{
+  GByteArray *byte_array = g_byte_array_new ();
+  GBytes *bytes = first_bytes;
+  va_list args;
+
+  va_start (args, first_bytes);
+  while (bytes != NULL)
+    {
+      gsize len;
+      const guint8 *data = g_bytes_get_data (bytes, &len);
+      if (len > 0)
+        g_byte_array_append (byte_array, data, len);
+      g_bytes_unref (bytes);
+      bytes = va_arg (args, GBytes *);
+    }
+  va_end (args);
+
+  return g_byte_array_free_to_bytes (byte_array);
+}
+
+static void
+gsk_gl_driver_dispose (GObject *object)
+{
+  GskGLDriver *self = (GskGLDriver *)object;
+
+  g_assert (GSK_IS_GL_DRIVER (self));
+  g_assert (self->in_frame == FALSE);
+
+  if (self->shared_command_queue)
+    gsk_gl_command_queue_make_current (self->shared_command_queue);
+
+#define GSK_GL_NO_UNIFORMS
+#define GSK_GL_SHADER_RESOURCE(name)
+#define GSK_GL_SHADER_STRING(str)
+#define GSK_GL_SHADER_SINGLE(name)
+#define GSK_GL_SHADER_JOINED(kind, ...)
+#define GSK_GL_ADD_UNIFORM(pos, KEY, name)
+#define GSK_GL_DEFINE_PROGRAM(name, resource, uniforms) \
+  GSK_GL_DELETE_PROGRAM(name);                          \
+  GSK_GL_DELETE_PROGRAM(name ## _no_clip);              \
+  GSK_GL_DELETE_PROGRAM(name ## _rect_clip);
+#define GSK_GL_DEFINE_PROGRAM_NO_CLIP(name, resource, uniforms) \
+  GSK_GL_DELETE_PROGRAM(name);
+#define GSK_GL_DELETE_PROGRAM(name)                     \
+  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_SHADER_RESOURCE
+#undef GSK_GL_SHADER_STRING
+#undef GSK_GL_SHADER_SINGLE
+#undef GSK_GL_SHADER_JOINED
+#undef GSK_GL_ADD_UNIFORM
+#undef GSK_GL_DEFINE_PROGRAM
+#undef GSK_GL_DEFINE_PROGRAM_NO_CLIP
+
+G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+  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_gl_driver_shader_weak_cb,
+                               self);
+          g_hash_table_iter_remove (&iter);
+        }
+
+      g_clear_pointer (&self->shader_cache, g_hash_table_unref);
+    }
+G_GNUC_END_IGNORE_DEPRECATIONS
+
+  if (self->command_queue != NULL)
+    {
+      gsk_gl_command_queue_make_current (self->command_queue);
+      gsk_gl_driver_collect_unused_textures (self, 0);
+      g_clear_object (&self->command_queue);
+    }
+
+  if (self->autorelease_framebuffers != NULL &&
+      self->autorelease_framebuffers->len > 0)
+    {
+      glDeleteFramebuffers (self->autorelease_framebuffers->len,
+                            (GLuint *)(gpointer)self->autorelease_framebuffers->data);
+      self->autorelease_framebuffers->len = 0;
+    }
+
+  g_clear_object (&self->glyphs_library);
+  g_clear_object (&self->icons_library);
+  g_clear_object (&self->shadows_library);
+
+  g_clear_pointer (&self->texture_pool, g_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_gl_driver_parent_class)->dispose (object);
+}
+
+static void
+gsk_gl_driver_class_init (GskGLDriverClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->dispose = gsk_gl_driver_dispose;
+}
+
+static void
+gsk_gl_driver_init (GskGLDriver *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);
+  self->texture_pool = g_array_new (FALSE, FALSE, sizeof (guint));
+  self->render_targets = g_ptr_array_new ();
+}
+
+static gboolean
+gsk_gl_driver_load_programs (GskGLDriver  *self,
+                             GError      **error)
+{
+  GskGLCompiler *compiler;
+  gboolean ret = FALSE;
+  G_GNUC_UNUSED gint64 start_time = GDK_PROFILER_CURRENT_TIME;
+
+  g_assert (GSK_IS_GL_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/gl/preamble.glsl");
+  gsk_gl_compiler_set_preamble_from_resource (compiler,
+                                              GSK_GL_COMPILER_VERTEX,
+                                              "/org/gtk/libgsk/gl/preamble.vs.glsl");
+  gsk_gl_compiler_set_preamble_from_resource (compiler,
+                                              GSK_GL_COMPILER_FRAGMENT,
+                                              "/org/gtk/libgsk/gl/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);
+  gsk_gl_compiler_bind_attribute (compiler, "aColor", 2);
+  gsk_gl_compiler_bind_attribute (compiler, "aColor2", 3);
+
+  /* Use XMacros to register all of our programs and their uniforms */
+#define GSK_GL_NO_UNIFORMS
+#define GSK_GL_SHADER_RESOURCE(name)                                                            \
+  g_resources_lookup_data("/org/gtk/libgsk/gl/" name, 0, NULL)
+#define GSK_GL_SHADER_STRING(str)                                                               \
+  g_bytes_new_static(str, strlen(str))
+#define GSK_GL_SHADER_SINGLE(bytes)                                                             \
+  G_STMT_START {                                                                                \
+    GBytes *b = bytes;                                                                          \
+    gsk_gl_compiler_set_source (compiler, GSK_GL_COMPILER_ALL, b);                              \
+    g_bytes_unref (b);                                                                          \
+  } G_STMT_END;
+#define GSK_GL_SHADER_JOINED(kind, ...)                                                         \
+  G_STMT_START {                                                                                \
+    GBytes *bytes = join_sources(__VA_ARGS__);                                                  \
+    gsk_gl_compiler_set_source (compiler, GSK_GL_COMPILER_##kind, bytes);                       \
+    g_bytes_unref (bytes);                                                                      \
+  } G_STMT_END;
+#define GSK_GL_ADD_UNIFORM(pos, KEY, name)                                                      \
+  gsk_gl_program_add_uniform (program, #name, UNIFORM_##KEY);
+#define GSK_GL_DEFINE_PROGRAM(name, sources, uniforms)                                          \
+  gsk_gl_compiler_set_source (compiler, GSK_GL_COMPILER_VERTEX, NULL);                          \
+  gsk_gl_compiler_set_source (compiler, GSK_GL_COMPILER_FRAGMENT, NULL);                        \
+  sources                                                                                       \
+  GSK_GL_COMPILE_PROGRAM(name ## _no_clip, uniforms, "#define NO_CLIP 1\n");                    \
+  GSK_GL_COMPILE_PROGRAM(name ## _rect_clip, uniforms, "#define RECT_CLIP 1\n");                \
+  GSK_GL_COMPILE_PROGRAM(name, uniforms, "");
+#define GSK_GL_DEFINE_PROGRAM_NO_CLIP(name, sources, uniforms)                                  \
+  gsk_gl_compiler_set_source (compiler, GSK_GL_COMPILER_VERTEX, NULL);                          \
+  gsk_gl_compiler_set_source (compiler, GSK_GL_COMPILER_FRAGMENT, NULL);                        \
+  sources                                                                                       \
+  GSK_GL_COMPILE_PROGRAM(name, uniforms, "#define NO_CLIP 1\n");
+#define GSK_GL_COMPILE_PROGRAM(name, uniforms, clip)                                            \
+  G_STMT_START {                                                                                \
+    GskGLProgram *program;                                                                      \
+    gboolean have_alpha;                                                                        \
+    gboolean have_source;                                                                       \
+                                                                                                \
+    if (!(program = gsk_gl_compiler_compile (compiler, #name, clip, 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 (GskGLDriver, name)) =                \
+         g_steal_pointer (&program);                                                            \
+  } G_STMT_END;
+# include "gskglprograms.defs"
+#undef GSK_GL_DEFINE_PROGRAM
+#undef GSK_GL_DEFINE_PROGRAM_NO_CLIP
+#undef GSK_GL_ADD_UNIFORM
+#undef GSK_GL_SHADER_SINGLE
+#undef GSK_GL_SHADER_JOINED
+#undef GSK_GL_SHADER_RESOURCE
+#undef GSK_GL_SHADER_STRING
+#undef GSK_GL_NO_UNIFORMS
+
+  ret = TRUE;
+
+failure:
+  g_clear_object (&compiler);
+
+  gdk_profiler_end_mark (start_time, "Load GL programs", NULL);
+
+  return ret;
+}
+
+/**
+ * gsk_gl_driver_autorelease_framebuffer:
+ * @self: a `GskGLDriver`
+ * @framebuffer_id: the id of the OpenGL framebuffer
+ *
+ * Marks @framebuffer_id to be deleted when the current frame has cmopleted.
+ */
+static void
+gsk_gl_driver_autorelease_framebuffer (GskGLDriver *self,
+                                       guint        framebuffer_id)
+{
+  g_assert (GSK_IS_GL_DRIVER (self));
+
+  g_array_append_val (self->autorelease_framebuffers, framebuffer_id);
+}
+
+static GskGLDriver *
+gsk_gl_driver_new (GskGLCommandQueue  *command_queue,
+                   gboolean            debug_shaders,
+                   GError            **error)
+{
+  GskGLDriver *self;
+  GdkGLContext *context;
+  gint64 before G_GNUC_UNUSED;
+
+  g_return_val_if_fail (GSK_IS_GL_COMMAND_QUEUE (command_queue), NULL);
+
+  before = GDK_PROFILER_CURRENT_TIME;
+
+  context = gsk_gl_command_queue_get_context (command_queue);
+
+  gdk_gl_context_make_current (context);
+
+  self = g_object_new (GSK_TYPE_GL_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_gl_driver_load_programs (self, error))
+    {
+      g_object_unref (self);
+      return NULL;
+    }
+
+  self->glyphs_library = gsk_gl_glyph_library_new (self);
+  self->icons_library = gsk_gl_icon_library_new (self);
+  self->shadows_library = gsk_gl_shadow_library_new (self);
+
+  gdk_profiler_end_mark (before, "Create GL driver", NULL);
+
+  return g_steal_pointer (&self);
+}
+
+static void
+free_driver (GskGLDriver *driver)
+{
+  g_object_run_dispose (G_OBJECT (driver));
+  g_object_unref (driver);
+}
+
+static void
+display_closed (GdkDisplay *display)
+{
+  g_object_set_data (G_OBJECT (display), "GSK_GL_DRIVER", NULL);
+}
+
+/**
+ * gsk_gl_driver_for_display:
+ * @display: A #GdkDisplay that is known to support GL
+ * @debug_shaders: if debug information for shaders should be displayed
+ * @error: location for error information
+ *
+ * Retrieves a driver for a shared display. Generally this is shared across all GL
+ * contexts for a display so that fewer programs are necessary for driving output.
+ *
+ * Returns: (transfer full): a `GskGLDriver` if successful; otherwise %NULL and
+ *   @error is set.
+ */
+GskGLDriver *
+gsk_gl_driver_for_display (GdkDisplay  *display,
+                           gboolean     debug_shaders,
+                           GError     **error)
+{
+  GdkGLContext *context;
+  GskGLCommandQueue *command_queue = NULL;
+  GskGLDriver *driver;
+
+  g_return_val_if_fail (GDK_IS_DISPLAY (display), NULL);
+
+  if ((driver = g_object_get_data (G_OBJECT (display), "GSK_GL_DRIVER")))
+    return g_object_ref (driver);
+
+  context = gdk_display_get_gl_context (display);
+  g_assert (context);
+
+  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_gl_driver_new (command_queue, debug_shaders, error)))
+    goto failure;
+
+  g_object_set_data_full (G_OBJECT (display),
+                          "GSK_GL_DRIVER",
+                          g_object_ref (driver),
+                          (GDestroyNotify) free_driver);
+  g_signal_connect (display, "closed", G_CALLBACK (display_closed), NULL);
+
+failure:
+  g_clear_object (&command_queue);
+
+  return g_steal_pointer (&driver);
+}
+
+/**
+ * gsk_gl_driver_begin_frame:
+ * @self: a `GskGLDriver`
+ * @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_gl_driver_begin_frame (GskGLDriver       *self,
+                           GskGLCommandQueue *command_queue)
+{
+  gint64 last_frame_id;
+
+  g_return_if_fail (GSK_IS_GL_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);
+
+  /* Mark unused pixel regions of the atlases */
+  gsk_gl_texture_library_begin_frame (GSK_GL_TEXTURE_LIBRARY (self->icons_library),
+                                      self->current_frame_id);
+  gsk_gl_texture_library_begin_frame (GSK_GL_TEXTURE_LIBRARY (self->glyphs_library),
+                                      self->current_frame_id);
+
+  /* Cleanup old shadows */
+  gsk_gl_shadow_library_begin_frame (self->shadows_library);
+
+  /* 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_gl_driver_collect_unused_textures (self, last_frame_id - 1);
+}
+
+/**
+ * gsk_gl_driver_end_frame:
+ * @self: a `GskGLDriver`
+ *
+ * Clean up resources from drawing the current frame.
+ *
+ * Temporary resources used while drawing will be released.
+ */
+void
+gsk_gl_driver_end_frame (GskGLDriver *self)
+{
+  g_return_if_fail (GSK_IS_GL_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);
+
+  self->in_frame = FALSE;
+}
+
+/**
+ * gsk_gl_driver_after_frame:
+ * @self: a `GskGLDriver`
+ *
+ * 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_gl_driver_after_frame (GskGLDriver *self)
+{
+  g_return_if_fail (GSK_IS_GL_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_gl_driver_autorelease_framebuffer (self, render_target->framebuffer_id);
+      gsk_gl_driver_autorelease_texture (self, render_target->texture_id);
+      g_free (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 */
+  if (self->texture_pool->len > 0)
+    {
+      glDeleteTextures (self->texture_pool->len,
+                        (GLuint *)(gpointer)self->texture_pool->data);
+      self->texture_pool->len = 0;
+    }
+
+  /* Reset command queue to our shared queue in case 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_gl_driver_get_context (GskGLDriver *self)
+{
+  g_return_val_if_fail (GSK_IS_GL_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_gl_driver_cache_texture:
+ * @self: a `GskGLDriver`
+ * @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_gl_driver_lookup_texture().
+ *
+ * Textures that have not been used within a number of frames will be
+ * purged from the texture cache automatically.
+ */
+void
+gsk_gl_driver_cache_texture (GskGLDriver         *self,
+                             const GskTextureKey *key,
+                             guint                texture_id)
+{
+  g_assert (GSK_IS_GL_DRIVER (self));
+  g_assert (key != NULL);
+  g_assert (texture_id > 0);
+  g_assert (g_hash_table_contains (self->textures, GUINT_TO_POINTER (texture_id)));
+
+  if (!g_hash_table_contains (self->key_to_texture_id, key))
+    {
+      GskTextureKey *k;
+
+      k = g_memdup2 (key, sizeof *key);
+
+      g_assert (!g_hash_table_contains (self->texture_id_to_key, GUINT_TO_POINTER (texture_id)));
+      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);
+    }
+}
+
+#if defined(HAVE_DMABUF) && defined (HAVE_EGL)
+static void
+set_viewport_for_size (GskGLDriver  *self,
+                       GskGLProgram *program,
+                       float         width,
+                       float         height)
+{
+  float viewport[4] = { 0, 0, width, height };
+
+  gsk_gl_uniform_state_set4fv (program->uniforms,
+                               program->program_info,
+                               UNIFORM_SHARED_VIEWPORT, 0,
+                               1,
+                               (const float *)&viewport);
+  self->stamps[UNIFORM_SHARED_VIEWPORT]++;
+}
+
+#define ORTHO_NEAR_PLANE   -10000
+#define ORTHO_FAR_PLANE     10000
+
+static void
+set_projection_for_size (GskGLDriver  *self,
+                         GskGLProgram *program,
+                         float         width,
+                         float         height)
+{
+  graphene_matrix_t projection;
+
+  graphene_matrix_init_ortho (&projection, 0, width, 0, height, ORTHO_NEAR_PLANE, ORTHO_FAR_PLANE);
+  graphene_matrix_scale (&projection, 1, -1, 1);
+
+  gsk_gl_uniform_state_set_matrix (program->uniforms,
+                                   program->program_info,
+                                   UNIFORM_SHARED_PROJECTION, 0,
+                                   &projection);
+  self->stamps[UNIFORM_SHARED_PROJECTION]++;
+}
+
+static void
+reset_modelview (GskGLDriver  *self,
+                 GskGLProgram *program)
+{
+  graphene_matrix_t modelview;
+
+  graphene_matrix_init_identity (&modelview);
+
+  gsk_gl_uniform_state_set_matrix (program->uniforms,
+                                   program->program_info,
+                                   UNIFORM_SHARED_MODELVIEW, 0,
+                                   &modelview);
+  self->stamps[UNIFORM_SHARED_MODELVIEW]++;
+}
+
+static void
+draw_rect (GskGLCommandQueue *command_queue,
+           float              min_x,
+           float              min_y,
+           float              max_x,
+           float              max_y)
+{
+  GskGLDrawVertex *vertices = gsk_gl_command_queue_add_vertices (command_queue);
+  float min_u = 0;
+  float max_u = 1;
+  float min_v = 1;
+  float max_v = 0;
+  guint16 c = FP16_ZERO;
+
+  vertices[0] = (GskGLDrawVertex) { .position = { min_x, min_y }, .uv = { min_u, min_v }, .color = { c, c, c, c } };
+  vertices[1] = (GskGLDrawVertex) { .position = { min_x, max_y }, .uv = { min_u, max_v }, .color = { c, c, c, c } };
+  vertices[2] = (GskGLDrawVertex) { .position = { max_x, min_y }, .uv = { max_u, min_v }, .color = { c, c, c, c } };
+  vertices[3] = (GskGLDrawVertex) { .position = { max_x, max_y }, .uv = { max_u, max_v }, .color = { c, c, c, c } };
+  vertices[4] = (GskGLDrawVertex) { .position = { min_x, max_y }, .uv = { min_u, max_v }, .color = { c, c, c, c } };
+  vertices[5] = (GskGLDrawVertex) { .position = { max_x, min_y }, .uv = { max_u, min_v }, .color = { c, c, c, c } };
+}
+
+static unsigned int release_render_target (GskGLDriver       *self,
+                                           GskGLRenderTarget *render_target,
+                                           gboolean           release_texture,
+                                           gboolean           cache_texture);
+
+static guint
+gsk_gl_driver_import_dmabuf_texture (GskGLDriver      *self,
+                                     GdkDmabufTexture *texture)
+{
+  GdkGLContext *context = self->command_queue->context;
+  int max_texture_size = self->command_queue->max_texture_size;
+  const GdkDmabuf *dmabuf;
+  guint texture_id;
+  int width, height;
+  GskGLProgram *program;
+  GskGLRenderTarget *render_target;
+  guint prev_fbo;
+  gboolean external;
+  GdkMemoryFormat format;
+  gboolean premultiply;
+
+  gdk_gl_context_make_current (context);
+
+  width = gdk_texture_get_width (GDK_TEXTURE (texture));
+  height = gdk_texture_get_height (GDK_TEXTURE (texture));
+
+  if (width > max_texture_size || height > max_texture_size)
+    {
+      GDK_DISPLAY_DEBUG (gdk_gl_context_get_display (context), DMABUF,
+                         "Can't import dmabuf bigger than MAX_TEXTURE_SIZE (%d)",
+                         max_texture_size);
+      return 0;
+    }
+
+  dmabuf = gdk_dmabuf_texture_get_dmabuf (texture);
+  format = gdk_texture_get_format (GDK_TEXTURE (texture));
+  premultiply = gdk_memory_format_alpha (format) == GDK_MEMORY_ALPHA_STRAIGHT;
+
+  texture_id = gdk_gl_context_import_dmabuf (context,
+                                             width, height,
+                                             dmabuf,
+                                             &external);
+  if (texture_id == 0)
+    return 0;
+
+  if (!external && !premultiply)
+    return texture_id;
+
+  gsk_gl_driver_autorelease_texture (self, texture_id);
+
+  if (external)
+    program = self->external;
+  else
+    program = self->premultiply;
+
+  if (!gsk_gl_driver_create_render_target (self, width, height, GL_RGBA8, &render_target))
+    return texture_id;
+
+  prev_fbo = gsk_gl_command_queue_bind_framebuffer (self->command_queue, render_target->framebuffer_id);
+  gsk_gl_command_queue_clear (self->command_queue, 0, &GRAPHENE_RECT_INIT (0, 0, width, height));
+
+  if (gsk_gl_command_queue_begin_draw (self->command_queue, program->program_info, width, height))
+    {
+      set_projection_for_size (self, program, width, height);
+      set_viewport_for_size (self, program, width, height);
+      reset_modelview (self, program);
+
+      if (external)
+        {
+          gsk_gl_program_set_uniform_texture (program,
+                                              UNIFORM_EXTERNAL_SOURCE, 0,
+                                              GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE0, texture_id);
+
+          gsk_gl_program_set_uniform1i (program, UNIFORM_PREMULTIPLY, 0, premultiply);
+        }
+      else
+        {
+          gsk_gl_program_set_uniform_texture (program,
+                                              UNIFORM_SHARED_SOURCE, 0,
+                                              GL_TEXTURE_2D, GL_TEXTURE0, texture_id);
+        }
+
+      draw_rect (self->command_queue, 0, 0, width, height);
+
+      gsk_gl_command_queue_end_draw (self->command_queue);
+    }
+
+  gsk_gl_command_queue_bind_framebuffer (self->command_queue, prev_fbo);
+
+  return release_render_target (self, render_target, FALSE, FALSE);
+}
+
+#else
+
+static guint
+gsk_gl_driver_import_dmabuf_texture (GskGLDriver      *self,
+                                     GdkDmabufTexture *texture)
+{
+  return 0;
+}
+
+#endif /* HAVE_DMABUF && HAVE_EGL */
+
+/**
+ * gsk_gl_driver_load_texture:
+ * @self: a `GdkTexture`
+ * @texture: a `GdkTexture`
+ * @ensure_mipmap: Mipmaps for this texture must exist for downscaling
+ *
+ * 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_gl_driver_load_texture (GskGLDriver *self,
+                            GdkTexture  *texture,
+                            gboolean     ensure_mipmap)
+{
+  GdkGLContext *context;
+  GdkMemoryTexture *downloaded_texture;
+  GskGLTexture *t;
+  guint texture_id;
+  int height;
+  int width;
+  gboolean can_mipmap = FALSE;
+
+  g_return_val_if_fail (GSK_IS_GL_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;
+
+  texture_id = 0;
+  downloaded_texture = NULL;
+
+  t = gdk_texture_get_render_data (texture, self);
+  if (t && t->texture_id)
+    {
+      if (ensure_mipmap && t->can_mipmap && !t->has_mipmap)
+        {
+          glBindTexture (GL_TEXTURE_2D, t->texture_id);
+          glGenerateMipmap (GL_TEXTURE_2D);
+          t->has_mipmap = TRUE;
+        }
+
+      if (!ensure_mipmap || t->has_mipmap)
+        return t->texture_id;
+
+      gdk_texture_clear_render_data (texture);
+    }
+
+  if (GDK_IS_DMABUF_TEXTURE (texture) && !ensure_mipmap)
+    {
+      texture_id = gsk_gl_driver_import_dmabuf_texture (self, GDK_DMABUF_TEXTURE (texture));
+    }
+  else if (GDK_IS_GL_TEXTURE (texture))
+    {
+      GdkGLTexture *gl_texture = (GdkGLTexture *) texture;
+      GdkGLContext *texture_context = gdk_gl_texture_get_context (gl_texture);
+
+      if (gdk_gl_context_is_shared (context, texture_context) &&
+          (!ensure_mipmap || gdk_gl_texture_has_mipmap (gl_texture)) &&
+          gdk_memory_format_alpha (gdk_texture_get_format (texture)) != GDK_MEMORY_ALPHA_STRAIGHT)
+        {
+          /* A GL texture from the same GL context is a simple task... */
+          return gdk_gl_texture_get_id (gl_texture);
+        }
+    }
+
+  if (texture_id == 0)
+    {
+      downloaded_texture = gdk_memory_texture_from_texture (texture);
+
+      /* The download_texture() call may have switched the GL context. Make sure
+       * the right context is at work again.
+       */
+      gdk_gl_context_make_current (context);
+
+      texture_id = gsk_gl_command_queue_upload_texture (self->command_queue, GDK_TEXTURE (downloaded_texture), ensure_mipmap, &can_mipmap);
+    }
+
+  width = gdk_texture_get_width (texture);
+  height = gdk_texture_get_height (texture);
+
+  t = gsk_gl_texture_new (texture_id,
+                          width, height,
+                          self->current_frame_id);
+  t->can_mipmap = can_mipmap;
+  if (ensure_mipmap)
+    {
+      g_assert (can_mipmap);
+      glBindTexture (GL_TEXTURE_2D, t->texture_id);
+      glGenerateMipmap (GL_TEXTURE_2D);
+      t->has_mipmap = TRUE;
+    }
+
+  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 t->texture_id;
+}
+
+/**
+ * gsk_gl_driver_create_texture:
+ * @self: a `GskGLDriver`
+ * @width: the width of the texture
+ * @height: the height of the texture
+ * @format: format for the texture
+ *
+ * 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.
+ *
+ * Typical examples for @format are GL_RGBA8, GL_RGBA16F or GL_RGBA32F.
+ *
+ * Use gsk_gl_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_gl_driver_release_texture().
+ */
+GskGLTexture *
+gsk_gl_driver_create_texture (GskGLDriver *self,
+                              float        width,
+                              float        height,
+                              int          format)
+{
+  GskGLTexture *texture;
+  guint texture_id;
+
+  g_return_val_if_fail (GSK_IS_GL_DRIVER (self), NULL);
+
+  texture_id = gsk_gl_command_queue_create_texture (self->command_queue,
+                                                    width, height,
+                                                    format);
+  texture = gsk_gl_texture_new (texture_id,
+                                width, height,
+                                self->current_frame_id);
+  g_hash_table_insert (self->textures,
+                       GUINT_TO_POINTER (texture->texture_id),
+                       texture);
+
+  return texture;
+}
+
+/**
+ * gsk_gl_driver_release_texture:
+ * @self: a `GskGLDriver`
+ * @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_gl_driver_release_texture (GskGLDriver  *self,
+                               GskGLTexture *texture)
+{
+  guint texture_id;
+
+  g_assert (GSK_IS_GL_DRIVER (self));
+  g_assert (texture != NULL);
+
+  texture_id = texture->texture_id;
+  texture->texture_id = 0;
+  gsk_gl_texture_free (texture);
+
+  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_driver_autorelease_texture (self, texture_id);
+}
+
+/**
+ * gsk_gl_driver_create_render_target:
+ * @self: a `GskGLDriver`
+ * @width: the width for the render target
+ * @height: the height for the render target
+ * @format: the format to use
+ * @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.
+ *
+ * Typical examples for @format are GK_RGBA8, GL_RGBA16F or GL_RGBA32F.
+ *
+ * Use gsk_gl_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_gl_driver_create_render_target (GskGLDriver        *self,
+                                    int                 width,
+                                    int                 height,
+                                    int                 format,
+                                    GskGLRenderTarget **out_render_target)
+{
+  guint framebuffer_id;
+  guint texture_id;
+
+  g_return_val_if_fail (GSK_IS_GL_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)
+            {
+              *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,
+                                                  format,
+                                                  &framebuffer_id, &texture_id))
+    {
+      GskGLRenderTarget *render_target;
+
+      render_target = g_new0 (GskGLRenderTarget, 1);
+      render_target->format = format;
+      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;
+}
+
+static unsigned int
+release_render_target (GskGLDriver       *self,
+                       GskGLRenderTarget *render_target,
+                       gboolean           release_texture,
+                       gboolean           cache_texture)
+{
+  guint texture_id;
+
+  g_return_val_if_fail (GSK_IS_GL_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
+    {
+      texture_id = render_target->texture_id;
+
+      if (cache_texture)
+        {
+          GskGLTexture *texture;
+
+          texture = gsk_gl_texture_new (render_target->texture_id,
+                                        render_target->width,
+                                        render_target->height,
+                                        self->current_frame_id);
+          g_hash_table_insert (self->textures,
+                               GUINT_TO_POINTER (texture_id),
+                               g_steal_pointer (&texture));
+        }
+
+      gsk_gl_driver_autorelease_framebuffer (self, render_target->framebuffer_id);
+      g_free (render_target);
+
+    }
+
+  return texture_id;
+}
+
+/**
+ * gsk_gl_driver_release_render_target:
+ * @self: a `GskGLDriver`
+ * @render_target: a `GskGLRenderTarget` created with
+ *   gsk_gl_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_gl_driver_release_render_target (GskGLDriver       *self,
+                                     GskGLRenderTarget *render_target,
+                                     gboolean           release_texture)
+{
+  return release_render_target (self, render_target, release_texture, TRUE);
+}
+
+G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+
+/**
+ * gsk_gl_driver_lookup_shader:
+ * @self: a `GskGLDriver`
+ * @shader: the shader to lookup or load
+ * @error: a location for a `GError`
+ *
+ * Attempts to load @shader from the shader cache.
+ *
+ * If it has not been loaded, then it will compile the shader on demand.
+ *
+ * Returns: (nullable) (transfer none): a `GskGLShader`
+ */
+GskGLProgram *
+gsk_gl_driver_lookup_shader (GskGLDriver  *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 > GSK_GL_PROGRAM_MAX_CUSTOM_ARGS)
+        {
+          g_set_error (error,
+                       GDK_GL_ERROR,
+                       GDK_GL_ERROR_UNSUPPORTED_FORMAT,
+                       "Tried to use %d uniforms, while only %d is supported",
+                       n_uniforms,
+                       GSK_GL_PROGRAM_MAX_CUSTOM_ARGS);
+          return NULL;
+        }
+
+      n_required_textures = gsk_gl_shader_get_n_textures (shader);
+      if (n_required_textures > GSK_GL_PROGRAM_MAX_CUSTOM_TEXTURES)
+        {
+          g_set_error (error,
+                       GDK_GL_ERROR,
+                       GDK_GL_ERROR_UNSUPPORTED_FORMAT,
+                       "Tried to use %d textures, while only %d is supported",
+                       n_required_textures,
+                       GSK_GL_PROGRAM_MAX_CUSTOM_TEXTURES);
+          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/gl/preamble.glsl");
+      gsk_gl_compiler_set_preamble_from_resource (compiler,
+                                                  GSK_GL_COMPILER_VERTEX,
+                                                  "/org/gtk/libgsk/gl/preamble.vs.glsl");
+      gsk_gl_compiler_set_preamble_from_resource (compiler,
+                                                  GSK_GL_COMPILER_FRAGMENT,
+                                                  "/org/gtk/libgsk/gl/preamble.fs.glsl");
+      gsk_gl_compiler_set_source_from_resource (compiler,
+                                                GSK_GL_COMPILER_ALL,
+                                                "/org/gtk/libgsk/gl/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);
+      gsk_gl_compiler_bind_attribute (compiler, "aColor", 2);
+      gsk_gl_compiler_bind_attribute (compiler, "aColor2", 3);
+
+      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);
+
+          /* Custom arguments (max is 8) */
+          for (guint i = 0; i < n_uniforms; i++)
+            gsk_gl_program_add_uniform (program, uniforms[i].name, UNIFORM_CUSTOM_ARG0+i);
+
+          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_gl_driver_shader_weak_cb,
+                             self);
+        }
+
+      g_object_unref (compiler);
+    }
+
+  return program;
+}
+
+G_GNUC_END_IGNORE_DEPRECATIONS
+
+#if 0
+void
+gsk_gl_driver_save_texture_to_png (GskGLDriver       *driver,
+                                   int                texture_id,
+                                   int                width,
+                                   int                height,
+                                   const char        *filename)
+{
+  GdkGLTextureBuilder *builder;
+  GdkTexture *texture;
+
+  builder = gdk_gl_texture_builder_new ();
+  gdk_gl_texture_builder_set_context (builder, gsk_gl_driver_get_context (driver));
+  gdk_gl_texture_builder_set_id (builder, texture_id);
+  gdk_gl_texture_builder_set_width (builder, width);
+  gdk_gl_texture_builder_set_height (builder, height);
+
+  texture = gdk_gl_texture_builder_build (builder, NULL, NULL);
+  gdk_texture_save_to_png (texture, filename);
+
+  g_object_unref (texture);
+  g_object_unref (builder);
+}
+
+void
+gsk_gl_driver_save_atlases_to_png (GskGLDriver *self,
+                                   const char  *directory)
+{
+  GPtrArray *atlases;
+
+  g_return_if_fail (GSK_IS_GL_DRIVER (self));
+
+  if (directory == NULL)
+    directory = ".";
+
+#define copy_atlases(dst, library) \
+  g_ptr_array_extend(dst, GSK_GL_TEXTURE_LIBRARY(library)->atlases, NULL, NULL)
+  atlases = g_ptr_array_new ();
+  copy_atlases (atlases, self->glyphs_library);
+  copy_atlases (atlases, self->icons_library);
+#undef copy_atlases
+
+  for (guint i = 0; i < atlases->len; i++)
+    {
+      GskGLTextureAtlas *atlas = g_ptr_array_index (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);
+      gsk_gl_driver_save_texture_to_png (self, atlas->texture_id, atlas->width, atlas->height, filename);
+      g_free (filename);
+    }
+
+  g_ptr_array_unref (atlases);
+}
+#endif
+
+GskGLCommandQueue *
+gsk_gl_driver_create_command_queue (GskGLDriver *self,
+                                     GdkGLContext *context)
+{
+  g_return_val_if_fail (GSK_IS_GL_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_gl_driver_add_texture_slices (GskGLDriver        *self,
+                                  GdkTexture         *texture,
+                                  gboolean            ensure_mipmap,
+                                  GskGLTextureSlice **out_slices,
+                                  guint              *out_n_slices)
+{
+  int max_slice_size;
+  GskGLTextureSlice *slices;
+  GskGLTexture *t;
+  guint n_slices;
+  guint cols;
+  guint rows;
+  int tex_width;
+  int tex_height;
+  int x = 0, y = 0;
+  GdkMemoryTexture *memtex;
+  GdkMemoryFormat format;
+  gsize bpp;
+  int extra_pixels;
+  GdkMemoryTexture *memtex1 = NULL;
+  GdkMemoryTexture *memtex2 = NULL;
+  GdkMemoryTexture *memtex3 = NULL;
+  GdkMemoryTexture *memtex4 = NULL;
+  gboolean can_mipmap = TRUE, slice_can_mipmap;
+
+  g_assert (GSK_IS_GL_DRIVER (self));
+  g_assert (GDK_IS_TEXTURE (texture));
+  g_assert (out_slices != NULL);
+  g_assert (out_n_slices != NULL);
+
+  max_slice_size = self->command_queue->max_texture_size / 2;
+  tex_width = texture->width;
+  tex_height = texture->height;
+
+  cols = (texture->width / max_slice_size) + 1;
+  rows = (texture->height / max_slice_size) + 1;
+
+  n_slices = cols * rows;
+
+  t = gdk_texture_get_render_data (texture, self);
+
+  if (t)
+    {
+      if (t->n_slices == n_slices &&
+          (t->has_mipmap || !ensure_mipmap))
+        {
+          *out_slices = t->slices;
+          *out_n_slices = t->n_slices;
+          return;
+        }
+
+      gdk_texture_clear_render_data (texture);
+    }
+
+  slices = g_new0 (GskGLTextureSlice, n_slices);
+  memtex = gdk_memory_texture_from_texture (texture);
+
+  if (ensure_mipmap)
+    {
+      guchar *data1, *data2, *data3, *data4;
+      guchar *top_row, *bot_row, *left_row, *right_row;
+      GdkTexture *tmp;
+      int w;
+      GBytes *bytes;
+
+      /* We need some extra pixels around our tiles, in order for
+       * GL to properly determine the right level of detail to use.
+       * This number should probably depend on the scale, but for
+       * now we just hardcode it.
+       *
+       * We create some auxiliary textures to hold the extra pixels:
+       *
+       *    +---------------------+
+       *    | memtex1             |
+       *    *---+-------------+---+
+       *    |   |             |   |
+       * memtex2|   memtex    |memtex3
+       *    |   |             |   |
+       *    *---+-------------+---+
+       *    | memtex4         |   |
+       *    +---------------------+
+       */
+
+      extra_pixels = 15;
+
+      top_row = g_malloc (4 * tex_width);
+      tmp = gdk_memory_texture_new_subtexture (memtex, 0, 0, tex_width, 1);
+      gdk_texture_download (tmp, top_row, 4 * tex_width);
+      g_object_unref (tmp);
+
+      bot_row = g_malloc (4 * tex_width);
+      tmp = gdk_memory_texture_new_subtexture (memtex, 0, tex_height - 1, tex_width, 1);
+      gdk_texture_download (tmp, bot_row, 4 * tex_width);
+      g_object_unref (tmp);
+
+      left_row = g_malloc (4 * tex_height);
+      tmp = gdk_memory_texture_new_subtexture (memtex, 0, 0, 1, tex_height);
+      gdk_texture_download (tmp, left_row, 4);
+      g_object_unref (tmp);
+
+      right_row = g_malloc (4 * tex_height);
+      tmp = gdk_memory_texture_new_subtexture (memtex, tex_width - 1, 0, 1, tex_height);
+      gdk_texture_download (tmp, right_row, 4);
+      g_object_unref (tmp);
+
+      w = tex_width + 2 * extra_pixels;
+
+      data1 = g_malloc (4 * w * extra_pixels);
+      data2 = g_malloc (4 * extra_pixels * tex_height);
+      data3 = g_malloc (4 * extra_pixels * tex_height);
+      data4 = g_malloc (4 * w * extra_pixels);
+
+      format = gdk_texture_get_format (GDK_TEXTURE (memtex));
+      bpp = gdk_memory_format_get_plane_block_bytes (format, 0);
+
+      for (int i = 0; i < w; i++)
+        {
+          int ii = CLAMP (i, extra_pixels, (tex_width - 1) + extra_pixels) - extra_pixels;
+
+          for (int j = 0; j < extra_pixels; j++)
+            {
+              for (int k = 0; k < bpp; k++)
+                {
+                  data1[(j * w + i) * 4 + k] = top_row[ii * 4 + k];
+                  data4[(j * w + i) * 4 + k] = bot_row[ii * 4 + k];
+                }
+            }
+        }
+
+      for (int i = 0; i < extra_pixels; i++)
+        {
+          for (int j = 0; j < tex_height; j++)
+            {
+              for (int k = 0; k < bpp; k++)
+                {
+                  data2[(j * extra_pixels + i) * 4 + k] = left_row[j * 4 + k];
+                  data3[(j * extra_pixels + i) * 4 + k] = right_row[j * 4 + k];
+                }
+            }
+        }
+
+      g_free (top_row);
+      g_free (bot_row);
+      g_free (left_row);
+      g_free (right_row);
+
+      bytes = g_bytes_new_take (data1, 4 * w * extra_pixels);
+      memtex1 = GDK_MEMORY_TEXTURE (gdk_memory_texture_new (w, extra_pixels, format, bytes, 4 * w));
+      g_bytes_unref (bytes);
+
+      bytes = g_bytes_new_take (data2, 4 * extra_pixels * tex_height);
+      memtex2 = GDK_MEMORY_TEXTURE (gdk_memory_texture_new (extra_pixels, tex_height, format, bytes, 4 * extra_pixels));
+      g_bytes_unref (bytes);
+
+      bytes = g_bytes_new_take (data3, 4 * extra_pixels * tex_height);
+      memtex3 = GDK_MEMORY_TEXTURE (gdk_memory_texture_new (extra_pixels, tex_height, format, bytes, 4 * extra_pixels));
+      g_bytes_unref (bytes);
+
+      bytes = g_bytes_new_take (data4, 4 * w * extra_pixels);
+      memtex4 = GDK_MEMORY_TEXTURE (gdk_memory_texture_new (w, extra_pixels, format, bytes, 4 * w));
+      g_bytes_unref (bytes);
+    }
+  else
+    extra_pixels = 0;
+
+  x = 0;
+  for (guint col = 0; col < cols; col++)
+    {
+      int slice_width = col + 1 < cols ? tex_width / cols : tex_width - x;
+
+      y = 0;
+      for (guint row = 0; row < rows; row++)
+        {
+          int slice_height = row + 1 < rows ? tex_height / rows : tex_height - y;
+          int slice_index = (col * rows) + row;
+          guint texture_id;
+
+          if (ensure_mipmap)
+            {
+              GskGLTextureChunk chunks[5];
+              unsigned int n_chunks = 0;
+
+              if (row == 0)
+                {
+                  chunks[n_chunks].texture = gdk_memory_texture_new_subtexture (memtex1,
+                                                                                x, 0,
+                                                                                slice_width + 2 * extra_pixels, extra_pixels);
+                  chunks[n_chunks].x = 0;
+                  chunks[n_chunks].y = 0;
+                  n_chunks++;
+                }
+
+              if (row == rows - 1)
+                {
+                  chunks[n_chunks].texture = gdk_memory_texture_new_subtexture (memtex4,
+                                                                                x, 0,
+                                                                                slice_width + 2 * extra_pixels, extra_pixels);
+                  chunks[n_chunks].x = 0;
+                  chunks[n_chunks].y = slice_height + extra_pixels;
+                  n_chunks++;
+                }
+
+              if (col == 0)
+                {
+                  int yy = y - extra_pixels;
+                  int hh = slice_height + 2 * extra_pixels;
+                  int y0 = 0;
+
+                  if (row == 0)
+                    {
+                      yy = 0;
+                      y0 = extra_pixels;
+                      hh -= extra_pixels;
+                    }
+                  if (row == rows - 1)
+                    {
+                      hh -= extra_pixels;
+                    }
+
+                  chunks[n_chunks].texture = gdk_memory_texture_new_subtexture (memtex2,
+                                                                                0, yy, extra_pixels, hh);
+                  chunks[n_chunks].x = 0;
+                  chunks[n_chunks].y = y0;
+                  n_chunks++;
+                }
+
+              if (col == cols - 1)
+                {
+                  int yy = y - extra_pixels;
+                  int hh = slice_height + 2 * extra_pixels;
+                  int y0 = 0;
+
+                  if (row == 0)
+                    {
+                      yy = 0;
+                      y0 = extra_pixels;
+                      hh -= extra_pixels;
+                    }
+                  if (row == rows - 1)
+                    {
+                      hh -= extra_pixels;
+                    }
+
+                  chunks[n_chunks].texture = gdk_memory_texture_new_subtexture (memtex3,
+                                                                                0, yy, extra_pixels, hh);
+                  chunks[n_chunks].x = slice_width + extra_pixels;
+                  chunks[n_chunks].y = y0;
+                  n_chunks++;
+                }
+
+              {
+                int xx = x - extra_pixels;
+                int yy = y - extra_pixels;
+                int ww = slice_width + 2 * extra_pixels;
+                int hh = slice_height + 2 * extra_pixels;
+                int x0 = 0;
+                int y0 = 0;
+                if (col == 0)
+                  {
+                    xx = 0;
+                    ww -= extra_pixels;
+                    x0 = extra_pixels;
+                  }
+                if (col == cols - 1)
+                  {
+                    ww -= extra_pixels;
+                  }
+                if (row == 0)
+                  {
+                    yy = 0;
+                    hh -= extra_pixels;
+                    y0 = extra_pixels;
+                  }
+                if (row == rows - 1)
+                  {
+                    hh -= extra_pixels;
+                  }
+
+                chunks[n_chunks].texture = gdk_memory_texture_new_subtexture (memtex, xx, yy, ww, hh);
+                chunks[n_chunks].x = x0;
+                chunks[n_chunks].y = y0;
+                n_chunks++;
+              }
+
+              texture_id = gsk_gl_command_queue_upload_texture_chunks (self->command_queue, TRUE, n_chunks, chunks, &slice_can_mipmap);
+
+              glBindTexture (GL_TEXTURE_2D, texture_id);
+              glGenerateMipmap (GL_TEXTURE_2D);
+
+              for (unsigned int i = 0; i < n_chunks; i++)
+                g_object_unref (chunks[i].texture);
+            }
+          else
+            {
+              GdkTexture *subtex;
+
+              subtex = gdk_memory_texture_new_subtexture (memtex, x, y, slice_width, slice_height);
+              texture_id = gsk_gl_command_queue_upload_texture (self->command_queue, subtex, FALSE, &slice_can_mipmap);
+              g_object_unref (subtex);
+            }
+
+          can_mipmap &= slice_can_mipmap;
+
+          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;
+          slices[slice_index].area.x = extra_pixels / (float) (slice_width + 2 * extra_pixels);
+          slices[slice_index].area.y = extra_pixels / (float) (slice_height + 2 * extra_pixels);
+          slices[slice_index].area.x2 = (extra_pixels + slice_width) / (float) (slice_width + 2 * extra_pixels);
+          slices[slice_index].area.y2 = (extra_pixels + slice_height) / (float) (slice_height + 2 * extra_pixels);
+
+          y += slice_height;
+        }
+
+      x += slice_width;
+    }
+
+  g_object_unref (memtex);
+  g_clear_object (&memtex1);
+  g_clear_object (&memtex2);
+  g_clear_object (&memtex3);
+  g_clear_object (&memtex4);
+
+  /* Allocate one Texture for the entire thing. */
+  t = gsk_gl_texture_new (0,
+                          tex_width, tex_height,
+                          self->current_frame_id);
+  t->can_mipmap = can_mipmap;
+  t->has_mipmap = ensure_mipmap;
+
+  /* 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_gl_driver_mark_texture_permanent (GskGLDriver *self,
+                                      guint        texture_id)
+{
+  GskGLTexture *t;
+
+  g_return_val_if_fail (GSK_IS_GL_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_gl_driver_release_texture_by_id (GskGLDriver *self,
+                                     guint        texture_id)
+{
+  GskGLTexture *texture;
+
+  g_return_if_fail (GSK_IS_GL_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_gl_driver_release_texture (self, texture);
+}
+
+typedef struct _GskGLTextureState
+{
+  GdkGLContext *context;
+  GLuint        texture_id;
+  GLsync        sync;
+} GskGLTextureState;
+
+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);
+  if (state->sync)
+    glDeleteSync (state->sync);
+  g_clear_object (&state->context);
+  g_free (state);
+}
+
+GdkTexture *
+gsk_gl_driver_create_gdk_texture (GskGLDriver     *self,
+                                  guint            texture_id,
+                                  GdkMemoryFormat  format)
+{
+  GskGLTextureState *state;
+  GdkGLTextureBuilder *builder;
+  GskGLTexture *texture;
+  GdkTexture *result;
+
+  g_return_val_if_fail (GSK_IS_GL_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_new0 (GskGLTextureState, 1);
+  state->texture_id = texture_id;
+  state->context = g_object_ref (self->shared_command_queue->context);
+  if (gdk_gl_context_has_feature (state->context, GDK_GL_FEATURE_SYNC))
+    state->sync = glFenceSync (GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
+
+  g_hash_table_steal (self->textures, GUINT_TO_POINTER (texture_id));
+
+  builder = gdk_gl_texture_builder_new ();
+  gdk_gl_texture_builder_set_context (builder, self->shared_command_queue->context);
+  gdk_gl_texture_builder_set_id (builder, texture_id);
+  gdk_gl_texture_builder_set_format (builder, format);
+  gdk_gl_texture_builder_set_width (builder, texture->width);
+  gdk_gl_texture_builder_set_height (builder, texture->height);
+  gdk_gl_texture_builder_set_sync (builder, state->sync);
+
+  result = gdk_gl_texture_builder_build (builder,
+                                         create_texture_from_texture_destroy,
+                                         state);
+
+  texture->texture_id = 0;
+  gsk_gl_texture_free (texture);
+  g_object_unref (builder);
+
+  return result;
+}
diff --git gsk/gl/gskgldriverprivate.h gsk/gl/gskgldriverprivate.h
new file mode 100644
index 0000000000..9eb84b17a2
--- /dev/null
+++ gsk/gl/gskgldriverprivate.h
@@ -0,0 +1,261 @@
+/* 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
+ */
+
+#pragma once
+
+#include <gdk/gdkgltextureprivate.h>
+
+#include "gskgltypesprivate.h"
+#include "gskgltextureprivate.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_ARG0,
+  UNIFORM_CUSTOM_ARG1,
+  UNIFORM_CUSTOM_ARG2,
+  UNIFORM_CUSTOM_ARG3,
+  UNIFORM_CUSTOM_ARG4,
+  UNIFORM_CUSTOM_ARG5,
+  UNIFORM_CUSTOM_ARG6,
+  UNIFORM_CUSTOM_ARG7,
+
+  UNIFORM_CUSTOM_LAST
+};
+
+typedef struct {
+  gconstpointer   pointer;
+  float           scale_x;
+  float           scale_y;
+  int             pointer_is_child;
+  graphene_rect_t parent_rect; /* Valid when pointer_is_child */
+} GskTextureKey;
+
+#define GSK_GL_NO_UNIFORMS CONCAT_EXPANDED(UNIFORM_INVALID_,__COUNTER__)
+#define CONCAT_EXPANDED(a,b) CONCAT_EXPANDED2(a,b)
+#define CONCAT_EXPANDED2(a,b) a##b
+#define GSK_GL_ADD_UNIFORM(pos, KEY, name) UNIFORM_##KEY = UNIFORM_SHARED_LAST + pos,
+#define GSK_GL_DEFINE_PROGRAM(name, resource, uniforms) enum { uniforms };
+#define GSK_GL_DEFINE_PROGRAM_NO_CLIP(name, resource, uniforms) enum { uniforms };
+# include "gskglprograms.defs"
+#undef GSK_GL_DEFINE_PROGRAM_NO_CLIP
+#undef GSK_GL_DEFINE_PROGRAM
+#undef GSK_GL_ADD_UNIFORM
+#undef GSK_GL_NO_UNIFORMS
+#undef CONCAT_EXPANDED
+#undef CONCAT_EXPANDED2
+
+#define GSK_TYPE_GL_DRIVER (gsk_gl_driver_get_type())
+
+G_DECLARE_FINAL_TYPE (GskGLDriver, gsk_gl_driver, GSK, GL_DRIVER, GObject)
+
+struct _GskGLRenderTarget
+{
+  guint framebuffer_id;
+  guint texture_id;
+  int format;
+  int width;
+  int height;
+};
+
+struct _GskGLDriver
+{
+  GObject parent_instance;
+
+  GskGLCommandQueue *shared_command_queue;
+  GskGLCommandQueue *command_queue;
+
+  GskGLGlyphLibrary *glyphs_library;
+  GskGLIconLibrary *icons_library;
+  GskGLShadowLibrary *shadows_library;
+
+  GArray *texture_pool;
+  GHashTable *textures;
+  GHashTable *key_to_texture_id;
+  GHashTable *texture_id_to_key;
+
+  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 ## _no_clip; \
+  GskGLProgram *name ## _rect_clip; \
+  GskGLProgram *name;
+#define GSK_GL_DEFINE_PROGRAM_NO_CLIP(name, resource, uniforms) \
+  GskGLProgram *name;
+# include "gskglprograms.defs"
+#undef GSK_GL_NO_UNIFORMS
+#undef GSK_GL_ADD_UNIFORM
+#undef GSK_GL_DEFINE_PROGRAM
+#undef GSK_GL_DEFINE_PROGRAM_NO_CLIP
+
+  gint64 current_frame_id;
+
+  /* Used to reduce number of comparisons */
+  guint stamps[UNIFORM_SHARED_LAST];
+
+  guint debug : 1;
+  guint in_frame : 1;
+};
+
+GskGLDriver       * gsk_gl_driver_for_display            (GdkDisplay          *display,
+                                                          gboolean             debug_shaders,
+                                                          GError             **error);
+GskGLCommandQueue * gsk_gl_driver_create_command_queue   (GskGLDriver         *self,
+                                                          GdkGLContext        *context);
+GdkGLContext      * gsk_gl_driver_get_context            (GskGLDriver         *self);
+gboolean            gsk_gl_driver_create_render_target   (GskGLDriver         *self,
+                                                          int                  width,
+                                                          int                  height,
+                                                          int                  format,
+                                                          GskGLRenderTarget  **render_target);
+guint               gsk_gl_driver_release_render_target  (GskGLDriver         *self,
+                                                          GskGLRenderTarget   *render_target,
+                                                          gboolean             release_texture);
+void                gsk_gl_driver_begin_frame            (GskGLDriver         *self,
+                                                          GskGLCommandQueue   *command_queue);
+void                gsk_gl_driver_end_frame              (GskGLDriver         *self);
+void                gsk_gl_driver_after_frame            (GskGLDriver         *self);
+GdkTexture        * gsk_gl_driver_create_gdk_texture     (GskGLDriver         *self,
+                                                          guint                texture_id,
+                                                          GdkMemoryFormat      format);
+void                gsk_gl_driver_cache_texture          (GskGLDriver         *self,
+                                                          const GskTextureKey *key,
+                                                          guint                texture_id);
+guint               gsk_gl_driver_load_texture           (GskGLDriver         *self,
+                                                          GdkTexture          *texture,
+                                                          gboolean             ensure_mipmap);
+GskGLTexture      * gsk_gl_driver_create_texture         (GskGLDriver         *self,
+                                                          float                width,
+                                                          float                height,
+                                                          int                  format);
+void                gsk_gl_driver_release_texture        (GskGLDriver         *self,
+                                                          GskGLTexture        *texture);
+void                gsk_gl_driver_release_texture_by_id  (GskGLDriver         *self,
+                                                          guint                texture_id);
+GskGLTexture      * gsk_gl_driver_mark_texture_permanent (GskGLDriver         *self,
+                                                          guint                texture_id);
+void                gsk_gl_driver_add_texture_slices     (GskGLDriver         *self,
+                                                          GdkTexture          *texture,
+                                                          gboolean             ensure_mipmap,
+                                                          GskGLTextureSlice  **out_slices,
+                                                          guint               *out_n_slices);
+G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+GskGLProgram      * gsk_gl_driver_lookup_shader          (GskGLDriver         *self,
+                                                          GskGLShader         *shader,
+                                                          GError             **error);
+G_GNUC_END_IGNORE_DEPRECATIONS
+
+#if 0
+void                gsk_gl_driver_save_texture_to_png    (GskGLDriver         *self,
+                                                          int                  texture_id,
+                                                          int                  width,
+                                                          int                  height,
+                                                          const char          *filename);
+void                gsk_gl_driver_save_atlases_to_png    (GskGLDriver         *self,
+                                                          const char          *filename);
+#endif
+
+static inline GskGLTexture *
+gsk_gl_driver_get_texture_by_id (GskGLDriver *self,
+                                 guint        texture_id)
+{
+  return g_hash_table_lookup (self->textures, GUINT_TO_POINTER (texture_id));
+}
+
+/**
+ * gsk_gl_driver_lookup_texture:
+ * @self: a `GskGLDriver`
+ * @key: the key for the texture
+ * @has_mipmap: (out): Return location for whether the texture has a mipmap
+ *
+ * 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_gl_driver_lookup_texture (GskGLDriver         *self,
+                              const GskTextureKey *key,
+                              gboolean            *has_mipmap)
+{
+  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;
+
+       if (has_mipmap)
+         *has_mipmap = texture ? texture->has_mipmap : FALSE;
+
+      return GPOINTER_TO_UINT (id);
+    }
+
+  return 0;
+}
+
+static inline void
+gsk_gl_driver_slice_texture (GskGLDriver        *self,
+                             GdkTexture         *texture,
+                             gboolean            ensure_mipmap,
+                             GskGLTextureSlice **out_slices,
+                             guint              *out_n_slices)
+{
+  GskGLTexture *t;
+
+  t = gdk_texture_get_render_data (texture, self);
+
+  if (t && t->slices &&
+      (t->has_mipmap || !ensure_mipmap))
+    {
+      *out_slices = t->slices;
+      *out_n_slices = t->n_slices;
+      return;
+    }
+
+  gsk_gl_driver_add_texture_slices (self, texture, ensure_mipmap, out_slices, out_n_slices);
+}
+
+G_END_DECLS
+
diff --git gsk/gl/gskglglyphlibrary.c gsk/gl/gskglglyphlibrary.c
new file mode 100644
index 0000000000..90963c9274
--- /dev/null
+++ gsk/gl/gskglglyphlibrary.c
@@ -0,0 +1,437 @@
+/* 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/gdkmemoryformatprivate.h>
+#include <gdk/gdkprofilerprivate.h>
+
+#include "gskglcommandqueueprivate.h"
+#include "gskgldriverprivate.h"
+#include "gskglglyphlibraryprivate.h"
+
+#include "gskdebugprivate.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 (GskGLDriver *driver)
+{
+  g_return_val_if_fail (GSK_IS_GL_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_free (key);
+}
+
+static void
+gsk_gl_glyph_value_free (gpointer data)
+{
+  g_free (data);
+}
+
+static void
+gsk_gl_glyph_library_clear_cache (GskGLTextureLibrary *library)
+{
+  GskGLGlyphLibrary *self = (GskGLGlyphLibrary *)library;
+
+  g_assert (GSK_IS_GL_GLYPH_LIBRARY (self));
+
+  memset (self->front, 0, sizeof self->front);
+}
+
+static void
+gsk_gl_glyph_library_init_atlas (GskGLTextureLibrary *self,
+                                 GskGLTextureAtlas   *atlas)
+{
+  gboolean packed G_GNUC_UNUSED;
+  int x, y;
+  guint gl_format;
+  guint gl_type;
+  guint8 pixel_data[4 * 3 * 3];
+
+  g_assert (GSK_IS_GL_GLYPH_LIBRARY (self));
+  g_assert (atlas != NULL);
+
+  /* Insert a single pixel at 0,0 for use in coloring */
+
+  gdk_gl_context_push_debug_group_printf (gdk_gl_context_get_current (),
+                                          "Initializing Atlas");
+
+  packed = gsk_gl_texture_library_allocate (self, atlas, 3, 3, &x, &y);
+  g_assert (packed);
+  g_assert (x == 0 && y == 0);
+
+  memset (pixel_data, 255, sizeof pixel_data);
+
+  if (gdk_gl_context_get_use_es (gdk_gl_context_get_current ()))
+    {
+      gl_format = GL_RGBA;
+      gl_type = GL_UNSIGNED_BYTE;
+    }
+  else
+    {
+      gl_format = GL_BGRA;
+      gl_type = GL_UNSIGNED_INT_8_8_8_8_REV;
+    }
+  glBindTexture (GL_TEXTURE_2D, atlas->texture_id);
+
+  glTexSubImage2D (GL_TEXTURE_2D, 0,
+                   0, 0,
+                   3, 3,
+                   gl_format, gl_type,
+                   pixel_data);
+
+  gdk_gl_context_pop_debug_group (gdk_gl_context_get_current ());
+
+  self->driver->command_queue->n_uploads++;
+}
+
+
+static void
+gsk_gl_glyph_library_finalize (GObject *object)
+{
+  GskGLGlyphLibrary *self = (GskGLGlyphLibrary *)object;
+
+  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);
+  GskGLTextureLibraryClass *library_class = GSK_GL_TEXTURE_LIBRARY_CLASS (klass);
+
+  object_class->finalize = gsk_gl_glyph_library_finalize;
+
+  library_class->clear_cache = gsk_gl_glyph_library_clear_cache;
+  library_class->init_atlas = gsk_gl_glyph_library_init_atlas;
+}
+
+static void
+gsk_gl_glyph_library_init (GskGLGlyphLibrary *self)
+{
+  GskGLTextureLibrary *tl = (GskGLTextureLibrary *)self;
+
+  tl->max_entry_size = MAX_GLYPH_SIZE;
+  gsk_gl_texture_library_set_funcs (tl,
+                                    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,
+                                     int                uwidth,
+                                     int                uheight)
+{
+  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, width / (double)uwidth, height / (double)uheight);
+
+  return surface;
+}
+
+static void
+render_glyph (cairo_surface_t           *surface,
+              const GskGLGlyphKey       *key,
+              const GskGLGlyphValue     *value)
+{
+  cairo_t *cr;
+  PangoGlyphString glyph_string;
+  PangoGlyphInfo glyph_info;
+
+  g_assert (surface != NULL);
+
+  cr = cairo_create (surface);
+  cairo_set_source_rgba (cr, 1, 1, 1, 1);
+
+  glyph_info.glyph = key->glyph;
+  glyph_info.geometry.width = value->ink_rect.width * 1024;
+  glyph_info.geometry.x_offset = (0.25 * key->xshift - value->ink_rect.x) * 1024;
+  glyph_info.geometry.y_offset = (0.25 * 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                    packed_x,
+                                   int                    packed_y,
+                                   int                    width,
+                                   int                    height,
+                                   int                    uwidth,
+                                   int                    uheight)
+{
+  GskGLTextureLibrary *tl = (GskGLTextureLibrary *)self;
+  G_GNUC_UNUSED gint64 start_time = GDK_PROFILER_CURRENT_TIME;
+  cairo_surface_t *surface;
+  guchar *pixel_data;
+  guchar *free_data = NULL;
+  guint gl_format;
+  guint gl_type;
+  guint texture_id;
+  gsize stride;
+
+  g_assert (GSK_IS_GL_GLYPH_LIBRARY (self));
+  g_assert (key != NULL);
+  g_assert (value != NULL);
+
+  stride = cairo_format_stride_for_width (CAIRO_FORMAT_ARGB32, width);
+
+  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, uwidth, uheight);
+  render_glyph (surface, key, value);
+
+  texture_id = GSK_GL_TEXTURE_ATLAS_ENTRY_TEXTURE (value);
+
+  g_assert (texture_id > 0);
+
+  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,
+                    &GDK_MEMORY_LAYOUT_SIMPLE (GDK_MEMORY_R8G8B8A8_PREMULTIPLIED, width, height, width * 4),
+                    GDK_COLOR_STATE_SRGB,
+                    cairo_image_surface_get_data (surface),
+                    &GDK_MEMORY_LAYOUT_SIMPLE (GDK_MEMORY_DEFAULT, width, height, stride),
+                    GDK_COLOR_STATE_SRGB);
+      stride = width * 4;
+      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;
+    }
+
+  glPixelStorei (GL_UNPACK_ROW_LENGTH, stride / 4);
+  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);
+
+  cairo_surface_destroy (surface);
+  g_free (free_data);
+
+  gdk_gl_context_pop_debug_group (gdk_gl_context_get_current ());
+
+  tl->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)
+{
+  GskGLTextureLibrary *tl = (GskGLTextureLibrary *)self;
+  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);
+
+  ink_rect.x -= 1;
+  ink_rect.width += 2;
+  ink_rect.y -= 1;
+  ink_rect.height += 2;
+
+  width = (int) ceil (ink_rect.width * key->scale / 1024.0);
+  height = (int) ceil (ink_rect.height * key->scale / 1024.0);
+
+  GSK_DEBUG (CACHE, "font %p glyph %u: %u x %u pixels", key->font, key->glyph, width, height);
+
+  value = gsk_gl_texture_library_pack (tl,
+                                       key,
+                                       sizeof *value,
+                                       width,
+                                       height,
+                                       1,
+                                       &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,
+                                       packed_x,
+                                       packed_y,
+                                       width,
+                                       height,
+                                       ink_rect.width,
+                                       ink_rect.height);
+
+  *out_value = value;
+
+  return GSK_GL_TEXTURE_ATLAS_ENTRY_TEXTURE (value) != 0;
+}
diff --git gsk/gl/gskglglyphlibraryprivate.h gsk/gl/gskglglyphlibraryprivate.h
new file mode 100644
index 0000000000..b3022b4630
--- /dev/null
+++ gsk/gl/gskglglyphlibraryprivate.h
@@ -0,0 +1,103 @@
+/* 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
+ */
+
+#pragma once
+
+#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 : 2;
+  guint yshift : 2;
+  guint scale  : 28; /* 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;
+  guint8 *surface_data;
+  gsize surface_data_len;
+  struct {
+    GskGLGlyphKey key;
+    const GskGLGlyphValue *value;
+  } front[256];
+};
+
+GskGLGlyphLibrary *gsk_gl_glyph_library_new (GskGLDriver            *driver);
+gboolean           gsk_gl_glyph_library_add (GskGLGlyphLibrary      *self,
+                                             GskGLGlyphKey          *key,
+                                             const GskGLGlyphValue **out_value);
+
+static inline guint
+gsk_gl_glyph_library_lookup_or_add (GskGLGlyphLibrary      *self,
+                                    const GskGLGlyphKey    *key,
+                                    const GskGLGlyphValue **out_value)
+{
+  GskGLTextureAtlasEntry *entry;
+  guint front_index = ((key->glyph << 2) | key->xshift) & 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;
+      k = g_new (GskGLGlyphKey, 1);
+      memcpy (k, key, sizeof (GskGLGlyphKey));
+      g_object_ref (k->font);
+      gsk_gl_glyph_library_add (self, k, out_value);
+      self->front[front_index].key = *key;
+      self->front[front_index].value = *out_value;
+    }
+
+  return GSK_GL_TEXTURE_ATLAS_ENTRY_TEXTURE (*out_value);
+}
+
+G_END_DECLS
+
diff --git gsk/gl/gskgliconlibrary.c gsk/gl/gskgliconlibrary.c
new file mode 100644
index 0000000000..63b25dea4b
--- /dev/null
+++ gsk/gl/gskgliconlibrary.c
@@ -0,0 +1,212 @@
+/* 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/gdkmemoryformatprivate.h>
+#include <gdk/gdkprofilerprivate.h>
+#include <gdk/gdktextureprivate.h>
+#include <gdk/gdktexturedownloaderprivate.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 (GskGLDriver *driver)
+{
+  g_return_val_if_fail (GSK_IS_GL_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_free (icon_data);
+}
+
+static void
+gsk_gl_icon_library_class_init (GskGLIconLibraryClass *klass)
+{
+}
+
+static void
+gsk_gl_icon_library_init (GskGLIconLibrary *self)
+{
+  GskGLTextureLibrary *tl = (GskGLTextureLibrary *)self;
+
+  tl->max_entry_size = 128;
+  gsk_gl_texture_library_set_funcs (tl,
+                                    NULL, NULL, NULL,
+                                    gsk_gl_icon_data_free);
+}
+
+void
+gsk_gl_icon_library_add (GskGLIconLibrary     *self,
+                         GdkTexture           *key,
+                         const GskGLIconData **out_value)
+{
+  GskGLTextureLibrary *tl = (GskGLTextureLibrary *)self;
+  G_GNUC_UNUSED gint64 start_time = GDK_PROFILER_CURRENT_TIME;
+  GskGLIconData *icon_data;
+  GdkTextureDownloader downloader;
+  guint8 *pixel_data;
+  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 (tl,
+                                           key,
+                                           sizeof (GskGLIconData),
+                                           width, height, 1,
+                                           &packed_x, &packed_y);
+  icon_data->source_texture = g_object_ref (key);
+
+  /* actually upload the texture */
+  gdk_texture_downloader_init (&downloader, key);
+  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 ()))
+    {
+      gdk_texture_downloader_set_format (&downloader, GDK_MEMORY_R8G8B8A8_PREMULTIPLIED);
+      gl_format = GL_RGBA;
+      gl_type = GL_UNSIGNED_BYTE;
+    }
+  else
+    {
+      gdk_texture_downloader_set_format (&downloader, GDK_MEMORY_DEFAULT);
+      gl_format = GL_BGRA;
+      gl_type = GL_UNSIGNED_INT_8_8_8_8_REV;
+    }
+  pixel_data = g_malloc (width * height * 4);
+  gdk_texture_downloader_download_into (&downloader, pixel_data, width * 4);
+  gdk_texture_downloader_finish (&downloader);
+
+  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 */
+  glPixelStorei (GL_UNPACK_ROW_LENGTH, width);
+  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_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;
+
+  g_free (pixel_data);
+
+  tl->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 gsk/gl/gskgliconlibraryprivate.h gsk/gl/gskgliconlibraryprivate.h
new file mode 100644
index 0000000000..dc99b546ce
--- /dev/null
+++ gsk/gl/gskgliconlibraryprivate.h
@@ -0,0 +1,58 @@
+/* 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
+ */
+
+#pragma once
+
+#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 (GskGLDriver          *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
+
diff --git gsk/gl/gskglprofiler.c gsk/gl/gskglprofiler.c
new file mode 100644
index 0000000000..6aff5ab0a5
--- /dev/null
+++ gsk/gl/gskglprofiler.c
@@ -0,0 +1,179 @@
+#include "config.h"
+
+#include "gskglprofilerprivate.h"
+
+#include <epoxy/gl.h>
+
+#define N_QUERIES       4
+
+struct _GskGLProfiler
+{
+  GObject parent_instance;
+
+  GdkGLContext *gl_context;
+
+  /* Creating GL queries is kind of expensive, so we pay the
+   * price upfront and create a circular buffer of queries
+   */
+  GLuint gl_queries[N_QUERIES];
+  GLuint active_query;
+
+  unsigned int has_queries : 1;
+  unsigned int has_timer : 1;
+  unsigned int first_frame : 1;
+};
+
+enum {
+  PROP_GL_CONTEXT = 1,
+
+  N_PROPERTIES
+};
+
+static GParamSpec *gsk_gl_profiler_properties[N_PROPERTIES];
+
+G_DEFINE_TYPE (GskGLProfiler, gsk_gl_profiler, G_TYPE_OBJECT)
+
+static void
+gsk_gl_profiler_finalize (GObject *gobject)
+{
+  GskGLProfiler *self = GSK_GL_PROFILER (gobject);
+
+  if (self->has_queries)
+    glDeleteQueries (N_QUERIES, self->gl_queries);
+
+  g_clear_object (&self->gl_context);
+
+  G_OBJECT_CLASS (gsk_gl_profiler_parent_class)->finalize (gobject);
+}
+
+static void
+gsk_gl_profiler_set_property (GObject      *gobject,
+                              guint         prop_id,
+                              const GValue *value,
+                              GParamSpec   *pspec)
+{
+  GskGLProfiler *self = GSK_GL_PROFILER (gobject);
+
+  switch (prop_id)
+    {
+    case PROP_GL_CONTEXT:
+      self->gl_context = g_value_dup_object (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
+    }
+}
+
+static void
+gsk_gl_profiler_get_property (GObject    *gobject,
+                              guint       prop_id,
+                              GValue     *value,
+                              GParamSpec *pspec)
+{
+  GskGLProfiler *self = GSK_GL_PROFILER (gobject);
+
+  switch (prop_id)
+    {
+    case PROP_GL_CONTEXT:
+      g_value_set_object (value, self->gl_context);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
+    }
+}
+
+static void
+gsk_gl_profiler_class_init (GskGLProfilerClass *klass)
+{
+  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+  gobject_class->set_property = gsk_gl_profiler_set_property;
+  gobject_class->get_property = gsk_gl_profiler_get_property;
+  gobject_class->finalize = gsk_gl_profiler_finalize;
+
+  gsk_gl_profiler_properties[PROP_GL_CONTEXT] =
+    g_param_spec_object ("gl-context", NULL, NULL,
+                         GDK_TYPE_GL_CONTEXT,
+                         G_PARAM_READWRITE |
+                         G_PARAM_CONSTRUCT_ONLY |
+                         G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (gobject_class, N_PROPERTIES, gsk_gl_profiler_properties);
+}
+
+static void
+gsk_gl_profiler_init (GskGLProfiler *self)
+{
+  self->has_queries = epoxy_is_desktop_gl();
+  self->has_timer = epoxy_is_desktop_gl() && (epoxy_gl_version () >= 33 || epoxy_has_gl_extension ("GL_ARB_timer_query"));
+
+  if (!self->has_queries)
+    return;
+
+  glGenQueries (N_QUERIES, self->gl_queries);
+  self->first_frame = TRUE;
+}
+
+GskGLProfiler *
+gsk_gl_profiler_new (GdkGLContext *context)
+{
+  g_return_val_if_fail (GDK_IS_GL_CONTEXT (context), NULL);
+
+  return g_object_new (GSK_TYPE_GL_PROFILER, "gl-context", context, NULL);
+}
+
+void
+gsk_gl_profiler_begin_gpu_region (GskGLProfiler *profiler)
+{
+  GLuint query_id;
+
+  g_return_if_fail (GSK_IS_GL_PROFILER (profiler));
+
+  if (!profiler->has_timer || !profiler->has_queries)
+    return;
+
+  query_id = profiler->gl_queries[profiler->active_query];
+  glBeginQuery (GL_TIME_ELAPSED, query_id);
+}
+
+guint64
+gsk_gl_profiler_end_gpu_region (GskGLProfiler *profiler)
+{
+  GLuint last_query_id;
+  GLint res;
+  GLuint64 elapsed;
+
+  g_return_val_if_fail (GSK_IS_GL_PROFILER (profiler), 0);
+
+  if (!profiler->has_timer || !profiler->has_queries)
+    return 0;
+
+  glEndQuery (GL_TIME_ELAPSED);
+
+  if (profiler->active_query == 0)
+    last_query_id = N_QUERIES - 1;
+  else
+    last_query_id = profiler->active_query - 1;
+
+  /* Advance iterator */
+  profiler->active_query += 1;
+  if (profiler->active_query == N_QUERIES)
+    profiler->active_query = 0;
+
+  /* If this is the first frame we already have a result */
+  if (profiler->first_frame)
+    {
+      profiler->first_frame = FALSE;
+      return 0;
+    }
+
+  glGetQueryObjectiv (profiler->gl_queries[last_query_id], GL_QUERY_RESULT_AVAILABLE, &res);
+  if (res == 1)
+    glGetQueryObjectui64v (profiler->gl_queries[last_query_id], GL_QUERY_RESULT, &elapsed);
+  else
+    elapsed = 0;
+
+  return elapsed / 1000; /* Convert to usec to match other profiler APIs */
+}
diff --git gsk/gl/gskglprofilerprivate.h gsk/gl/gskglprofilerprivate.h
new file mode 100644
index 0000000000..3aa0650c11
--- /dev/null
+++ gsk/gl/gskglprofilerprivate.h
@@ -0,0 +1,16 @@
+#pragma once
+
+#include <gsk/gsktypes.h>
+
+G_BEGIN_DECLS
+
+#define GSK_TYPE_GL_PROFILER (gsk_gl_profiler_get_type ())
+G_DECLARE_FINAL_TYPE (GskGLProfiler, gsk_gl_profiler, GSK, GL_PROFILER, GObject)
+
+GskGLProfiler * gsk_gl_profiler_new                     (GdkGLContext  *context);
+
+void            gsk_gl_profiler_begin_gpu_region        (GskGLProfiler *profiler);
+guint64         gsk_gl_profiler_end_gpu_region          (GskGLProfiler *profiler);
+
+G_END_DECLS
+
diff --git gsk/gl/gskglprogram.c gsk/gl/gskglprogram.c
new file mode 100644
index 0000000000..7288284593
--- /dev/null
+++ gsk/gl/gskglprogram.c
@@ -0,0 +1,173 @@
+/* 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 (GskGLDriver *driver,
+                    const char  *name,
+                    int          program_id)
+{
+  GskGLProgram *self;
+
+  g_return_val_if_fail (GSK_IS_GL_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_mappings = 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->mappings); i++)
+    self->mappings[i].location = -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 < G_N_ELEMENTS (self->mappings), FALSE);
+
+  location = glGetUniformLocation (self->id, name);
+
+  /* Register the information even if unused */
+  self->mappings[key].name = g_intern_string (name);
+  self->mappings[key].location = location;
+  if (key >= self->n_mappings)
+    self->n_mappings = key + 1;
+
+#if 0
+  g_print ("program [%d] %s uniform %s [%u of %u] at location %d.\n",
+           self->id, self->name, name, key, self->n_mappings, location);
+#endif
+
+  return location > -1;
+}
+
+/**
+ * gsk_gl_program_delete:
+ * @self: a `GskGLProgram`
+ *
+ * Deletes the GLSL program.
+ */
+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 have
+ * 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->mappings, self->n_mappings);
+  self->program_info->has_attachments = has_attachments;
+}
diff --git gsk/gl/gskglprogramprivate.h gsk/gl/gskglprogramprivate.h
new file mode 100644
index 0000000000..6bfbe48f02
--- /dev/null
+++ gsk/gl/gskglprogramprivate.h
@@ -0,0 +1,318 @@
+/* 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
+ */
+
+#pragma once
+
+#include "gskgltypesprivate.h"
+
+#include "gskglattachmentstateprivate.h"
+#include "gskglcommandqueueprivate.h"
+#include "gskgldriverprivate.h"
+
+G_BEGIN_DECLS
+
+#define GSK_TYPE_GL_PROGRAM (gsk_gl_program_get_type())
+#define GSK_GL_PROGRAM_MAX_CUSTOM_TEXTURES 4
+#define GSK_GL_PROGRAM_MAX_CUSTOM_ARGS 8
+
+G_DECLARE_FINAL_TYPE (GskGLProgram, gsk_gl_program, GSK, GL_PROGRAM, GObject)
+
+struct _GskGLProgram
+{
+  GObject parent_instance;
+
+  int id;
+  char *name;
+  GskGLDriver *driver;
+
+  /* Cached pointer to avoid lots of pointer chasing/lookups */
+  GskGLUniformState *uniforms;
+  GskGLUniformProgram *program_info;
+
+  /* Static array for key->location transforms */
+  GskGLUniformMapping mappings[32];
+  guint n_mappings;
+};
+
+GskGLProgram * gsk_gl_program_new            (GskGLDriver  *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);
+
+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,
+                               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,
+                               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,
+                               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,
+                                         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,
+                              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,
+                              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,
+                              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,
+                              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,
+                              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,
+                              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,
+                              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,
+                              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,
+                                  key,
+                                  stamp,
+                                  color);
+}
+
+static inline void
+gsk_gl_program_set_uniform_texture_with_filter (GskGLProgram *self,
+                                                guint         key,
+                                                guint         stamp,
+                                                GLenum        texture_target,
+                                                GLenum        texture_slot,
+                                                guint         texture_id,
+                                                GLint         min_filter,
+                                                GLint         mag_filter)
+{
+  gsk_gl_attachment_state_bind_texture (self->driver->command_queue->attachments,
+                                        texture_target,
+                                        texture_slot,
+                                        texture_id,
+                                        min_filter,
+                                        mag_filter);
+  gsk_gl_uniform_state_set_texture (self->uniforms,
+                                    self->program_info,
+                                    key,
+                                    stamp,
+                                    texture_slot);
+}
+
+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_program_set_uniform_texture_with_filter (self,
+                                                  key,
+                                                  stamp,
+                                                  texture_target,
+                                                  texture_slot,
+                                                  texture_id,
+                                                  GL_LINEAR,
+                                                  GL_LINEAR);
+}
+
+static inline void
+gsk_gl_program_set_uniform_texture_with_sync (GskGLProgram *self,
+                                              guint         key,
+                                              guint         stamp,
+                                              GLenum        texture_target,
+                                              GLenum        texture_slot,
+                                              guint         texture_id,
+                                              GLint         min_filter,
+                                              GLint         max_filter,
+                                              gpointer      sync)
+{
+  gsk_gl_program_set_uniform_texture_with_filter (self, key, stamp, texture_target, texture_slot, texture_id,
+                                                  min_filter, max_filter);
+  gsk_gl_syncs_add_sync (&self->driver->command_queue->syncs, texture_id, sync);
+}
+
+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,
+                                   key,
+                                   stamp,
+                                   matrix);
+}
+
+G_END_DECLS
+
diff --git gsk/gl/gskglprograms.defs gsk/gl/gskglprograms.defs
new file mode 100644
index 0000000000..0eabd8a9bd
--- /dev/null
+++ gsk/gl/gskglprograms.defs
@@ -0,0 +1,104 @@
+GSK_GL_DEFINE_PROGRAM (blend,
+                       GSK_GL_SHADER_SINGLE (GSK_GL_SHADER_RESOURCE ("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,
+                       GSK_GL_SHADER_SINGLE (GSK_GL_SHADER_RESOURCE ("blit.glsl")),
+                       GSK_GL_NO_UNIFORMS)
+
+GSK_GL_DEFINE_PROGRAM (blur,
+                       GSK_GL_SHADER_SINGLE (GSK_GL_SHADER_RESOURCE ("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,
+                       GSK_GL_SHADER_SINGLE (GSK_GL_SHADER_RESOURCE ("border.glsl")),
+                       GSK_GL_ADD_UNIFORM (1, BORDER_WIDTHS, u_widths)
+                       GSK_GL_ADD_UNIFORM (2, BORDER_OUTLINE_RECT, u_outline_rect))
+
+GSK_GL_DEFINE_PROGRAM (color,
+                       GSK_GL_SHADER_SINGLE (GSK_GL_SHADER_RESOURCE ("color.glsl")),
+                       GSK_GL_NO_UNIFORMS)
+
+GSK_GL_DEFINE_PROGRAM (coloring,
+                       GSK_GL_SHADER_SINGLE (GSK_GL_SHADER_RESOURCE ("coloring.glsl")),
+                       GSK_GL_NO_UNIFORMS)
+
+GSK_GL_DEFINE_PROGRAM (color_matrix,
+                       GSK_GL_SHADER_SINGLE (GSK_GL_SHADER_RESOURCE ("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,
+                       GSK_GL_SHADER_SINGLE (GSK_GL_SHADER_RESOURCE ("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,
+                       GSK_GL_SHADER_SINGLE (GSK_GL_SHADER_RESOURCE ("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 (filled_border,
+                       GSK_GL_SHADER_SINGLE (GSK_GL_SHADER_RESOURCE ("filled_border.glsl")),
+                       GSK_GL_ADD_UNIFORM (1, FILLED_BORDER_WIDTHS, u_widths)
+                       GSK_GL_ADD_UNIFORM (2, FILLED_BORDER_OUTLINE_RECT, u_outline_rect))
+
+GSK_GL_DEFINE_PROGRAM (inset_shadow,
+                       GSK_GL_SHADER_SINGLE (GSK_GL_SHADER_RESOURCE ("inset_shadow.glsl")),
+                       GSK_GL_ADD_UNIFORM (1, INSET_SHADOW_SPREAD, u_spread)
+                       GSK_GL_ADD_UNIFORM (2, INSET_SHADOW_OFFSET, u_offset)
+                       GSK_GL_ADD_UNIFORM (3, INSET_SHADOW_OUTLINE_RECT, u_outline_rect))
+
+GSK_GL_DEFINE_PROGRAM (linear_gradient,
+                       GSK_GL_SHADER_SINGLE (GSK_GL_SHADER_RESOURCE ("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 (mask,
+                       GSK_GL_SHADER_SINGLE (GSK_GL_SHADER_RESOURCE ("mask.glsl")),
+                       GSK_GL_ADD_UNIFORM (1, MASK_SOURCE, u_mask)
+                       GSK_GL_ADD_UNIFORM (2, MASK_MODE, u_mode))
+
+GSK_GL_DEFINE_PROGRAM (outset_shadow,
+                       GSK_GL_SHADER_SINGLE (GSK_GL_SHADER_RESOURCE ("outset_shadow.glsl")),
+                       GSK_GL_ADD_UNIFORM (1, OUTSET_SHADOW_OUTLINE_RECT, u_outline_rect))
+
+GSK_GL_DEFINE_PROGRAM (radial_gradient,
+                       GSK_GL_SHADER_SINGLE (GSK_GL_SHADER_RESOURCE ("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,
+                       GSK_GL_SHADER_SINGLE (GSK_GL_SHADER_RESOURCE ("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,
+                       GSK_GL_SHADER_SINGLE (GSK_GL_SHADER_RESOURCE ("unblurred_outset_shadow.glsl")),
+                       GSK_GL_ADD_UNIFORM (1, UNBLURRED_OUTSET_SHADOW_SPREAD, u_spread)
+                       GSK_GL_ADD_UNIFORM (2, UNBLURRED_OUTSET_SHADOW_OFFSET, u_offset)
+                       GSK_GL_ADD_UNIFORM (3, UNBLURRED_OUTSET_SHADOW_OUTLINE_RECT, u_outline_rect))
+
+/* Texture conversion shaders.
+ *
+ * Note: If you add new formats here, they need to be added
+ * to the list of supported formats in gdk/gdkdmabuftexture.c.
+ */
+
+GSK_GL_DEFINE_PROGRAM_NO_CLIP (external,
+                               GSK_GL_SHADER_SINGLE (GSK_GL_SHADER_RESOURCE ("external.glsl")),
+                               GSK_GL_ADD_UNIFORM (1, EXTERNAL_SOURCE, u_external_source)
+                               GSK_GL_ADD_UNIFORM (2, PREMULTIPLY, u_premultiply))
+
+GSK_GL_DEFINE_PROGRAM_NO_CLIP (premultiply,
+                               GSK_GL_SHADER_SINGLE (GSK_GL_SHADER_RESOURCE ("premultiply.glsl")),
+                               GSK_GL_NO_UNIFORMS)
diff --git gsk/gl/gskglrenderer.c gsk/gl/gskglrenderer.c
new file mode 100644
index 0000000000..928cb83c68
--- /dev/null
+++ gsk/gl/gskglrenderer.c
@@ -0,0 +1,497 @@
+/* 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 "gskglrendererprivate.h"
+
+#include "gskglcommandqueueprivate.h"
+#include "gskgldriverprivate.h"
+#include "gskglprogramprivate.h"
+#include "gskglrenderjobprivate.h"
+
+#include <gdk/gdkprofilerprivate.h>
+#include <gdk/gdkdisplayprivate.h>
+#include <gdk/gdkdmabufdownloaderprivate.h>
+#include <gdk/gdkdmabuftextureprivate.h>
+#include <gdk/gdkglcontextprivate.h>
+#include <gdk/gdksurfaceprivate.h>
+#include <gdk/gdksubsurfaceprivate.h>
+#include <gdk/gdkswizzleprivate.h>
+#include <glib/gi18n-lib.h>
+#include <gsk/gskdebugprivate.h>
+#include <gsk/gskrendererprivate.h>
+#include <gsk/gskrendernodeprivate.h>
+#include <gsk/gskroundedrectprivate.h>
+#include <gsk/gskrectprivate.h>
+
+/**
+ * GskGLLegacyRenderer:
+ *
+ * A GL based renderer.
+ *
+ * See [class@Gsk.Renderer].
+ *
+ * Since: 4.2
+ */
+struct _GskGLLegacyRendererClass
+{
+  GskRendererClass parent_class;
+};
+
+struct _GskGLLegacyRenderer
+{
+  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.
+   */
+  GskGLDriver *driver;
+};
+
+static gboolean
+gsk_gl_legacy_renderer_dmabuf_downloader_supports (GdkDmabufDownloader  *downloader,
+                                            GdkDmabufTexture     *texture,
+                                            GError              **error)
+{
+  GdkDisplay *display = gdk_dmabuf_texture_get_display (texture);
+  const GdkDmabuf *dmabuf = gdk_dmabuf_texture_get_dmabuf (texture);
+
+  if (!gdk_dmabuf_formats_contains (display->egl_dmabuf_formats, dmabuf->fourcc, dmabuf->modifier))
+    {
+      g_set_error (error,
+                   GDK_DMABUF_ERROR, GDK_DMABUF_ERROR_UNSUPPORTED_FORMAT,
+                   "Unsupported dmabuf format: %.4s:%#" G_GINT64_MODIFIER "x",
+                   (char *) &dmabuf->fourcc, dmabuf->modifier);
+      return FALSE;
+    }
+
+  return TRUE;
+}
+
+static gboolean
+gsk_gl_legacy_renderer_dmabuf_downloader_download (GdkDmabufDownloader   *downloader_,
+                                             GdkDmabufTexture      *texture,
+                                             guchar                *data,
+                                             const GdkMemoryLayout *layout,
+                                             GdkColorState         *color_state)
+{
+  GskRenderer *renderer = GSK_RENDERER (downloader_);
+  GdkGLContext *previous;
+  GdkTexture *native;
+  GdkTextureDownloader *downloader;
+  int width, height;
+  GskRenderNode *node;
+  gsize stride;
+
+  previous = gdk_gl_context_get_current ();
+
+  width = gdk_texture_get_width (GDK_TEXTURE (texture));
+  height = gdk_texture_get_height (GDK_TEXTURE (texture));
+
+  node = gsk_texture_node_new (GDK_TEXTURE (texture), &GRAPHENE_RECT_INIT (0, 0, width, height));
+  native = gsk_renderer_render_texture (renderer, node, &GRAPHENE_RECT_INIT (0, 0, width, height));
+  gsk_render_node_unref (node);
+
+  downloader = gdk_texture_downloader_new (native);
+  gdk_texture_downloader_set_format (downloader, layout->format);
+  gdk_texture_downloader_set_color_state (downloader, color_state);
+  gdk_texture_downloader_download_into (downloader, data, layout->planes[0].stride);
+  gdk_texture_downloader_free (downloader);
+
+  g_object_unref (native);
+
+  if (previous)
+    gdk_gl_context_make_current (previous);
+  else
+    gdk_gl_context_clear_current ();
+}
+
+static void
+gsk_gl_legacy_renderer_dmabuf_downloader_close (GdkDmabufDownloader *downloader)
+{
+  gsk_renderer_unrealize (GSK_RENDERER (downloader));
+}
+
+static void
+gsk_gl_legacy_renderer_dmabuf_downloader_init (GdkDmabufDownloaderInterface *iface)
+{
+  iface->close = gsk_gl_legacy_renderer_dmabuf_downloader_close;
+  iface->download = gsk_gl_legacy_renderer_dmabuf_downloader_download;
+}
+
+G_DEFINE_TYPE_EXTENDED (GskGLLegacyRenderer, gsk_gl_legacy_renderer, GSK_TYPE_RENDERER, 0,
+                        G_IMPLEMENT_INTERFACE (GDK_TYPE_DMABUF_DOWNLOADER,
+                                               gsk_gl_legacy_renderer_dmabuf_downloader_init))
+
+/**
+ * gsk_gl_legacy_renderer_new:
+ *
+ * Creates a new `GskRenderer` using the new OpenGL renderer.
+ *
+ * Returns: a new GL renderer
+ *
+ * Since: 4.2
+ */
+GskRenderer *
+gsk_gl_legacy_renderer_new (void)
+{
+  return g_object_new (GSK_TYPE_GL_LEGACY_RENDERER, NULL);
+}
+
+static gboolean
+gsk_gl_legacy_renderer_realize (GskRenderer  *renderer,
+                         GdkDisplay   *display,
+                         GdkSurface   *surface,
+                         gboolean      attach,
+                         GError      **error)
+{
+  G_GNUC_UNUSED gint64 start_time = GDK_PROFILER_CURRENT_TIME;
+  GskGLLegacyRenderer *self = (GskGLLegacyRenderer *)renderer;
+  GdkGLContext *context = NULL;
+  GskGLDriver *driver = NULL;
+  gboolean ret = FALSE;
+  gboolean debug_shaders = FALSE;
+
+  if (self->context != NULL)
+    return TRUE;
+
+  g_assert (self->driver == NULL);
+  g_assert (self->context == NULL);
+  g_assert (self->command_queue == NULL);
+
+  if (!gdk_display_prepare_gl (display, error))
+    goto failure;
+
+  context = gdk_gl_context_new (display, surface, surface != NULL);
+
+  if (!gdk_gl_context_realize (context, error))
+    goto failure;
+
+  gdk_gl_context_make_current (context);
+
+  if (!gdk_gl_context_check_version (context, "2.0", "2.0"))
+    {
+      g_set_error_literal (error, GDK_GL_ERROR,
+                           GDK_GL_ERROR_NOT_AVAILABLE,
+                           _("OpenGL 2.0 required"));
+      goto failure;
+    }
+
+  if (GSK_RENDERER_DEBUG_CHECK (GSK_RENDERER (self), SHADERS))
+    debug_shaders = TRUE;
+
+  if (!(driver = gsk_gl_driver_for_display (display, debug_shaders, error)))
+    goto failure;
+
+  self->command_queue = gsk_gl_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));
+
+  if (attach && !gdk_draw_context_attach (GDK_DRAW_CONTEXT (self->context), error))
+    goto failure;
+
+  ret = TRUE;
+
+failure:
+  g_clear_object (&driver);
+  g_clear_object (&context);
+
+  gdk_profiler_end_mark (start_time, "realize GskGLLegacyRenderer", NULL);
+
+  /* Assert either all or no state was set */
+  g_assert ((ret && self->driver != NULL && self->context != NULL && self->command_queue != NULL) ||
+            (!ret && self->driver == NULL && self->context == NULL && self->command_queue == NULL));
+
+  return ret;
+}
+
+static void
+gsk_gl_legacy_renderer_unrealize (GskRenderer *renderer)
+{
+  GskGLLegacyRenderer *self = (GskGLLegacyRenderer *)renderer;
+
+  g_assert (GSK_IS_GL_LEGACY_RENDERER (renderer));
+
+  gdk_gl_context_make_current (self->context);
+
+  g_clear_object (&self->driver);
+  g_clear_object (&self->command_queue);
+
+  gdk_draw_context_detach (GDK_DRAW_CONTEXT (self->context));
+
+  g_clear_object (&self->context);
+
+  gdk_gl_context_clear_current ();
+}
+
+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't have the scale factor applied.
+   */
+  damage = gdk_draw_context_get_render_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_gl_legacy_renderer_render (GskRenderer   *renderer,
+                        GskRenderNode        *root,
+                        const cairo_region_t *update_area)
+{
+  GskGLLegacyRenderer *self = (GskGLLegacyRenderer *)renderer;
+  cairo_region_t *render_region;
+  graphene_rect_t viewport;
+  GskGLRenderJob *job;
+  GdkSurface *surface;
+  graphene_rect_t opaque_tmp;
+  const graphene_rect_t *opaque;
+  float scale;
+
+  g_assert (GSK_IS_GL_LEGACY_RENDERER (renderer));
+  g_assert (root != NULL);
+
+  surface = gdk_draw_context_get_surface (GDK_DRAW_CONTEXT (self->context));
+  scale = gdk_gl_context_get_scale (self->context);
+
+  if (cairo_region_is_empty (update_area))
+    {
+      gdk_draw_context_empty_frame (GDK_DRAW_CONTEXT (self->context));
+      return;
+    }
+
+  viewport.origin.x = 0;
+  viewport.origin.y = 0;
+  viewport.size.width = gdk_surface_get_width (surface) * scale;
+  viewport.size.height = gdk_surface_get_height (surface) * scale;
+
+  if (gsk_render_node_get_opaque_rect (root, &opaque_tmp))
+    opaque = &opaque_tmp;
+  else
+    opaque = NULL;
+  gdk_draw_context_begin_frame_full (GDK_DRAW_CONTEXT (self->context),
+                                     NULL,
+                                     gsk_render_node_get_preferred_depth (root),
+                                     update_area,
+                                     opaque);
+
+  gdk_gl_context_make_current (self->context);
+
+  /* Must be called *AFTER* gdk_draw_context_begin_frame() */
+  render_region = get_render_region (surface, self->context);
+
+  gsk_gl_driver_begin_frame (self->driver, self->command_queue);
+  job = gsk_gl_render_job_new (self->driver, &viewport, scale, render_region, 0, TRUE);
+  gsk_gl_render_job_render (job, root);
+  gsk_gl_driver_end_frame (self->driver);
+  gsk_gl_render_job_free (job);
+
+  gdk_draw_context_end_frame_full (GDK_DRAW_CONTEXT (self->context), NULL);
+
+  gsk_gl_driver_after_frame (self->driver);
+
+  cairo_region_destroy (render_region);
+}
+
+static GdkTexture *
+gsk_gl_legacy_renderer_render_texture (GskRenderer    *renderer,
+                                GskRenderNode         *root,
+                                const graphene_rect_t *viewport)
+{
+  GskGLLegacyRenderer *self = (GskGLLegacyRenderer *)renderer;
+  GskGLRenderTarget *render_target;
+  GskGLRenderJob *job;
+  GdkTexture *texture;
+  guint texture_id;
+  GdkMemoryFormat gdk_format;
+  int width, height, max_size;
+  int format;
+
+  g_assert (GSK_IS_GL_LEGACY_RENDERER (renderer));
+  g_assert (root != NULL);
+
+  width = ceilf (viewport->size.width);
+  height = ceilf (viewport->size.height);
+  max_size = self->command_queue->max_texture_size;
+  if (width > max_size || height > max_size)
+    {
+      gsize x, y, size, stride;
+      GBytes *bytes;
+      guchar *data;
+
+      stride = width * 4;
+      size = stride * height;
+      data = g_malloc_n (stride, height);
+
+      for (y = 0; y < height; y += max_size)
+        {
+          for (x = 0; x < width; x += max_size)
+            {
+              texture = gsk_gl_legacy_renderer_render_texture (renderer, root,
+                                                        &GRAPHENE_RECT_INIT (viewport->origin.x + x,
+                                                                             viewport->origin.y + y,
+                                                                             MIN (max_size, viewport->size.width - x),
+                                                                             MIN (max_size, viewport->size.height - y)));
+              gdk_texture_download (texture,
+                                    data + stride * y + x * 4,
+                                    stride);
+              g_object_unref (texture);
+            }
+        }
+
+      bytes = g_bytes_new_take (data, size);
+      texture = gdk_memory_texture_new (width, height, GDK_MEMORY_DEFAULT, bytes, stride);
+      g_bytes_unref (bytes);
+      return texture;
+    }
+
+  /* Don't use float textures for SRGB or node-editor turns on high
+   * depth unconditionally. */
+  if (gsk_render_node_get_preferred_depth (root) != GDK_MEMORY_NONE &&
+      gsk_render_node_get_preferred_depth (root) != GDK_MEMORY_U8 &&
+      gdk_gl_context_check_version (self->context, "3.0", "3.0"))
+    {
+      gdk_format = GDK_MEMORY_R32G32B32A32_FLOAT_PREMULTIPLIED;
+      format = GL_RGBA32F;
+    }
+  else
+    {
+      format = GL_RGBA8;
+      gdk_format = GDK_MEMORY_R8G8B8A8_PREMULTIPLIED;
+    }
+
+  gdk_gl_context_make_current (self->context);
+
+  if (gsk_gl_driver_create_render_target (self->driver,
+                                          width, height,
+                                          format,
+                                          &render_target))
+    {
+      gsk_gl_driver_begin_frame (self->driver, self->command_queue);
+      job = gsk_gl_render_job_new (self->driver, viewport, 1, NULL, render_target->framebuffer_id, TRUE);
+      gsk_gl_render_job_render_flipped (job, root);
+      texture_id = gsk_gl_driver_release_render_target (self->driver, render_target, FALSE);
+      texture = gsk_gl_driver_create_gdk_texture (self->driver, texture_id, gdk_format);
+      gsk_gl_driver_end_frame (self->driver);
+      gsk_gl_render_job_free (job);
+
+      gsk_gl_driver_after_frame (self->driver);
+    }
+  else
+    {
+      g_assert_not_reached ();
+    }
+
+  return g_steal_pointer (&texture);
+}
+
+static void
+gsk_gl_legacy_renderer_dispose (GObject *object)
+{
+  GskGLLegacyRenderer *self = (GskGLLegacyRenderer *)object;
+
+  if (self->driver != NULL)
+    g_critical ("Attempt to dispose %s without calling gsk_renderer_unrealize()",
+                G_OBJECT_TYPE_NAME (self));
+
+  G_OBJECT_CLASS (gsk_gl_legacy_renderer_parent_class)->dispose (object);
+}
+
+static void
+gsk_gl_legacy_renderer_class_init (GskGLLegacyRendererClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GskRendererClass *renderer_class = GSK_RENDERER_CLASS (klass);
+
+  object_class->dispose = gsk_gl_legacy_renderer_dispose;
+
+  renderer_class->supports_offload = TRUE;
+
+  renderer_class->realize = gsk_gl_legacy_renderer_realize;
+  renderer_class->unrealize = gsk_gl_legacy_renderer_unrealize;
+  renderer_class->render = gsk_gl_legacy_renderer_render;
+  renderer_class->render_texture = gsk_gl_legacy_renderer_render_texture;
+}
+
+static void
+gsk_gl_legacy_renderer_init (GskGLLegacyRenderer *self)
+{
+}
+
+G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+
+gboolean
+gsk_gl_legacy_renderer_try_compile_gl_shader (GskGLLegacyRenderer  *renderer,
+                                       GskGLShader    *shader,
+                                       GError        **error)
+{
+  GskGLProgram *program;
+
+  g_return_val_if_fail (GSK_IS_GL_LEGACY_RENDERER (renderer), FALSE);
+  g_return_val_if_fail (shader != NULL, FALSE);
+
+  program = gsk_gl_driver_lookup_shader (renderer->driver, shader, error);
+
+  return program != NULL;
+}
+
+G_GNUC_END_IGNORE_DEPRECATIONS
diff --git gsk/gl/gskglrenderer.h gsk/gl/gskglrenderer.h
index 89c37a3b93..84def71701 100644
--- gsk/gl/gskglrenderer.h
+++ gsk/gl/gskglrenderer.h
@@ -20,14 +20,37 @@
 
 #pragma once
 
-#ifndef __GI_SCANNER__
-# define WARNING_MSG "#include <gsk/gsk.h> instead of <gsk/gl/gskglrenderer.h> to avoid this warning"
-# ifdef _MSC_VER
-#  pragma message("WARNING: " WARNING_MSG)
-# else
-#  warning WARNING_MSG
-# endif
-# undef WARNING_MSG
+/* Allow direct inclusion: if we are not inside <gsk/gsk.h> and not building
+ * GTK itself, pull in the umbrella header first so that all prerequisites
+ * (gdk version macros, GskRenderer forward-decl, …) are available.
+ * The #pragma once above makes the subsequent #include <gsk/gsktypes.h>
+ * a no-op if gsktypes.h was already included by gsk.h.
+ */
+#if !defined (__GSK_H_INSIDE__) && !defined (GTK_COMPILATION)
+#include <gsk/gsk.h>
 #endif
 
-#include <gsk/gsk.h>
+#include <gsk/gsktypes.h>
+
+G_BEGIN_DECLS
+
+#define GSK_TYPE_GL_LEGACY_RENDERER (gsk_gl_legacy_renderer_get_type ())
+
+#define GSK_GL_LEGACY_RENDERER(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), GSK_TYPE_GL_LEGACY_RENDERER, GskGLLegacyRenderer))
+#define GSK_IS_GL_LEGACY_RENDERER(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GSK_TYPE_GL_LEGACY_RENDERER))
+#define GSK_GL_LEGACY_RENDERER_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST    ((klass), GSK_TYPE_GL_LEGACY_RENDERER, GskGLLegacyRendererClass))
+#define GSK_IS_GL_LEGACY_RENDERER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE    ((klass), GSK_TYPE_GL_LEGACY_RENDERER))
+#define GSK_GL_LEGACY_RENDERER_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS  ((obj), GSK_TYPE_GL_LEGACY_RENDERER, GskGLLegacyRendererClass))
+
+typedef struct _GskGLLegacyRenderer      GskGLLegacyRenderer;
+typedef struct _GskGLLegacyRendererClass GskGLLegacyRendererClass;
+
+GDK_AVAILABLE_IN_4_2
+GType        gsk_gl_legacy_renderer_get_type (void) G_GNUC_CONST;
+
+GDK_AVAILABLE_IN_4_2
+GskRenderer *gsk_gl_legacy_renderer_new      (void);
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (GskGLLegacyRenderer, g_object_unref)
+
+G_END_DECLS
diff --git gsk/gl/gskglrendererprivate.h gsk/gl/gskglrendererprivate.h
new file mode 100644
index 0000000000..8a9e9b06c8
--- /dev/null
+++ gsk/gl/gskglrendererprivate.h
@@ -0,0 +1,38 @@
+/* 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
+ */
+
+#pragma once
+
+#include "gskglrenderer.h"
+
+#include "gskglshader.h"
+
+G_BEGIN_DECLS
+
+G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+
+gboolean gsk_gl_legacy_renderer_try_compile_gl_shader (GskGLLegacyRenderer  *renderer,
+                                                GskGLShader    *shader,
+                                                GError        **error);
+
+G_GNUC_END_IGNORE_DEPRECATIONS
+
+G_END_DECLS
+
diff --git gsk/gl/gskglrenderjob.c gsk/gl/gskglrenderjob.c
new file mode 100644
index 0000000000..5c078742d9
--- /dev/null
+++ gsk/gl/gskglrenderjob.c
@@ -0,0 +1,4749 @@
+/* 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/gdkcolorprivate.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 <gdk/gdkmemorytextureprivate.h>
+#include <gdk/gdkdmabuftexture.h>
+#include <gdk/gdksurfaceprivate.h>
+#include <gdk/gdksubsurfaceprivate.h>
+#include <gsk/gsktransformprivate.h>
+#include <gsk/gskroundedrectprivate.h>
+#include <gsk/gskrectprivate.h>
+#include <gsk/gskrendererprivate.h>
+#include <math.h>
+#include <string.h>
+
+#include "gskbordernodeprivate.h"
+#include "gskcolornodeprivate.h"
+#include "gskinsetshadownodeprivate.h"
+#include "gskoutsetshadownodeprivate.h"
+#include "gsktextnodeprivate.h"
+#include "gskglcommandqueueprivate.h"
+#include "gskgldriverprivate.h"
+#include "gskglglyphlibraryprivate.h"
+#include "gskgliconlibraryprivate.h"
+#include "gskbordernodeprivate.h"
+#include "gskglprogramprivate.h"
+#include "gskglrenderjobprivate.h"
+#include "gskglshadowlibraryprivate.h"
+#include "gskdebugprivate.h"
+
+#include "ninesliceprivate.h"
+#include "fp16private.h"
+
+#define ORTHO_NEAR_PLANE   -10000
+#define ORTHO_FAR_PLANE     10000
+#define MAX_GRADIENT_STOPS  6
+#define SHADOW_EXTRA_SIZE   4
+
+/* Make sure gradient stops fits in packed array_count */
+G_STATIC_ASSERT ((MAX_GRADIENT_STOPS * 5) < (1 << GSK_GL_UNIFORM_ARRAY_BITS));
+
+#define ALPHA_IS_CLEAR(alpha) ((alpha) < ((float) 0x00ff / (float) 0xffff))
+#define RGBA_IS_CLEAR(rgba) ALPHA_IS_CLEAR((rgba)->alpha)
+
+typedef struct _GskGLRenderClip
+{
+  GskRoundedRect rect;
+  guint          is_rectilinear : 1;
+  guint          is_fully_contained : 1;
+} GskGLRenderClip;
+
+#define GDK_ARRAY_NAME clips
+#define GDK_ARRAY_TYPE_NAME Clips
+#define GDK_ARRAY_ELEMENT_TYPE GskGLRenderClip
+#define GDK_ARRAY_BY_VALUE 1
+#define GDK_ARRAY_PREALLOC 16
+#define GDK_ARRAY_NO_MEMSET
+#include "gdk/gdkarrayimpl.c"
+
+typedef struct _GskGLRenderModelview
+{
+  GskTransform *transform;
+  float scale_x;
+  float scale_y;
+  float dx;
+  float dy;
+  float offset_x_before;
+  float offset_y_before;
+  graphene_matrix_t matrix;
+} GskGLRenderModelview;
+
+#define GDK_ARRAY_NAME modelviews
+#define GDK_ARRAY_TYPE_NAME Modelviews
+#define GDK_ARRAY_ELEMENT_TYPE GskGLRenderModelview
+#define GDK_ARRAY_BY_VALUE 1
+#define GDK_ARRAY_PREALLOC 16
+#define GDK_ARRAY_NO_MEMSET
+#include "gdk/gdkarrayimpl.c"
+
+struct _GskGLRenderJob
+{
+  /* The context containing the framebuffer we are drawing to. Generally this
+   * is the context of the surface but may be a shared context if rendering to
+   * an offscreen texture such as gsk_gl_renderer_render_texture().
+   */
+  GdkGLContext *context;
+
+  /* The driver to be used. This is shared among all the renderers on a given
+   * GdkDisplay and uses the shared GL context to send commands.
+   */
+  GskGLDriver *driver;
+
+  /* The command queue (which is just a faster pointer to the driver's
+   * command queue.
+   */
+  GskGLCommandQueue *command_queue;
+
+  /* The region that we are clipping. Normalized to a single rectangle region. */
+  cairo_region_t *region;
+
+  /* The framebuffer to draw to in the @context GL context. So 0 would be the
+   * default framebuffer of @context. This is important to note as many other
+   * operations could be done using objects shared from the command queues
+   * GL context.
+   */
+  guint framebuffer;
+  guint default_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.
+   */
+  Modelviews modelview;
+
+  /* An array of GskGLRenderClip updated as nodes are processed. The
+   * current clip is the last element.
+   */
+  Clips clip;
+
+  /* Our current alpha state as we process nodes */
+  float alpha;
+
+  /* Offset (delta x,y) as we process nodes. Occasionally this is merged into
+   * a transform that is referenced from child transform nodes.
+   */
+  float offset_x;
+  float offset_y;
+
+  /* The scale we are processing, possibly updated by transforms */
+  float scale_x;
+  float scale_y;
+
+  /* Cached pointers */
+  const GskGLRenderClip *current_clip;
+  const GskGLRenderModelview *current_modelview;
+  GskGLProgram *current_program;
+
+  guint source_is_glyph_atlas : 1;
+
+  /* In some cases we might want to avoid clearing the framebuffer
+   * because we're going to render over the existing contents.
+   */
+  guint clear_framebuffer : 1;
+
+  /* Format we want to use for intermediate textures, determined by
+   * looking at the format of the framebuffer we are rendering on.
+   */
+  int target_format;
+
+  guint offscreen_count; /* if > 0, we're rendering to an offscreen */
+};
+
+typedef struct _GskGLRenderOffscreen
+{
+  /* The bounds to render */
+  const graphene_rect_t *bounds;
+
+  /* Return location for texture coordinates */
+  struct {
+    float x;
+    float y;
+    float x2;
+    float y2;
+  } area;
+
+  /* Return location for texture ID */
+  guint texture_id;
+  gpointer sync;
+
+  /* Whether to force creating a new texture, even if the
+   * input already is a texture
+   */
+  guint force_offscreen : 1;
+  guint reset_clip : 1;
+  guint do_not_cache : 1;
+
+  /* Return location for whether we created a texture */
+  guint was_offscreen : 1;
+  guint has_mipmap : 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 void     gsk_gl_render_job_upload_texture            (GskGLRenderJob       *job,
+                                                             GdkTexture           *texture,
+                                                             gboolean              ensure_mipmap,
+                                                             GskGLRenderOffscreen *offscreen);
+
+static inline GskGLRenderClip *
+clips_grow_one (Clips *clips)
+{
+  guint len = clips_get_size (clips);
+  clips_set_size (clips, len + 1);
+  return clips_get (clips, len);
+}
+
+static inline GskGLRenderModelview *
+modelviews_grow_one (Modelviews *modelviews)
+{
+  guint len = modelviews_get_size (modelviews);
+  modelviews_set_size (modelviews, len + 1);
+  return modelviews_get (modelviews, len);
+}
+
+static inline int
+get_target_format (GskGLRenderJob      *job,
+                   const GskRenderNode *node)
+{
+  if (gsk_render_node_get_preferred_depth (node) != GDK_MEMORY_U8)
+    return job->target_format;
+
+  return GL_RGBA8;
+}
+
+static inline void
+init_full_texture_region (GskGLRenderOffscreen *offscreen)
+{
+  offscreen->area.x = 0;
+  offscreen->area.y = 0;
+  offscreen->area.x2 = 1;
+  offscreen->area.y2 = 1;
+}
+
+static inline gboolean G_GNUC_PURE
+node_is_invisible (const GskRenderNode *node)
+{
+  return node->bounds.size.width == 0.0f ||
+         node->bounds.size.height == 0.0f;
+}
+
+static inline gboolean G_GNUC_PURE
+rounded_rect_equal (const GskRoundedRect *r1,
+                    const GskRoundedRect *r2)
+{
+  return memcmp (r1, r2, sizeof (GskRoundedRect)) == 0;
+}
+
+static inline void
+gsk_rounded_rect_shrink_to_minimum (GskRoundedRect *self)
+{
+  self->bounds.size.width  = MAX (self->corner[0].width + self->corner[1].width,
+                                  self->corner[3].width + self->corner[2].width);
+  self->bounds.size.height = MAX (self->corner[0].height + self->corner[3].height,
+                                  self->corner[1].height + self->corner[2].height);
+}
+
+static inline gboolean G_GNUC_PURE
+node_supports_2d_transform (const GskRenderNode *node)
+{
+  switch (GSK_RENDER_NODE_TYPE (node))
+    {
+    case GSK_COLOR_NODE:
+    case GSK_OPACITY_NODE:
+    case GSK_COLOR_MATRIX_NODE:
+    case GSK_TEXTURE_NODE:
+    case GSK_TEXTURE_SCALE_NODE:
+    case GSK_CROSS_FADE_NODE:
+    case GSK_LINEAR_GRADIENT_NODE:
+    case GSK_REPEATING_LINEAR_GRADIENT_NODE:
+    case GSK_CONIC_GRADIENT_NODE:
+    case GSK_RADIAL_GRADIENT_NODE:
+    case GSK_REPEATING_RADIAL_GRADIENT_NODE:
+    case GSK_DEBUG_NODE:
+    case GSK_TEXT_NODE:
+    case GSK_CAIRO_NODE:
+    case GSK_BLEND_NODE:
+    case GSK_BLUR_NODE:
+    case GSK_MASK_NODE:
+    case GSK_FILL_NODE:
+    case GSK_STROKE_NODE:
+    case GSK_SUBSURFACE_NODE:
+      return TRUE;
+
+    case GSK_SHADOW_NODE:
+      return node_supports_2d_transform (gsk_shadow_node_get_child (node));
+
+    case GSK_TRANSFORM_NODE:
+      return node_supports_2d_transform (gsk_transform_node_get_child (node));
+
+    case GSK_CONTAINER_NODE:
+      for (guint i = 0, p = gsk_container_node_get_n_children (node); i < p; i++)
+        {
+          if (!node_supports_2d_transform (gsk_container_node_get_child (node, i)))
+            return FALSE;
+        }
+      return TRUE;
+
+    case GSK_BORDER_NODE:
+    case GSK_INSET_SHADOW_NODE:
+    case GSK_OUTSET_SHADOW_NODE:
+    case GSK_REPEAT_NODE:
+    case GSK_CLIP_NODE:
+    case GSK_ROUNDED_CLIP_NODE:
+    case GSK_GL_SHADER_NODE:
+    case GSK_COMPONENT_TRANSFER_NODE:
+    case GSK_COPY_NODE:
+    case GSK_PASTE_NODE:
+    case GSK_COMPOSITE_NODE:
+    case GSK_ISOLATION_NODE:
+    case GSK_DISPLACEMENT_NODE:
+    case GSK_ARITHMETIC_NODE:
+      return FALSE;
+
+    case GSK_NOT_A_RENDER_NODE:
+    default:
+      g_assert_not_reached ();
+    }
+}
+
+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 (GSK_RENDER_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_DEBUG_NODE:
+    case GSK_TEXT_NODE:
+    case GSK_CAIRO_NODE:
+    case GSK_BLEND_NODE:
+    case GSK_BLUR_NODE:
+    case GSK_MASK_NODE:
+    case GSK_FILL_NODE:
+    case GSK_STROKE_NODE:
+    case GSK_SUBSURFACE_NODE:
+      return TRUE;
+
+    case GSK_SHADOW_NODE:
+      return node_supports_transform (gsk_shadow_node_get_child (node));
+
+    case GSK_TRANSFORM_NODE:
+      return node_supports_transform (gsk_transform_node_get_child (node));
+
+    case GSK_CONTAINER_NODE:
+    case GSK_LINEAR_GRADIENT_NODE:
+    case GSK_REPEATING_LINEAR_GRADIENT_NODE:
+    case GSK_RADIAL_GRADIENT_NODE:
+    case GSK_REPEATING_RADIAL_GRADIENT_NODE:
+    case GSK_CONIC_GRADIENT_NODE:
+    case GSK_BORDER_NODE:
+    case GSK_INSET_SHADOW_NODE:
+    case GSK_OUTSET_SHADOW_NODE:
+    case GSK_REPEAT_NODE:
+    case GSK_CLIP_NODE:
+    case GSK_ROUNDED_CLIP_NODE:
+    case GSK_GL_SHADER_NODE:
+    case GSK_TEXTURE_SCALE_NODE:
+    case GSK_COMPONENT_TRANSFER_NODE:
+    case GSK_COPY_NODE:
+    case GSK_PASTE_NODE:
+    case GSK_COMPOSITE_NODE:
+    case GSK_ISOLATION_NODE:
+    case GSK_DISPLACEMENT_NODE:
+    case GSK_ARITHMETIC_NODE:
+      return FALSE;
+
+    case GSK_NOT_A_RENDER_NODE:
+
+    default:
+      g_assert_not_reached ();
+    }
+}
+
+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 void
+init_projection_matrix (graphene_matrix_t     *projection,
+                        const graphene_rect_t *viewport)
+{
+  graphene_matrix_init_ortho (projection,
+                              viewport->origin.x,
+                              viewport->origin.x + viewport->size.width,
+                              viewport->origin.y,
+                              viewport->origin.y + viewport->size.height,
+                              ORTHO_NEAR_PLANE,
+                              ORTHO_FAR_PLANE);
+  graphene_matrix_scale (projection, 1, -1, 1);
+}
+
+static inline float
+gsk_gl_render_job_set_alpha (GskGLRenderJob *job,
+                             float           alpha)
+{
+  if (job->alpha != alpha)
+    {
+      float ret = job->alpha;
+      job->alpha = alpha;
+      job->driver->stamps[UNIFORM_SHARED_ALPHA]++;
+      return ret;
+    }
+
+  return alpha;
+}
+
+static void
+extract_matrix_metadata (GskGLRenderModelview *modelview)
+{
+  gsk_transform_to_matrix (modelview->transform, &modelview->matrix);
+
+  switch (gsk_transform_get_category (modelview->transform))
+    {
+    case GSK_TRANSFORM_CATEGORY_IDENTITY:
+      modelview->scale_x = 1;
+      modelview->scale_y = 1;
+      modelview->dx = 0;
+      modelview->dy = 0;
+      break;
+
+    case GSK_TRANSFORM_CATEGORY_2D_TRANSLATE:
+      modelview->scale_x = 1;
+      modelview->scale_y = 1;
+      gsk_transform_to_translate (modelview->transform,
+                                  &modelview->dx, &modelview->dy);
+      break;
+
+    case GSK_TRANSFORM_CATEGORY_2D_AFFINE:
+      gsk_transform_to_affine (modelview->transform,
+                               &modelview->scale_x, &modelview->scale_y,
+                               &modelview->dx, &modelview->dy);
+      break;
+
+    case GSK_TRANSFORM_CATEGORY_2D:
+      {
+        float skew_x, skew_y, angle, dx, dy;
+
+        gsk_transform_to_2d_components (modelview->transform,
+                                        &skew_x, &skew_y,
+                                        &modelview->scale_x, &modelview->scale_y,
+                                        &angle, &dx, &dy);
+        modelview->dx = 0;
+        modelview->dy = 0;
+      }
+      break;
+
+    case GSK_TRANSFORM_CATEGORY_UNKNOWN:
+    case GSK_TRANSFORM_CATEGORY_ANY:
+    case GSK_TRANSFORM_CATEGORY_3D:
+      {
+        graphene_quaternion_t rotation;
+        graphene_vec4_t perspective;
+        graphene_vec3_t translation;
+        graphene_vec3_t scale;
+        graphene_vec3_t shear;
+
+        graphene_matrix_decompose (&modelview->matrix,
+                                   &translation,
+                                   &scale,
+                                   &rotation,
+                                   &shear,
+                                   &perspective);
+
+        modelview->scale_x = graphene_vec3_get_x (&scale);
+        modelview->scale_y = graphene_vec3_get_y (&scale);
+        modelview->dx = 0;
+        modelview->dy = 0;
+      }
+      break;
+
+    default:
+      break;
+    }
+}
+
+/* takes ownership of transform */
+static void
+gsk_gl_render_job_set_modelview (GskGLRenderJob *job,
+                                 GskTransform   *transform)
+{
+  GskGLRenderModelview *modelview;
+
+  g_assert (job != NULL);
+
+  job->driver->stamps[UNIFORM_SHARED_MODELVIEW]++;
+
+  modelview = modelviews_grow_one (&job->modelview);
+
+  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;
+}
+
+/* doesn't take ownership of transform */
+static void
+gsk_gl_render_job_push_modelview (GskGLRenderJob *job,
+                                  GskTransform   *transform)
+{
+  GskGLRenderModelview *modelview;
+
+  g_assert (job != NULL);
+  g_assert (transform != NULL);
+
+  job->driver->stamps[UNIFORM_SHARED_MODELVIEW]++;
+
+  modelview = modelviews_grow_one (&job->modelview);
+
+  if G_LIKELY (modelviews_get_size (&job->modelview) > 1)
+    {
+      GskGLRenderModelview *last = job->modelview.end - 2;
+      GskTransform *t = NULL;
+
+      /* Multiply given matrix with our previous modelview */
+      t = gsk_transform_translate (gsk_transform_ref (last->transform),
+                                   &(graphene_point_t) {
+                                     job->offset_x,
+                                     job->offset_y
+                                   });
+      t = gsk_transform_transform (t, transform);
+      modelview->transform = t;
+    }
+  else
+    {
+      modelview->transform = gsk_transform_ref (transform);
+    }
+
+  modelview->offset_x_before = job->offset_x;
+  modelview->offset_y_before = job->offset_y;
+
+  extract_matrix_metadata (modelview);
+
+  job->offset_x = 0;
+  job->offset_y = 0;
+  job->scale_x = modelview->scale_x;
+  job->scale_y = modelview->scale_y;
+
+  job->current_modelview = modelview;
+}
+
+static void
+gsk_gl_render_job_pop_modelview (GskGLRenderJob *job)
+{
+  const GskGLRenderModelview *head;
+
+  g_assert (job != NULL);
+  g_assert (modelviews_get_size (&job->modelview) > 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.end--;
+
+  if (modelviews_get_size (&job->modelview) >= 1)
+    {
+      head = job->modelview.end - 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 (rect != NULL);
+
+  job->driver->stamps[UNIFORM_SHARED_CLIP_RECT]++;
+
+  clip = clips_grow_one (&job->clip);
+
+  memcpy (&clip->rect, rect, sizeof *rect);
+  clip->is_rectilinear = gsk_rounded_rect_is_rectilinear (rect);
+  clip->is_fully_contained = FALSE;
+
+  job->current_clip = clip;
+}
+
+static void
+gsk_gl_render_job_push_contained_clip (GskGLRenderJob *job)
+{
+  GskGLRenderClip *clip;
+  GskGLRenderClip *old_clip;
+
+  g_assert (job != NULL);
+  g_assert (clips_get_size (&job->clip) > 0);
+
+  job->driver->stamps[UNIFORM_SHARED_CLIP_RECT]++;
+
+  clip = clips_grow_one (&job->clip);
+  old_clip = clips_get (&job->clip, clips_get_size (&job->clip) - 2);
+
+  memcpy (&clip->rect.bounds, &old_clip->rect.bounds, sizeof (graphene_rect_t));
+  memset (clip->rect.corner, 0, sizeof clip->rect.corner);
+  clip->is_rectilinear = TRUE;
+  clip->is_fully_contained = TRUE;
+
+  job->current_clip = clip;
+}
+
+static void
+gsk_gl_render_job_pop_clip (GskGLRenderJob *job)
+{
+  g_assert (job != NULL);
+  g_assert (clips_get_size (&job->clip) > 0);
+
+  job->driver->stamps[UNIFORM_SHARED_CLIP_RECT]++;
+  job->current_clip--;
+  job->clip.end--;
+}
+
+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 (modelviews_get_size (&job->modelview) > 0);
+  g_assert (rect != NULL);
+  g_assert (out_rect != NULL);
+
+  transform = job->current_modelview->transform;
+  category = gsk_transform_get_category (transform);
+
+  /* Our most common transform is 2d-affine, so inline it.
+   * Both identity and 2d-translate are virtually unseen here.
+   */
+  if G_LIKELY (category >= GSK_TRANSFORM_CATEGORY_2D_AFFINE)
+    {
+      float scale_x = job->current_modelview->scale_x;
+      float scale_y = job->current_modelview->scale_y;
+      float dx = job->current_modelview->dx;
+      float dy = job->current_modelview->dy;
+
+      /* Init directly into out rect */
+      out_rect->origin.x = ((rect->origin.x + job->offset_x) * scale_x) + dx;
+      out_rect->origin.y = ((rect->origin.y + job->offset_y) * scale_y) + dy;
+      out_rect->size.width = rect->size.width * scale_x;
+      out_rect->size.height = rect->size.height * scale_y;
+
+      /* Normalize in place */
+      if (out_rect->size.width < 0.f)
+        {
+          float size = fabsf (out_rect->size.width);
+
+          out_rect->origin.x -= size;
+          out_rect->size.width = size;
+        }
+
+      if (out_rect->size.height < 0.f)
+        {
+          float size = fabsf (out_rect->size.height);
+
+          out_rect->origin.y -= size;
+          out_rect->size.height = size;
+        }
+    }
+  else
+    {
+      graphene_rect_t r;
+
+      r.origin.x = rect->origin.x + job->offset_x;
+      r.origin.y = rect->origin.y + job->offset_y;
+      r.size.width = rect->size.width;
+      r.size.height = rect->size.height;
+
+      gsk_transform_transform_bounds (transform, &r, out_rect);
+    }
+}
+
+static inline void
+gsk_gl_render_job_untransform_bounds (GskGLRenderJob        *job,
+                                      const graphene_rect_t *rect,
+                                      graphene_rect_t       *out_rect)
+{
+  GskTransform *transform;
+
+  transform = gsk_transform_invert (gsk_transform_ref (job->current_modelview->transform));
+
+  gsk_transform_transform_bounds (transform, rect, out_rect);
+
+  out_rect->origin.x -= job->offset_x;
+  out_rect->origin.y -= job->offset_y;
+
+  gsk_transform_unref (transform);
+}
+
+static inline void
+gsk_gl_render_job_translate_rounded_rect (GskGLRenderJob       *job,
+                                          const GskRoundedRect *rect,
+                                          GskRoundedRect       *out_rect)
+ {
+  out_rect->bounds.origin.x = job->offset_x + rect->bounds.origin.x;
+  out_rect->bounds.origin.y = job->offset_y + rect->bounds.origin.y;
+  out_rect->bounds.size.width = rect->bounds.size.width;
+  out_rect->bounds.size.height = rect->bounds.size.height;
+  memcpy (out_rect->corner, rect->corner, sizeof rect->corner);
+}
+
+static inline void
+rounded_rect_scale_corners (const GskRoundedRect *rect,
+                            GskRoundedRect       *out_rect,
+                            float                 scale_x,
+                            float                 scale_y)
+{
+  for (guint i = 0; i < G_N_ELEMENTS (out_rect->corner); i++)
+    {
+      out_rect->corner[i].width = rect->corner[i].width * fabsf (scale_x);
+      out_rect->corner[i].height = rect->corner[i].height * fabsf (scale_y);
+    }
+
+  if (scale_x < 0)
+    {
+      graphene_size_t p;
+
+      p = out_rect->corner[GSK_CORNER_TOP_LEFT];
+      out_rect->corner[GSK_CORNER_TOP_LEFT] = out_rect->corner[GSK_CORNER_TOP_RIGHT];
+      out_rect->corner[GSK_CORNER_TOP_RIGHT] = p;
+      p = out_rect->corner[GSK_CORNER_BOTTOM_LEFT];
+      out_rect->corner[GSK_CORNER_BOTTOM_LEFT] = out_rect->corner[GSK_CORNER_BOTTOM_RIGHT];
+      out_rect->corner[GSK_CORNER_BOTTOM_RIGHT] = p;
+    }
+
+  if (scale_y < 0)
+    {
+      graphene_size_t p;
+
+      p = out_rect->corner[GSK_CORNER_TOP_LEFT];
+      out_rect->corner[GSK_CORNER_TOP_LEFT] = out_rect->corner[GSK_CORNER_BOTTOM_LEFT];
+      out_rect->corner[GSK_CORNER_BOTTOM_LEFT] = p;
+      p = out_rect->corner[GSK_CORNER_TOP_RIGHT];
+      out_rect->corner[GSK_CORNER_TOP_RIGHT] = out_rect->corner[GSK_CORNER_BOTTOM_RIGHT];
+      out_rect->corner[GSK_CORNER_BOTTOM_RIGHT] = p;
+    }
+}
+
+static inline void
+gsk_gl_render_job_transform_rounded_rect (GskGLRenderJob       *job,
+                                          const GskRoundedRect *rect,
+                                          GskRoundedRect       *out_rect)
+{
+  gsk_gl_render_job_transform_bounds (job, &rect->bounds, &out_rect->bounds);
+  rounded_rect_scale_corners (rect, out_rect, job->scale_x, job->scale_y);
+}
+
+static inline void
+rounded_rect_get_inner (const GskRoundedRect *rect,
+                        graphene_rect_t      *inner)
+{
+  float left = MAX (rect->corner[GSK_CORNER_TOP_LEFT].width, rect->corner[GSK_CORNER_BOTTOM_LEFT].width);
+  float right = MAX (rect->corner[GSK_CORNER_TOP_RIGHT].width, rect->corner[GSK_CORNER_BOTTOM_RIGHT].width);
+  float top = MAX (rect->corner[GSK_CORNER_TOP_LEFT].height, rect->corner[GSK_CORNER_TOP_RIGHT].height);
+  float bottom = MAX (rect->corner[GSK_CORNER_BOTTOM_LEFT].height, rect->corner[GSK_CORNER_BOTTOM_RIGHT].height);
+
+  inner->origin.x = rect->bounds.origin.x + left;
+  inner->size.width = rect->bounds.size.width - (left + right);
+
+  inner->origin.y = rect->bounds.origin.y + top;
+  inner->size.height = rect->bounds.size.height - (top + bottom);
+}
+
+static inline gboolean
+interval_contains (float p1, float w1,
+                   float p2, float w2)
+{
+  if (p2 < p1)
+    return FALSE;
+
+  if (p2 + w2 > p1 + w1)
+    return FALSE;
+
+  return TRUE;
+}
+
+static inline gboolean
+gsk_gl_render_job_update_clip (GskGLRenderJob        *job,
+                               const graphene_rect_t *bounds,
+                               gboolean              *pushed_clip)
+{
+  graphene_rect_t transformed_bounds;
+  gboolean no_clip = FALSE;
+  gboolean rect_clip = FALSE;
+
+  *pushed_clip = FALSE;
+
+  if (job->current_clip->is_fully_contained)
+    {
+      /* Already fully contained - no further checks needed */
+      return TRUE;
+    }
+
+  gsk_gl_render_job_transform_bounds (job, bounds, &transformed_bounds);
+
+  if (!gsk_rect_intersects (&job->current_clip->rect.bounds, &transformed_bounds))
+    {
+      /* Completely clipped away */
+      return FALSE;
+    }
+
+  if (job->current_clip->is_rectilinear)
+    {
+      if (gsk_rect_contains_rect (&job->current_clip->rect.bounds, &transformed_bounds))
+        no_clip = TRUE;
+      else
+        rect_clip = TRUE;
+    }
+  else if (gsk_rounded_rect_contains_rect (&job->current_clip->rect, &transformed_bounds))
+    {
+      no_clip = TRUE;
+    }
+  else
+    {
+      graphene_rect_t inner;
+
+      rounded_rect_get_inner (&job->current_clip->rect, &inner);
+
+      if (interval_contains (inner.origin.x, inner.size.width,
+                             transformed_bounds.origin.x, transformed_bounds.size.width) ||
+          interval_contains (inner.origin.y, inner.size.height,
+                             transformed_bounds.origin.y, transformed_bounds.size.height))
+        rect_clip = TRUE;
+    }
+
+  if (no_clip)
+    {
+      /* This node is completely contained inside the clip.
+       * Record this fact on the clip stack, so we don't do more work
+       * for child nodes.
+       */
+
+      gsk_gl_render_job_push_contained_clip (job);
+
+      *pushed_clip = TRUE;
+    }
+  else if (rect_clip && !job->current_clip->is_rectilinear)
+    {
+      graphene_rect_t rect;
+
+      /* The clip gets simpler for this node */
+
+      gsk_rect_intersection (&job->current_clip->rect.bounds, &transformed_bounds, &rect);
+      gsk_gl_render_job_push_clip (job, &GSK_ROUNDED_RECT_INIT_FROM_RECT (rect));
+
+      *pushed_clip = TRUE;
+    }
+
+  return TRUE;
+}
+
+static inline void
+rgba_to_half (const GdkRGBA *rgba,
+              guint16        h[4])
+{
+  float_to_half4 ((const float *)rgba, h);
+}
+
+/* fill_vertex_data */
+static void
+gsk_gl_render_job_draw_coords (GskGLRenderJob *job,
+                               float           min_x,
+                               float           min_y,
+                               float           max_x,
+                               float           max_y,
+                               float           min_u,
+                               float           min_v,
+                               float           max_u,
+                               float           max_v,
+                               guint16         c[4])
+{
+  GskGLDrawVertex *vertices = gsk_gl_command_queue_add_vertices (job->command_queue);
+
+  vertices[0] = (GskGLDrawVertex) { .position = { min_x, min_y }, .uv = { min_u, min_v }, .color = { c[0], c[1], c[2], c[3] } };
+  vertices[1] = (GskGLDrawVertex) { .position = { min_x, max_y }, .uv = { min_u, max_v }, .color = { c[0], c[1], c[2], c[3] } };
+  vertices[2] = (GskGLDrawVertex) { .position = { max_x, min_y }, .uv = { max_u, min_v }, .color = { c[0], c[1], c[2], c[3] } };
+  vertices[3] = (GskGLDrawVertex) { .position = { max_x, max_y }, .uv = { max_u, max_v }, .color = { c[0], c[1], c[2], c[3] } };
+  vertices[4] = (GskGLDrawVertex) { .position = { min_x, max_y }, .uv = { min_u, max_v }, .color = { c[0], c[1], c[2], c[3] } };
+  vertices[5] = (GskGLDrawVertex) { .position = { max_x, min_y }, .uv = { max_u, min_v }, .color = { c[0], c[1], c[2], c[3] } };
+}
+
+/* load_vertex_data_with_region */
+static inline void
+gsk_gl_render_job_draw_offscreen_with_color (GskGLRenderJob             *job,
+                                             const graphene_rect_t      *bounds,
+                                             const GskGLRenderOffscreen *offscreen,
+                                             guint16                     color[4])
+{
+  float min_x = job->offset_x + bounds->origin.x;
+  float min_y = job->offset_y + bounds->origin.y;
+  float max_x = min_x + bounds->size.width;
+  float max_y = min_y + bounds->size.height;
+  float y1 = offscreen->was_offscreen ? offscreen->area.y2 : offscreen->area.y;
+  float y2 = offscreen->was_offscreen ? offscreen->area.y : offscreen->area.y2;
+
+  gsk_gl_render_job_draw_coords (job,
+                                 min_x, min_y, max_x, max_y,
+                                 offscreen->area.x, y1, offscreen->area.x2, y2,
+                                 color);
+}
+
+static inline void
+gsk_gl_render_job_draw_offscreen (GskGLRenderJob             *job,
+                                  const graphene_rect_t      *bounds,
+                                  const GskGLRenderOffscreen *offscreen)
+{
+  gsk_gl_render_job_draw_offscreen_with_color (job, bounds, offscreen,
+                                               (guint16[]) { FP16_ZERO, FP16_ZERO, FP16_ZERO, FP16_ZERO });
+}
+
+/* load_float_vertex_data */
+static inline void
+gsk_gl_render_job_draw_with_color (GskGLRenderJob *job,
+                                   float           x,
+                                   float           y,
+                                   float           width,
+                                   float           height,
+                                   guint16         color[4])
+{
+  float min_x = job->offset_x + x;
+  float min_y = job->offset_y + y;
+  float max_x = min_x + width;
+  float max_y = min_y + height;
+
+  gsk_gl_render_job_draw_coords (job, min_x, min_y, max_x, max_y, 0, 0, 1, 1, color);
+}
+
+static inline void
+gsk_gl_render_job_draw (GskGLRenderJob *job,
+                        float           x,
+                        float           y,
+                        float           width,
+                        float           height)
+{
+  gsk_gl_render_job_draw_with_color (job, x, y, width, height,
+                                     (guint16[]) { FP_ZERO, FP_ZERO, FP_ZERO, FP_ZERO });
+}
+
+/* load_vertex_data */
+static inline void
+gsk_gl_render_job_draw_rect_with_color (GskGLRenderJob        *job,
+                                        const graphene_rect_t *bounds,
+                                        guint16                color[4])
+{
+  gsk_gl_render_job_draw_with_color (job,
+                                     bounds->origin.x,
+                                     bounds->origin.y,
+                                     bounds->size.width,
+                                     bounds->size.height,
+                                     color);
+}
+static inline void
+gsk_gl_render_job_draw_rect (GskGLRenderJob        *job,
+                             const graphene_rect_t *bounds)
+{
+  gsk_gl_render_job_draw (job,
+                          bounds->origin.x,
+                          bounds->origin.y,
+                          bounds->size.width,
+                          bounds->size.height);
+}
+
+/* load_offscreen_vertex_data */
+static inline void
+gsk_gl_render_job_draw_offscreen_rect (GskGLRenderJob        *job,
+                                       const graphene_rect_t *bounds)
+{
+  float min_x = job->offset_x + bounds->origin.x;
+  float min_y = job->offset_y + bounds->origin.y;
+  float max_x = min_x + bounds->size.width;
+  float max_y = min_y + bounds->size.height;
+  guint16 color[4] = { FP16_ZERO, FP16_ZERO, FP16_ZERO, FP16_ZERO };
+
+  gsk_gl_render_job_draw_coords (job,
+                                 min_x, min_y, max_x, max_y,
+                                 0, 1, 1, 0,
+                                 color);
+}
+
+static inline gboolean
+gsk_gl_render_job_begin_draw (GskGLRenderJob *job,
+                              GskGLProgram   *program)
+{
+  job->current_program = program;
+
+  if (!gsk_gl_command_queue_begin_draw (job->command_queue,
+                                        program->program_info,
+                                        job->viewport.size.width,
+                                        job->viewport.size.height))
+    return FALSE;
+
+  gsk_gl_uniform_state_set4fv (program->uniforms,
+                               program->program_info,
+                               UNIFORM_SHARED_VIEWPORT,
+                               job->driver->stamps[UNIFORM_SHARED_VIEWPORT],
+                               1,
+                               (const float *)&job->viewport);
+
+  gsk_gl_uniform_state_set_matrix (program->uniforms,
+                                   program->program_info,
+                                   UNIFORM_SHARED_MODELVIEW,
+                                   job->driver->stamps[UNIFORM_SHARED_MODELVIEW],
+                                   &job->current_modelview->matrix);
+
+  gsk_gl_uniform_state_set_matrix (program->uniforms,
+                                   program->program_info,
+                                   UNIFORM_SHARED_PROJECTION,
+                                   job->driver->stamps[UNIFORM_SHARED_PROJECTION],
+                                   &job->projection);
+
+  gsk_gl_uniform_state_set_rounded_rect (program->uniforms,
+                                         program->program_info,
+                                         UNIFORM_SHARED_CLIP_RECT,
+                                         job->driver->stamps[UNIFORM_SHARED_CLIP_RECT],
+                                         &job->current_clip->rect);
+
+  gsk_gl_uniform_state_set1f (program->uniforms,
+                              program->program_info,
+                              UNIFORM_SHARED_ALPHA,
+                              job->driver->stamps[UNIFORM_SHARED_ALPHA],
+                              job->alpha);
+
+  return TRUE;
+}
+
+#define CHOOSE_PROGRAM(job,name) \
+  (job->current_clip->is_fully_contained \
+      ? job->driver->name ## _no_clip \
+      : (job->current_clip->is_rectilinear \
+        ? job->driver->name ## _rect_clip \
+        : job->driver->name))
+
+static inline void
+gsk_gl_render_job_split_draw (GskGLRenderJob *job)
+{
+  gsk_gl_command_queue_split_draw (job->command_queue);
+}
+
+static inline void
+gsk_gl_render_job_end_draw (GskGLRenderJob *job)
+{
+  gsk_gl_command_queue_end_draw (job->command_queue);
+
+  job->current_program = NULL;
+}
+
+static inline void
+gsk_gl_render_job_visit_as_fallback (GskGLRenderJob      *job,
+                                     const GskRenderNode *node)
+{
+  float scale_x = job->scale_x;
+  float scale_y = job->scale_y;
+  int surface_width = ceilf (node->bounds.size.width * fabsf (scale_x));
+  int surface_height = ceilf (node->bounds.size.height * fabsf (scale_y));
+  GdkTexture *texture;
+  cairo_surface_t *surface;
+  cairo_surface_t *rendered_surface;
+  cairo_t *cr;
+  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;
+
+  texture_id = gsk_gl_driver_lookup_texture (job->driver, &key, NULL);
+
+  if (texture_id != 0)
+    goto done;
+
+  /* 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, fabsf (scale_x), fabsf (scale_y));
+    cr = cairo_create (rendered_surface);
+
+    cairo_save (cr);
+    cairo_translate (cr, - node->bounds.origin.x, - node->bounds.origin.y);
+    /* Render nodes don't modify state, so casting away the const is fine here */
+    gsk_render_node_draw_fallback ((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, fabsf (scale_x), fabsf (scale_y));
+  cr = cairo_create (surface);
+
+  /* We draw upside down here, so it matches what GL does. */
+  cairo_save (cr);
+  cairo_scale (cr, scale_x < 0 ? -1 : 1, scale_y < 0 ? 1 : -1);
+  cairo_translate (cr, scale_x < 0 ? - surface_width / fabsf (scale_x) : 0,
+                       scale_y < 0 ? 0 : - surface_height / fabsf (scale_y));
+  cairo_set_source_surface (cr, rendered_surface, 0, 0);
+  cairo_rectangle (cr, 0, 0, surface_width / fabsf (scale_x), surface_height / fabsf (scale_y));
+  cairo_fill (cr);
+  cairo_restore (cr);
+  cairo_destroy (cr);
+
+  /* Create texture to upload */
+  texture = gdk_texture_new_for_surface (surface);
+  texture_id = gsk_gl_driver_load_texture (job->driver, texture, FALSE);
+
+  if (gdk_gl_context_has_feature (job->command_queue->context, GDK_GL_FEATURE_DEBUG))
+    gdk_gl_context_label_object_printf (job->command_queue->context, GL_TEXTURE, texture_id,
+                                        "Fallback %s %d",
+                                        g_type_name_from_instance ((GTypeInstance *) node),
+                                        texture_id);
+
+  g_object_unref (texture);
+  cairo_surface_destroy (surface);
+  cairo_surface_destroy (rendered_surface);
+
+  gsk_gl_driver_cache_texture (job->driver, &key, texture_id);
+
+done:
+  if (scale_x < 0 || scale_y < 0)
+    {
+      GskTransform *transform = gsk_transform_translate (gsk_transform_scale (NULL, scale_x < 0 ? -1 : 1, scale_y < 0 ? -1 : 1),
+                                                         &GRAPHENE_POINT_INIT (scale_x < 0 ? - (node->bounds.size.width + 2 * node->bounds.origin.x) : 0,
+                                                                               scale_y < 0 ? - (node->bounds.size.height + 2 * node->bounds.origin.y) : 0));
+      gsk_gl_render_job_push_modelview (job, transform);
+      gsk_transform_unref (transform);
+    }
+
+  if (!gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, blit)))
+    goto out;
+
+  gsk_gl_program_set_uniform_texture (job->current_program,
+                                      UNIFORM_SHARED_SOURCE, 0,
+                                      GL_TEXTURE_2D,
+                                      GL_TEXTURE0,
+                                      texture_id);
+  job->source_is_glyph_atlas = FALSE;
+  gsk_gl_render_job_draw_offscreen_rect (job, &node->bounds);
+  gsk_gl_render_job_end_draw (job);
+
+out:
+  if (scale_x < 0 || scale_y < 0)
+    gsk_gl_render_job_pop_modelview (job);
+}
+
+static guint
+blur_offscreen (GskGLRenderJob       *job,
+                GskGLRenderOffscreen *offscreen,
+                int                   texture_to_blur_width,
+                int                   texture_to_blur_height,
+                float                 blur_radius_x,
+                float                 blur_radius_y)
+{
+  const GskRoundedRect new_clip = GSK_ROUNDED_RECT_INIT (0, 0, texture_to_blur_width, texture_to_blur_height);
+  GskGLRenderTarget *pass1;
+  GskGLRenderTarget *pass2;
+  graphene_matrix_t prev_projection;
+  graphene_rect_t prev_viewport;
+  guint prev_fbo;
+
+  g_assert (blur_radius_x > 0);
+  g_assert (blur_radius_y > 0);
+  g_assert (offscreen->texture_id > 0);
+  g_assert (offscreen->area.x2 > offscreen->area.x);
+  g_assert (offscreen->area.y2 > offscreen->area.y);
+
+  if (!gsk_gl_driver_create_render_target (job->driver,
+                                           MAX (texture_to_blur_width, 1),
+                                           MAX (texture_to_blur_height, 1),
+                                           job->target_format,
+                                           &pass1))
+    return 0;
+
+  if (texture_to_blur_width <= 0 || texture_to_blur_height <= 0)
+    return gsk_gl_driver_release_render_target (job->driver, pass1, FALSE);
+
+  if (!gsk_gl_driver_create_render_target (job->driver,
+                                           texture_to_blur_width,
+                                           texture_to_blur_height,
+                                           job->target_format,
+                                           &pass2))
+    return gsk_gl_driver_release_render_target (job->driver, pass1, FALSE);
+
+  gsk_gl_render_job_set_viewport (job, &new_clip.bounds, &prev_viewport);
+  gsk_gl_render_job_set_projection_from_rect (job, &new_clip.bounds, &prev_projection);
+  gsk_gl_render_job_set_modelview (job, NULL);
+  gsk_gl_render_job_push_clip (job, &new_clip);
+
+  /* Bind new framebuffer and clear it */
+  prev_fbo = gsk_gl_command_queue_bind_framebuffer (job->command_queue, pass1->framebuffer_id);
+  gsk_gl_command_queue_clear (job->command_queue, 0, &job->viewport);
+
+  /* Begin drawing the first horizontal pass, using offscreen as the
+   * source texture for the program.
+   */
+  if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, blur)))
+    {
+      gsk_gl_program_set_uniform_texture (job->current_program,
+                                          UNIFORM_SHARED_SOURCE, 0,
+                                          GL_TEXTURE_2D,
+                                          GL_TEXTURE0,
+                                          offscreen->texture_id);
+      job->source_is_glyph_atlas = FALSE;
+      gsk_gl_program_set_uniform1f (job->current_program,
+                                    UNIFORM_BLUR_RADIUS, 0,
+                                    blur_radius_x);
+      gsk_gl_program_set_uniform2f (job->current_program,
+                                    UNIFORM_BLUR_SIZE, 0,
+                                    texture_to_blur_width,
+                                    texture_to_blur_height);
+      gsk_gl_program_set_uniform2f (job->current_program,
+                                    UNIFORM_BLUR_DIR, 0,
+                                    1, 0);
+      gsk_gl_render_job_draw_coords (job,
+                                     0, 0, texture_to_blur_width, texture_to_blur_height,
+                                     0, 1, 1, 0,
+                                     (guint16[]) { FP16_ZERO, FP16_ZERO, FP16_ZERO, FP16_ZERO });
+      gsk_gl_render_job_end_draw (job);
+    }
+
+  /* Bind second pass framebuffer and clear it */
+  gsk_gl_command_queue_bind_framebuffer (job->command_queue, pass2->framebuffer_id);
+  gsk_gl_command_queue_clear (job->command_queue, 0, &job->viewport);
+
+  /* Draw using blur program with first pass as source texture */
+  if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, blur)))
+    {
+      gsk_gl_program_set_uniform_texture (job->current_program,
+                                          UNIFORM_SHARED_SOURCE, 0,
+                                          GL_TEXTURE_2D,
+                                          GL_TEXTURE0,
+                                          pass1->texture_id);
+      job->source_is_glyph_atlas = FALSE;
+      gsk_gl_program_set_uniform1f (job->current_program,
+                                    UNIFORM_BLUR_RADIUS, 0,
+                                    blur_radius_y);
+      gsk_gl_program_set_uniform2f (job->current_program,
+                                    UNIFORM_BLUR_SIZE, 0,
+                                    texture_to_blur_width,
+                                    texture_to_blur_height);
+      gsk_gl_program_set_uniform2f (job->current_program,
+                                    UNIFORM_BLUR_DIR, 0,
+                                    0, 1);
+      gsk_gl_render_job_draw_coords (job,
+                                     0, 0, texture_to_blur_width, texture_to_blur_height,
+                                     0, 1, 1, 0,
+                                     (guint16[]) { FP16_ZERO, FP16_ZERO, FP16_ZERO, FP16_ZERO });
+      gsk_gl_render_job_end_draw (job);
+    }
+
+  gsk_gl_render_job_pop_modelview (job);
+  gsk_gl_render_job_pop_clip (job);
+  gsk_gl_render_job_set_viewport (job, &prev_viewport, NULL);
+  gsk_gl_render_job_set_projection (job, &prev_projection);
+  gsk_gl_command_queue_bind_framebuffer (job->command_queue, prev_fbo);
+
+  gsk_gl_driver_release_render_target (job->driver, pass1, TRUE);
+
+  return gsk_gl_driver_release_render_target (job->driver, pass2, FALSE);
+}
+
+static void
+blur_node (GskGLRenderJob       *job,
+           GskGLRenderOffscreen *offscreen,
+           const GskRenderNode  *node,
+           float                 blur_radius,
+           float                *min_x,
+           float                *max_x,
+           float                *min_y,
+           float                *max_y)
+{
+  const float blur_extra = blur_radius * 2.0; /* 2.0 = shader radius_multiplier */
+  const float half_blur_extra = (blur_extra / 2.0);
+  float scale_x = job->scale_x;
+  float scale_y = job->scale_y;
+  float texture_width;
+  float texture_height;
+
+  g_assert (blur_radius > 0);
+
+  /* Increase texture size for the given blur radius and scale it */
+  texture_width  = ceilf ((node->bounds.size.width  + blur_extra));
+  texture_height = ceilf ((node->bounds.size.height + blur_extra));
+
+  /* Only blur this if the out region has no texture id yet */
+  if (offscreen->texture_id == 0)
+    {
+      const graphene_rect_t bounds = GRAPHENE_RECT_INIT (node->bounds.origin.x - half_blur_extra,
+                                                         node->bounds.origin.y - half_blur_extra,
+                                                         texture_width, texture_height);
+
+      offscreen->bounds = &bounds;
+      offscreen->reset_clip = TRUE;
+      offscreen->force_offscreen = TRUE;
+
+      if (!gsk_gl_render_job_visit_node_with_offscreen (job, node, offscreen))
+        g_assert_not_reached ();
+
+      /* Ensure that we actually got a real texture_id */
+      g_assert (offscreen->texture_id != 0);
+
+      offscreen->texture_id = blur_offscreen (job,
+                                              offscreen,
+                                              texture_width * fabsf (scale_x),
+                                              texture_height * fabsf (scale_y),
+                                              blur_radius * fabsf (scale_x),
+                                              blur_radius * fabsf (scale_y));
+      init_full_texture_region (offscreen);
+    }
+
+  *min_x = job->offset_x + node->bounds.origin.x - half_blur_extra;
+  *max_x = job->offset_x + node->bounds.origin.x + node->bounds.size.width + half_blur_extra;
+  *min_y = job->offset_y + node->bounds.origin.y - half_blur_extra;
+  *max_y = job->offset_y + node->bounds.origin.y + node->bounds.size.height + half_blur_extra;
+}
+
+#define ATLAS_SIZE 512
+
+static void
+get_color_node_color_as_srgb (const GskRenderNode *node,
+                              GdkRGBA             *rgba)
+{
+  const GdkColor *color = gsk_color_node_get_gdk_color (node);
+  gdk_color_to_float (color, GDK_COLOR_STATE_SRGB, (float *) rgba);
+}
+
+static inline void
+gsk_gl_render_job_visit_color_node (GskGLRenderJob      *job,
+                                    const GskRenderNode *node)
+{
+  GdkRGBA rgba;
+  guint16 color[4];
+  GskGLProgram *program;
+  GskGLCommandBatch *batch;
+
+  get_color_node_color_as_srgb (node, &rgba);
+  if (RGBA_IS_CLEAR (&rgba))
+    return;
+
+  rgba_to_half (&rgba, color);
+
+  /* Avoid switching away from the coloring program for
+   * rendering a solid color.
+   */
+  program = CHOOSE_PROGRAM (job, coloring);
+  batch = gsk_gl_command_queue_get_batch (job->command_queue);
+
+  /* Limit the size, or we end up with a coordinate overflow somewhere. */
+  if (job->source_is_glyph_atlas &&
+      node->bounds.size.width < 300 &&
+      node->bounds.size.height < 300 &&
+      batch->any.kind == GSK_GL_COMMAND_KIND_DRAW &&
+      batch->any.program == program->id)
+    {
+      GskGLRenderOffscreen offscreen = {0};
+
+      if (gsk_gl_render_job_begin_draw (job, program))
+        {
+          /* The top left few pixels in our atlases are always
+           * solid white, so we can use it here, without
+           * having to choose any particular atlas texture.
+           */
+          offscreen.was_offscreen = FALSE;
+          offscreen.area.x = 1.f / ATLAS_SIZE;
+          offscreen.area.y = 1.f / ATLAS_SIZE;
+          offscreen.area.x2 = 2.f / ATLAS_SIZE;
+          offscreen.area.y2 = 2.f / ATLAS_SIZE;
+
+          gsk_gl_render_job_draw_offscreen_with_color (job,
+                                                       &node->bounds,
+                                                       &offscreen,
+                                                       color);
+
+          gsk_gl_render_job_end_draw (job);
+       }
+    }
+  else
+    {
+      if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, color)))
+        {
+          gsk_gl_render_job_draw_rect_with_color (job, &node->bounds, color);
+          gsk_gl_render_job_end_draw (job);
+        }
+    }
+}
+
+static inline void
+gsk_gl_render_job_visit_linear_gradient_node (GskGLRenderJob      *job,
+                                              const GskRenderNode *node)
+{
+  const GskColorStop *stops = gsk_linear_gradient_node_get_color_stops (node, NULL);
+  const graphene_point_t *start = gsk_linear_gradient_node_get_start (node);
+  const graphene_point_t *end = gsk_linear_gradient_node_get_end (node);
+  int n_color_stops = gsk_linear_gradient_node_get_n_color_stops (node);
+  gboolean repeat = GSK_RENDER_NODE_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);
+
+  if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, linear_gradient)))
+    {
+      gsk_gl_program_set_uniform1i (job->current_program,
+                                    UNIFORM_LINEAR_GRADIENT_NUM_COLOR_STOPS, 0,
+                                    n_color_stops);
+      gsk_gl_program_set_uniform1fv (job->current_program,
+                                     UNIFORM_LINEAR_GRADIENT_COLOR_STOPS, 0,
+                                     n_color_stops * 5,
+                                     (const float *)stops);
+      gsk_gl_program_set_uniform4f (job->current_program,
+                                    UNIFORM_LINEAR_GRADIENT_POINTS, 0,
+                                    x1, y1, x2 - x1, y2 - y1);
+      gsk_gl_program_set_uniform1i (job->current_program,
+                                    UNIFORM_LINEAR_GRADIENT_REPEAT, 0,
+                                    repeat);
+      gsk_gl_render_job_draw_rect (job, &node->bounds);
+      gsk_gl_render_job_end_draw (job);
+    }
+}
+
+static inline void
+gsk_gl_render_job_visit_conic_gradient_node (GskGLRenderJob      *job,
+                                             const GskRenderNode *node)
+{
+  static const float scale = 0.5f * M_1_PI;
+
+  const GskColorStop *stops = gsk_conic_gradient_node_get_color_stops (node, NULL);
+  const graphene_point_t *center = gsk_conic_gradient_node_get_center (node);
+  int n_color_stops = gsk_conic_gradient_node_get_n_color_stops (node);
+  float angle = gsk_conic_gradient_node_get_angle (node);
+  float bias = angle * scale + 2.0f;
+
+  g_assert (n_color_stops < MAX_GRADIENT_STOPS);
+
+  if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, conic_gradient)))
+    {
+      gsk_gl_program_set_uniform1i (job->current_program,
+                                    UNIFORM_CONIC_GRADIENT_NUM_COLOR_STOPS, 0,
+                                    n_color_stops);
+      gsk_gl_program_set_uniform1fv (job->current_program,
+                                     UNIFORM_CONIC_GRADIENT_COLOR_STOPS, 0,
+                                     n_color_stops * 5,
+                                     (const float *)stops);
+      gsk_gl_program_set_uniform4f (job->current_program,
+                                    UNIFORM_CONIC_GRADIENT_GEOMETRY, 0,
+                                    job->offset_x + center->x,
+                                    job->offset_y + center->y,
+                                    scale,
+                                    bias);
+      gsk_gl_render_job_draw_rect (job, &node->bounds);
+      gsk_gl_render_job_end_draw (job);
+    }
+}
+
+static inline void
+gsk_gl_render_job_visit_radial_gradient_node (GskGLRenderJob      *job,
+                                              const GskRenderNode *node)
+{
+  int n_color_stops = gsk_radial_gradient_node_get_n_color_stops (node);
+  const GskColorStop *stops = gsk_radial_gradient_node_get_color_stops (node, NULL);
+  const graphene_point_t *center = gsk_radial_gradient_node_get_center (node);
+  float start = gsk_radial_gradient_node_get_start (node);
+  float end = gsk_radial_gradient_node_get_end (node);
+  float hradius = gsk_radial_gradient_node_get_hradius (node);
+  float vradius = gsk_radial_gradient_node_get_vradius (node);
+  gboolean repeat = GSK_RENDER_NODE_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);
+
+  if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, radial_gradient)))
+    {
+      gsk_gl_program_set_uniform1i (job->current_program,
+                                    UNIFORM_RADIAL_GRADIENT_NUM_COLOR_STOPS, 0,
+                                    n_color_stops);
+      gsk_gl_program_set_uniform1fv (job->current_program,
+                                     UNIFORM_RADIAL_GRADIENT_COLOR_STOPS, 0,
+                                     n_color_stops * 5,
+                                     (const float *)stops);
+      gsk_gl_program_set_uniform1i (job->current_program,
+                                    UNIFORM_RADIAL_GRADIENT_REPEAT, 0,
+                                    repeat);
+      gsk_gl_program_set_uniform2f (job->current_program,
+                                    UNIFORM_RADIAL_GRADIENT_RANGE, 0,
+                                    scale, bias);
+      gsk_gl_program_set_uniform4f (job->current_program,
+                                    UNIFORM_RADIAL_GRADIENT_GEOMETRY, 0,
+                                    job->offset_x + center->x,
+                                    job->offset_y + center->y,
+                                    1.0f / (hradius * job->scale_x),
+                                    1.0f / (vradius * job->scale_y));
+      gsk_gl_render_job_draw_rect (job, &node->bounds);
+      gsk_gl_render_job_end_draw (job);
+    }
+}
+
+static inline void
+gsk_gl_render_job_visit_clipped_child (GskGLRenderJob        *job,
+                                       const GskRenderNode   *child,
+                                       const graphene_rect_t *clip)
+{
+  graphene_rect_t transformed_clip;
+  GskRoundedRect intersection;
+  GskRoundedRectIntersection result;
+
+  gsk_gl_render_job_transform_bounds (job, clip, &transformed_clip);
+
+  if (job->current_clip->is_rectilinear)
+    {
+      memset (&intersection.corner, 0, sizeof intersection.corner);
+      gsk_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);
+      return;
+    }
+
+  result = gsk_rounded_rect_intersect_with_rect (&job->current_clip->rect,
+                                                 &transformed_clip,
+                                                 &intersection);
+
+  if (result == GSK_INTERSECTION_EMPTY)
+    return;
+
+  if (result == GSK_INTERSECTION_NONEMPTY)
+    {
+      gsk_gl_render_job_push_clip (job, &intersection);
+      gsk_gl_render_job_visit_node (job, child);
+      gsk_gl_render_job_pop_clip (job);
+    }
+  else
+    {
+      GskGLRenderOffscreen offscreen = {0};
+
+      offscreen.bounds = clip;
+      offscreen.force_offscreen = TRUE;
+      offscreen.reset_clip = TRUE;
+      offscreen.do_not_cache = TRUE;
+
+      gsk_gl_render_job_visit_node_with_offscreen (job, child, &offscreen);
+
+      g_assert (offscreen.texture_id);
+
+      if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, blit)))
+        {
+          gsk_gl_program_set_uniform_texture (job->current_program,
+                                              UNIFORM_SHARED_SOURCE, 0,
+                                              GL_TEXTURE_2D,
+                                              GL_TEXTURE0,
+                                              offscreen.texture_id);
+          job->source_is_glyph_atlas = FALSE;
+          gsk_gl_render_job_draw_offscreen_rect (job, clip);
+          gsk_gl_render_job_end_draw (job);
+        }
+    }
+}
+
+static inline void
+gsk_gl_render_job_visit_clip_node (GskGLRenderJob      *job,
+                                   const GskRenderNode *node)
+{
+  const graphene_rect_t *clip = gsk_clip_node_get_clip (node);
+  const GskRenderNode *child = gsk_clip_node_get_child (node);
+
+  gsk_gl_render_job_visit_clipped_child (job, child, clip);
+}
+
+static inline void
+gsk_gl_render_job_visit_rounded_clip_node (GskGLRenderJob      *job,
+                                           const GskRenderNode *node)
+{
+  const GskRenderNode *child = gsk_rounded_clip_node_get_child (node);
+  const GskRoundedRect *clip = gsk_rounded_clip_node_get_clip (node);
+  GskRoundedRect transformed_clip;
+  gboolean need_offscreen;
+
+  if (node_is_invisible (child))
+    return;
+
+  gsk_gl_render_job_transform_rounded_rect (job, clip, &transformed_clip);
+
+  if (job->current_clip->is_rectilinear)
+    {
+      GskRoundedRect intersected_clip;
+      GskRoundedRectIntersection result;
+
+      result = gsk_rounded_rect_intersect_with_rect (&transformed_clip,
+                                                     &job->current_clip->rect.bounds,
+                                                     &intersected_clip);
+
+      if (result == GSK_INTERSECTION_EMPTY)
+        return;
+
+      if (result == GSK_INTERSECTION_NONEMPTY)
+        {
+          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 (clips_get_size (&job->clip) <= 1)
+    need_offscreen = FALSE;
+  else if (gsk_rounded_rect_contains_rect (&job->current_clip->rect, &transformed_clip.bounds))
+    need_offscreen = FALSE;
+  else
+    need_offscreen = TRUE;
+
+  if (!need_offscreen)
+    {
+      gsk_gl_render_job_push_clip (job, &transformed_clip);
+      gsk_gl_render_job_visit_node (job, child);
+      gsk_gl_render_job_pop_clip (job);
+    }
+  else
+    {
+      GskGLRenderOffscreen offscreen = {0};
+
+      offscreen.bounds = &node->bounds;
+      offscreen.force_offscreen = TRUE;
+      offscreen.reset_clip = FALSE;
+
+      gsk_gl_render_job_push_clip (job, &transformed_clip);
+      if (!gsk_gl_render_job_visit_node_with_offscreen (job, child, &offscreen))
+        g_assert_not_reached ();
+      gsk_gl_render_job_pop_clip (job);
+
+      g_assert (offscreen.texture_id);
+
+      if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, blit)))
+        {
+          gsk_gl_program_set_uniform_texture (job->current_program,
+                                              UNIFORM_SHARED_SOURCE, 0,
+                                              GL_TEXTURE_2D,
+                                              GL_TEXTURE0,
+                                              offscreen.texture_id);
+          job->source_is_glyph_atlas = FALSE;
+          gsk_gl_render_job_draw_offscreen (job, &node->bounds, &offscreen);
+          gsk_gl_render_job_end_draw (job);
+        }
+    }
+}
+
+static inline void
+gsk_gl_render_job_visit_rect_border_node (GskGLRenderJob      *job,
+                                          const GskRenderNode *node)
+{
+  const GdkRGBA *colors = gsk_border_node_get_colors (node);
+  const float *widths = gsk_border_node_get_widths (node);
+  const graphene_point_t *origin = &node->bounds.origin;
+  const graphene_size_t *size = &node->bounds.size;
+  guint16 color[4];
+
+  if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, color)))
+    {
+      if (widths[0] > 0)
+        {
+          rgba_to_half (&colors[0], color);
+          gsk_gl_render_job_draw_rect_with_color (job,
+                                                  &GRAPHENE_RECT_INIT (origin->x, origin->y, size->width - widths[1], widths[0]),
+                                                  color);
+        }
+
+      if (widths[1] > 0)
+        {
+          rgba_to_half (&colors[1], color);
+          gsk_gl_render_job_draw_rect_with_color (job,
+                                                  &GRAPHENE_RECT_INIT (origin->x + size->width - widths[1], origin->y, widths[1], size->height - widths[2]),
+                                                  color);
+        }
+
+      if (widths[2] > 0)
+        {
+          rgba_to_half (&colors[2], color);
+          gsk_gl_render_job_draw_rect_with_color (job,
+                                                  &GRAPHENE_RECT_INIT (origin->x + widths[3], origin->y + size->height - widths[2], size->width - widths[3], widths[2]),
+                                                  color);
+        }
+
+      if (widths[3] > 0)
+        {
+          rgba_to_half (&colors[3], color);
+          gsk_gl_render_job_draw_rect_with_color (job,
+                                                  &GRAPHENE_RECT_INIT (origin->x, origin->y + widths[0], widths[3], size->height - widths[0]),
+                                                  color);
+        }
+
+      gsk_gl_render_job_end_draw (job);
+    }
+}
+
+static inline void
+gsk_gl_render_job_visit_border_node (GskGLRenderJob      *job,
+                                     const GskRenderNode *node)
+{
+  const GskRoundedRect *rounded_outline = gsk_border_node_get_outline (node);
+  const GdkRGBA *colors = gsk_border_node_get_colors (node);
+  const float *widths = gsk_border_node_get_widths (node);
+  struct {
+    float w;
+    float h;
+  } sizes[4];
+  float min_x = job->offset_x + node->bounds.origin.x;
+  float min_y = job->offset_y + node->bounds.origin.y;
+  float max_x = min_x + node->bounds.size.width;
+  float max_y = min_y + node->bounds.size.height;
+  GskRoundedRect outline;
+  guint16 color[4];
+
+  memset (sizes, 0, sizeof sizes);
+
+  if (widths[0] > 0)
+    {
+      sizes[0].h = MAX (widths[0], rounded_outline->corner[0].height);
+      sizes[1].h = MAX (widths[0], rounded_outline->corner[1].height);
+    }
+
+  if (widths[1] > 0)
+    {
+      sizes[1].w = MAX (widths[1], rounded_outline->corner[1].width);
+      sizes[2].w = MAX (widths[1], rounded_outline->corner[2].width);
+    }
+
+  if (widths[2] > 0)
+    {
+      sizes[2].h = MAX (widths[2], rounded_outline->corner[2].height);
+      sizes[3].h = MAX (widths[2], rounded_outline->corner[3].height);
+    }
+
+  if (widths[3] > 0)
+    {
+      sizes[0].w = MAX (widths[3], rounded_outline->corner[0].width);
+      sizes[3].w = MAX (widths[3], rounded_outline->corner[3].width);
+    }
+
+  gsk_gl_render_job_translate_rounded_rect (job, rounded_outline, &outline);
+
+  if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, border)))
+    {
+      gsk_gl_program_set_uniform4fv (job->current_program,
+                                     UNIFORM_BORDER_WIDTHS, 0,
+                                     1,
+                                     widths);
+      gsk_gl_program_set_uniform_rounded_rect (job->current_program,
+                                               UNIFORM_BORDER_OUTLINE_RECT, 0,
+                                               &outline);
+
+      if (widths[0] > 0)
+        {
+          GskGLDrawVertex *vertices = gsk_gl_command_queue_add_vertices (job->command_queue);
+
+          rgba_to_half (&colors[0], color);
+
+          vertices[0] = (GskGLDrawVertex) { .position = { min_x,              min_y              }, .uv = { 0, 1 }, .color = { color[0], color[1], color[2], color[3] } };
+          vertices[1] = (GskGLDrawVertex) { .position = { min_x + sizes[0].w, min_y + sizes[0].h }, .uv = { 0, 0 }, .color = { color[0], color[1], color[2], color[3] } };
+          vertices[2] = (GskGLDrawVertex) { .position = { max_x,              min_y              }, .uv = { 1, 1 }, .color = { color[0], color[1], color[2], color[3] } };
+
+          vertices[3] = (GskGLDrawVertex) { .position = { max_x - sizes[1].w, min_y + sizes[1].h }, .uv = { 1, 0 }, .color = { color[0], color[1], color[2], color[3] } };
+          vertices[4] = (GskGLDrawVertex) { .position = { min_x + sizes[0].w, min_y + sizes[0].h }, .uv = { 0, 0 }, .color = { color[0], color[1], color[2], color[3] } };
+          vertices[5] = (GskGLDrawVertex) { .position = { max_x,              min_y              }, .uv = { 1, 1 }, .color = { color[0], color[1], color[2], color[3] } };
+        }
+
+      if (widths[1] > 0)
+        {
+          GskGLDrawVertex *vertices = gsk_gl_command_queue_add_vertices (job->command_queue);
+
+          rgba_to_half (&colors[1], color);
+
+          vertices[0] = (GskGLDrawVertex) { .position = { max_x - sizes[1].w, min_y + sizes[1].h }, .uv = { 0, 1 }, .color = { color[0], color[1], color[2], color[3] } };
+          vertices[1] = (GskGLDrawVertex) { .position = { max_x - sizes[2].w, max_y - sizes[2].h }, .uv = { 0, 0 }, .color = { color[0], color[1], color[2], color[3] } };
+          vertices[2] = (GskGLDrawVertex) { .position = { max_x,              min_y              }, .uv = { 1, 1 }, .color = { color[0], color[1], color[2], color[3] } };
+
+          vertices[3] = (GskGLDrawVertex) { .position = { max_x,              max_y              }, .uv = { 1, 0 }, .color = { color[0], color[1], color[2], color[3] } };
+          vertices[4] = (GskGLDrawVertex) { .position = { max_x - sizes[2].w, max_y - sizes[2].h }, .uv = { 0, 0 }, .color = { color[0], color[1], color[2], color[3] } };
+          vertices[5] = (GskGLDrawVertex) { .position = { max_x,              min_y              }, .uv = { 1, 1 }, .color = { color[0], color[1], color[2], color[3] } };
+        }
+
+      if (widths[2] > 0)
+        {
+          GskGLDrawVertex *vertices = gsk_gl_command_queue_add_vertices (job->command_queue);
+
+          rgba_to_half (&colors[2], color);
+
+          vertices[0] = (GskGLDrawVertex) { .position = { min_x + sizes[3].w, max_y - sizes[3].h }, .uv = { 0, 1 }, .color = { color[0], color[1], color[2], color[3] } };
+          vertices[1] = (GskGLDrawVertex) { .position = { min_x,              max_y              }, .uv = { 0, 0 }, .color = { color[0], color[1], color[2], color[3] } };
+          vertices[2] = (GskGLDrawVertex) { .position = { max_x - sizes[2].w, max_y - sizes[2].h }, .uv = { 1, 1 }, .color = { color[0], color[1], color[2], color[3] } };
+
+          vertices[3] = (GskGLDrawVertex) { .position = { max_x,              max_y              }, .uv = { 1, 0 }, .color = { color[0], color[1], color[2], color[3] } };
+          vertices[4] = (GskGLDrawVertex) { .position = { min_x            ,  max_y              }, .uv = { 0, 0 }, .color = { color[0], color[1], color[2], color[3] } };
+          vertices[5] = (GskGLDrawVertex) { .position = { max_x - sizes[2].w, max_y - sizes[2].h }, .uv = { 1, 1 }, .color = { color[0], color[1], color[2], color[3] } };
+        }
+
+      if (widths[3] > 0)
+        {
+          GskGLDrawVertex *vertices = gsk_gl_command_queue_add_vertices (job->command_queue);
+
+          rgba_to_half (&colors[3], color);
+
+          vertices[0] = (GskGLDrawVertex) { .position = { min_x,              min_y              }, .uv = { 0, 1 }, .color = { color[0], color[1], color[2], color[3] } };
+          vertices[1] = (GskGLDrawVertex) { .position = { min_x,              max_y              }, .uv = { 0, 0 }, .color = { color[0], color[1], color[2], color[3] } };
+          vertices[2] = (GskGLDrawVertex) { .position = { min_x + sizes[0].w, min_y + sizes[0].h }, .uv = { 1, 1 }, .color = { color[0], color[1], color[2], color[3] } };
+
+          vertices[3] = (GskGLDrawVertex) { .position = { min_x + sizes[3].w, max_y - sizes[3].h }, .uv = { 1, 0 }, .color = { color[0], color[1], color[2], color[3] } };
+          vertices[4] = (GskGLDrawVertex) { .position = { min_x,              max_y              }, .uv = { 0, 0 }, .color = { color[0], color[1], color[2], color[3] } };
+          vertices[5] = (GskGLDrawVertex) { .position = { min_x + sizes[0].w, min_y + sizes[0].h }, .uv = { 1, 1 }, .color = { color[0], color[1], color[2], color[3] } };
+        }
+
+      gsk_gl_render_job_end_draw (job);
+    }
+}
+
+/* A special case for a pattern that occurs frequently with CSS
+ * backgrounds: two sibling nodes, the first of which is a rounded
+ * clip node with a color node as child, and the second one is a
+ * border node, with the same outline as the clip node. We render
+ * this using the filled_border shader.
+ */
+static void
+gsk_gl_render_job_visit_css_background (GskGLRenderJob      *job,
+                                        const GskRenderNode *node,
+                                        const GskRenderNode *node2)
+{
+  const GskRenderNode *child = gsk_rounded_clip_node_get_child (node);
+  const GskRoundedRect *rounded_outline = gsk_border_node_get_outline (node2);
+  const float *widths = gsk_border_node_get_widths (node2);
+  float min_x = job->offset_x + node2->bounds.origin.x;
+  float min_y = job->offset_y + node2->bounds.origin.y;
+  float max_x = min_x + node2->bounds.size.width;
+  float max_y = min_y + node2->bounds.size.height;
+  GskRoundedRect outline;
+  GskGLDrawVertex *vertices;
+  guint16 color[4];
+  guint16 color2[4];
+  GdkRGBA rgba;
+
+  if (node_is_invisible (node2))
+    return;
+
+  get_color_node_color_as_srgb (child, &rgba);
+  rgba_to_half (&gsk_border_node_get_colors (node2)[0], color);
+  rgba_to_half (&rgba, color2);
+
+  gsk_gl_render_job_translate_rounded_rect (job, rounded_outline, &outline);
+
+  if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, filled_border)))
+    {
+      gsk_gl_program_set_uniform4fv (job->current_program,
+                                     UNIFORM_FILLED_BORDER_WIDTHS, 0,
+                                     1,
+                                     widths);
+      gsk_gl_program_set_uniform_rounded_rect (job->current_program,
+                                               UNIFORM_FILLED_BORDER_OUTLINE_RECT, 0,
+                                               &outline);
+
+      vertices = gsk_gl_command_queue_add_vertices (job->command_queue);
+
+      vertices[0] = (GskGLDrawVertex) { .position = { min_x, min_y }, .color = { color[0], color[1], color[2], color[3] }, .color2 = { color2[0], color2[1], color2[2], color2[3] } };
+      vertices[1] = (GskGLDrawVertex) { .position = { min_x, max_y }, .color = { color[0], color[1], color[2], color[3] }, .color2 = { color2[0], color2[1], color2[2], color2[3] } };
+      vertices[2] = (GskGLDrawVertex) { .position = { max_x, min_y }, .color = { color[0], color[1], color[2], color[3] }, .color2 = { color2[0], color2[1], color2[2], color2[3] } };
+      vertices[3] = (GskGLDrawVertex) { .position = { max_x, max_y }, .color = { color[0], color[1], color[2], color[3] }, .color2 = { color2[0], color2[1], color2[2], color2[3] } };
+      vertices[4] = (GskGLDrawVertex) { .position = { min_x, max_y }, .color = { color[0], color[1], color[2], color[3] }, .color2 = { color2[0], color2[1], color2[2], color2[3] } };
+      vertices[5] = (GskGLDrawVertex) { .position = { max_x, min_y }, .color = { color[0], color[1], color[2], color[3] }, .color2 = { color2[0], color2[1], color2[2], color2[3] } };
+
+      gsk_gl_render_job_end_draw (job);
+    }
+}
+
+/* Returns TRUE if applying @transform to @bounds
+ * yields an axis-aligned rectangle
+ */
+static gboolean
+result_is_axis_aligned (GskTransform          *transform,
+                        const graphene_rect_t *bounds)
+{
+  graphene_matrix_t m;
+  graphene_quad_t q;
+  graphene_rect_t b;
+  graphene_point_t b1, b2;
+  const graphene_point_t *p;
+
+  gsk_transform_to_matrix (transform, &m);
+  gsk_matrix_transform_rect (&m, bounds, &q);
+  graphene_quad_bounds (&q, &b);
+  graphene_rect_get_top_left (&b, &b1);
+  graphene_rect_get_bottom_right (&b, &b2);
+
+  for (guint i = 0; i < 4; i++)
+    {
+      p = graphene_quad_get_point (&q, i);
+      if (fabsf (p->x - b1.x) > FLT_EPSILON && fabsf (p->x - b2.x) > FLT_EPSILON)
+        return FALSE;
+      if (fabsf (p->y - b1.y) > FLT_EPSILON && fabsf (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 (gsk_transform_node_get_transform (node), &dx, &dy);
+        gsk_gl_render_job_offset (job, dx, dy);
+        gsk_gl_render_job_visit_node (job, child);
+        gsk_gl_render_job_offset (job, -dx, -dy);
+      }
+    break;
+
+    case GSK_TRANSFORM_CATEGORY_2D_AFFINE:
+      {
+        gsk_gl_render_job_push_modelview (job, transform);
+        gsk_gl_render_job_visit_node (job, child);
+        gsk_gl_render_job_pop_modelview (job);
+      }
+    break;
+
+    case GSK_TRANSFORM_CATEGORY_2D:
+      if (node_supports_2d_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);
+          return;
+        }
+      G_GNUC_FALLTHROUGH;
+    case GSK_TRANSFORM_CATEGORY_3D:
+    case GSK_TRANSFORM_CATEGORY_ANY:
+    case GSK_TRANSFORM_CATEGORY_UNKNOWN:
+      if (node_supports_transform (child))
+        {
+          gsk_gl_render_job_push_modelview (job, transform);
+          gsk_gl_render_job_visit_node (job, child);
+          gsk_gl_render_job_pop_modelview (job);
+        }
+      else
+        {
+          GskGLRenderOffscreen offscreen = {0};
+          float sx = 1, sy  = 1;
+          gboolean linear_filter = FALSE;
+
+          offscreen.bounds = &child->bounds;
+          offscreen.force_offscreen = FALSE;
+          offscreen.reset_clip = TRUE;
+
+          if (!result_is_axis_aligned (transform, &child->bounds))
+            linear_filter = TRUE;
+
+          if (category == GSK_TRANSFORM_CATEGORY_2D)
+            {
+              graphene_matrix_t m;
+              double a, b, c, d, tx, ty;
+
+              g_assert (transform != NULL);
+              gsk_transform_to_matrix (transform, &m);
+              if (graphene_matrix_to_2d (&m, &a, &b, &c, &d, &tx, &ty))
+                {
+                  sx = sqrt (a * a + b * b);
+                  sy = sqrt (c * c + d * d);
+                }
+              else
+                sx = sy = 1;
+
+              if (sx != 1 || sy != 1)
+                {
+                  GskTransform *scale;
+
+                  scale = gsk_transform_translate (gsk_transform_scale (NULL, sx, sy), &GRAPHENE_POINT_INIT (tx, ty));
+                  gsk_gl_render_job_push_modelview (job, scale);
+                  transform = gsk_transform_transform (gsk_transform_invert (scale), transform);
+                }
+            }
+
+          if (gsk_gl_render_job_visit_node_with_offscreen (job, child, &offscreen))
+            {
+              /* For non-trivial transforms, we draw everything on a texture and then
+               * draw the texture transformed.
+               */
+              if (transform)
+                gsk_gl_render_job_push_modelview (job, transform);
+
+              if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, blit)))
+                {
+                  gsk_gl_program_set_uniform_texture_with_filter (job->current_program,
+                                                                  UNIFORM_SHARED_SOURCE, 0,
+                                                                  GL_TEXTURE_2D,
+                                                                  GL_TEXTURE0,
+                                                                  offscreen.texture_id,
+                                                                  linear_filter ? GL_LINEAR : GL_NEAREST,
+                                                                  linear_filter ? GL_LINEAR : GL_NEAREST);
+                  job->source_is_glyph_atlas = FALSE;
+                  gsk_gl_render_job_draw_offscreen (job, &child->bounds, &offscreen);
+                  gsk_gl_render_job_end_draw (job);
+                }
+
+              if (transform)
+                gsk_gl_render_job_pop_modelview (job);
+            }
+
+          if (category == GSK_TRANSFORM_CATEGORY_2D)
+            {
+              if (sx != 1 || sy != 1)
+                {
+                  gsk_gl_render_job_pop_modelview (job);
+                  gsk_transform_unref (transform);
+                }
+            }
+        }
+    break;
+
+    default:
+      g_assert_not_reached ();
+    }
+}
+
+static inline void
+gsk_gl_render_job_visit_unblurred_inset_shadow_node (GskGLRenderJob      *job,
+                                                     const GskRenderNode *node)
+{
+  const GskRoundedRect *outline = gsk_inset_shadow_node_get_outline (node);
+  GskRoundedRect transformed_outline;
+  guint16 color[4];
+
+  gsk_gl_render_job_translate_rounded_rect (job, outline, &transformed_outline);
+
+  if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, inset_shadow)))
+    {
+      const GdkRGBA rgba;
+
+      gdk_color_to_float (gsk_inset_shadow_node_get_gdk_color (node), GDK_COLOR_STATE_SRGB, (float *) &rgba);
+
+      gsk_gl_program_set_uniform_rounded_rect (job->current_program,
+                                               UNIFORM_INSET_SHADOW_OUTLINE_RECT, 0,
+                                               &transformed_outline);
+      gsk_gl_program_set_uniform1f (job->current_program,
+                                    UNIFORM_INSET_SHADOW_SPREAD, 0,
+                                    gsk_inset_shadow_node_get_spread (node));
+      gsk_gl_program_set_uniform2f (job->current_program,
+                                    UNIFORM_INSET_SHADOW_OFFSET, 0,
+                                    gsk_inset_shadow_node_get_dx (node),
+                                    gsk_inset_shadow_node_get_dy (node));
+      rgba_to_half (&rgba, color);
+      gsk_gl_render_job_draw_rect_with_color (job, &node->bounds, color);
+      gsk_gl_render_job_end_draw (job);
+    }
+}
+
+static inline void
+gsk_gl_render_job_visit_blurred_inset_shadow_node (GskGLRenderJob      *job,
+                                                   const GskRenderNode *node)
+{
+  const GskRoundedRect *node_outline = gsk_inset_shadow_node_get_outline (node);
+  float blur_radius = gsk_inset_shadow_node_get_blur_radius (node);
+  float offset_x = gsk_inset_shadow_node_get_dx (node);
+  float offset_y = gsk_inset_shadow_node_get_dy (node);
+  float scale_x = job->scale_x;
+  float scale_y = job->scale_y;
+  float blur_extra = blur_radius * 2.0; /* 2.0 = shader radius_multiplier */
+  float half_blur_extra = blur_radius;
+  float texture_width;
+  float texture_height;
+  int blurred_texture_id;
+  GskTextureKey key;
+  GskGLRenderOffscreen offscreen = {0};
+  guint16 color[4];
+
+  g_assert (blur_radius > 0);
+
+  texture_width = ceilf ((node_outline->bounds.size.width + blur_extra) * scale_x);
+  texture_height = ceilf ((node_outline->bounds.size.height + blur_extra) * scale_y);
+
+  key.pointer = node;
+  key.pointer_is_child = FALSE;
+  key.scale_x = scale_x;
+  key.scale_y = scale_y;
+
+  blurred_texture_id = gsk_gl_driver_lookup_texture (job->driver, &key, NULL);
+
+  if (blurred_texture_id == 0)
+    {
+      float spread = gsk_inset_shadow_node_get_spread (node) + half_blur_extra;
+      GskRoundedRect transformed_outline;
+      GskRoundedRect outline_to_blur;
+      GskGLRenderTarget *render_target;
+      graphene_matrix_t prev_projection;
+      graphene_rect_t prev_viewport;
+      guint prev_fbo;
+
+      /* TODO: In the following code, we have to be careful about where we apply the scale.
+       * We're manually scaling stuff (e.g. the outline) so we can later use texture_width
+       * and texture_height (which are already scaled) as the geometry and keep the modelview
+       * at a scale of 1. That's kinda complicated though... */
+
+      /* Outline of what we actually want to blur later.
+       * Spread grows inside, so we don't need to account for that. But the blur will need
+       * to read outside of the inset shadow, so we need to draw some color in there. */
+      outline_to_blur = *node_outline;
+      gsk_rounded_rect_shrink (&outline_to_blur,
+                               -half_blur_extra,
+                               -half_blur_extra,
+                               -half_blur_extra,
+                               -half_blur_extra);
+
+      /* Fit to our texture */
+      outline_to_blur.bounds.origin.x = 0;
+      outline_to_blur.bounds.origin.y = 0;
+      outline_to_blur.bounds.size.width *= scale_x;
+      outline_to_blur.bounds.size.height *= scale_y;
+
+      for (guint i = 0; i < 4; i ++)
+        {
+          outline_to_blur.corner[i].width *= scale_x;
+          outline_to_blur.corner[i].height *= scale_y;
+        }
+
+      if (!gsk_gl_driver_create_render_target (job->driver,
+                                               texture_width, texture_height,
+                                               get_target_format (job, node),
+                                               &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_translate_rounded_rect (job, &outline_to_blur, &transformed_outline);
+
+      /* Actual inset shadow outline drawing */
+      if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, inset_shadow)))
+        {
+          const GdkRGBA rgba;
+
+          gdk_color_to_float (gsk_inset_shadow_node_get_gdk_color (node), GDK_COLOR_STATE_SRGB, (float *) &rgba);
+
+          gsk_gl_program_set_uniform_rounded_rect (job->current_program,
+                                                   UNIFORM_INSET_SHADOW_OUTLINE_RECT, 0,
+                                                   &transformed_outline);
+          gsk_gl_program_set_uniform1f (job->current_program,
+                                        UNIFORM_INSET_SHADOW_SPREAD, 0,
+                                        spread * MAX (scale_x, scale_y));
+          gsk_gl_program_set_uniform2f (job->current_program,
+                                        UNIFORM_INSET_SHADOW_OFFSET, 0,
+                                        offset_x * scale_x,
+                                        offset_y * scale_y);
+          rgba_to_half (&rgba, color);
+          gsk_gl_render_job_draw_with_color (job,
+                                             0, 0, texture_width, texture_height,
+                                             color);
+          gsk_gl_render_job_end_draw (job);
+        }
+
+      gsk_gl_render_job_pop_modelview (job);
+      gsk_gl_render_job_pop_clip (job);
+      gsk_gl_render_job_set_projection (job, &prev_projection);
+      gsk_gl_render_job_set_viewport (job, &prev_viewport, NULL);
+      gsk_gl_command_queue_bind_framebuffer (job->command_queue, prev_fbo);
+
+      offscreen.texture_id = render_target->texture_id;
+      init_full_texture_region (&offscreen);
+
+      blurred_texture_id = blur_offscreen (job,
+                                           &offscreen,
+                                           texture_width,
+                                           texture_height,
+                                           blur_radius * fabsf (scale_x),
+                                           blur_radius * fabsf (scale_y));
+
+      gsk_gl_driver_release_render_target (job->driver, render_target, TRUE);
+
+      gsk_gl_driver_cache_texture (job->driver, &key, blurred_texture_id);
+    }
+
+  g_assert (blurred_texture_id != 0);
+
+  /* Blur the rendered unblurred inset shadow */
+  /* Use a clip to cut away the unwanted parts outside of the original outline */
+  {
+    const gboolean needs_clip = !gsk_rounded_rect_is_rectilinear (node_outline);
+    const float tx1 = half_blur_extra * scale_x / texture_width;
+    const float tx2 = 1.0 - tx1;
+    const float ty1 = half_blur_extra * scale_y / texture_height;
+    const float ty2 = 1.0 - ty1;
+
+    if (needs_clip)
+      {
+        GskRoundedRect node_clip;
+
+        gsk_gl_render_job_translate_rounded_rect (job, node_outline, &node_clip);
+        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;
+
+    if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, blit)))
+      {
+        gsk_gl_program_set_uniform_texture (job->current_program,
+                                            UNIFORM_SHARED_SOURCE, 0,
+                                            GL_TEXTURE_2D,
+                                            GL_TEXTURE0,
+                                            blurred_texture_id);
+        job->source_is_glyph_atlas = FALSE;
+        gsk_gl_render_job_draw_offscreen (job, &node->bounds, &offscreen);
+        gsk_gl_render_job_end_draw (job);
+      }
+
+    if (needs_clip)
+      gsk_gl_render_job_pop_clip (job);
+  }
+}
+
+static inline void
+gsk_gl_render_job_visit_unblurred_outset_shadow_node (GskGLRenderJob      *job,
+                                                      const GskRenderNode *node)
+{
+  const GskRoundedRect *outline = gsk_outset_shadow_node_get_outline (node);
+  GskRoundedRect transformed_outline;
+  float x = node->bounds.origin.x;
+  float y = node->bounds.origin.y;
+  float w = node->bounds.size.width;
+  float h = node->bounds.size.height;
+  float spread = gsk_outset_shadow_node_get_spread (node);
+  float dx = gsk_outset_shadow_node_get_dx (node);
+  float dy = gsk_outset_shadow_node_get_dy (node);
+  GdkRGBA rgba;
+  guint16 color[4];
+  const float edge_sizes[] = { // Top, right, bottom, left
+    spread - dy, spread + dx, spread + dy, spread - dx
+  };
+  const float corner_sizes[][2] = { // top left, top right, bottom right, bottom left
+    { outline->corner[0].width + spread - dx, outline->corner[0].height + spread - dy },
+    { outline->corner[1].width + spread + dx, outline->corner[1].height + spread - dy },
+    { outline->corner[2].width + spread + dx, outline->corner[2].height + spread + dy },
+    { outline->corner[3].width + spread - dx, outline->corner[3].height + spread + dy },
+  };
+
+  gdk_color_to_float (gsk_outset_shadow_node_get_gdk_color (node), GDK_COLOR_STATE_SRGB, (float *) &rgba);
+  rgba_to_half (&rgba, color);
+
+  gsk_gl_render_job_translate_rounded_rect (job, outline, &transformed_outline);
+
+  if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, unblurred_outset_shadow)))
+    {
+      gsk_gl_program_set_uniform_rounded_rect (job->current_program,
+                                               UNIFORM_UNBLURRED_OUTSET_SHADOW_OUTLINE_RECT, 0,
+                                               &transformed_outline);
+      gsk_gl_program_set_uniform1f (job->current_program,
+                                    UNIFORM_UNBLURRED_OUTSET_SHADOW_SPREAD, 0,
+                                    spread);
+      gsk_gl_program_set_uniform2f (job->current_program,
+                                    UNIFORM_UNBLURRED_OUTSET_SHADOW_OFFSET, 0,
+                                    dx, dy);
+
+      /* Corners... */
+      if (corner_sizes[0][0] > 0 && corner_sizes[0][1] > 0) /* Top left */
+        gsk_gl_render_job_draw_with_color (job,
+                                           x, y, corner_sizes[0][0], corner_sizes[0][1],
+                                           color);
+      if (corner_sizes[1][0] > 0 && corner_sizes[1][1] > 0) /* Top right */
+        gsk_gl_render_job_draw_with_color (job,
+                                           x + w - corner_sizes[1][0], y,
+                                           corner_sizes[1][0], corner_sizes[1][1],
+                                           color);
+      if (corner_sizes[2][0] > 0 && corner_sizes[2][1] > 0) /* Bottom right */
+        gsk_gl_render_job_draw_with_color (job,
+                                           x + w - corner_sizes[2][0], y + h - corner_sizes[2][1],
+                                           corner_sizes[2][0], corner_sizes[2][1],
+                                           color);
+      if (corner_sizes[3][0] > 0 && corner_sizes[3][1] > 0) /* Bottom left */
+        gsk_gl_render_job_draw_with_color (job,
+                                           x, y + h - corner_sizes[3][1],
+                                           corner_sizes[3][0], corner_sizes[3][1],
+                                           color);
+      /* Edges... */;
+      if (edge_sizes[0] > 0) /* Top */
+        gsk_gl_render_job_draw_with_color (job,
+                                           x + corner_sizes[0][0], y,
+                                           w - corner_sizes[0][0] - corner_sizes[1][0], edge_sizes[0],
+                                           color);
+      if (edge_sizes[1] > 0) /* Right */
+        gsk_gl_render_job_draw_with_color (job,
+                                           x + w - edge_sizes[1], y + corner_sizes[1][1],
+                                           edge_sizes[1], h - corner_sizes[1][1] - corner_sizes[2][1],
+                                           color);
+      if (edge_sizes[2] > 0) /* Bottom */
+        gsk_gl_render_job_draw_with_color (job,
+                                           x + corner_sizes[3][0], y + h - edge_sizes[2],
+                                           w - corner_sizes[3][0] - corner_sizes[2][0], edge_sizes[2],
+                                           color);
+      if (edge_sizes[3] > 0) /* Left */
+        gsk_gl_render_job_draw_with_color (job,
+                                           x, y + corner_sizes[0][1],
+                                           edge_sizes[3], h - corner_sizes[0][1] - corner_sizes[3][1],
+                                           color);
+
+      gsk_gl_render_job_end_draw (job);
+    }
+}
+
+static inline void
+gsk_gl_render_job_visit_blurred_outset_shadow_node (GskGLRenderJob      *job,
+                                                    const GskRenderNode *node)
+{
+  const GskRoundedRect *outline = gsk_outset_shadow_node_get_outline (node);
+  float scale_x = fabsf (job->scale_x);
+  float scale_y = fabsf (job->scale_y);
+  float blur_radius = gsk_outset_shadow_node_get_blur_radius (node);
+  float blur_extra = blur_radius * 2.0f; /* 2.0 = shader radius_multiplier */
+  float half_blur_extra = blur_extra / 2.0f;
+  int extra_blur_pixels_x = ceilf (half_blur_extra * scale_x);
+  int extra_blur_pixels_y = ceilf (half_blur_extra * scale_y);
+  float spread = gsk_outset_shadow_node_get_spread (node);
+  float dx = gsk_outset_shadow_node_get_dx (node);
+  float dy = gsk_outset_shadow_node_get_dy (node);
+  GskRoundedRect scaled_outline;
+  GskRoundedRect transformed_outline;
+  GskGLRenderOffscreen offscreen = {0};
+  int texture_width, texture_height;
+  int blurred_texture_id;
+  int cached_tid;
+  gboolean do_slicing;
+  GdkRGBA rgba;
+  guint16 color[4];
+  float half_width = outline->bounds.size.width / 2;
+  float half_height = outline->bounds.size.height / 2;
+
+  gdk_color_to_float (gsk_outset_shadow_node_get_gdk_color (node), GDK_COLOR_STATE_SRGB, (float *)  &rgba);
+  rgba_to_half (&rgba, color);
+
+  /* scaled_outline is the minimal outline we need to draw the given drop shadow,
+   * enlarged by the spread and offset by the blur radius. */
+  scaled_outline = *outline;
+
+  if (outline->bounds.size.width < blur_extra ||
+      outline->bounds.size.height < blur_extra ||
+      outline->corner[0].width >= half_width ||
+      outline->corner[1].width >= half_width ||
+      outline->corner[2].width >= half_width ||
+      outline->corner[3].width >= half_width ||
+      outline->corner[0].height >= half_height ||
+      outline->corner[1].height >= half_height ||
+      outline->corner[2].height >= half_height ||
+      outline->corner[3].height >= half_height)
+    {
+      do_slicing = FALSE;
+      gsk_rounded_rect_shrink (&scaled_outline, -spread, -spread, -spread, -spread);
+    }
+  else
+    {
+      /* Shrink our outline to the minimum size that can still hold all the border radii */
+      gsk_rounded_rect_shrink_to_minimum (&scaled_outline);
+      /* Increase by the spread */
+      gsk_rounded_rect_shrink (&scaled_outline, -spread, -spread, -spread, -spread);
+      /* Grow bounds but don't grow corners */
+      graphene_rect_inset (&scaled_outline.bounds, - blur_extra / 2.0, - blur_extra / 2.0);
+      /* For the center part, we add a few pixels */
+      scaled_outline.bounds.size.width += SHADOW_EXTRA_SIZE;
+      scaled_outline.bounds.size.height += SHADOW_EXTRA_SIZE;
+
+      do_slicing = TRUE;
+    }
+
+  texture_width = (int)ceilf ((scaled_outline.bounds.size.width + blur_extra) * scale_x);
+  texture_height = (int)ceilf ((scaled_outline.bounds.size.height + blur_extra) * scale_y);
+
+  scaled_outline.bounds.origin.x = extra_blur_pixels_x;
+  scaled_outline.bounds.origin.y = extra_blur_pixels_y;
+  scaled_outline.bounds.size.width = texture_width - (extra_blur_pixels_x * 2);
+  scaled_outline.bounds.size.height = texture_height - (extra_blur_pixels_y * 2);
+
+  for (guint i = 0; i < G_N_ELEMENTS (scaled_outline.corner); i++)
+    {
+      scaled_outline.corner[i].width *= scale_x;
+      scaled_outline.corner[i].height *= scale_y;
+    }
+
+  cached_tid = gsk_gl_shadow_library_lookup (job->driver->shadows_library,
+                                             &scaled_outline,
+                                             blur_radius);
+
+  if (cached_tid == 0)
+    {
+      GdkGLContext *context = job->command_queue->context;
+      GskGLRenderTarget *render_target;
+      graphene_matrix_t prev_projection;
+      graphene_rect_t prev_viewport;
+      guint prev_fbo;
+
+      gsk_gl_driver_create_render_target (job->driver,
+                                          texture_width, texture_height,
+                                          get_target_format (job, node),
+                                          &render_target);
+
+      if (gdk_gl_context_has_feature (context, GDK_GL_FEATURE_DEBUG))
+        {
+          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 */
+      if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, color)))
+        {
+          gsk_gl_render_job_draw_with_color (job, 0, 0, texture_width, texture_height,
+                                             (guint16[]){ FP16_ONE, FP16_ONE, FP16_ONE, FP16_ONE });
+          gsk_gl_render_job_end_draw (job);
+        }
+
+      /* Reset state from offscreen */
+      gsk_gl_render_job_pop_clip (job);
+      gsk_gl_render_job_pop_modelview (job);
+      gsk_gl_render_job_set_viewport (job, &prev_viewport, NULL);
+      gsk_gl_render_job_set_projection (job, &prev_projection);
+
+      /* Now blur the outline */
+      init_full_texture_region (&offscreen);
+      offscreen.texture_id = gsk_gl_driver_release_render_target (job->driver, render_target, FALSE);
+      blurred_texture_id = blur_offscreen (job,
+                                           &offscreen,
+                                           texture_width,
+                                           texture_height,
+                                           blur_radius * scale_x,
+                                           blur_radius * scale_y);
+
+      gsk_gl_shadow_library_insert (job->driver->shadows_library,
+                                    &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_translate_rounded_rect (job, outline, &transformed_outline);
+
+  if (!do_slicing)
+    {
+      float min_x = floorf (outline->bounds.origin.x - spread - half_blur_extra + dx);
+      float min_y = floorf (outline->bounds.origin.y - spread - half_blur_extra + dy);
+
+      offscreen.was_offscreen = TRUE;
+      offscreen.texture_id = blurred_texture_id;
+      init_full_texture_region (&offscreen);
+
+      if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, outset_shadow)))
+        {
+          gsk_gl_program_set_uniform_texture (job->current_program,
+                                              UNIFORM_SHARED_SOURCE, 0,
+                                              GL_TEXTURE_2D,
+                                              GL_TEXTURE0,
+                                              blurred_texture_id);
+          job->source_is_glyph_atlas = FALSE;
+          gsk_gl_program_set_uniform_rounded_rect (job->current_program,
+                                                   UNIFORM_OUTSET_SHADOW_OUTLINE_RECT, 0,
+                                                   &transformed_outline);
+          gsk_gl_render_job_draw_offscreen_with_color (job,
+                                                       &GRAPHENE_RECT_INIT (min_x,
+                                                                            min_y,
+                                                                            texture_width / scale_x,
+                                                                            texture_height / scale_y),
+                                                       &offscreen,
+                                                       color);
+          gsk_gl_render_job_end_draw (job);
+        }
+
+      return;
+    }
+
+  /* slicing */
+
+  if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, outset_shadow)))
+    {
+      gsk_gl_program_set_uniform_texture (job->current_program,
+                                          UNIFORM_SHARED_SOURCE, 0,
+                                          GL_TEXTURE_2D,
+                                          GL_TEXTURE0,
+                                          blurred_texture_id);
+      job->source_is_glyph_atlas = FALSE;
+      gsk_gl_program_set_uniform_rounded_rect (job->current_program,
+                                               UNIFORM_OUTSET_SHADOW_OUTLINE_RECT, 0,
+                                               &transformed_outline);
+
+      {
+        float min_x = floorf (outline->bounds.origin.x - spread - half_blur_extra + dx);
+        float min_y = floorf (outline->bounds.origin.y - spread - half_blur_extra + dy);
+        float max_x = ceilf (outline->bounds.origin.x + outline->bounds.size.width +
+                             half_blur_extra + dx + spread);
+        float max_y = ceilf (outline->bounds.origin.y + outline->bounds.size.height +
+                             half_blur_extra + dy + spread);
+        const GskGLTextureNineSlice *slices;
+        float left_width, center_width, right_width;
+        float top_height, center_height, bottom_height;
+        GskGLTexture *texture;
+
+        texture = gsk_gl_driver_get_texture_by_id (job->driver, blurred_texture_id);
+        slices = gsk_gl_texture_get_nine_slice (texture, &scaled_outline, extra_blur_pixels_x, extra_blur_pixels_y);
+
+        offscreen.was_offscreen = TRUE;
+
+        /* Our texture coordinates MUST be scaled, while the actual vertex coords
+         * MUST NOT be scaled.
+         */
+
+        left_width = slices[NINE_SLICE_TOP_LEFT].rect.width / scale_x;
+        right_width = slices[NINE_SLICE_TOP_RIGHT].rect.width / scale_x;
+        center_width = (max_x - min_x) - (left_width + right_width);
+
+        top_height = slices[NINE_SLICE_TOP_LEFT].rect.height / scale_y;
+        bottom_height = slices[NINE_SLICE_BOTTOM_LEFT].rect.height / scale_y;
+        center_height = (max_y - min_y) - (top_height + bottom_height);
+
+        /* Top left */
+        if (nine_slice_is_visible (&slices[NINE_SLICE_TOP_LEFT]))
+          {
+            memcpy (&offscreen.area, &slices[NINE_SLICE_TOP_LEFT].area, sizeof offscreen.area);
+            gsk_gl_render_job_draw_offscreen_with_color (job,
+                                                         &GRAPHENE_RECT_INIT (min_x,
+                                                                              min_y,
+                                                                              left_width,
+                                                                              top_height),
+                                                         &offscreen,
+                                                         color);
+          }
+
+        /* Top center */
+        if (nine_slice_is_visible (&slices[NINE_SLICE_TOP_CENTER]))
+        {
+          memcpy (&offscreen.area, &slices[NINE_SLICE_TOP_CENTER].area, sizeof offscreen.area);
+          gsk_gl_render_job_draw_offscreen_with_color (job,
+                                                       &GRAPHENE_RECT_INIT (min_x + left_width,
+                                                                            min_y,
+                                                                            center_width,
+                                                                            top_height),
+                                                       &offscreen,
+                                                       color);
+        }
+
+        /* Top right */
+        if (nine_slice_is_visible (&slices[NINE_SLICE_TOP_RIGHT]))
+        {
+          memcpy (&offscreen.area, &slices[NINE_SLICE_TOP_RIGHT].area, sizeof offscreen.area);
+          gsk_gl_render_job_draw_offscreen_with_color (job,
+                                                       &GRAPHENE_RECT_INIT (max_x - right_width,
+                                                                            min_y,
+                                                                            right_width,
+                                                                            top_height),
+                                                       &offscreen,
+                                                       color);
+        }
+
+        /* Bottom right */
+        if (nine_slice_is_visible (&slices[NINE_SLICE_BOTTOM_RIGHT]))
+        {
+          memcpy (&offscreen.area, &slices[NINE_SLICE_BOTTOM_RIGHT].area, sizeof offscreen.area);
+          gsk_gl_render_job_draw_offscreen_with_color (job,
+                                                       &GRAPHENE_RECT_INIT (max_x - right_width,
+                                                                            max_y - bottom_height,
+                                                                            right_width,
+                                                                            bottom_height),
+                                                       &offscreen,
+                                                       color);
+        }
+
+        /* Bottom left */
+        if (nine_slice_is_visible (&slices[NINE_SLICE_BOTTOM_LEFT]))
+          {
+            memcpy (&offscreen.area, &slices[NINE_SLICE_BOTTOM_LEFT].area, sizeof offscreen.area);
+            gsk_gl_render_job_draw_offscreen_with_color (job,
+                                                         &GRAPHENE_RECT_INIT (min_x,
+                                                                              max_y - bottom_height,
+                                                                              left_width,
+                                                                              bottom_height),
+                                                         &offscreen,
+                                                         color);
+          }
+
+        /* Left side */
+        if (nine_slice_is_visible (&slices[NINE_SLICE_LEFT_CENTER]))
+          {
+            memcpy (&offscreen.area, &slices[NINE_SLICE_LEFT_CENTER].area, sizeof offscreen.area);
+            gsk_gl_render_job_draw_offscreen_with_color (job,
+                                                         &GRAPHENE_RECT_INIT (min_x,
+                                                                              min_y + top_height,
+                                                                              left_width,
+                                                                              center_height),
+                                                         &offscreen,
+                                                         color);
+          }
+
+        /* Right side */
+        if (nine_slice_is_visible (&slices[NINE_SLICE_RIGHT_CENTER]))
+          {
+            memcpy (&offscreen.area, &slices[NINE_SLICE_RIGHT_CENTER].area, sizeof offscreen.area);
+            gsk_gl_render_job_draw_offscreen_with_color (job,
+                                                         &GRAPHENE_RECT_INIT (max_x - right_width,
+                                                                              min_y + top_height,
+                                                                              right_width,
+                                                                              center_height),
+                                                         &offscreen,
+                                                         color);
+          }
+
+        /* Bottom side */
+        if (nine_slice_is_visible (&slices[NINE_SLICE_BOTTOM_CENTER]))
+          {
+            memcpy (&offscreen.area, &slices[NINE_SLICE_BOTTOM_CENTER].area, sizeof offscreen.area);
+            gsk_gl_render_job_draw_offscreen_with_color (job,
+                                                         &GRAPHENE_RECT_INIT (min_x + left_width,
+                                                                              max_y - bottom_height,
+                                                                              center_width,
+                                                                              bottom_height),
+                                                         &offscreen,
+                                                         color);
+          }
+
+        /* Middle */
+        if (nine_slice_is_visible (&slices[NINE_SLICE_CENTER]))
+          {
+            if (!gsk_rounded_rect_contains_rect (outline, &GRAPHENE_RECT_INIT (min_x + left_width,
+                                                                               min_y + top_height,
+                                                                               center_width,
+                                                                               center_height)))
+              {
+                memcpy (&offscreen.area, &slices[NINE_SLICE_CENTER].area, sizeof offscreen.area);
+                gsk_gl_render_job_draw_offscreen_with_color (job,
+                                                             &GRAPHENE_RECT_INIT (min_x + left_width,
+                                                                                  min_y + top_height,
+                                                                                  center_width,
+                                                                                  center_height),
+                                                             &offscreen,
+                                                             color);
+              }
+          }
+      }
+
+      gsk_gl_render_job_end_draw (job);
+    }
+}
+
+static inline gboolean G_GNUC_PURE
+equal_texture_nodes (const GskRenderNode *node1,
+                     const GskRenderNode *node2)
+{
+  if (GSK_RENDER_NODE_TYPE (node1) != GSK_TEXTURE_NODE ||
+      GSK_RENDER_NODE_TYPE (node2) != GSK_TEXTURE_NODE)
+    return FALSE;
+
+  if (gsk_texture_node_get_texture (node1) !=
+      gsk_texture_node_get_texture (node2))
+    return FALSE;
+
+  return gsk_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;
+
+  gsk_gl_render_job_set_modelview (job, gsk_transform_scale (NULL, fabsf (job->scale_x), fabsf (job->scale_y)));
+
+  if (!gsk_gl_render_job_visit_node_with_offscreen (job, start_node, &offscreen_start))
+    {
+      gsk_gl_render_job_pop_modelview (job);
+
+      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_pop_modelview (job);
+
+      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;
+    }
+
+  gsk_gl_render_job_pop_modelview (job);
+
+  g_assert (offscreen_end.texture_id);
+
+  if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, cross_fade)))
+    {
+      gsk_gl_program_set_uniform_texture (job->current_program,
+                                          UNIFORM_SHARED_SOURCE, 0,
+                                          GL_TEXTURE_2D,
+                                          GL_TEXTURE0,
+                                          offscreen_start.texture_id);
+      gsk_gl_program_set_uniform_texture (job->current_program,
+                                          UNIFORM_CROSS_FADE_SOURCE2, 0,
+                                          GL_TEXTURE_2D,
+                                          GL_TEXTURE1,
+                                          offscreen_end.texture_id);
+      job->source_is_glyph_atlas = FALSE;
+      gsk_gl_program_set_uniform1f (job->current_program,
+                                    UNIFORM_CROSS_FADE_PROGRESS, 0,
+                                    progress);
+      gsk_gl_render_job_draw_offscreen (job, &node->bounds, &offscreen_end);
+      gsk_gl_render_job_end_draw (job);
+    }
+}
+
+static 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_needs_blending (child))
+        {
+          gsk_gl_render_job_visit_node (job, child);
+          gsk_gl_render_job_set_alpha (job, prev_alpha);
+        }
+      else
+        {
+          GskGLRenderOffscreen offscreen = {0};
+
+          offscreen.bounds = &child->bounds;
+          offscreen.force_offscreen = TRUE;
+          offscreen.reset_clip = TRUE;
+
+          /* Note: offscreen rendering resets alpha to 1.0 */
+          if (!gsk_gl_render_job_visit_node_with_offscreen (job, child, &offscreen))
+            return;
+
+          g_assert (offscreen.texture_id);
+
+          if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, blit)))
+            {
+              gsk_gl_program_set_uniform_texture (job->current_program,
+                                                  UNIFORM_SHARED_SOURCE, 0,
+                                                  GL_TEXTURE_2D,
+                                                  GL_TEXTURE0,
+                                                  offscreen.texture_id);
+              job->source_is_glyph_atlas = FALSE;
+              gsk_gl_render_job_draw_offscreen (job, &node->bounds, &offscreen);
+              gsk_gl_render_job_end_draw (job);
+            }
+        }
+
+      gsk_gl_render_job_set_alpha (job, prev_alpha);
+    }
+}
+
+static inline int
+compute_phase_and_pos (float value, float *pos)
+{
+  float v;
+
+  *pos = floorf (value);
+
+  v = value - *pos;
+
+  if (v < 0.125)
+    return 0;
+  else if (v < 0.375)
+    return 1;
+  else if (v < 0.625)
+    return 2;
+  else if (v < 0.875)
+    return 3;
+  else
+    {
+      *pos += 1;
+      return 0;
+    }
+}
+
+static inline void
+gsk_gl_render_job_visit_text_node (GskGLRenderJob      *job,
+                                   const GskRenderNode *node,
+                                   const GdkRGBA       *color,
+                                   gboolean             force_color)
+{
+  const PangoFont *font = gsk_text_node_get_font (node);
+  const PangoGlyphInfo *glyphs = gsk_text_node_get_glyphs (node, NULL);
+  const graphene_point_t *offset = gsk_text_node_get_offset (node);
+  float text_scale = MAX (fabsf (job->scale_x), fabsf (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_library;
+  GskGLCommandBatch *batch;
+  int x_position = 0;
+  GskGLGlyphKey lookup;
+  guint last_texture = 0;
+  GskGLDrawVertex *vertices;
+  guint used = 0;
+  guint16 nc[4] = { FP16_MINUS_ONE, FP16_MINUS_ONE, FP16_MINUS_ONE, FP16_MINUS_ONE };
+  guint16 cc[4];
+  const guint16 *c;
+  const PangoGlyphInfo *gi;
+  guint i;
+  int yshift;
+  float ypos;
+
+  if (num_glyphs == 0)
+    return;
+
+  if ((force_color || !gsk_text_node_has_color_glyphs (node)) &&
+      RGBA_IS_CLEAR (color))
+    return;
+
+  rgba_to_half (color, cc);
+
+  lookup.font = (PangoFont *)font;
+  lookup.scale = (guint) (text_scale * 1024);
+
+  yshift = compute_phase_and_pos (y, &ypos);
+
+  if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, coloring)))
+    {
+      batch = gsk_gl_command_queue_get_batch (job->command_queue);
+      vertices = gsk_gl_command_queue_add_n_vertices (job->command_queue, num_glyphs);
+
+      /* We use one quad per character */
+      for (i = 0, gi = glyphs; i < num_glyphs; i++, gi++)
+        {
+          const GskGLGlyphValue *glyph;
+          float glyph_x, glyph_y, glyph_x2, glyph_y2;
+          float tx, ty, tx2, ty2;
+          float cx;
+          float cy;
+          guint texture_id;
+
+          lookup.glyph = gi->glyph;
+
+          /* If the glyph has color, we don't need to recolor anything.
+           * We tell the shader by setting the color to vec4(-1).
+           */
+          if (!force_color && gi->attr.is_color)
+            c = nc;
+          else
+            c = cc;
+
+          cx = (float)(x_position + gi->geometry.x_offset) / PANGO_SCALE;
+          lookup.xshift = compute_phase_and_pos (x + cx, &cx);
+
+          if G_UNLIKELY (gi->geometry.y_offset != 0)
+            {
+              cy = (float)(gi->geometry.y_offset) / PANGO_SCALE;
+              lookup.yshift = compute_phase_and_pos (y + cy, &cy);
+            }
+          else
+            {
+              lookup.yshift = yshift;
+              cy = ypos;
+            }
+
+          x_position += gi->geometry.width;
+
+          texture_id = gsk_gl_glyph_library_lookup_or_add (library, &lookup, &glyph);
+          if G_UNLIKELY (texture_id == 0)
+            continue;
+
+          if G_UNLIKELY (last_texture != texture_id || batch->draw.vbo_count + GSK_GL_N_VERTICES > 0xffff)
+            {
+              if G_LIKELY (last_texture != 0)
+                {
+                  guint vbo_offset = batch->draw.vbo_offset + batch->draw.vbo_count;
+
+                  /* Since we have batched added our VBO vertices to avoid repeated
+                   * calls to the buffer, we need to manually tweak the vbo offset
+                   * of the new batch as otherwise it will point at the end of our
+                   * vbo array.
+                   */
+                  gsk_gl_render_job_split_draw (job);
+                  batch = gsk_gl_command_queue_get_batch (job->command_queue);
+                  batch->draw.vbo_offset = vbo_offset;
+                }
+
+              gsk_gl_program_set_uniform_texture (job->current_program,
+                                                  UNIFORM_SHARED_SOURCE, 0,
+                                                  GL_TEXTURE_2D,
+                                                  GL_TEXTURE0,
+                                                  texture_id);
+              job->source_is_glyph_atlas = TRUE;
+              last_texture = texture_id;
+            }
+
+          tx = glyph->entry.area.x;
+          ty = glyph->entry.area.y;
+          tx2 = glyph->entry.area.x2;
+          ty2 = glyph->entry.area.y2;
+
+          glyph_x = cx + glyph->ink_rect.x;
+          glyph_y = cy + glyph->ink_rect.y;
+          glyph_x2 = glyph_x + glyph->ink_rect.width;
+          glyph_y2 = glyph_y + glyph->ink_rect.height;
+
+          *(vertices++) = (GskGLDrawVertex) { .position = { glyph_x,  glyph_y  }, .uv = { tx,  ty  }, .color = { c[0], c[1], c[2], c[3] } };
+          *(vertices++) = (GskGLDrawVertex) { .position = { glyph_x,  glyph_y2 }, .uv = { tx,  ty2 }, .color = { c[0], c[1], c[2], c[3] } };
+          *(vertices++) = (GskGLDrawVertex) { .position = { glyph_x2, glyph_y  }, .uv = { tx2, ty  }, .color = { c[0], c[1], c[2], c[3] } };
+
+          *(vertices++) = (GskGLDrawVertex) { .position = { glyph_x2, glyph_y2 }, .uv = { tx2, ty2 }, .color = { c[0], c[1], c[2], c[3] } };
+          *(vertices++) = (GskGLDrawVertex) { .position = { glyph_x,  glyph_y2 }, .uv = { tx,  ty2 }, .color = { c[0], c[1], c[2], c[3] } };
+          *(vertices++) = (GskGLDrawVertex) { .position = { glyph_x2, glyph_y  }, .uv = { tx2, ty  }, .color = { c[0], c[1], c[2], c[3] } };
+
+          batch->draw.vbo_count += GSK_GL_N_VERTICES;
+          used++;
+        }
+
+      if (used != num_glyphs)
+        gsk_gl_command_queue_retract_n_vertices (job->command_queue, num_glyphs - used);
+
+      gsk_gl_render_job_end_draw (job);
+    }
+}
+
+static inline void
+gsk_gl_render_job_visit_shadow_node (GskGLRenderJob      *job,
+                                     const GskRenderNode *node)
+{
+  const gsize n_shadows = gsk_shadow_node_get_n_shadows (node);
+  const GskRenderNode *original_child = gsk_shadow_node_get_child (node);
+  const GskRenderNode *shadow_child = original_child;
+
+  /* Shadow nodes recolor every pixel of the source texture, but leave the alpha in tact.
+   * If the child is a color matrix node that doesn't touch the alpha, we can throw that away. */
+  if (GSK_RENDER_NODE_TYPE (shadow_child) == GSK_COLOR_MATRIX_NODE &&
+      !color_matrix_modifies_alpha (shadow_child))
+    shadow_child = gsk_color_matrix_node_get_child (shadow_child);
+
+  for (guint i = 0; i < n_shadows; i++)
+    {
+      const GskShadow *shadow = gsk_shadow_node_get_shadow (node, i);
+      const float dx = shadow->dx;
+      const float dy = shadow->dy;
+      GskGLRenderOffscreen offscreen = {0};
+      graphene_rect_t bounds;
+      guint16 color[4];
+
+      if (RGBA_IS_CLEAR (&shadow->color))
+        continue;
+
+      if (node_is_invisible (shadow_child))
+        continue;
+
+      if (shadow->radius == 0 &&
+          GSK_RENDER_NODE_TYPE (shadow_child) == GSK_TEXT_NODE)
+        {
+          if (dx != 0 || dy != 0)
+            {
+              gsk_gl_render_job_offset (job, dx, dy);
+              gsk_gl_render_job_visit_text_node (job, shadow_child, &shadow->color, TRUE);
+              gsk_gl_render_job_offset (job, -dx, -dy);
+            }
+          continue;
+        }
+
+      if (shadow->radius > 0)
+        {
+          float min_x;
+          float min_y;
+          float max_x;
+          float max_y;
+
+          offscreen.do_not_cache = TRUE;
+
+          blur_node (job,
+                     &offscreen,
+                     shadow_child,
+                     shadow->radius,
+                     &min_x, &max_x,
+                     &min_y, &max_y);
+
+          bounds.origin.x = min_x - job->offset_x;
+          bounds.origin.y = min_y - job->offset_y;
+          bounds.size.width = max_x - min_x;
+          bounds.size.height = max_y - min_y;
+
+          offscreen.was_offscreen = TRUE;
+        }
+      else
+        {
+          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);
+      if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, coloring)))
+        {
+          gsk_gl_program_set_uniform_texture (job->current_program,
+                                              UNIFORM_SHARED_SOURCE, 0,
+                                              GL_TEXTURE_2D,
+                                              GL_TEXTURE0,
+                                              offscreen.texture_id);
+          job->source_is_glyph_atlas = FALSE;
+          rgba_to_half (&shadow->color, color);
+          gsk_gl_render_job_draw_offscreen_with_color (job, &bounds, &offscreen, color);
+          gsk_gl_render_job_end_draw (job);
+        }
+      gsk_gl_render_job_offset (job, -dx, -dy);
+    }
+
+  /* Now draw the child normally */
+  gsk_gl_render_job_visit_node (job, original_child);
+}
+
+static inline void
+gsk_gl_render_job_visit_blur_node (GskGLRenderJob      *job,
+                                   const GskRenderNode *node)
+{
+  const GskRenderNode *child = gsk_blur_node_get_child (node);
+  float blur_radius = gsk_blur_node_get_radius (node);
+  GskGLRenderOffscreen offscreen = {0};
+  GskTextureKey key;
+  gboolean cache_texture;
+  float min_x;
+  float max_x;
+  float min_y;
+  float max_y;
+
+  g_assert (blur_radius > 0);
+
+  if (node_is_invisible (child))
+    return;
+
+  key.pointer = node;
+  key.pointer_is_child = FALSE;
+  key.scale_x = job->scale_x;
+  key.scale_y = job->scale_y;
+
+  offscreen.texture_id = gsk_gl_driver_lookup_texture (job->driver, &key, NULL);
+  cache_texture = offscreen.texture_id == 0;
+
+  blur_node (job,
+             &offscreen,
+             child,
+             blur_radius,
+             &min_x, &max_x, &min_y, &max_y);
+
+  g_assert (offscreen.texture_id != 0);
+
+  if (cache_texture)
+    gsk_gl_driver_cache_texture (job->driver, &key, offscreen.texture_id);
+
+  if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, blit)))
+    {
+      gsk_gl_program_set_uniform_texture (job->current_program,
+                                          UNIFORM_SHARED_SOURCE, 0,
+                                          GL_TEXTURE_2D,
+                                          GL_TEXTURE0,
+                                          offscreen.texture_id);
+      job->source_is_glyph_atlas = FALSE;
+      gsk_gl_render_job_draw_coords (job,
+                                     min_x, min_y, max_x, max_y,
+                                     0, 1, 1, 0,
+                                     (guint16[]) { FP16_ZERO, FP16_ZERO, FP16_ZERO, FP16_ZERO } );
+      gsk_gl_render_job_end_draw (job);
+    }
+}
+
+static inline void
+gsk_gl_render_job_visit_blend_node (GskGLRenderJob      *job,
+                                    const GskRenderNode *node)
+{
+  const GskRenderNode *top_child = gsk_blend_node_get_top_child (node);
+  const GskRenderNode *bottom_child = gsk_blend_node_get_bottom_child (node);
+  GskGLRenderOffscreen top_offscreen = {0};
+  GskGLRenderOffscreen bottom_offscreen = {0};
+
+  top_offscreen.bounds = &node->bounds;
+  top_offscreen.force_offscreen = TRUE;
+  top_offscreen.reset_clip = TRUE;
+
+  bottom_offscreen.bounds = &node->bounds;
+  bottom_offscreen.force_offscreen = TRUE;
+  bottom_offscreen.reset_clip = TRUE;
+
+  gsk_gl_render_job_set_modelview (job, gsk_transform_scale (NULL, fabsf (job->scale_x), fabsf (job->scale_y)));
+
+  /* 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_pop_modelview (job);
+
+      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_pop_modelview (job);
+
+      if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, blit)))
+        {
+          gsk_gl_program_set_uniform_texture (job->current_program,
+                                              UNIFORM_SHARED_SOURCE, 0,
+                                              GL_TEXTURE_2D,
+                                              GL_TEXTURE0,
+                                              bottom_offscreen.texture_id);
+          job->source_is_glyph_atlas = FALSE;
+          gsk_gl_render_job_draw_offscreen (job, &node->bounds, &bottom_offscreen);
+          gsk_gl_render_job_end_draw (job);
+        }
+      return;
+    }
+
+  g_assert (top_offscreen.was_offscreen);
+
+  gsk_gl_render_job_pop_modelview (job);
+
+  if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, blend)))
+    {
+      gsk_gl_program_set_uniform_texture (job->current_program,
+                                          UNIFORM_SHARED_SOURCE, 0,
+                                          GL_TEXTURE_2D,
+                                          GL_TEXTURE0,
+                                          bottom_offscreen.texture_id);
+      gsk_gl_program_set_uniform_texture (job->current_program,
+                                          UNIFORM_BLEND_SOURCE2, 0,
+                                          GL_TEXTURE_2D,
+                                          GL_TEXTURE1,
+                                          top_offscreen.texture_id);
+      job->source_is_glyph_atlas = FALSE;
+      gsk_gl_program_set_uniform1i (job->current_program,
+                                    UNIFORM_BLEND_MODE, 0,
+                                    gsk_blend_node_get_blend_mode (node));
+      gsk_gl_render_job_draw_offscreen_rect (job, &node->bounds);
+      gsk_gl_render_job_end_draw (job);
+    }
+}
+
+static gboolean
+gsk_gl_render_job_texture_mask_for_color (GskGLRenderJob        *job,
+                                          const GskRenderNode   *mask,
+                                          const GskRenderNode   *color,
+                                          const graphene_rect_t *bounds)
+{
+  int max_texture_size = job->command_queue->max_texture_size;
+  GdkTexture *texture = gsk_texture_node_get_texture (mask);
+  GdkRGBA rgba;
+
+  get_color_node_color_as_srgb (color, &rgba);
+  if (RGBA_IS_CLEAR (&rgba))
+    return TRUE;
+
+  if G_LIKELY (texture->width <= max_texture_size &&
+               texture->height <= max_texture_size &&
+               gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, coloring)))
+    {
+      GskGLRenderOffscreen offscreen = {0};
+      float scale_x = mask->bounds.size.width / texture->width;
+      float scale_y = mask->bounds.size.height / texture->height;
+      gboolean use_mipmap;
+      guint16 cc[4];
+
+      use_mipmap = (scale_x * fabsf (job->scale_x)) < 0.5 ||
+                   (scale_y * fabsf (job->scale_y)) < 0.5;
+
+      rgba_to_half (&rgba, cc);
+      gsk_gl_render_job_upload_texture (job, texture, use_mipmap, &offscreen);
+      gsk_gl_program_set_uniform_texture_with_sync (job->current_program,
+                                                    UNIFORM_SHARED_SOURCE, 0,
+                                                    GL_TEXTURE_2D,
+                                                    GL_TEXTURE0,
+                                                    offscreen.texture_id,
+                                                    offscreen.has_mipmap ? GL_LINEAR_MIPMAP_LINEAR : GL_LINEAR,
+                                                    GL_LINEAR,
+                                                    offscreen.sync);
+      job->source_is_glyph_atlas = FALSE;
+      gsk_gl_render_job_draw_offscreen_with_color (job, bounds, &offscreen, cc);
+      gsk_gl_render_job_end_draw (job);
+
+      return TRUE;
+    }
+
+  return FALSE;
+}
+
+static inline void
+gsk_gl_render_job_visit_mask_node (GskGLRenderJob      *job,
+                                   const GskRenderNode *node)
+{
+  const GskRenderNode *source = gsk_mask_node_get_source (node);
+  const GskRenderNode *mask = gsk_mask_node_get_mask (node);
+  GskGLRenderOffscreen source_offscreen = {0};
+  GskGLRenderOffscreen mask_offscreen = {0};
+
+  /* If the mask is a texture and the source is a color node
+   * then we can take a shortcut and avoid offscreens.
+   */
+  if (GSK_RENDER_NODE_TYPE (mask) == GSK_TEXTURE_NODE &&
+      GSK_RENDER_NODE_TYPE (source) == GSK_COLOR_NODE &&
+      gsk_mask_node_get_mask_mode (node) == GSK_MASK_MODE_ALPHA)
+    {
+      if (gsk_gl_render_job_texture_mask_for_color (job, mask, source, &node->bounds))
+        return;
+    }
+
+  source_offscreen.bounds = &node->bounds;
+  source_offscreen.force_offscreen = TRUE;
+  source_offscreen.reset_clip = TRUE;
+
+  mask_offscreen.bounds = &node->bounds;
+  mask_offscreen.force_offscreen = TRUE;
+  mask_offscreen.reset_clip = TRUE;
+  mask_offscreen.do_not_cache = TRUE;
+
+  gsk_gl_render_job_set_modelview (job, gsk_transform_scale (NULL, fabsf (job->scale_x), fabsf (job->scale_y)));
+
+  /* TODO: We create 2 textures here as big as the mask node, but both
+   * nodes might be a lot smaller than that.
+   */
+  if (!gsk_gl_render_job_visit_node_with_offscreen (job, source, &source_offscreen))
+    {
+      gsk_gl_render_job_pop_modelview (job);
+      gsk_gl_render_job_visit_node (job, source);
+      return;
+    }
+
+  g_assert (source_offscreen.was_offscreen);
+
+  if (!gsk_gl_render_job_visit_node_with_offscreen (job, mask, &mask_offscreen))
+    {
+      gsk_gl_render_job_pop_modelview (job);
+      if (gsk_mask_node_get_mask_mode (node) == GSK_MASK_MODE_INVERTED_ALPHA)
+        gsk_gl_render_job_visit_node (job, source);
+      return;
+    }
+
+  g_assert (mask_offscreen.was_offscreen);
+
+  gsk_gl_render_job_pop_modelview (job);
+
+  if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, mask)))
+    {
+      gsk_gl_program_set_uniform_texture (job->current_program,
+                                          UNIFORM_SHARED_SOURCE, 0,
+                                          GL_TEXTURE_2D,
+                                          GL_TEXTURE0,
+                                          source_offscreen.texture_id);
+      gsk_gl_program_set_uniform_texture (job->current_program,
+                                          UNIFORM_MASK_SOURCE, 0,
+                                          GL_TEXTURE_2D,
+                                          GL_TEXTURE1,
+                                          mask_offscreen.texture_id);
+      job->source_is_glyph_atlas = FALSE;
+      gsk_gl_program_set_uniform1i (job->current_program,
+                                    UNIFORM_MASK_MODE, 0,
+                                    gsk_mask_node_get_mask_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);
+
+  if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, color_matrix)))
+    {
+      gsk_gl_program_set_uniform_texture (job->current_program,
+                                          UNIFORM_SHARED_SOURCE, 0,
+                                          GL_TEXTURE_2D,
+                                          GL_TEXTURE0,
+                                          offscreen.texture_id);
+      job->source_is_glyph_atlas = FALSE;
+      gsk_gl_program_set_uniform_matrix (job->current_program,
+                                         UNIFORM_COLOR_MATRIX_COLOR_MATRIX, 0,
+                                         gsk_color_matrix_node_get_color_matrix (node));
+      gsk_gl_program_set_uniform4fv (job->current_program,
+                                     UNIFORM_COLOR_MATRIX_COLOR_OFFSET, 0,
+                                     1,
+                                     offset);
+      gsk_gl_render_job_draw_offscreen (job, &node->bounds, &offscreen);
+      gsk_gl_render_job_end_draw (job);
+    }
+}
+
+static inline void
+gsk_gl_render_job_visit_gl_shader_node_fallback (GskGLRenderJob      *job,
+                                                 const GskRenderNode *node)
+{
+  guint16 pink[4] = { 15360, 13975, 14758, 15360 }; /* 255 105 180 */
+
+  if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, color)))
+    {
+      gsk_gl_render_job_draw_rect_with_color (job, &node->bounds, pink);
+      gsk_gl_render_job_end_draw (job);
+    }
+}
+
+static inline void
+gsk_gl_render_job_visit_gl_shader_node (GskGLRenderJob      *job,
+                                        const GskRenderNode *node)
+{
+G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+  GError *error = NULL;
+  GskGLShader *shader;
+  GskGLProgram *program;
+  int n_children;
+
+  shader = gsk_gl_shader_node_get_shader (node);
+  program = gsk_gl_driver_lookup_shader (job->driver, shader, &error);
+  n_children = gsk_gl_shader_node_get_n_children (node);
+
+  if G_UNLIKELY (program == NULL)
+    {
+      if (g_object_get_data (G_OBJECT (shader), "gsk-did-warn") == NULL)
+        {
+          g_object_set_data (G_OBJECT (shader), "gsk-did-warn", GUINT_TO_POINTER (1));
+          g_warning ("Failed to compile gl shader: %s", error->message);
+        }
+      gsk_gl_render_job_visit_gl_shader_node_fallback (job, node);
+      g_clear_error (&error);
+    }
+  else
+    {
+      GskGLRenderOffscreen offscreens[4] = {{0}};
+      const GskGLUniform *uniforms;
+      const guint8 *base;
+      GBytes *args;
+      int n_uniforms;
+      guint n_children;
+
+      g_assert (n_children < G_N_ELEMENTS (offscreens));
+
+      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);
+
+          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);
+
+      if (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);
+          job->source_is_glyph_atlas = FALSE;
+          gsk_gl_program_set_uniform2f (program,
+                                        UNIFORM_CUSTOM_SIZE, 0,
+                                        node->bounds.size.width,
+                                        node->bounds.size.height);
+          for (guint i = 0; i < n_uniforms; i++)
+            {
+              const GskGLUniform *u = &uniforms[i];
+              const guint8 *data = base + u->offset;
+
+              switch (u->type)
+                {
+                default:
+                case GSK_GL_UNIFORM_TYPE_NONE:
+                  break;
+                case GSK_GL_UNIFORM_TYPE_FLOAT:
+                  gsk_gl_uniform_state_set1fv (job->command_queue->uniforms,
+                                               program->program_info,
+                                               UNIFORM_CUSTOM_ARG0 + i,
+                                               0, 1, (const float *)data);
+                  break;
+                case GSK_GL_UNIFORM_TYPE_INT:
+                  gsk_gl_uniform_state_set1i (job->command_queue->uniforms,
+                                              program->program_info,
+                                              UNIFORM_CUSTOM_ARG0 + i,
+                                              0, *(const gint32 *)data);
+                  break;
+                case GSK_GL_UNIFORM_TYPE_UINT:
+                case GSK_GL_UNIFORM_TYPE_BOOL:
+                  gsk_gl_uniform_state_set1ui (job->command_queue->uniforms,
+                                               program->program_info,
+                                               UNIFORM_CUSTOM_ARG0 + i,
+                                               0, *(const guint32 *)data);
+                  break;
+                case GSK_GL_UNIFORM_TYPE_VEC2:
+                  gsk_gl_uniform_state_set2fv (job->command_queue->uniforms,
+                                               program->program_info,
+                                               UNIFORM_CUSTOM_ARG0 + i,
+                                               0, 1, (const float *)data);
+                  break;
+                case GSK_GL_UNIFORM_TYPE_VEC3:
+                  gsk_gl_uniform_state_set3fv (job->command_queue->uniforms,
+                                               program->program_info,
+                                               UNIFORM_CUSTOM_ARG0 + i,
+                                               0, 1, (const float *)data);
+                  break;
+                case GSK_GL_UNIFORM_TYPE_VEC4:
+                  gsk_gl_uniform_state_set4fv (job->command_queue->uniforms,
+                                               program->program_info,
+                                               UNIFORM_CUSTOM_ARG0 + i,
+                                               0, 1, (const float *)data);
+                  break;
+                }
+            }
+          gsk_gl_render_job_draw_offscreen_rect (job, &node->bounds);
+          gsk_gl_render_job_end_draw (job);
+        }
+    }
+G_GNUC_END_IGNORE_DEPRECATIONS
+}
+
+static void
+gsk_gl_render_job_upload_texture (GskGLRenderJob       *job,
+                                  GdkTexture           *texture,
+                                  gboolean              ensure_mipmap,
+                                  GskGLRenderOffscreen *offscreen)
+{
+  /* Don't put GL or dmabuf textures into icon caches, they are already on the GPU side */
+  if (!ensure_mipmap &&
+      gsk_gl_texture_library_can_cache ((GskGLTextureLibrary *)job->driver->icons_library,
+                                        texture->width,
+                                        texture->height) &&
+      !(GDK_IS_GL_TEXTURE (texture) || GDK_IS_DMABUF_TEXTURE (texture)))
+    {
+      const GskGLIconData *icon_data;
+
+      gsk_gl_icon_library_lookup_or_add (job->driver->icons_library, texture, &icon_data);
+      offscreen->texture_id = GSK_GL_TEXTURE_ATLAS_ENTRY_TEXTURE (icon_data);
+      memcpy (&offscreen->area, &icon_data->entry.area, sizeof offscreen->area);
+      offscreen->has_mipmap = FALSE;
+    }
+  else
+    {
+      /* Only generate a mipmap if it does not make use reupload
+       * a GL texture which we could otherwise use directly.
+       */
+      if (GDK_IS_GL_TEXTURE (texture) &&
+          gdk_gl_context_is_shared (gdk_gl_texture_get_context (GDK_GL_TEXTURE (texture)),
+                                    job->command_queue->context))
+        ensure_mipmap = gdk_gl_texture_has_mipmap (GDK_GL_TEXTURE (texture));
+      else if (GDK_IS_DMABUF_TEXTURE (texture))
+        ensure_mipmap = FALSE;
+
+      offscreen->texture_id = gsk_gl_driver_load_texture (job->driver, texture, ensure_mipmap);
+      init_full_texture_region (offscreen);
+      offscreen->has_mipmap = ensure_mipmap;
+
+      if (GDK_IS_GL_TEXTURE (texture) &&
+          offscreen->texture_id == gdk_gl_texture_get_id (GDK_GL_TEXTURE (texture)))
+        offscreen->sync = gdk_gl_texture_get_sync (GDK_GL_TEXTURE (texture));
+    }
+}
+
+static inline void
+gsk_gl_render_job_visit_texture (GskGLRenderJob        *job,
+                                 GdkTexture            *texture,
+                                 const graphene_rect_t *bounds)
+{
+  int max_texture_size = job->command_queue->max_texture_size;
+  float scale_x = bounds->size.width / texture->width;
+  float scale_y = bounds->size.height / texture->height;
+  gboolean use_mipmap;
+
+  use_mipmap = (scale_x * fabsf (job->scale_x)) < 0.5 ||
+               (scale_y * fabsf (job->scale_y)) < 0.5;
+
+  if G_LIKELY (texture->width <= max_texture_size &&
+               texture->height <= max_texture_size)
+    {
+      GskGLRenderOffscreen offscreen = {0};
+
+      gsk_gl_render_job_upload_texture (job, texture, use_mipmap, &offscreen);
+
+      g_assert (offscreen.texture_id);
+      g_assert (offscreen.was_offscreen == FALSE);
+
+      if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, blit)))
+        {
+          gsk_gl_program_set_uniform_texture_with_sync (job->current_program,
+                                                        UNIFORM_SHARED_SOURCE, 0,
+                                                        GL_TEXTURE_2D,
+                                                        GL_TEXTURE0,
+                                                        offscreen.texture_id,
+                                                        offscreen.has_mipmap ? GL_LINEAR_MIPMAP_LINEAR : GL_LINEAR,
+                                                        GL_LINEAR,
+                                                        offscreen.sync);
+          job->source_is_glyph_atlas = FALSE;
+          gsk_gl_render_job_draw_offscreen (job, bounds, &offscreen);
+          gsk_gl_render_job_end_draw (job);
+        }
+    }
+  else
+    {
+      float min_x = job->offset_x + bounds->origin.x;
+      float min_y = job->offset_y + bounds->origin.y;
+      GskGLTextureSlice *slices = NULL;
+      guint n_slices = 0;
+
+      gsk_gl_driver_slice_texture (job->driver, texture, use_mipmap, &slices, &n_slices);
+
+      g_assert (slices != NULL);
+      g_assert (n_slices > 0);
+
+      if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, blit)))
+        {
+          for (unsigned int i = 0; i < n_slices; i++)
+            {
+              const GskGLTextureSlice *slice = &slices[i];
+              float x1, x2, y1, y2;
+
+              x1 = min_x + (scale_x * slice->rect.x);
+              x2 = x1 + (slice->rect.width * scale_x);
+              y1 = min_y + (scale_y * slice->rect.y);
+              y2 = y1 + (slice->rect.height * scale_y);
+
+              if (i > 0)
+                gsk_gl_render_job_split_draw (job);
+              gsk_gl_program_set_uniform_texture_with_filter (job->current_program,
+                                                              UNIFORM_SHARED_SOURCE, 0,
+                                                              GL_TEXTURE_2D,
+                                                              GL_TEXTURE0,
+                                                              slice->texture_id,
+                                                              use_mipmap ? GL_LINEAR_MIPMAP_LINEAR : GL_LINEAR,
+                                                              GL_LINEAR);
+              job->source_is_glyph_atlas = FALSE;
+
+              gsk_gl_render_job_draw_coords (job,
+                                             x1, y1, x2, y2,
+                                             slice->area.x, slice->area.y,
+                                             slice->area.x2, slice->area.y2,
+                                             (guint16[]) { FP16_ZERO, FP16_ZERO, FP16_ZERO, FP16_ZERO });
+            }
+
+          gsk_gl_render_job_end_draw (job);
+        }
+    }
+}
+
+static inline void
+gsk_gl_render_job_visit_texture_node (GskGLRenderJob      *job,
+                                      const GskRenderNode *node)
+{
+  GdkTexture *texture = gsk_texture_node_get_texture (node);
+  const graphene_rect_t *bounds = &node->bounds;
+
+  gsk_gl_render_job_visit_texture (job, texture, bounds);
+}
+
+static inline void
+gsk_gl_render_job_visit_texture_scale_node (GskGLRenderJob      *job,
+                                            const GskRenderNode *node)
+{
+  GdkTexture *texture = gsk_texture_scale_node_get_texture (node);
+  const graphene_rect_t *bounds = &node->bounds;
+  GskScalingFilter filter = gsk_texture_scale_node_get_filter (node);
+  int min_filters[] = { GL_LINEAR, GL_NEAREST, GL_LINEAR_MIPMAP_LINEAR };
+  int mag_filters[] = { GL_LINEAR, GL_NEAREST, GL_LINEAR };
+  int min_filter = min_filters[filter];
+  int mag_filter = mag_filters[filter];
+  int max_texture_size = job->command_queue->max_texture_size;
+  graphene_rect_t clip_rect;
+  GskGLRenderTarget *render_target;
+  graphene_rect_t viewport;
+  graphene_rect_t prev_viewport;
+  graphene_matrix_t prev_projection;
+  float prev_alpha;
+  guint prev_fbo;
+  float u0, u1, v0, v1;
+  GskTextureKey key;
+  guint texture_id;
+  gboolean need_mipmap;
+  gboolean has_mipmap;
+
+  gsk_gl_render_job_untransform_bounds (job, &job->current_clip->rect.bounds, &clip_rect);
+
+  if (!gsk_rect_intersection (bounds, &clip_rect, &clip_rect))
+    return;
+
+  key.pointer = node;
+  key.pointer_is_child = TRUE;
+  key.parent_rect = clip_rect;
+  key.scale_x = 1.;
+  key.scale_y = 1.;
+
+  need_mipmap = (filter == GSK_SCALING_FILTER_TRILINEAR);
+
+  texture_id = gsk_gl_driver_lookup_texture (job->driver, &key, &has_mipmap);
+
+  if (texture_id != 0 && (!need_mipmap || has_mipmap))
+    goto render_texture;
+
+  viewport = GRAPHENE_RECT_INIT (0, 0,
+                                 clip_rect.size.width,
+                                 clip_rect.size.height);
+
+  if (!gsk_gl_driver_create_render_target (job->driver,
+                                           (int) ceilf (clip_rect.size.width),
+                                           (int) ceilf (clip_rect.size.height),
+                                           get_target_format (job, node),
+                                           &render_target))
+    {
+      gsk_gl_render_job_visit_texture (job, texture, bounds);
+      return;
+    }
+
+  gsk_gl_render_job_set_viewport (job, &viewport, &prev_viewport);
+  gsk_gl_render_job_set_projection_from_rect (job, &viewport, &prev_projection);
+  gsk_gl_render_job_set_modelview (job, NULL);
+  prev_alpha = gsk_gl_render_job_set_alpha (job, 1.0f);
+  gsk_gl_render_job_push_clip (job, &GSK_ROUNDED_RECT_INIT_FROM_RECT (viewport));
+
+  prev_fbo = gsk_gl_command_queue_bind_framebuffer (job->command_queue, render_target->framebuffer_id);
+  gsk_gl_command_queue_clear (job->command_queue, 0, &viewport);
+
+  if G_LIKELY (texture->width <= max_texture_size &&
+               texture->height <= max_texture_size)
+    {
+      gpointer sync;
+
+      texture_id = gsk_gl_driver_load_texture (job->driver, texture, need_mipmap);
+
+      if (GDK_IS_GL_TEXTURE (texture) && texture_id == gdk_gl_texture_get_id (GDK_GL_TEXTURE (texture)))
+        sync = gdk_gl_texture_get_sync (GDK_GL_TEXTURE (texture));
+      else
+        sync = NULL;
+
+      u0 = (clip_rect.origin.x - bounds->origin.x) / bounds->size.width;
+      v0 = (clip_rect.origin.y - bounds->origin.y) / bounds->size.height;
+      u1 = (clip_rect.origin.x + clip_rect.size.width - bounds->origin.x) / bounds->size.width;
+      v1 = (clip_rect.origin.y + clip_rect.size.height - bounds->origin.y) / bounds->size.height;
+
+      if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, blit)))
+        {
+          gsk_gl_program_set_uniform_texture_with_sync (job->current_program,
+                                                        UNIFORM_SHARED_SOURCE, 0,
+                                                        GL_TEXTURE_2D,
+                                                        GL_TEXTURE0,
+                                                        texture_id,
+                                                        min_filter,
+                                                        mag_filter,
+                                                        sync);
+          job->source_is_glyph_atlas = FALSE;
+          gsk_gl_render_job_draw_coords (job,
+                                         0, 0, clip_rect.size.width, clip_rect.size.height,
+                                         u0, v0, u1, v1,
+                                         (guint16[]) { FP16_ZERO, FP16_ZERO, FP16_ZERO, FP16_ZERO });
+          gsk_gl_render_job_end_draw (job);
+        }
+    }
+  else
+    {
+      float scale_x = bounds->size.width / texture->width;
+      float scale_y = bounds->size.height / texture->height;
+      GskGLTextureSlice *slices = NULL;
+      guint n_slices = 0;
+
+      gsk_gl_driver_slice_texture (job->driver, texture, need_mipmap, &slices, &n_slices);
+
+      if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, blit)))
+        {
+          for (guint i = 0; i < n_slices; i++)
+            {
+              const GskGLTextureSlice *slice = &slices[i];
+              graphene_rect_t slice_bounds;
+
+              slice_bounds.origin.x = bounds->origin.x - clip_rect.origin.x + slice->rect.x * scale_x;
+              slice_bounds.origin.y = bounds->origin.y - clip_rect.origin.y + slice->rect.y * scale_y;
+              slice_bounds.size.width = slice->rect.width * scale_x;
+              slice_bounds.size.height = slice->rect.height * scale_y;
+
+              if (!gsk_rect_intersects (&slice_bounds, &viewport))
+                continue;
+
+              if (i > 0)
+                gsk_gl_render_job_split_draw (job);
+
+              gsk_gl_program_set_uniform_texture_with_filter (job->current_program,
+                                                              UNIFORM_SHARED_SOURCE, 0,
+                                                              GL_TEXTURE_2D,
+                                                              GL_TEXTURE0,
+                                                              slice->texture_id,
+                                                              min_filter,
+                                                              mag_filter);
+              job->source_is_glyph_atlas = FALSE;
+              gsk_gl_render_job_draw_coords (job,
+                                             slice_bounds.origin.x,
+                                             slice_bounds.origin.y,
+                                             slice_bounds.origin.x + slice_bounds.size.width,
+                                             slice_bounds.origin.y + slice_bounds.size.height,
+                                             slice->area.x, slice->area.y,
+                                             slice->area.x2, slice->area.y2,
+                                             (guint16[]){ FP16_ZERO, FP16_ZERO, FP16_ZERO, FP16_ZERO } );
+            }
+
+          gsk_gl_render_job_end_draw (job);
+        }
+    }
+
+  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);
+
+  texture_id = gsk_gl_driver_release_render_target (job->driver, render_target, FALSE);
+  gsk_gl_driver_cache_texture (job->driver, &key, texture_id);
+
+render_texture:
+  if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, blit)))
+    {
+      gsk_gl_program_set_uniform_texture (job->current_program,
+                                          UNIFORM_SHARED_SOURCE, 0,
+                                          GL_TEXTURE_2D,
+                                          GL_TEXTURE0,
+                                          texture_id);
+      job->source_is_glyph_atlas = FALSE;
+      gsk_gl_render_job_draw_coords (job,
+                                     job->offset_x + clip_rect.origin.x,
+                                     job->offset_y + clip_rect.origin.y,
+                                     job->offset_x + clip_rect.origin.x + clip_rect.size.width,
+                                     job->offset_y + clip_rect.origin.y + clip_rect.size.height,
+                                     0, clip_rect.size.width / ceilf (clip_rect.size.width),
+                                     clip_rect.size.height / ceilf (clip_rect.size.height), 0,
+                                     (guint16[]){ FP16_ZERO, FP16_ZERO, FP16_ZERO, FP16_ZERO } );
+      gsk_gl_render_job_end_draw (job);
+    }
+}
+
+static inline void
+gsk_gl_render_job_visit_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 (!gsk_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 (gsk_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 ();
+
+  if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, repeat)))
+    {
+      gsk_gl_program_set_uniform_texture (job->current_program,
+                                          UNIFORM_SHARED_SOURCE, 0,
+                                          GL_TEXTURE_2D,
+                                          GL_TEXTURE0,
+                                          offscreen.texture_id);
+      job->source_is_glyph_atlas = FALSE;
+      gsk_gl_program_set_uniform4f (job->current_program,
+                                    UNIFORM_REPEAT_CHILD_BOUNDS, 0,
+                                    (node->bounds.origin.x - child_bounds->origin.x) / child_bounds->size.width,
+                                    (node->bounds.origin.y - child_bounds->origin.y) / child_bounds->size.height,
+                                    node->bounds.size.width / child_bounds->size.width,
+                                    node->bounds.size.height / child_bounds->size.height);
+      gsk_gl_program_set_uniform4f (job->current_program,
+                                    UNIFORM_REPEAT_TEXTURE_RECT, 0,
+                                    offscreen.area.x,
+                                    offscreen.was_offscreen ? offscreen.area.y2 : offscreen.area.y,
+                                    offscreen.area.x2,
+                                    offscreen.was_offscreen ? offscreen.area.y : offscreen.area.y2);
+      gsk_gl_render_job_draw_offscreen (job, &node->bounds, &offscreen);
+      gsk_gl_render_job_end_draw (job);
+    }
+}
+
+static inline void
+gsk_gl_render_job_visit_subsurface_node (GskGLRenderJob      *job,
+                                         const GskRenderNode *node)
+{
+  GdkSubsurface *subsurface;
+
+  subsurface = (GdkSubsurface *) gsk_subsurface_node_get_subsurface (node);
+
+  if (subsurface &&
+      gdk_subsurface_get_texture (subsurface) &&
+      gdk_subsurface_get_parent (subsurface) == gdk_gl_context_get_surface (job->command_queue->context))
+    {
+      if (!gdk_subsurface_is_above_parent (subsurface))
+        {
+          if (job->offscreen_count > 0)
+            {
+              GDK_DISPLAY_DEBUG (gdk_gl_context_get_display (job->command_queue->context), OFFLOAD, "Hiding subsurface %p in offscreen context", subsurface);
+              gdk_subsurface_detach (subsurface);
+              gsk_gl_render_job_visit_node (job, gsk_subsurface_node_get_child (node));
+            }
+          else if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, color)))
+            {
+              /* Clear the area so we can see through */
+              GskGLCommandBatch *batch;
+              guint16 color[4];
+              rgba_to_half (&GDK_RGBA_TRANSPARENT, color);
+
+              batch = gsk_gl_command_queue_get_batch (job->command_queue);
+              batch->draw.blend = 0;
+              gsk_gl_render_job_draw_rect_with_color (job, &node->bounds, color);
+              gsk_gl_render_job_end_draw (job);
+            }
+        }
+    }
+  else
+    {
+      gsk_gl_render_job_visit_node (job, gsk_subsurface_node_get_child (node));
+    }
+}
+
+static void
+gsk_gl_render_job_visit_node (GskGLRenderJob      *job,
+                              const GskRenderNode *node)
+{
+  gboolean has_clip;
+
+  g_assert (job != NULL);
+  g_assert (node != NULL);
+  g_assert (GSK_IS_GL_DRIVER (job->driver));
+  g_assert (GSK_IS_GL_COMMAND_QUEUE (job->command_queue));
+
+  if (node_is_invisible (node))
+    return;
+
+  if (!gsk_gl_render_job_update_clip (job, &node->bounds, &has_clip))
+    return;
+
+  switch (GSK_RENDER_NODE_TYPE (node))
+    {
+    case GSK_BLEND_NODE:
+      gsk_gl_render_job_visit_blend_node (job, node);
+    break;
+
+    case GSK_BLUR_NODE:
+      if (gsk_blur_node_get_radius (node) > 0)
+        gsk_gl_render_job_visit_blur_node (job, node);
+      else
+        gsk_gl_render_job_visit_node (job, gsk_blur_node_get_child (node));
+    break;
+
+    case GSK_BORDER_NODE:
+      if (gsk_border_node_get_uniform_color (node) &&
+          gsk_rounded_rect_is_rectilinear (gsk_border_node_get_outline (node)))
+        gsk_gl_render_job_visit_rect_border_node (job, node);
+      else
+        gsk_gl_render_job_visit_border_node (job, node);
+    break;
+
+    case GSK_CLIP_NODE:
+      gsk_gl_render_job_visit_clip_node (job, node);
+    break;
+
+    case GSK_COLOR_NODE:
+      gsk_gl_render_job_visit_color_node (job, node);
+    break;
+
+    case GSK_COLOR_MATRIX_NODE:
+      gsk_gl_render_job_visit_color_matrix_node (job, node);
+    break;
+
+    case GSK_CONIC_GRADIENT_NODE:
+      if (gsk_conic_gradient_node_get_n_color_stops (node) < MAX_GRADIENT_STOPS)
+        gsk_gl_render_job_visit_conic_gradient_node (job, node);
+      else
+        gsk_gl_render_job_visit_as_fallback (job, node);
+    break;
+
+    case GSK_CONTAINER_NODE:
+      {
+        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);
+
+            if (i + 1 < n_children &&
+                job->current_clip->is_fully_contained &&
+                GSK_RENDER_NODE_TYPE (child) == GSK_ROUNDED_CLIP_NODE)
+              {
+                const GskRenderNode *grandchild = gsk_rounded_clip_node_get_child (child);
+                const GskRenderNode *child2 = gsk_container_node_get_child (node, i + 1);
+                if (GSK_RENDER_NODE_TYPE (grandchild) == GSK_COLOR_NODE &&
+                    GSK_RENDER_NODE_TYPE (child2) == GSK_BORDER_NODE &&
+                    gsk_border_node_get_uniform_color (child2) &&
+                    rounded_rect_equal (gsk_rounded_clip_node_get_clip (child),
+                                        gsk_border_node_get_outline (child2)))
+                  {
+                    gsk_gl_render_job_visit_css_background (job, child, child2);
+                    i++;
+                    continue;
+                  }
+              }
+
+            gsk_gl_render_job_visit_node (job, child);
+          }
+      }
+    break;
+
+    case GSK_CROSS_FADE_NODE:
+      {
+        const GskRenderNode *start_node = gsk_cross_fade_node_get_start_child (node);
+        const GskRenderNode *end_node = gsk_cross_fade_node_get_end_child (node);
+        float progress = gsk_cross_fade_node_get_progress (node);
+
+        if (progress <= 0.0f)
+          gsk_gl_render_job_visit_node (job, gsk_cross_fade_node_get_start_child (node));
+        else if (progress >= 1.0f || equal_texture_nodes (start_node, end_node))
+          gsk_gl_render_job_visit_node (job, gsk_cross_fade_node_get_end_child (node));
+        else
+          gsk_gl_render_job_visit_cross_fade_node (job, node);
+      }
+    break;
+
+    case GSK_DEBUG_NODE:
+      /* Debug nodes are ignored because draws get reordered anyway */
+      gsk_gl_render_job_visit_node (job, gsk_debug_node_get_child (node));
+    break;
+
+    case GSK_GL_SHADER_NODE:
+      gsk_gl_render_job_visit_gl_shader_node (job, node);
+    break;
+
+    case GSK_INSET_SHADOW_NODE:
+      if (gsk_inset_shadow_node_get_blur_radius (node) > 0)
+        gsk_gl_render_job_visit_blurred_inset_shadow_node (job, node);
+      else
+        gsk_gl_render_job_visit_unblurred_inset_shadow_node (job, node);
+    break;
+
+    case GSK_LINEAR_GRADIENT_NODE:
+    case GSK_REPEATING_LINEAR_GRADIENT_NODE:
+      if (gsk_linear_gradient_node_get_n_color_stops (node) < MAX_GRADIENT_STOPS)
+        gsk_gl_render_job_visit_linear_gradient_node (job, node);
+      else
+        gsk_gl_render_job_visit_as_fallback (job, node);
+    break;
+
+    case GSK_MASK_NODE:
+      gsk_gl_render_job_visit_mask_node (job, node);
+    break;
+
+    case GSK_OPACITY_NODE:
+      gsk_gl_render_job_visit_opacity_node (job, node);
+    break;
+
+    case GSK_OUTSET_SHADOW_NODE:
+      if (gsk_outset_shadow_node_get_blur_radius (node) > 0)
+        gsk_gl_render_job_visit_blurred_outset_shadow_node (job, node);
+      else
+        gsk_gl_render_job_visit_unblurred_outset_shadow_node (job, node);
+    break;
+
+    case GSK_RADIAL_GRADIENT_NODE:
+    case GSK_REPEATING_RADIAL_GRADIENT_NODE:
+      if (gsk_radial_gradient_node_get_n_color_stops (node) < MAX_GRADIENT_STOPS)
+        gsk_gl_render_job_visit_radial_gradient_node (job, node);
+      else
+        gsk_gl_render_job_visit_as_fallback (job, node);
+    break;
+
+    case GSK_REPEAT_NODE:
+      gsk_gl_render_job_visit_repeat_node (job, node);
+    break;
+
+    case GSK_ROUNDED_CLIP_NODE:
+      gsk_gl_render_job_visit_rounded_clip_node (job, node);
+    break;
+
+    case GSK_SHADOW_NODE:
+      gsk_gl_render_job_visit_shadow_node (job, node);
+    break;
+
+    case GSK_TEXT_NODE:
+      {
+        GdkRGBA rgba;
+
+        gdk_color_to_float (gsk_text_node_get_gdk_color (node), GDK_COLOR_STATE_SRGB, (float *) &rgba);
+        gsk_gl_render_job_visit_text_node (job, node, &rgba, FALSE);
+      }
+    break;
+
+    case GSK_TEXTURE_NODE:
+      gsk_gl_render_job_visit_texture_node (job, node);
+    break;
+
+    case GSK_TEXTURE_SCALE_NODE:
+      gsk_gl_render_job_visit_texture_scale_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_FILL_NODE:
+      gsk_gl_render_job_visit_as_fallback (job, node);
+    break;
+
+    case GSK_STROKE_NODE:
+      gsk_gl_render_job_visit_as_fallback (job, node);
+    break;
+
+    case GSK_SUBSURFACE_NODE:
+      gsk_gl_render_job_visit_subsurface_node (job, node);
+    break;
+
+    case GSK_NOT_A_RENDER_NODE:
+    default:
+      g_assert_not_reached ();
+    break;
+    }
+
+  if (has_clip)
+    gsk_gl_render_job_pop_clip (job);
+}
+
+static gboolean
+gsk_gl_render_job_visit_node_with_offscreen (GskGLRenderJob       *job,
+                                             const GskRenderNode  *node,
+                                             GskGLRenderOffscreen *offscreen)
+{
+  GskTextureKey key;
+  guint cached_id;
+
+  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_TYPE (node) == GSK_TEXTURE_NODE &&
+      !offscreen->force_offscreen)
+    {
+      GdkTexture *texture = gsk_texture_node_get_texture (node);
+      gsk_gl_render_job_upload_texture (job, texture, FALSE, offscreen);
+      return TRUE;
+    }
+
+  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;
+
+  float offset_x = job->offset_x;
+  float offset_y = job->offset_y;
+  gboolean flipped_x = job->scale_x < 0;
+  gboolean flipped_y = job->scale_y < 0;
+  graphene_rect_t viewport;
+  gboolean reset_clip = FALSE;
+
+  if (flipped_x || flipped_y)
+    {
+      GskTransform *transform = gsk_transform_scale (NULL,
+                                                     flipped_x ? -1 : 1,
+                                                     flipped_y ? -1 : 1);
+      gsk_gl_render_job_push_modelview (job, transform);
+      gsk_transform_unref (transform);
+    }
+
+  gsk_gl_render_job_transform_bounds (job, offscreen->bounds, &viewport);
+
+  float aligned_x = floorf (viewport.origin.x);
+  float padding_left = viewport.origin.x - aligned_x;
+  float aligned_width = ceilf (viewport.size.width + padding_left);
+  float padding_right = aligned_width - viewport.size.width - padding_left;
+
+  float aligned_y = floorf (viewport.origin.y);
+  float padding_top = viewport.origin.y - aligned_y;
+  float aligned_height = ceilf (viewport.size.height + padding_top);
+  float padding_bottom = aligned_height - viewport.size.height - padding_top;
+
+  /* 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.
+   */
+
+  g_assert (job->command_queue->max_texture_size > 0);
+
+  float downscale_x = 1;
+  float downscale_y = 1;
+  int texture_width;
+  int texture_height;
+  int max_texture_size = job->command_queue->max_texture_size;
+
+  if (aligned_width > max_texture_size)
+    downscale_x = (float)max_texture_size / viewport.size.width;
+
+  if (aligned_height > max_texture_size)
+    downscale_y = (float)max_texture_size / viewport.size.height;
+
+  if (downscale_x != 1 || downscale_y != 1)
+    {
+      GskTransform *transform = gsk_transform_scale (NULL, downscale_x, downscale_y);
+      gsk_gl_render_job_push_modelview (job, transform);
+      gsk_transform_unref (transform);
+      gsk_gl_render_job_transform_bounds (job, offscreen->bounds, &viewport);
+      gsk_rect_scale (&viewport, downscale_x, downscale_y, &viewport);
+    }
+
+  if (downscale_x == 1)
+    {
+      viewport.origin.x = aligned_x;
+      viewport.size.width = aligned_width;
+      offscreen->area.x = padding_left / aligned_width;
+      offscreen->area.x2 = 1.0f - (padding_right / aligned_width);
+      texture_width = aligned_width;
+    }
+  else
+    {
+      offscreen->area.x = 0;
+      offscreen->area.x2 = 1;
+      texture_width = max_texture_size;
+    }
+
+  if (downscale_y == 1)
+    {
+      viewport.origin.y = aligned_y;
+      viewport.size.height = aligned_height;
+      offscreen->area.y = padding_bottom / aligned_height;
+      offscreen->area.y2 = 1.0f - padding_top / aligned_height;
+      texture_height = aligned_height;
+    }
+   else
+    {
+      offscreen->area.y = 0;
+      offscreen->area.y2 = 1;
+      texture_height = max_texture_size;
+    }
+
+  /* Check if we've already cached the drawn texture. */
+  cached_id = gsk_gl_driver_lookup_texture (job->driver, &key, NULL);
+
+  if (cached_id != 0)
+    {
+      if (downscale_x != 1 || downscale_y != 1)
+        gsk_gl_render_job_pop_modelview (job);
+      if (flipped_x || flipped_y)
+        gsk_gl_render_job_pop_modelview (job);
+      offscreen->texture_id = cached_id;
+      /* We didn't render it offscreen, but hand out an offscreen texture id */
+      offscreen->was_offscreen = TRUE;
+      return TRUE;
+    }
+
+  GskGLRenderTarget *render_target;
+  graphene_matrix_t prev_projection;
+  graphene_rect_t prev_viewport;
+  float prev_alpha;
+  guint prev_fbo;
+
+  if (!gsk_gl_driver_create_render_target (job->driver,
+                                           texture_width, texture_height,
+                                           get_target_format (job, node),
+                                           &render_target))
+    g_assert_not_reached ();
+
+  if (gdk_gl_context_has_feature (job->command_queue->context, GDK_GL_FEATURE_DEBUG))
+    {
+      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_set_viewport (job, &viewport, &prev_viewport);
+  gsk_gl_render_job_set_projection_from_rect (job, &job->viewport, &prev_projection);
+  prev_alpha = gsk_gl_render_job_set_alpha (job, 1.0f);
+
+  prev_fbo = gsk_gl_command_queue_bind_framebuffer (job->command_queue, render_target->framebuffer_id);
+  gsk_gl_command_queue_clear (job->command_queue, 0, &job->viewport);
+
+  if (offscreen->reset_clip)
+    {
+      gsk_gl_render_job_push_clip (job, &GSK_ROUNDED_RECT_INIT_FROM_RECT (job->viewport));
+      reset_clip = TRUE;
+    }
+  else if (flipped_x || flipped_y || downscale_x != 1 || downscale_y != 1)
+    {
+      GskRoundedRect new_clip;
+      float scale_x = flipped_x ? - downscale_x : downscale_x;
+      float scale_y = flipped_y ? - downscale_y : downscale_y;
+
+      gsk_rect_scale (&job->current_clip->rect.bounds, scale_x, scale_y, &new_clip.bounds);
+      rounded_rect_scale_corners (&job->current_clip->rect, &new_clip, scale_x, scale_y);
+      gsk_gl_render_job_push_clip (job, &new_clip);
+      reset_clip = TRUE;
+    }
+
+  job->offscreen_count++;
+
+  gsk_gl_render_job_visit_node (job, node);
+
+  job->offscreen_count--;
+
+  if (reset_clip)
+    gsk_gl_render_job_pop_clip (job);
+
+  if (downscale_x != 1 || downscale_y != 1)
+    gsk_gl_render_job_pop_modelview (job);
+
+  if (flipped_x || flipped_y)
+    gsk_gl_render_job_pop_modelview (job);
+
+  gsk_gl_render_job_set_viewport (job, &prev_viewport, NULL);
+  gsk_gl_render_job_set_projection (job, &prev_projection);
+  gsk_gl_render_job_set_alpha (job, prev_alpha);
+  gsk_gl_command_queue_bind_framebuffer (job->command_queue, prev_fbo);
+
+  job->offset_x = offset_x;
+  job->offset_y = offset_y;
+
+  offscreen->was_offscreen = TRUE;
+  offscreen->texture_id = gsk_gl_driver_release_render_target (job->driver,
+                                                               render_target,
+                                                               FALSE);
+
+  if (!offscreen->do_not_cache)
+    gsk_gl_driver_cache_texture (job->driver, &key, offscreen->texture_id);
+
+  return TRUE;
+}
+
+void
+gsk_gl_render_job_render_flipped (GskGLRenderJob *job,
+                                  GskRenderNode  *root)
+{
+  graphene_matrix_t proj;
+  guint framebuffer_id;
+  guint texture_id;
+  guint surface_height;
+
+  g_return_if_fail (job != NULL);
+  g_return_if_fail (root != NULL);
+  g_return_if_fail (GSK_IS_GL_DRIVER (job->driver));
+
+  surface_height = job->viewport.size.height;
+
+  graphene_matrix_init_ortho (&proj,
+                              job->viewport.origin.x,
+                              job->viewport.origin.x + job->viewport.size.width,
+                              job->viewport.origin.y,
+                              job->viewport.origin.y + job->viewport.size.height,
+                              ORTHO_NEAR_PLANE,
+                              ORTHO_FAR_PLANE);
+  graphene_matrix_scale (&proj, 1, -1, 1);
+
+  if (!gsk_gl_command_queue_create_render_target (job->command_queue,
+                                                  MAX (1, job->viewport.size.width),
+                                                  MAX (1, job->viewport.size.height),
+                                                  job->target_format,
+                                                  &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);
+  if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, blit)))
+    {
+      gsk_gl_program_set_uniform_texture (job->current_program,
+                                          UNIFORM_SHARED_SOURCE, 0,
+                                          GL_TEXTURE_2D,
+                                          GL_TEXTURE0,
+                                          texture_id);
+      job->source_is_glyph_atlas = FALSE;
+      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, job->default_framebuffer);
+  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;
+  float scale;
+  guint surface_height;
+
+  g_return_if_fail (job != NULL);
+  g_return_if_fail (root != NULL);
+  g_return_if_fail (GSK_IS_GL_DRIVER (job->driver));
+
+  scale = 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);
+  if (job->clear_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_end_mark (start_time, "Build GL command queue", "");
+
+#if 0
+  /* At this point the atlases have uploaded content while we processed
+   * nodes but have not necessarily been used by the commands in the queue.
+   */
+  gsk_gl_driver_save_atlases_to_png (job->driver, NULL);
+#endif
+
+  /* But now for executing the command queue, we want to use the context
+   * that was provided to us when creating the render job as framebuffer 0
+   * is bound to that context.
+   */
+  start_time = GDK_PROFILER_CURRENT_TIME;
+  gsk_gl_command_queue_make_current (job->command_queue);
+  gdk_gl_context_push_debug_group (job->command_queue->context, "Executing command queue");
+  gsk_gl_command_queue_execute (job->command_queue, surface_height, scale, job->region, job->default_framebuffer);
+  gdk_gl_context_pop_debug_group (job->command_queue->context);
+  gdk_profiler_end_mark (start_time, "Execute GL command queue", "");
+}
+
+static int
+get_framebuffer_format (GdkGLContext *context,
+                        guint         framebuffer)
+{
+  int size;
+
+  /* Both the query below and the float formats it can select need GL 3.0;
+   * before that this used to be NULL, ie "any desktop GL", which was fine
+   * while GTK required GL 3.3.
+   */
+  if (!gdk_gl_context_check_version (context, "3.0", "3.0"))
+    return GL_RGBA8;
+
+  glBindFramebuffer (GL_FRAMEBUFFER, framebuffer);
+  glGetFramebufferAttachmentParameteriv (GL_FRAMEBUFFER,
+                                         framebuffer ? GL_COLOR_ATTACHMENT0
+                                                     : gdk_gl_context_get_use_es (context) ? GL_BACK
+                                                                                           : GL_BACK_LEFT,
+                                         GL_FRAMEBUFFER_ATTACHMENT_RED_SIZE, &size);
+
+  if (size > 16)
+    return GL_RGBA32F;
+  else if (size > 8)
+    return GL_RGBA16F;
+  else
+    return GL_RGBA8;
+}
+
+GskGLRenderJob *
+gsk_gl_render_job_new (GskGLDriver           *driver,
+                       const graphene_rect_t *viewport,
+                       float                  scale,
+                       const cairo_region_t  *region,
+                       guint                  framebuffer,
+                       gboolean               clear_framebuffer)
+{
+  const graphene_rect_t *clip_rect = viewport;
+  graphene_rect_t transformed_extents;
+  GskGLRenderJob *job;
+  GdkGLContext *context;
+  GLint default_framebuffer = 0;
+
+  g_return_val_if_fail (GSK_IS_GL_DRIVER (driver), NULL);
+  g_return_val_if_fail (viewport != NULL, NULL);
+  g_return_val_if_fail (scale > 0, NULL);
+
+  /* Check for non-standard framebuffer binding as we might not be using
+   * the default framebuffer on systems like macOS where we've bound an
+   * IOSurface to a GL_TEXTURE_RECTANGLE. Otherwise, no scissor clip will
+   * be applied in the command queue causing overdrawing.
+   */
+  context = driver->command_queue->context;
+  default_framebuffer = GDK_GL_CONTEXT_GET_CLASS (context)->get_default_framebuffer (context);
+  if (framebuffer == 0 && default_framebuffer != 0)
+    framebuffer = default_framebuffer;
+
+  job = g_new0 (GskGLRenderJob, 1);
+  job->driver = g_object_ref (driver);
+  job->command_queue = job->driver->command_queue;
+  clips_init (&job->clip);
+  modelviews_init (&job->modelview);
+  job->framebuffer = framebuffer;
+  job->clear_framebuffer = !!clear_framebuffer;
+  job->default_framebuffer = default_framebuffer;
+  job->offset_x = 0;
+  job->offset_y = 0;
+  job->scale_x = scale;
+  job->scale_y = scale;
+  job->viewport = *viewport;
+  job->target_format = get_framebuffer_format (job->command_queue->context, framebuffer);
+
+  gsk_gl_render_job_set_alpha (job, 1.0f);
+  gsk_gl_render_job_set_projection_from_rect (job, viewport, NULL);
+  gsk_gl_render_job_set_modelview (job, gsk_transform_scale (NULL, scale, scale));
+
+  /* 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.end > job->modelview.start)
+    {
+      GskGLRenderModelview *modelview = job->modelview.end-1;
+      g_clear_pointer (&modelview->transform, gsk_transform_unref);
+      job->modelview.end--;
+    }
+
+  g_clear_object (&job->driver);
+  g_clear_pointer (&job->region, cairo_region_destroy);
+  modelviews_clear (&job->modelview);
+  clips_clear (&job->clip);
+  g_free (job);
+}
diff --git gsk/gl/gskglrenderjobprivate.h gsk/gl/gskglrenderjobprivate.h
new file mode 100644
index 0000000000..2e38b07a2e
--- /dev/null
+++ gsk/gl/gskglrenderjobprivate.h
@@ -0,0 +1,36 @@
+/* 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
+ */
+
+#pragma once
+
+#include "gskgltypesprivate.h"
+
+GskGLRenderJob *gsk_gl_render_job_new                (GskGLDriver           *driver,
+                                                      const graphene_rect_t *viewport,
+                                                      float                  scale,
+                                                      const cairo_region_t  *region,
+                                                      guint                  framebuffer,
+                                                      gboolean               clear_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);
+
diff --git gsk/gl/gskglshadowlibrary.c gsk/gl/gskglshadowlibrary.c
new file mode 100644
index 0000000000..d7c22c2588
--- /dev/null
+++ gsk/gl/gskglshadowlibrary.c
@@ -0,0 +1,256 @@
+/* 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;
+  GskGLDriver *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 (GskGLDriver *driver)
+{
+  g_return_val_if_fail (GSK_IS_GL_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_gl_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", NULL, NULL,
+                         GSK_TYPE_GL_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_gl_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;
+}
+
+#if 0
+static void
+write_shadow_to_png (GskGLDriver *driver,
+                     const Shadow *shadow)
+{
+  int width = shadow->outline.bounds.size.width + (shadow->outline.bounds.origin.x * 2);
+  int height = shadow->outline.bounds.size.height + (shadow->outline.bounds.origin.y * 2);
+  char *filename = g_strdup_printf ("shadow_cache_%d_%d_%d.png",
+                                    width, height, shadow->texture_id);
+  GdkTexture *texture;
+
+  texture = gdk_gl_texture_new (gsk_gl_driver_get_context (driver),
+                                shadow->texture_id,
+                                width, height,
+                                NULL, NULL);
+  gdk_texture_save_to_png (texture, filename);
+
+  g_object_unref (texture);
+  g_free (filename);
+}
+#endif
+
+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));
+
+#if 0
+  for (i = 0, p = self->shadows->len; i < p; i++)
+    {
+      const Shadow *shadow = &g_array_index (self->shadows, Shadow, i);
+      write_shadow_to_png (self->driver, shadow);
+    }
+#endif
+
+  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_gl_driver_release_texture_by_id (self->driver, shadow->texture_id);
+          g_array_remove_index_fast (self->shadows, i);
+          p--;
+          i--;
+        }
+    }
+}
diff --git gsk/gl/gskglshadowlibraryprivate.h gsk/gl/gskglshadowlibraryprivate.h
new file mode 100644
index 0000000000..3c3c77c9e3
--- /dev/null
+++ gsk/gl/gskglshadowlibraryprivate.h
@@ -0,0 +1,42 @@
+/* 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
+ */
+
+#pragma once
+
+#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         (GskGLDriver          *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
+
diff --git gsk/gl/gskgltexture.c gsk/gl/gskgltexture.c
new file mode 100644
index 0000000000..a578e80358
--- /dev/null
+++ gsk/gl/gskgltexture.c
@@ -0,0 +1,95 @@
+/* gskgltexture.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/gdktextureprivate.h>
+
+#include "gskgltextureprivate.h"
+#include "ninesliceprivate.h"
+
+void
+gsk_gl_texture_free (GskGLTexture *texture)
+{
+  if (texture != NULL)
+    {
+      g_assert (texture->link.prev == NULL);
+      g_assert (texture->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_free (texture);
+    }
+}
+
+GskGLTexture *
+gsk_gl_texture_new (guint  texture_id,
+                    int    width,
+                    int    height,
+                    gint64 frame_id)
+{
+  GskGLTexture *texture;
+
+  texture = g_new0 (GskGLTexture, 1);
+  texture->texture_id = texture_id;
+  texture->link.data = texture;
+  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_x,
+                               float                 extra_pixels_y)
+{
+  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_x, extra_pixels_y);
+      nine_slice_to_texture_coords (texture->nine_slice, texture->width, texture->height);
+    }
+
+  return texture->nine_slice;
+}
diff --git gsk/gl/gskgltexturelibrary.c gsk/gl/gskgltexturelibrary.c
new file mode 100644
index 0000000000..772b382477
--- /dev/null
+++ gsk/gl/gskgltexturelibrary.c
@@ -0,0 +1,583 @@
+/* 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 <gdk/gdkglcontextprivate.h>
+#include <gsk/gskdebugprivate.h>
+
+#include "gskglcommandqueueprivate.h"
+#include "gskgldriverprivate.h"
+#include "gskgltexturelibraryprivate.h"
+
+#define DEFAULT_MAX_FRAME_AGE 60
+#define DEFAULT_ATLAS_WIDTH 512
+#define DEFAULT_ATLAS_HEIGHT 512
+#define MAX_OLD_RATIO 0.5
+
+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_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_free (atlas);
+}
+
+static gboolean
+gsk_gl_texture_library_real_compact (GskGLTextureLibrary *self,
+                                     gint64               frame_id)
+{
+  GPtrArray *removed = NULL;
+  gboolean ret = FALSE;
+  gboolean periodic_scan;
+
+  g_assert (GSK_IS_GL_TEXTURE_LIBRARY (self));
+
+  periodic_scan = (self->max_frame_age > 0 &&
+                   (frame_id % self->max_frame_age) == 0);
+
+  for (guint i = self->atlases->len; i > 0; i--)
+    {
+      GskGLTextureAtlas *atlas = g_ptr_array_index (self->atlases, i - 1);
+
+      if (gsk_gl_texture_atlas_get_unused_ratio (atlas) > MAX_OLD_RATIO)
+        {
+          GSK_DEBUG (CACHE,
+                     "Dropping atlas %d (%g.2%% old)", i,
+                     100.0 * gsk_gl_texture_atlas_get_unused_ratio (atlas));
+          if (removed == NULL)
+            removed = g_ptr_array_new_with_free_func ((GDestroyNotify)gsk_gl_texture_atlas_free);
+          g_ptr_array_add (removed, g_ptr_array_steal_index (self->atlases, i - 1));
+        }
+    }
+
+  if (periodic_scan || removed != NULL)
+    {
+      GskGLTextureAtlasEntry *entry;
+      GHashTableIter iter;
+      guint dropped = 0;
+      G_GNUC_UNUSED guint atlased = 0;
+
+      g_hash_table_iter_init (&iter, self->hash_table);
+      while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&entry))
+        {
+          if (entry->is_atlased)
+            {
+              if (removed && g_ptr_array_find (removed, entry->atlas, NULL))
+                {
+                  g_hash_table_iter_remove (&iter);
+                  dropped++;
+                }
+              else if (periodic_scan)
+                {
+                  gsk_gl_texture_atlas_entry_mark_unused (entry);
+                  entry->accessed = FALSE;
+                  if (entry->is_atlased)
+                    atlased++;
+                }
+            }
+          else
+            {
+              if (!entry->accessed)
+                {
+                  if (entry->texture)
+                    gsk_gl_driver_release_texture (self->driver, entry->texture);
+                  g_hash_table_iter_remove (&iter);
+                  dropped++;
+                }
+              else if (periodic_scan)
+                entry->accessed = FALSE;
+            }
+        }
+
+      GSK_DEBUG (CACHE, "%s: Dropped %d individual items",
+                              G_OBJECT_TYPE_NAME (self),
+                              dropped);
+      GSK_DEBUG (CACHE, "%s: %d items cached (%d atlased, %d individually)",
+                              G_OBJECT_TYPE_NAME (self),
+                              g_hash_table_size (self->hash_table),
+                              atlased,
+                              g_hash_table_size (self->hash_table) - atlased);
+
+      if (dropped > 0)
+        gsk_gl_texture_library_clear_cache (self);
+
+      ret = TRUE;
+
+      g_clear_pointer (&removed, g_ptr_array_unref);
+    }
+
+  if (GSK_DEBUG_CHECK (CACHE))
+    {
+      static gint64 last_message;
+      gint64 now = g_get_monotonic_time ();
+      if (now - last_message > G_USEC_PER_SEC)
+        {
+          last_message = now;
+          gdk_debug_message ("%s contains %d atlases",
+                             G_OBJECT_TYPE_NAME (self),
+                             self->atlases->len);
+        }
+    }
+
+  return ret;
+}
+
+static gboolean
+gsk_gl_texture_library_real_allocate (GskGLTextureLibrary *self,
+                                      GskGLTextureAtlas   *atlas,
+                                      int                  width,
+                                      int                  height,
+                                      int                 *out_x,
+                                      int                 *out_y)
+{
+  stbrp_rect rect;
+
+  g_assert (GSK_IS_GL_TEXTURE_LIBRARY (self));
+  g_assert (atlas != NULL);
+  g_assert (width > 0);
+  g_assert (height > 0);
+  g_assert (out_x != NULL);
+  g_assert (out_y != NULL);
+
+  rect.w = width;
+  rect.h = height;
+
+  stbrp_pack_rects (&atlas->context, &rect, 1);
+
+  if (rect.was_packed)
+    {
+      *out_x = rect.x;
+      *out_y = rect.y;
+    }
+
+  return rect.was_packed;
+}
+
+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_pointer (&self->atlases, g_ptr_array_unref);
+  g_clear_object (&self->driver);
+  g_clear_pointer (&self->hash_table, g_hash_table_unref);
+
+  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;
+
+  klass->compact = gsk_gl_texture_library_real_compact;
+  klass->allocate = gsk_gl_texture_library_real_allocate;
+
+  properties [PROP_DRIVER] =
+    g_param_spec_object ("driver", NULL, NULL,
+                         GSK_TYPE_GL_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)
+{
+  self->max_frame_age = DEFAULT_MAX_FRAME_AGE;
+  self->atlas_width = DEFAULT_ATLAS_WIDTH;
+  self->atlas_height = DEFAULT_ATLAS_HEIGHT;
+  self->atlases = g_ptr_array_new_with_free_func ((GDestroyNotify)gsk_gl_texture_atlas_free);
+}
+
+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,
+                                    gint64               frame_id)
+{
+  g_return_if_fail (GSK_IS_GL_TEXTURE_LIBRARY (self));
+
+  gsk_gl_texture_library_compact (self, frame_id);
+
+  if (GSK_GL_TEXTURE_LIBRARY_GET_CLASS (self)->begin_frame)
+    GSK_GL_TEXTURE_LIBRARY_GET_CLASS (self)->begin_frame (self, frame_id);
+}
+
+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_gl_driver_create_texture (self->driver, width, height, GL_RGBA8);
+  texture->permanent = TRUE;
+
+  return texture;
+}
+
+static void
+gsk_gl_texture_library_pack_any_atlas (GskGLTextureLibrary  *self,
+                                       int                   width,
+                                       int                   height,
+                                       GskGLTextureAtlas   **out_atlas,
+                                       int                  *out_x,
+                                       int                  *out_y)
+{
+  GskGLTextureAtlas *atlas = NULL;
+  int x, y;
+
+  g_assert (GSK_IS_GL_TEXTURE_LIBRARY (self));
+  g_assert (width > 0);
+  g_assert (height > 0);
+  g_assert (out_atlas != NULL);
+  g_assert (out_x != NULL);
+  g_assert (out_y != NULL);
+
+  for (guint i = 0; i < self->atlases->len; i++)
+    {
+      atlas = g_ptr_array_index (self->atlases, i);
+
+      if (gsk_gl_texture_library_allocate (self, atlas, width, height, &x, &y))
+        break;
+
+      atlas = NULL;
+    }
+
+  if (atlas == NULL)
+    {
+      /* No atlas has enough space, so create a new one... */
+      atlas = gsk_gl_texture_library_acquire_atlas (self);
+
+      /* Pack it onto that one, which surely has enough space... */
+      if (!gsk_gl_texture_library_allocate (self, 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_malloc0 (valuelen);
+  entry->n_pixels = width * height;
+  entry->accessed = TRUE;
+  entry->used = 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 (self->max_entry_size == 0 ||
+           (width <= self->max_entry_size &&
+            height <= self->max_entry_size))
+    {
+      int packed_x;
+      int packed_y;
+
+      gsk_gl_texture_library_pack_any_atlas (self,
+                                             padding + width + padding,
+                                             padding + height + padding,
+                                             &atlas,
+                                             &packed_x,
+                                             &packed_y);
+
+      entry->atlas = atlas;
+      entry->is_atlased = TRUE;
+      entry->area.x = (packed_x + padding) / (float)atlas->width;
+      entry->area.y = (packed_y + padding) / (float)atlas->height;
+      entry->area.x2 = (packed_x + padding + width) / (float)atlas->width;
+      entry->area.y2 = (packed_y + padding + height) / (float)atlas->height;
+
+      *out_packed_x = packed_x;
+      *out_packed_y = packed_y;
+    }
+  else
+    {
+      GskGLTexture *texture = gsk_gl_texture_library_pack_one (self,
+                                                               padding + width + padding,
+                                                               padding + height + padding);
+
+      entry->texture = texture;
+      entry->is_atlased = FALSE;
+      entry->area.x = padding / (float) (padding + width + padding);
+      entry->area.y = padding / (float) (padding + height + padding);
+      entry->area.x2 = (padding + width) / (float) (padding + width + padding);
+      entry->area.y2 = (padding + height) / (float) (padding + height + padding);
+
+      *out_packed_x = 0;
+      *out_packed_y = 0;
+    }
+
+  g_hash_table_insert (self->hash_table, key, entry);
+
+  return entry;
+}
+
+/*
+ * gsk_gl_texture_library_clear_cache:
+ *
+ * Clear the front cache if the texture library is using one. For
+ * example the glyph cache would drop it's front cache to force
+ * next lookups to fall through to the GHashTable key lookup.
+ */
+void
+gsk_gl_texture_library_clear_cache (GskGLTextureLibrary *self)
+{
+  g_return_if_fail (GSK_IS_GL_TEXTURE_LIBRARY (self));
+
+  if (GSK_GL_TEXTURE_LIBRARY_GET_CLASS (self)->clear_cache)
+    GSK_GL_TEXTURE_LIBRARY_GET_CLASS (self)->clear_cache (self);
+}
+
+/*
+ * gsk_gl_texture_library_compact:
+ *
+ * Requests that the texture library compact it's altases. That
+ * generally means to traverse them to look for unused pixels over
+ * a certain threshold and release them if necessary.
+ *
+ * Returns: %TRUE if any compaction occurred.
+ */
+gboolean
+gsk_gl_texture_library_compact (GskGLTextureLibrary *self,
+                                gint64               frame_id)
+{
+  g_return_val_if_fail (GSK_IS_GL_TEXTURE_LIBRARY (self), FALSE);
+
+  if (GSK_GL_TEXTURE_LIBRARY_GET_CLASS (self)->compact)
+    return GSK_GL_TEXTURE_LIBRARY_GET_CLASS (self)->compact (self, frame_id);
+
+  return FALSE;
+}
+
+void
+gsk_gl_texture_library_reset (GskGLTextureLibrary *self)
+{
+  g_return_if_fail (GSK_IS_GL_TEXTURE_LIBRARY (self));
+
+  gsk_gl_texture_library_clear_cache (self);
+
+  g_hash_table_remove_all (self->hash_table);
+
+  if (self->atlases->len)
+    g_ptr_array_remove_range (self->atlases, 0, self->atlases->len);
+}
+
+void
+gsk_gl_texture_library_set_atlas_size (GskGLTextureLibrary *self,
+                                       int                  width,
+                                       int                  height)
+{
+  g_return_if_fail (GSK_IS_GL_TEXTURE_LIBRARY (self));
+
+  if (width <= 0)
+    width = DEFAULT_ATLAS_WIDTH;
+
+  if (height <= 0)
+    height = DEFAULT_ATLAS_HEIGHT;
+
+  self->atlas_height = height;
+  self->atlas_width = width;
+
+  gsk_gl_texture_library_reset (self);
+}
+
+/*
+ * gsk_gl_texture_library_acquire_atlas:
+ *
+ * Allocates a new texture atlas based on the current size
+ * and format requirements.
+ */
+GskGLTextureAtlas *
+gsk_gl_texture_library_acquire_atlas (GskGLTextureLibrary *self)
+{
+  GskGLTextureAtlas *atlas;
+
+  g_return_val_if_fail (GSK_IS_GL_TEXTURE_LIBRARY (self), NULL);
+  g_return_val_if_fail (GSK_IS_GL_DRIVER (self->driver), NULL);
+  g_return_val_if_fail (GSK_IS_GL_COMMAND_QUEUE (self->driver->command_queue), NULL);
+  g_return_val_if_fail (self->atlas_width > 0, NULL);
+  g_return_val_if_fail (self->atlas_height > 0, NULL);
+
+  atlas = g_new0 (GskGLTextureAtlas, 1);
+  atlas->width = self->atlas_width;
+  atlas->height = self->atlas_height;
+  /* 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->driver->command_queue,
+                                                           atlas->width,
+                                                           atlas->height,
+                                                           GL_RGBA8);
+
+  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);
+
+  if (GSK_GL_TEXTURE_LIBRARY_GET_CLASS (self)->init_atlas)
+    GSK_GL_TEXTURE_LIBRARY_GET_CLASS (self)->init_atlas (self, atlas);
+
+  return atlas;
+}
+
+gboolean
+gsk_gl_texture_library_allocate (GskGLTextureLibrary *self,
+                                 GskGLTextureAtlas   *atlas,
+                                 int                  width,
+                                 int                  height,
+                                 int                 *out_x,
+                                 int                 *out_y)
+{
+  g_assert (GSK_IS_GL_TEXTURE_LIBRARY (self));
+  g_assert (atlas != NULL);
+  g_assert (width > 0);
+  g_assert (height > 0);
+  g_assert (out_x != NULL);
+  g_assert (out_y != NULL);
+
+  return GSK_GL_TEXTURE_LIBRARY_GET_CLASS (self)->allocate (self, atlas, width, height, out_x, out_y);
+}
diff --git gsk/gl/gskgltexturelibraryprivate.h gsk/gl/gskgltexturelibraryprivate.h
new file mode 100644
index 0000000000..e2c3589fab
--- /dev/null
+++ gsk/gl/gskgltexturelibraryprivate.h
@@ -0,0 +1,232 @@
+/* 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
+ */
+
+#pragma once
+
+#include "gskgltypesprivate.h"
+#include "gskgltextureprivate.h"
+
+#include "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;
+
+} 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;
+} GskGLTextureAtlasEntry;
+
+typedef struct _GskGLTextureLibrary
+{
+  GObject      parent_instance;
+  GskGLDriver *driver;
+  GPtrArray   *atlases;
+  GHashTable  *hash_table;
+  guint        max_entry_size;
+  guint        max_frame_age;
+  guint        atlas_width;
+  guint        atlas_height;
+} GskGLTextureLibrary;
+
+typedef struct _GskGLTextureLibraryClass
+{
+  GObjectClass parent_class;
+
+  void     (*begin_frame) (GskGLTextureLibrary *library,
+                           gint64               frame_id);
+  gboolean (*compact)     (GskGLTextureLibrary *library,
+                           gint64               frame_id);
+  void     (*clear_cache) (GskGLTextureLibrary *library);
+  void     (*init_atlas)  (GskGLTextureLibrary *library,
+                           GskGLTextureAtlas   *atlas);
+  gboolean (*allocate)    (GskGLTextureLibrary *library,
+                           GskGLTextureAtlas   *atlas,
+                           int                  width,
+                           int                  height,
+                           int                 *out_x,
+                           int                 *out_y);
+} GskGLTextureLibraryClass;
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (GskGLTextureLibrary, g_object_unref)
+
+GType              gsk_gl_texture_library_get_type       (void) G_GNUC_CONST;
+gboolean           gsk_gl_texture_library_compact        (GskGLTextureLibrary *self,
+                                                          gint64               frame_id);
+void               gsk_gl_texture_library_clear_cache    (GskGLTextureLibrary *self);
+void               gsk_gl_texture_library_reset          (GskGLTextureLibrary *self);
+void               gsk_gl_texture_library_set_atlas_size (GskGLTextureLibrary *self,
+                                                          int                  width,
+                                                          int                  height);
+GskGLTextureAtlas *gsk_gl_texture_library_acquire_atlas  (GskGLTextureLibrary *self);
+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,
+                                                          gint64               frame_id);
+gboolean           gsk_gl_texture_library_allocate       (GskGLTextureLibrary *self,
+                                                          GskGLTextureAtlas   *atlas,
+                                                          int                  width,
+                                                          int                  height,
+                                                          int                 *out_x,
+                                                          int                 *out_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);
+
+static inline void
+gsk_gl_texture_atlas_mark_unused (GskGLTextureAtlas *self,
+                                  int                n_pixels)
+{
+  g_assert (n_pixels >= 0);
+
+  self->unused_pixels += n_pixels;
+}
+
+static inline void
+gsk_gl_texture_atlas_entry_mark_used (GskGLTextureAtlasEntry *entry)
+{
+  if (entry->used == TRUE || entry->is_atlased == FALSE)
+    return;
+
+  entry->atlas->unused_pixels -= entry->n_pixels;
+  entry->used = TRUE;
+}
+
+static inline void
+gsk_gl_texture_atlas_entry_mark_unused (GskGLTextureAtlasEntry *entry)
+{
+  if (entry->used == FALSE || entry->is_atlased == FALSE)
+    return;
+
+  entry->atlas->unused_pixels += entry->n_pixels;
+  entry->used = FALSE;
+}
+
+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)
+    {
+      gsk_gl_texture_atlas_entry_mark_used (entry);
+      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
+
diff --git gsk/gl/gskgltextureprivate.h gsk/gl/gskgltextureprivate.h
new file mode 100644
index 0000000000..620a9ed076
--- /dev/null
+++ gsk/gl/gskgltextureprivate.h
@@ -0,0 +1,93 @@
+/* gskgltextureprivate.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
+ */
+
+#pragma once
+
+#include "gskgltypesprivate.h"
+
+G_BEGIN_DECLS
+
+struct _GskGLTextureSlice
+{
+  cairo_rectangle_int_t rect;
+  struct {
+    float x;
+    float y;
+    float x2;
+    float y2;
+  } area;
+  guint texture_id;
+};
+
+struct _GskGLTextureNineSlice
+{
+  cairo_rectangle_int_t rect;
+  struct {
+    float x;
+    float y;
+    float x2;
+    float y2;
+  } area;
+};
+
+struct _GskGLTexture
+{
+  /* Used to insert into queue */
+  GList 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 nine-slice textures */
+  GskGLTextureNineSlice *nine_slice;
+
+  /* Only used by sliced textures */
+  GskGLTextureSlice *slices;
+  guint n_slices;
+
+  /* The actual GL texture identifier in some shared context */
+  guint texture_id;
+
+  int width;
+  int height;
+
+  /* Set when used by an atlas so we don't drop the texture */
+  guint              permanent : 1;
+  /* we are allowed to call glGenerateMipmap() for this texture */
+  guint              can_mipmap : 1;
+  /* we called glGenerateMipmap() for this texture */
+  guint              has_mipmap : 1;
+};
+
+GskGLTexture                * gsk_gl_texture_new            (guint                 texture_id,
+                                                             int                   width,
+                                                             int                   height,
+                                                             gint64                frame_id);
+const GskGLTextureNineSlice * gsk_gl_texture_get_nine_slice (GskGLTexture         *texture,
+                                                             const GskRoundedRect *outline,
+                                                             float                 extra_pixels_x,
+                                                             float                 extra_pixels_y);
+void                          gsk_gl_texture_free           (GskGLTexture         *texture);
+
+G_END_DECLS
+
diff --git gsk/gl/gskgltypesprivate.h gsk/gl/gskgltypesprivate.h
new file mode 100644
index 0000000000..8f7e8fdbfd
--- /dev/null
+++ gsk/gl/gskgltypesprivate.h
@@ -0,0 +1,84 @@
+/* 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
+ */
+
+#pragma once
+
+#include <gdk/gdk.h>
+#include <gsk/gsk.h>
+#ifdef GDK_WINDOWING_WIN32
+/* epoxy needs this, see https://github.com/anholt/libepoxy/issues/299 */
+#include <windows.h>
+#endif
+#include <epoxy/gl.h>
+#include <graphene.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 _GskGLDriver GskGLDriver;
+
+struct _GskGLDrawVertex
+{
+  float position[2];
+  union {
+    float uv[2];
+    guint16 color2[4];
+  };
+  guint16 color[4];
+};
+
+/* Same vertex, but with the colors expanded to full floats. Used on drivers
+ * that cannot source vertex attributes from half floats (pre-GL 3.0 without
+ * GL_ARB_half_float_vertex). Note that uv and color2 alias each other in
+ * GskGLDrawVertex, so both members are filled in from the same bytes and the
+ * shader picks whichever one it declared.
+ */
+typedef struct _GskGLDrawVertexFloat GskGLDrawVertexFloat;
+
+struct _GskGLDrawVertexFloat
+{
+  float position[2];
+  float uv[2];
+  float color[4];
+  float color2[4];
+};
+
+G_END_DECLS
+
diff --git gsk/gl/gskgluniformstate.c gsk/gl/gskgluniformstate.c
new file mode 100644
index 0000000000..77cd45a859
--- /dev/null
+++ gsk/gl/gskgluniformstate.c
@@ -0,0 +1,269 @@
+/* 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);
+
+  memset (state->apply_hash, 0, sizeof state->apply_hash);
+
+  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                 key,
+                                 GskGLUniformMapping **infoptr)
+{
+  GskGLUniformMapping *mapping;
+  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 (key < program->n_mappings);
+
+  mapping = &program->mappings[key];
+
+  if (mapping->location == -1)
+    {
+      *infoptr = NULL;
+      return NULL;
+    }
+
+  if G_LIKELY (format == mapping->info.format)
+    {
+      if G_LIKELY (array_count <= mapping->info.array_count)
+        {
+          *infoptr = mapping;
+          return GSK_GL_UNIFORM_VALUE (state->values_buf, mapping->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 (mapping->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, key, mapping->info.format, format,
+                  mapping->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));
+
+  mapping->info.format = format;
+  mapping->info.offset = offset;
+  mapping->info.array_count = array_count;
+  mapping->info.initial = TRUE;
+  mapping->stamp = 0;
+
+  *infoptr = mapping;
+
+  return GSK_GL_UNIFORM_VALUE (state->values_buf, mapping->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_mappings; j++)
+        {
+          GskGLUniformMapping *mapping = &program->mappings[j];
+          guint size;
+
+          /* Skip unused uniform mappings */
+          if (mapping->info.format == 0 || mapping->location == -1)
+            continue;
+
+          /* Calculate how much size is needed for the uniform, including arrays */
+          size = uniform_sizes[mapping->info.format] * MAX (1, mapping->info.array_count);
+
+          /* Adjust alignment for value */
+          allocator += gsk_gl_uniform_state_align (allocator, size);
+
+          /* Offset is in slots of 4 bytes */
+          mapping->info.offset = allocator / 4;
+          mapping->info.initial = TRUE;
+          mapping->stamp = 0;
+
+          /* Now advance for this items data */
+          allocator += size;
+        }
+    }
+
+  state->values_pos = allocator;
+
+  /* It can happen that our space requirements grow due to
+   * difference in order increasing padding. As a pragmatic
+   * solution to this, just increase the allocation to cover
+   * the predefined mappins.
+   */
+  if (allocator > state->values_len)
+    {
+      while (allocator > state->values_len)
+        state->values_len *= 2;
+      state->values_buf = g_realloc (state->values_buf, state->values_len);
+    }
+
+  memset (state->apply_hash, 0, sizeof state->apply_hash);
+}
+
+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,
+                                  const GskGLUniformMapping *mappings,
+                                  guint                      n_mappings)
+{
+  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);
+  g_return_val_if_fail (n_mappings <= G_N_ELEMENTS (ret->mappings), NULL);
+
+  ret = g_hash_table_lookup (state->programs, GUINT_TO_POINTER (program));
+
+  if (ret == NULL)
+    {
+      ret = g_new0 (GskGLUniformProgram, 1);
+      ret->program_id = program;
+      ret->n_mappings = n_mappings;
+
+      memcpy (ret->mappings, mappings, n_mappings * sizeof *mappings);
+
+      g_hash_table_insert (state->programs, GUINT_TO_POINTER (program), ret);
+    }
+
+  return ret;
+}
diff --git gsk/gl/gskgluniformstateprivate.h gsk/gl/gskgluniformstateprivate.h
new file mode 100644
index 0000000000..3704be957c
--- /dev/null
+++ gsk/gl/gskgluniformstateprivate.h
@@ -0,0 +1,834 @@
+/* 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
+ */
+
+#pragma once
+
+#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 _GskGLUniformMapping
+{
+  const char *name;
+  GskGLUniformInfo info;
+  guint stamp;
+  int location;
+} GskGLUniformMapping;
+
+typedef struct _GskGLUniformProgram
+{
+  guint program_id;
+  guint n_uniforms : 12;
+  guint has_attachments : 1;
+  guint n_mappings;
+  GskGLUniformMapping mappings[32];
+} GskGLUniformProgram;
+
+typedef struct _GskGLUniformState
+{
+  GHashTable *programs;
+  guint8 *values_buf;
+  guint values_pos;
+  guint values_len;
+  GskGLUniformInfo apply_hash[512];
+} GskGLUniformState;
+
+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,
+                                                       const GskGLUniformMapping  *mappings,
+                                                       guint                       n_mappings);
+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                       key,
+                                                       GskGLUniformMapping       **out_mapping);
+
+#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)
+
+static inline gpointer
+gsk_gl_uniform_state_get_value (GskGLUniformState    *state,
+                                GskGLUniformProgram  *program,
+                                GskGLUniformFormat    format,
+                                guint                 array_count,
+                                guint                 key,
+                                guint                 stamp,
+                                GskGLUniformMapping **out_mapping)
+{
+  GskGLUniformMapping *mapping;
+
+  g_assert (key < G_N_ELEMENTS (program->mappings));
+  g_assert (key < program->n_mappings);
+
+  mapping = &program->mappings[key];
+
+  /* Short-circuit if the program optimized the uniform out */
+  if (mapping->location == -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.
+   */
+  if (stamp != 0 && stamp == mapping->stamp)
+    return NULL;
+
+  if G_LIKELY (format == mapping->info.format && array_count <= mapping->info.array_count)
+    {
+      *out_mapping = mapping;
+      return GSK_GL_UNIFORM_VALUE (state->values_buf, mapping->info.offset);
+    }
+
+  return gsk_gl_uniform_state_init_value (state, program, format, array_count, key, out_mapping);
+}
+
+G_GNUC_PURE 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 (GskGLUniformMapping *info,
+                             guint                stamp)
+{
+  info->stamp = stamp;
+  info->info.initial = FALSE;
+}
+
+static inline void
+gsk_gl_uniform_state_set1f (GskGLUniformState   *state,
+                            GskGLUniformProgram *program,
+                            guint                key,
+                            guint                stamp,
+                            float                value0)
+{
+  Uniform1f *u;
+  GskGLUniformMapping *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, key, stamp, &info)))
+    {
+      if (info->info.initial || u->v0 != value0)
+        {
+          GSK_GL_UNIFORM_STATE_REPLACE (info, u, Uniform1f , 1);
+          u->v0 = value0;
+          gsk_gl_uniform_info_changed (info, stamp);
+        }
+    }
+}
+
+static inline void
+gsk_gl_uniform_state_set2f (GskGLUniformState   *state,
+                            GskGLUniformProgram *program,
+                            guint                key,
+                            guint                stamp,
+                            float                value0,
+                            float                value1)
+{
+  Uniform2f *u;
+  GskGLUniformMapping *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, key, stamp, &info)))
+    {
+      if (info->info.initial || u->v0 != value0 || u->v1 != value1)
+        {
+          GSK_GL_UNIFORM_STATE_REPLACE (info, u, Uniform2f, 1);
+          u->v0 = value0;
+          u->v1 = value1;
+          gsk_gl_uniform_info_changed (info, stamp);
+        }
+    }
+}
+
+static inline void
+gsk_gl_uniform_state_set3f (GskGLUniformState   *state,
+                            GskGLUniformProgram *program,
+                            guint                key,
+                            guint                stamp,
+                            float                value0,
+                            float                value1,
+                            float                value2)
+{
+  Uniform3f *u;
+  GskGLUniformMapping *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, key, stamp, &info)))
+    {
+      if (info->info.initial || u->v0 != value0 || u->v1 != value1 || u->v2 != value2)
+        {
+          GSK_GL_UNIFORM_STATE_REPLACE (info, u, Uniform3f, 1);
+          u->v0 = value0;
+          u->v1 = value1;
+          u->v2 = value2;
+          gsk_gl_uniform_info_changed (info, stamp);
+        }
+    }
+}
+
+static inline void
+gsk_gl_uniform_state_set4f (GskGLUniformState   *state,
+                            GskGLUniformProgram *program,
+                            guint                key,
+                            guint                stamp,
+                            float                value0,
+                            float                value1,
+                            float                value2,
+                            float                value3)
+{
+  Uniform4f *u;
+  GskGLUniformMapping *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, key, stamp, &info)))
+    {
+      if (info->info.initial || u->v0 != value0 || u->v1 != value1 || u->v2 != value2 || u->v3 != value3)
+        {
+          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, stamp);
+        }
+    }
+}
+
+static inline void
+gsk_gl_uniform_state_set1ui (GskGLUniformState   *state,
+                             GskGLUniformProgram *program,
+                             guint                key,
+                             guint                stamp,
+                             guint                value0)
+{
+  Uniform1ui *u;
+  GskGLUniformMapping *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, key, stamp, &info)))
+    {
+      if (info->info.initial || u->v0 != value0)
+        {
+          GSK_GL_UNIFORM_STATE_REPLACE (info, u, Uniform1ui, 1);
+          u->v0 = value0;
+          gsk_gl_uniform_info_changed (info, stamp);
+        }
+    }
+}
+
+static inline void
+gsk_gl_uniform_state_set1i (GskGLUniformState   *state,
+                            GskGLUniformProgram *program,
+                            guint                key,
+                            guint                stamp,
+                            int                  value0)
+{
+  Uniform1i *u;
+  GskGLUniformMapping *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, key, stamp, &info)))
+    {
+      if (info->info.initial || u->v0 != value0)
+        {
+          GSK_GL_UNIFORM_STATE_REPLACE (info, u, Uniform1i, 1);
+          u->v0 = value0;
+          gsk_gl_uniform_info_changed (info, stamp);
+        }
+    }
+}
+
+static inline void
+gsk_gl_uniform_state_set2i (GskGLUniformState   *state,
+                            GskGLUniformProgram *program,
+                            guint                key,
+                            guint                stamp,
+                            int                  value0,
+                            int                  value1)
+{
+  Uniform2i *u;
+  GskGLUniformMapping *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, key, stamp, &info)))
+    {
+      if (info->info.initial || u->v0 != value0 || u->v1 != value1)
+        {
+          GSK_GL_UNIFORM_STATE_REPLACE (info, u, Uniform2i, 1);
+          u->v0 = value0;
+          u->v1 = value1;
+          gsk_gl_uniform_info_changed (info, stamp);
+        }
+    }
+}
+
+static inline void
+gsk_gl_uniform_state_set3i (GskGLUniformState   *state,
+                            GskGLUniformProgram *program,
+                            guint                key,
+                            guint                stamp,
+                            int                  value0,
+                            int                  value1,
+                            int                  value2)
+{
+  Uniform3i *u;
+  GskGLUniformMapping *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, key, stamp, &info)))
+    {
+      if (info->info.initial || u->v0 != value0 || u->v1 != value1 || u->v2 != value2)
+        {
+          GSK_GL_UNIFORM_STATE_REPLACE (info, u, Uniform3i, 1);
+          u->v0 = value0;
+          u->v1 = value1;
+          u->v2 = value2;
+          gsk_gl_uniform_info_changed (info, stamp);
+        }
+    }
+}
+
+static inline void
+gsk_gl_uniform_state_set4i (GskGLUniformState   *state,
+                            GskGLUniformProgram *program,
+                            guint                key,
+                            guint                stamp,
+                            int                  value0,
+                            int                  value1,
+                            int                  value2,
+                            int                  value3)
+{
+  Uniform4i *u;
+  GskGLUniformMapping *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, key, stamp, &info)))
+    {
+      if (info->info.initial || u->v0 != value0 || u->v1 != value1 || u->v2 != value2 || u->v3 != value3)
+        {
+          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, stamp);
+        }
+    }
+}
+
+static inline void
+gsk_gl_uniform_state_set_rounded_rect (GskGLUniformState    *state,
+                                       GskGLUniformProgram  *program,
+                                       guint                 key,
+                                       guint                 stamp,
+                                       const GskRoundedRect *rounded_rect)
+{
+  GskRoundedRect *u;
+  GskGLUniformMapping *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, key, stamp, &info)))
+    {
+      if (info->info.initial || memcmp (u, rounded_rect, sizeof *u) != 0)
+        {
+          GSK_GL_UNIFORM_STATE_REPLACE (info, u, GskRoundedRect, 1);
+          memcpy (u, rounded_rect, sizeof *rounded_rect);
+          gsk_gl_uniform_info_changed (info, stamp);
+        }
+    }
+}
+
+static inline void
+gsk_gl_uniform_state_set_matrix (GskGLUniformState       *state,
+                                 GskGLUniformProgram     *program,
+                                 guint                    key,
+                                 guint                    stamp,
+                                 const graphene_matrix_t *matrix)
+{
+  graphene_matrix_t *u;
+  GskGLUniformMapping *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, key, stamp, &info)))
+    {
+      if (info->info.initial || memcmp (u, matrix, sizeof *u) != 0)
+        {
+          GSK_GL_UNIFORM_STATE_REPLACE (info, u, graphene_matrix_t, 1);
+          memcpy (u, matrix, sizeof *matrix);
+          gsk_gl_uniform_info_changed (info, 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                key,
+                                  guint                stamp,
+                                  guint                texture_slot)
+{
+  GskGLUniformMapping *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, key, stamp, &info)))
+    {
+      if (info->info.initial || *u != texture_slot)
+        {
+          GSK_GL_UNIFORM_STATE_REPLACE (info, u, guint, 1);
+          *u = texture_slot;
+          gsk_gl_uniform_info_changed (info, 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                key,
+                                guint                stamp,
+                                const GdkRGBA       *color)
+{
+  static const GdkRGBA transparent = {0};
+  GskGLUniformMapping *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, key, stamp, &info)))
+    {
+      if (color == NULL)
+        color = &transparent;
+
+      if (info->info.initial || memcmp (color, u, sizeof *u) != 0)
+        {
+          GSK_GL_UNIFORM_STATE_REPLACE (info, u, GdkRGBA, 1);
+          memcpy (u, color, sizeof *color);
+          gsk_gl_uniform_info_changed (info, stamp);
+        }
+    }
+}
+
+static inline void
+gsk_gl_uniform_state_set1fv (GskGLUniformState   *state,
+                             GskGLUniformProgram *program,
+                             guint                key,
+                             guint                stamp,
+                             guint                count,
+                             const float         *value)
+{
+  Uniform1f *u;
+  GskGLUniformMapping *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, key, stamp, &info)))
+    {
+      if (info->info.initial || count != info->info.array_count || memcmp (u, value, sizeof *u * count) != 0)
+        {
+          GSK_GL_UNIFORM_STATE_REPLACE (info, u, Uniform1f, count);
+          memcpy (u, value, sizeof (Uniform1f) * count);
+          gsk_gl_uniform_info_changed (info, stamp);
+        }
+    }
+}
+
+static inline void
+gsk_gl_uniform_state_set2fv (GskGLUniformState   *state,
+                             GskGLUniformProgram *program,
+                             guint                key,
+                             guint                stamp,
+                             guint                count,
+                             const float         *value)
+{
+  Uniform2f *u;
+  GskGLUniformMapping *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, key, stamp, &info)))
+    {
+      if (info->info.initial || count != info->info.array_count || memcmp (u, value, sizeof *u * count) != 0)
+        {
+          GSK_GL_UNIFORM_STATE_REPLACE (info, u, Uniform2f, count);
+          memcpy (u, value, sizeof (Uniform2f) * count);
+          gsk_gl_uniform_info_changed (info, stamp);
+        }
+    }
+}
+
+static inline void
+gsk_gl_uniform_state_set3fv (GskGLUniformState   *state,
+                             GskGLUniformProgram *program,
+                             guint                key,
+                             guint                stamp,
+                             guint                count,
+                             const float         *value)
+{
+  Uniform3f *u;
+  GskGLUniformMapping *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, key, stamp, &info)))
+    {
+      if (info->info.initial || count != info->info.array_count || memcmp (u, value, sizeof *u * count) != 0)
+        {
+          GSK_GL_UNIFORM_STATE_REPLACE (info, u, Uniform3f, count);
+          memcpy (u, value, sizeof (Uniform3f) * count);
+          gsk_gl_uniform_info_changed (info, stamp);
+        }
+    }
+}
+
+static inline void
+gsk_gl_uniform_state_set4fv (GskGLUniformState   *state,
+                             GskGLUniformProgram *program,
+                             guint                key,
+                             guint                stamp,
+                             guint                count,
+                             const float         *value)
+{
+  Uniform4f *u;
+  GskGLUniformMapping *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, key, stamp, &info)))
+    {
+      if (info->info.initial || count != info->info.array_count || memcmp (u, value, sizeof *u * count) != 0)
+        {
+          GSK_GL_UNIFORM_STATE_REPLACE (info, u, Uniform4f, count);
+          memcpy (u, value, sizeof (Uniform4f) * count);
+          gsk_gl_uniform_info_changed (info, stamp);
+        }
+    }
+}
+
+static inline guint
+gsk_gl_uniform_state_fmix (guint program,
+                           guint location)
+{
+  guint h = (program << 16) | location;
+
+   h ^= h >> 16;
+   h *= 0x85ebca6b;
+   h ^= h >> 13;
+   h *= 0xc2b2ae35;
+   h ^= h >> 16;
+
+   return h;
+}
+
+/*
+ * gsk_gl_uniform_state_apply:
+ * @state: the uniform state
+ * @program: the program id
+ * @location: the location of the uniform
+ * @offset: the offset of the data within the buffer
+ * @info: the uniform info
+ *
+ * This function can be used to apply state that was previously recorded
+ * by the `GskGLUniformState`.
+ *
+ * It is specifically useful from the `GskGLCommandQueue` to execute uniform
+ * changes but only when they have changed from the current value.
+ */
+static inline void
+gsk_gl_uniform_state_apply (GskGLUniformState *state,
+                            guint              program,
+                            guint              location,
+                            GskGLUniformInfo   info)
+{
+  guint index = gsk_gl_uniform_state_fmix (program, location) % G_N_ELEMENTS (state->apply_hash);
+  gconstpointer dataptr = GSK_GL_UNIFORM_VALUE (state->values_buf, info.offset);
+
+  /* aligned, can treat as unsigned */
+  if (*(guint *)&info == *(guint *)&state->apply_hash[index])
+    return;
+
+  state->apply_hash[index] = info;
+
+  /* TODO: We could do additional comparisons here to make sure we are
+   *       changing state.
+   */
+
+  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 ();
+    }
+}
+
+G_END_DECLS
+
diff --git gsk/gl/inlinearray.h gsk/gl/inlinearray.h
new file mode 100644
index 0000000000..204513d23c
--- /dev/null
+++ gsk/gl/inlinearray.h
@@ -0,0 +1,75 @@
+#pragma once
+
+#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];                                 \
+  }
+
diff --git gsk/gl/ninesliceprivate.h gsk/gl/ninesliceprivate.h
new file mode 100644
index 0000000000..7191ec5f13
--- /dev/null
+++ gsk/gl/ninesliceprivate.h
@@ -0,0 +1,308 @@
+/* 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
+ */
+
+#pragma once
+
+#include "gskgltextureprivate.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_x,
+                 int                    amount_y)
+{
+  if (amount_x == 0 && amount_y == 0)
+    return;
+
+  /* top left */
+  slices[0].rect.x -= amount_x;
+  slices[0].rect.y -= amount_y;
+  if (amount_x > slices[0].rect.width)
+    slices[0].rect.width += amount_x * 2;
+  else
+    slices[0].rect.width += amount_x;
+
+  if (amount_y > slices[0].rect.height)
+    slices[0].rect.height += amount_y * 2;
+  else
+    slices[0].rect.height += amount_y;
+
+
+  /* Top center */
+  slices[1].rect.y -= amount_y;
+  if (amount_y > slices[1].rect.height)
+    slices[1].rect.height += amount_y * 2;
+  else
+    slices[1].rect.height += amount_y;
+
+  /* top right */
+  slices[2].rect.y -= amount_y;
+  if (amount_x > slices[2].rect.width)
+    {
+      slices[2].rect.x -= amount_x;
+      slices[2].rect.width += amount_x * 2;
+    }
+  else
+    {
+     slices[2].rect.width += amount_x;
+    }
+
+  if (amount_y > slices[2].rect.height)
+    slices[2].rect.height += amount_y * 2;
+  else
+    slices[2].rect.height += amount_y;
+
+
+
+  slices[3].rect.x -= amount_x;
+  if (amount_x > slices[3].rect.width)
+    slices[3].rect.width += amount_x * 2;
+  else
+    slices[3].rect.width += amount_x;
+
+  /* Leave center alone */
+
+  if (amount_x > slices[5].rect.width)
+    {
+      slices[5].rect.x -= amount_x;
+      slices[5].rect.width += amount_x * 2;
+    }
+  else
+    {
+      slices[5].rect.width += amount_x;
+    }
+
+
+  /* Bottom left */
+  slices[6].rect.x -= amount_x;
+  if (amount_x > slices[6].rect.width)
+    {
+      slices[6].rect.width += amount_x * 2;
+    }
+  else
+    {
+      slices[6].rect.width += amount_x;
+    }
+
+  if (amount_y > slices[6].rect.height)
+    {
+      slices[6].rect.y -= amount_y;
+      slices[6].rect.height += amount_y * 2;
+    }
+  else
+    {
+      slices[6].rect.height += amount_y;
+    }
+
+
+  /* Bottom center */
+  if (amount_y > slices[7].rect.height)
+    {
+      slices[7].rect.y -= amount_y;
+      slices[7].rect.height += amount_y * 2;
+    }
+  else
+    {
+      slices[7].rect.height += amount_y;
+    }
+
+  if (amount_x > slices[8].rect.width)
+    {
+      slices[8].rect.x -= amount_x;
+      slices[8].rect.width += amount_x * 2;
+    }
+  else
+    {
+      slices[8].rect.width += amount_x;
+    }
+
+  if (amount_y > slices[8].rect.height)
+    {
+      slices[8].rect.y -= amount_y;
+      slices[8].rect.height += amount_y * 2;
+    }
+  else
+    {
+      slices[8].rect.height += amount_y;
+    }
+
+#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
+
diff --git gsk/gl/resources/blend.glsl gsk/gl/resources/blend.glsl
new file mode 100644
index 0000000000..41301c4a8b
--- /dev/null
+++ gsk/gl/resources/blend.glsl
@@ -0,0 +1,314 @@
+// VERTEX_SHADER:
+// blend.glsl
+
+void main() {
+  gl_Position = u_projection * u_modelview * vec4(aPosition, 0.0, 1.0);
+
+  vUv = vec2(aUv.x, aUv.y);
+}
+
+// FRAGMENT_SHADER:
+// blend.glsl
+
+uniform int u_mode;
+uniform sampler2D u_source2;
+
+float
+combine (float source, float backdrop)
+{
+  return source + backdrop * (1.0 - source);
+}
+
+vec4
+composite (vec4 Cs, vec4 Cb, vec3 B)
+{
+  float ao = Cs.a + Cb.a * (1.0 - Cs.a);
+  vec3 Co = (Cs.a*(1.0 - Cb.a)*Cs.rgb + Cs.a*Cb.a*B + (1.0 - Cs.a)*Cb.a*Cb.rgb) / ao;
+  return vec4(Co, ao);
+}
+
+vec4
+normal (vec4 Cs, vec4 Cb)
+{
+  return composite (Cs, Cb, Cs.rgb);
+}
+
+vec4
+multiply (vec4 Cs, vec4 Cb)
+{
+  return composite (Cs, Cb, Cs.rgb * Cb.rgb);
+}
+
+vec4
+difference (vec4 Cs, vec4 Cb)
+{
+  return composite (Cs, Cb, abs(Cs.rgb - Cb.rgb));
+}
+
+vec4
+screen (vec4 Cs, vec4 Cb)
+{
+  return composite (Cs, Cb, Cs.rgb + Cb.rgb - Cs.rgb * Cb.rgb);
+}
+
+float
+hard_light (float source, float backdrop)
+{
+  if (source <= 0.5)
+    return 2.0 * backdrop * source;
+  else
+    return 2.0 * (backdrop + source - backdrop * source) - 1.0;
+}
+
+vec4
+hard_light (vec4 Cs, vec4 Cb)
+{
+  vec3 B = vec3 (hard_light (Cs.r, Cb.r),
+                 hard_light (Cs.g, Cb.g),
+                 hard_light (Cs.b, Cb.b));
+  return composite (Cs, Cb, B);
+}
+
+float
+soft_light (float source, float backdrop)
+{
+  float db;
+
+  if (backdrop <= 0.25)
+    db = ((16.0 * backdrop - 12.0) * backdrop + 4.0) * backdrop;
+  else
+    db = sqrt (backdrop);
+
+  if (source <= 0.5)
+    return backdrop - (1.0 - 2.0 * source) * backdrop * (1.0 - backdrop);
+  else
+    return backdrop + (2.0 * source - 1.0) * (db - backdrop);
+}
+
+vec4
+soft_light (vec4 Cs, vec4 Cb)
+{
+  vec3 B = vec3 (soft_light (Cs.r, Cb.r),
+                 soft_light (Cs.g, Cb.g),
+                 soft_light (Cs.b, Cb.b));
+  return composite (Cs, Cb, B);
+}
+
+vec4
+overlay (vec4 Cs, vec4 Cb)
+{
+  vec3 B = vec3 (hard_light (Cb.r, Cs.r),
+                 hard_light (Cb.g, Cs.g),
+                 hard_light (Cb.b, Cs.b));
+  return composite (Cs, Cb, B);
+}
+
+vec4
+darken (vec4 Cs, vec4 Cb)
+{
+  vec3 B = min (Cs.rgb, Cb.rgb);
+  return composite (Cs, Cb, B);
+}
+
+vec4
+lighten (vec4 Cs, vec4 Cb)
+{
+  vec3 B = max (Cs.rgb, Cb.rgb);
+  return composite (Cs, Cb, B);
+}
+
+float
+color_dodge (float source, float backdrop)
+{
+  return (source == 1.0) ? source : min (backdrop / (1.0 - source), 1.0);
+}
+
+vec4
+color_dodge (vec4 Cs, vec4 Cb)
+{
+  vec3 B = vec3 (color_dodge (Cs.r, Cb.r),
+                 color_dodge (Cs.g, Cb.g),
+                 color_dodge (Cs.b, Cb.b));
+  return composite (Cs, Cb, B);
+}
+
+
+float
+color_burn (float source, float backdrop)
+{
+  return (source == 0.0) ? source : max ((1.0 - ((1.0 - backdrop) / source)), 0.0);
+}
+
+vec4
+color_burn (vec4 Cs, vec4 Cb)
+{
+  vec3 B = vec3 (color_burn (Cs.r, Cb.r),
+                 color_burn (Cs.g, Cb.g),
+                 color_burn (Cs.b, Cb.b));
+  return composite (Cs, Cb, B);
+}
+
+vec4
+exclusion (vec4 Cs, vec4 Cb)
+{
+  vec3 B = Cb.rgb + Cs.rgb - 2.0 * Cb.rgb * Cs.rgb;
+  return composite (Cs, Cb, B);
+}
+
+float
+lum (vec3 c)
+{
+  return 0.3 * c.r + 0.59 * c.g + 0.11 * c.b;
+}
+
+vec3
+clip_color (vec3 c)
+{
+  float l = clamp (lum (c), 0.0, 1.0);
+  float n = min (c.r, min (c.g, c.b));
+  float x = max (c.r, max (c.g, c.b));
+  if (n < 0.0) c = l + (((c - l) * l) / (l - n));
+  if (x > 1.0) c = l + (((c - l) * (1.0 - l)) / (x - l));
+  return c;
+}
+
+vec3
+set_lum (vec3 c, float l)
+{
+  float d = l - lum (c);
+  return clip_color (vec3 (c.r + d, c.g + d, c.b + d));
+}
+
+float
+sat (vec3 c)
+{
+  return max (c.r, max (c.g, c.b)) - min (c.r, min (c.g, c.b));
+}
+
+vec3
+set_sat (vec3 c, float s)
+{
+  float cmin = min (c.r, min (c.g, c.b));
+  float cmax = max (c.r, max (c.g, c.b));
+  vec3 res;
+
+  if (cmax == cmin)
+    res = vec3 (0, 0, 0);
+  else
+    {
+      if (c.r == cmax)
+        {
+          if (c.g == cmin)
+            {
+              res.b = ((c.b - cmin) * s) / (cmax - cmin);
+              res.g = 0.0;
+            }
+          else
+            {
+              res.g = ((c.g - cmin) * s) / (cmax - cmin);
+              res.b = 0.0;
+            }
+          res.r = s;
+        }
+      else if (c.g == cmax)
+        {
+          if (c.r == cmin)
+            {
+              res.b = ((c.b - cmin) * s) / (cmax - cmin);
+              res.r = 0.0;
+            }
+          else
+            {
+              res.r = ((c.r - cmin) * s) / (cmax - cmin);
+              res.b = 0.0;
+            }
+          res.g = s;
+        }
+      else
+        {
+          if (c.r == cmin)
+            {
+              res.g = ((c.g - cmin) * s) / (cmax - cmin);
+              res.r = 0.0;
+            }
+          else
+            {
+              res.r = ((c.r - cmin) * s) / (cmax - cmin);
+              res.g = 0.0;
+            }
+          res.b = s;
+        }
+    }
+  return res;
+}
+
+vec4
+color (vec4 Cs, vec4 Cb)
+{
+  vec3 B = set_lum (Cs.rgb, lum (Cb.rgb));
+  return composite (Cs, Cb, B);
+}
+
+vec4
+hue (vec4 Cs, vec4 Cb)
+{
+  vec3 B = set_lum (set_sat (Cs.rgb, sat (Cb.rgb)), lum (Cb.rgb));
+  return composite (Cs, Cb, B);
+}
+
+vec4
+saturation (vec4 Cs, vec4 Cb)
+{
+  vec3 B = set_lum (set_sat (Cb.rgb, sat (Cs.rgb)), lum (Cb.rgb));
+  return composite (Cs, Cb, B);
+}
+
+vec4
+luminosity (vec4 Cs, vec4 Cb)
+{
+  vec3 B = set_lum (Cb.rgb, lum (Cs.rgb));
+  return composite (Cs, Cb, B);
+}
+
+void main() {
+  vec4 bottom_color = GskTexture(u_source, vUv);
+  vec4 top_color = GskTexture(u_source2, vUv);
+
+  vec4 result;
+  if (u_mode == 0)
+    result = normal(top_color, bottom_color);
+  else if (u_mode == 1)
+    result = multiply(top_color, bottom_color);
+  else if (u_mode == 2)
+    result = screen(top_color, bottom_color);
+  else if (u_mode == 3)
+    result = overlay(top_color, bottom_color);
+  else if (u_mode == 4)
+    result = darken(top_color, bottom_color);
+  else if (u_mode == 5)
+    result = lighten(top_color, bottom_color);
+  else if (u_mode == 6)
+    result = color_dodge(top_color, bottom_color);
+  else if (u_mode == 7)
+    result = color_burn(top_color, bottom_color);
+  else if (u_mode == 8)
+    result = hard_light(top_color, bottom_color);
+  else if (u_mode == 9)
+    result = soft_light(top_color, bottom_color);
+  else if (u_mode == 10)
+    result = difference(top_color, bottom_color);
+  else if (u_mode == 11)
+    result = exclusion(top_color, bottom_color);
+  else if (u_mode == 12)
+    result = color(top_color, bottom_color);
+  else if (u_mode == 13)
+    result = hue(top_color, bottom_color);
+  else if (u_mode == 14)
+    result = saturation(top_color, bottom_color);
+  else if (u_mode == 15)
+    result = luminosity(top_color, bottom_color);
+  else
+    discard;
+
+  gskSetScaledOutputColor(result, u_alpha);
+}
diff --git gsk/gl/resources/blit.glsl gsk/gl/resources/blit.glsl
new file mode 100644
index 0000000000..ced047b9a9
--- /dev/null
+++ gsk/gl/resources/blit.glsl
@@ -0,0 +1,17 @@
+// VERTEX_SHADER:
+// blit.glsl
+
+void main() {
+  gl_Position = u_projection * u_modelview * vec4(aPosition, 0.0, 1.0);
+
+  vUv = vec2(aUv.x, aUv.y);
+}
+
+// FRAGMENT_SHADER:
+// blit.glsl
+
+void main() {
+  vec4 diffuse = GskTexture(u_source, vUv);
+
+  gskSetScaledOutputColor(diffuse, u_alpha);
+}
diff --git gsk/gl/resources/blur.glsl gsk/gl/resources/blur.glsl
new file mode 100644
index 0000000000..7e7c1c2518
--- /dev/null
+++ gsk/gl/resources/blur.glsl
@@ -0,0 +1,59 @@
+// VERTEX_SHADER:
+// blur.glsl
+
+uniform float u_blur_radius;
+uniform vec2 u_blur_size;
+uniform vec2 u_blur_dir;
+
+_OUT_ vec2 pixel_step;
+_OUT_ float pixels_per_side;
+_OUT_ vec3 initial_gaussian;
+
+const float PI = 3.14159265;
+const float RADIUS_MULTIPLIER = 2.0;
+
+void main() {
+  gl_Position = u_projection * u_modelview * vec4(aPosition, 0.0, 1.0);
+
+  vUv = vec2(aUv.x, aUv.y);
+
+  pixel_step = (vec2(1.0) / u_blur_size) * u_blur_dir;
+  pixels_per_side = floor(u_blur_radius * RADIUS_MULTIPLIER / 2.0);
+
+  float sigma = u_blur_radius / 2.0; // *shrug*
+  initial_gaussian.x = 1.0 / (sqrt(2.0 * PI) * sigma);
+  initial_gaussian.y = exp(-0.5 / (sigma * sigma));
+  initial_gaussian.z = initial_gaussian.y * initial_gaussian.y;
+}
+
+// FRAGMENT_SHADER:
+// blur.glsl
+
+_IN_ vec2 pixel_step;
+_IN_ float pixels_per_side;
+_IN_ vec3 initial_gaussian;
+
+// blur_radius 0 is NOT supported and MUST be caught before.
+
+// Partially from http://callumhay.blogspot.com/2010/09/gaussian-blur-shader-glsl.html
+void main() {
+  vec3 incrementalGaussian = initial_gaussian;
+
+  float coefficientSum = 0.0;
+  vec4 sum = GskTexture(u_source, vUv) * incrementalGaussian.x;
+  coefficientSum += incrementalGaussian.x;
+  incrementalGaussian.xy *= incrementalGaussian.yz;
+
+  vec2 p = pixel_step;
+  for (int i = 1; i <= int(pixels_per_side); i++) {
+    sum += GskTexture(u_source, vUv - p) * incrementalGaussian.x;
+    sum += GskTexture(u_source, vUv + p) * incrementalGaussian.x;
+
+    coefficientSum += 2.0 * incrementalGaussian.x;
+    incrementalGaussian.xy *= incrementalGaussian.yz;
+
+    p += pixel_step;
+  }
+
+  gskSetOutputColor(sum / coefficientSum);
+}
diff --git gsk/gl/resources/border.glsl gsk/gl/resources/border.glsl
new file mode 100644
index 0000000000..b2436affd1
--- /dev/null
+++ gsk/gl/resources/border.glsl
@@ -0,0 +1,53 @@
+// VERTEX_SHADER:
+// border.glsl
+
+uniform vec4 u_widths;
+#if defined(GSK_LEGACY) || defined(GSK_GLES)
+uniform vec4 u_outline_rect[3];
+#else
+uniform vec4[3] u_outline_rect;
+#endif
+
+_OUT_ vec4 final_color;
+_GSK_ROUNDED_RECT_OUT_(transformed_outside_outline);
+_GSK_ROUNDED_RECT_OUT_(transformed_inside_outline);
+
+void main() {
+  gl_Position = u_projection * u_modelview * vec4(aPosition, 0.0, 1.0);
+
+  final_color = gsk_scaled_premultiply(aColor, u_alpha);
+
+  GskRoundedRect outside = gsk_create_rect(u_outline_rect);
+  GskRoundedRect inside = gsk_rounded_rect_shrink (outside, u_widths);
+
+  gsk_rounded_rect_transform(outside, u_modelview);
+  gsk_rounded_rect_transform(inside, u_modelview);
+
+  gsk_rounded_rect_normalize(outside);
+  gsk_rounded_rect_normalize(inside);
+
+  gsk_rounded_rect_encode(outside, transformed_outside_outline);
+  gsk_rounded_rect_encode(inside, transformed_inside_outline);
+}
+
+// FRAGMENT_SHADER:
+// border.glsl
+
+#if defined(GSK_LEGACY) || defined(GSK_GLES)
+uniform vec4 u_outline_rect[3];
+#else
+uniform vec4[3] u_outline_rect;
+#endif
+
+_IN_ vec4 final_color;
+_GSK_ROUNDED_RECT_IN_(transformed_outside_outline);
+_GSK_ROUNDED_RECT_IN_(transformed_inside_outline);
+
+void main() {
+  vec2 frag = gsk_get_frag_coord();
+  float outer_coverage = gsk_rounded_rect_coverage(gsk_decode_rect(transformed_outside_outline), frag);
+  float inner_coverage = gsk_rounded_rect_coverage(gsk_decode_rect(transformed_inside_outline), frag);
+  float alpha = clamp(outer_coverage - inner_coverage, 0.0, 1.0);
+
+  gskSetScaledOutputColor(final_color, alpha);
+}
diff --git gsk/gl/resources/color.glsl gsk/gl/resources/color.glsl
new file mode 100644
index 0000000000..ba98b1b170
--- /dev/null
+++ gsk/gl/resources/color.glsl
@@ -0,0 +1,20 @@
+// VERTEX_SHADER:
+// color.glsl
+
+_OUT_ vec4 final_color;
+
+void main() {
+  gl_Position = u_projection * u_modelview * vec4(aPosition, 0.0, 1.0);
+
+  final_color = gsk_scaled_premultiply(aColor, u_alpha);
+}
+
+// FRAGMENT_SHADER:
+// color.glsl
+
+_IN_ vec4 final_color;
+
+void main() {
+  gskSetOutputColor(final_color);
+}
+
diff --git gsk/gl/resources/color_matrix.glsl gsk/gl/resources/color_matrix.glsl
new file mode 100644
index 0000000000..51835f056f
--- /dev/null
+++ gsk/gl/resources/color_matrix.glsl
@@ -0,0 +1,27 @@
+// VERTEX_SHADER:
+// color_matrix.glsl
+
+void main() {
+  gl_Position = u_projection * u_modelview * vec4(aPosition, 0.0, 1.0);
+
+  vUv = vec2(aUv.x, aUv.y);
+}
+
+// FRAGMENT_SHADER:
+// color_matrix.glsl
+
+uniform mat4 u_color_matrix;
+uniform vec4 u_color_offset;
+
+void main() {
+  vec4 color = GskTexture(u_source, vUv);
+
+  // Un-premultilpy
+  if (color.a != 0.0)
+    color.rgb /= color.a;
+
+  color = u_color_matrix * color + u_color_offset;
+  color = clamp(color, 0.0, 1.0);
+
+  gskSetOutputColor(gsk_scaled_premultiply(color, u_alpha));
+}
diff --git gsk/gl/resources/coloring.glsl gsk/gl/resources/coloring.glsl
new file mode 100644
index 0000000000..0abe06ff05
--- /dev/null
+++ gsk/gl/resources/coloring.glsl
@@ -0,0 +1,33 @@
+// VERTEX_SHADER:
+// coloring.glsl
+
+_OUT_ vec4 final_color;
+_OUT_ float use_color;
+
+void main() {
+  gl_Position = u_projection * u_modelview * vec4(aPosition, 0.0, 1.0);
+
+  vUv = vec2(aUv.x, aUv.y);
+
+  // We use this shader for both plain glyphs (used as mask)
+  // and color glyphs (used as source). The renderer sets
+  // aColor to vec4(-1) for color glyphs.
+  if (distance(aColor,vec4(-1)) < 0.1)
+    use_color = 0.0;
+  else
+    use_color = 1.0;
+
+  final_color = gsk_scaled_premultiply(aColor, u_alpha);
+}
+
+// FRAGMENT_SHADER:
+// coloring.glsl
+
+_IN_ vec4 final_color;
+_IN_ float use_color;
+
+void main() {
+  vec4 diffuse = GskTexture(u_source, vUv);
+
+  gskSetOutputColor(mix(diffuse * u_alpha, final_color * diffuse.a, use_color));
+}
diff --git gsk/gl/resources/conic_gradient.glsl gsk/gl/resources/conic_gradient.glsl
new file mode 100644
index 0000000000..9f56978439
--- /dev/null
+++ gsk/gl/resources/conic_gradient.glsl
@@ -0,0 +1,85 @@
+// VERTEX_SHADER
+// conic_gradient.glsl
+
+uniform vec4 u_geometry;
+
+_OUT_ vec2 coord;
+
+void main() {
+  gl_Position = u_projection * (u_modelview * vec4(aPosition, 0.0, 1.0));
+
+  vec2 mv0 = u_modelview[0].xy;
+  vec2 mv1 = u_modelview[1].xy;
+  vec2 offset = aPosition - u_geometry.xy;
+
+  coord = vec2(dot(mv0, offset), dot(mv1, offset));
+}
+
+// FRAGMENT_SHADER:
+// conic_gradient.glsl
+
+#define MAX_COLOR_STOPS 6
+
+#ifdef GSK_LEGACY
+uniform int u_num_color_stops;
+#else
+uniform highp int u_num_color_stops; // Why? Because it works like this.
+#endif
+
+uniform vec4 u_geometry;
+uniform float u_color_stops[MAX_COLOR_STOPS * 5];
+
+_IN_ vec2 coord;
+
+float get_offset(int index) {
+  // u_color_stops[5 * index] makes Intel Windows driver crash.
+  // See https://gitlab.gnome.org/GNOME/gtk/-/issues/3783
+  int base = 5 * index;
+  return u_color_stops[base];
+}
+
+vec4 get_color(int index) {
+  int base = 5 * index + 1;
+
+  return vec4(u_color_stops[base],
+              u_color_stops[base + 1],
+              u_color_stops[base + 2],
+              u_color_stops[base + 3]);
+}
+
+void main() {
+  // direction of point in range [-PI, PI]
+  vec2 pos = floor(coord);
+  float angle = atan(pos.y, pos.x);
+
+  // fract() does the modulo here, so now we have progress
+  // into the current conic
+  float offset = fract(angle * u_geometry.z + u_geometry.w);
+  float curr_offset;
+  float next_offset;
+
+  next_offset = get_offset(0);
+  if (offset < next_offset) {
+    gskSetOutputColor(gsk_scaled_premultiply(get_color(0), u_alpha));
+    return;
+  }
+
+  if (offset >= get_offset(u_num_color_stops - 1)) {
+    gskSetOutputColor(gsk_scaled_premultiply(get_color(u_num_color_stops - 1), u_alpha));
+    return;
+  }
+
+  for (int i = 0; i < MAX_COLOR_STOPS; i++) {
+    curr_offset = next_offset;
+    next_offset = get_offset(i + 1);
+
+    if (offset < next_offset) {
+      float f = (offset - curr_offset) / (next_offset - curr_offset);
+      vec4 curr_color = gsk_scaled_premultiply (get_color(i), u_alpha);
+      vec4 next_color = gsk_scaled_premultiply (get_color(i + 1), u_alpha);
+      vec4 color = mix(curr_color, next_color, f);
+      gskSetOutputColor(color);
+      return;
+    }
+  }
+}
diff --git gsk/gl/resources/cross_fade.glsl gsk/gl/resources/cross_fade.glsl
new file mode 100644
index 0000000000..e61af597e9
--- /dev/null
+++ gsk/gl/resources/cross_fade.glsl
@@ -0,0 +1,24 @@
+// VERTEX_SHADER:
+// cross_fade.glsl
+
+void main() {
+  gl_Position = u_projection * u_modelview * vec4(aPosition, 0.0, 1.0);
+
+  vUv = vec2(aUv.x, aUv.y);
+}
+
+// FRAGMENT_SHADER:
+// cross_fade.glsl
+
+uniform float u_progress;
+uniform sampler2D u_source2;
+
+void main() {
+  vec4 source1 = GskTexture(u_source, vUv);  // start child
+  vec4 source2 = GskTexture(u_source2, vUv); // end child
+
+  float p_start = (1.0 - u_progress) * u_alpha;
+  float p_end = u_progress * u_alpha;
+  vec4 color = (p_start * source1) + (p_end * source2);
+  gskSetOutputColor(color);
+}
diff --git gsk/gl/resources/custom.glsl gsk/gl/resources/custom.glsl
new file mode 100644
index 0000000000..6f91df6cf4
--- /dev/null
+++ gsk/gl/resources/custom.glsl
@@ -0,0 +1,25 @@
+// VERTEX_SHADER:
+// custom.glsl
+
+void main() {
+  gl_Position = u_projection * u_modelview * vec4(aPosition, 0.0, 1.0);
+  vUv = vec2(aUv.x, aUv.y);
+}
+
+// FRAGMENT_SHADER:
+// custom.glsl
+
+// The shader supplies:
+void mainImage(out vec4 fragColor, in vec2 fragCoord, in vec2 resolution, in vec2 uv);
+
+uniform vec2 u_size;
+uniform sampler2D u_source2;
+uniform sampler2D u_source3;
+uniform sampler2D u_source4;
+
+void main() {
+  vec4 fragColor;
+  vec2 fragCoord = vec2(vUv.x * u_size.x, (1.0-vUv.y) * u_size.y);
+  mainImage(fragColor, fragCoord, u_size, vUv);
+  gskSetOutputColor(fragColor);
+}
diff --git gsk/gl/resources/external.glsl gsk/gl/resources/external.glsl
new file mode 100644
index 0000000000..d2aaadd003
--- /dev/null
+++ gsk/gl/resources/external.glsl
@@ -0,0 +1,34 @@
+// VERTEX_SHADER:
+// external.glsl
+
+void main() {
+  gl_Position = u_projection * u_modelview * vec4(aPosition, 0.0, 1.0);
+
+  vUv = vec2(aUv.x, aUv.y);
+}
+
+// FRAGMENT_SHADER:
+// external.glsl
+
+#if defined(GSK_GLES) || defined(GSK_GLES3)
+uniform samplerExternalOES u_external_source;
+#else
+/* Just to make this compile, we won't use it without GLES */
+uniform sampler2D u_external_source;
+#endif
+
+uniform int u_premultiply;
+
+void main() {
+/* Open-code this here, since GskTexture() expects a sampler2D */
+#if defined(GSK_GLES) || defined(GSK_LEGACY)
+  vec4 color = texture2D(u_external_source, vUv);
+#else
+  vec4 color = texture(u_external_source, vUv);
+#endif
+
+  if (u_premultiply == 1)
+    color.rgb *= color.a;
+
+  gskSetOutputColor(color);
+}
diff --git gsk/gl/resources/filled_border.glsl gsk/gl/resources/filled_border.glsl
new file mode 100644
index 0000000000..5c2c221655
--- /dev/null
+++ gsk/gl/resources/filled_border.glsl
@@ -0,0 +1,58 @@
+// VERTEX_SHADER:
+// filled_border.glsl
+
+uniform vec4 u_widths;
+#if defined(GSK_LEGACY) || defined(GSK_GLES)
+uniform vec4 u_outline_rect[3];
+#else
+uniform vec4[3] u_outline_rect;
+#endif
+
+_OUT_ vec4 outer_color;
+_OUT_ vec4 inner_color;
+_GSK_ROUNDED_RECT_OUT_(transformed_outside_outline);
+_GSK_ROUNDED_RECT_OUT_(transformed_inside_outline);
+
+void main() {
+  gl_Position = u_projection * u_modelview * vec4(aPosition, 0.0, 1.0);
+
+  outer_color = gsk_scaled_premultiply(aColor, u_alpha);
+  inner_color = gsk_scaled_premultiply(aColor2, u_alpha);
+
+  GskRoundedRect outside = gsk_create_rect(u_outline_rect);
+  GskRoundedRect inside = gsk_rounded_rect_shrink (outside, u_widths);
+
+  gsk_rounded_rect_transform(outside, u_modelview);
+  gsk_rounded_rect_transform(inside, u_modelview);
+
+  gsk_rounded_rect_normalize(outside);
+  gsk_rounded_rect_normalize(inside);
+
+  gsk_rounded_rect_encode(outside, transformed_outside_outline);
+  gsk_rounded_rect_encode(inside, transformed_inside_outline);
+}
+
+// FRAGMENT_SHADER:
+// filled_border.glsl
+
+#if defined(GSK_LEGACY) || defined(GSK_GLES)
+uniform vec4 u_outline_rect[3];
+#else
+uniform vec4[3] u_outline_rect;
+#endif
+
+_IN_ vec4 outer_color;
+_IN_ vec4 inner_color;
+_GSK_ROUNDED_RECT_IN_(transformed_outside_outline);
+_GSK_ROUNDED_RECT_IN_(transformed_inside_outline);
+
+void main() {
+  vec2 frag = gsk_get_frag_coord();
+  float outer_coverage = gsk_rounded_rect_coverage(gsk_decode_rect(transformed_outside_outline), frag);
+  float inner_coverage = gsk_rounded_rect_coverage(gsk_decode_rect(transformed_inside_outline), frag);
+
+  float alpha = clamp(outer_coverage - inner_coverage, 0.0, 1.0);
+  float alpha2 = clamp(inner_coverage, 0.0, 1.0);
+
+  gskSetOutputColor((outer_color * alpha) + (inner_color * alpha2));
+}
diff --git gsk/gl/resources/inset_shadow.glsl gsk/gl/resources/inset_shadow.glsl
new file mode 100644
index 0000000000..dd636c2e7f
--- /dev/null
+++ gsk/gl/resources/inset_shadow.glsl
@@ -0,0 +1,51 @@
+// VERTEX_SHADER:
+// inset_shadow.glsl
+
+uniform float u_spread;
+uniform vec2 u_offset;
+#if defined(GSK_LEGACY) || defined(GSK_GLES)
+uniform vec4 u_outline_rect[3];
+#else
+uniform vec4[3] u_outline_rect;
+#endif
+
+_OUT_ vec4 final_color;
+_GSK_ROUNDED_RECT_OUT_(transformed_outside_outline);
+_GSK_ROUNDED_RECT_OUT_(transformed_inside_outline);
+
+void main() {
+  gl_Position = u_projection * u_modelview * vec4(aPosition, 0.0, 1.0);
+
+  final_color = gsk_scaled_premultiply(aColor, u_alpha);
+
+  GskRoundedRect outside = gsk_create_rect(u_outline_rect);
+  GskRoundedRect inside = gsk_rounded_rect_shrink(outside, vec4(u_spread));
+
+  gsk_rounded_rect_offset(inside, u_offset);
+
+  gsk_rounded_rect_transform(outside, u_modelview);
+  gsk_rounded_rect_transform(inside, u_modelview);
+
+  gsk_rounded_rect_normalize(outside);
+  gsk_rounded_rect_normalize(inside);
+
+  gsk_rounded_rect_encode(outside, transformed_outside_outline);
+  gsk_rounded_rect_encode(inside, transformed_inside_outline);
+}
+
+// FRAGMENT_SHADER:
+// inset_shadow.glsl
+
+_IN_ vec4 final_color;
+_GSK_ROUNDED_RECT_IN_(transformed_outside_outline);
+_GSK_ROUNDED_RECT_IN_(transformed_inside_outline);
+
+void main() {
+  vec2 frag = gsk_get_frag_coord();
+
+  float alpha = clamp(gsk_rounded_rect_coverage(gsk_decode_rect(transformed_outside_outline), frag) -
+                      gsk_rounded_rect_coverage(gsk_decode_rect(transformed_inside_outline), frag),
+                      0.0, 1.0);
+
+  gskSetScaledOutputColor(final_color, alpha);
+}
diff --git gsk/gl/resources/linear_gradient.glsl gsk/gl/resources/linear_gradient.glsl
new file mode 100644
index 0000000000..d266cdbc79
--- /dev/null
+++ gsk/gl/resources/linear_gradient.glsl
@@ -0,0 +1,107 @@
+// VERTEX_SHADER
+// linear_gradient.glsl
+uniform vec4 u_points;
+
+_OUT_ vec4 info;
+
+void main() {
+  gl_Position = u_projection * (u_modelview * vec4(aPosition, 0.0, 1.0));
+
+  vec2 mv0 = u_modelview[0].xy;
+  vec2 mv1 = u_modelview[1].xy;
+  vec2 offset = aPosition - u_points.xy;
+  vec2 coord = vec2(dot(mv0, offset),
+                    dot(mv1, offset));
+
+  // Original equation:
+  // VS | maxDist = length(end - start);
+  // VS | gradient = end - start;
+  // VS | gradientLength = length(gradient);
+  // FS | pos = frag_coord - start
+  // FS | proj = (dot(gradient, pos) / (gradientLength * gradientLength)) * gradient
+  // FS | offset = length(proj) / maxDist
+
+  // Simplified formula derivation:
+  // 1. Notice that maxDist = gradientLength:
+  // offset = length(proj) / gradientLength
+  // 2. Let gnorm = gradient / gradientLength, then:
+  // proj = (dot(gnorm * gradientLength, pos) / (gradientLength * gradientLength)) * (gnorm * gradientLength) =
+  //      = dot(gnorm, pos) * gnorm
+  // 3. Since gnorm is unit length then:
+  // length(proj) = length(dot(gnorm, pos) * gnorm) = dot(gnorm, pos)
+  // 4. We can avoid the FS division by passing a scaled pos from the VS:
+  // offset = dot(gnorm, pos) / gradientLength = dot(gnorm, pos / gradientLength)
+  // 5. 1.0 / length(gradient) is inversesqrt(dot(gradient, gradient)) in GLSL
+  vec2 gradient = vec2(dot(mv0, u_points.zw),
+                       dot(mv1, u_points.zw));
+  float rcp_gradient_length = inversesqrt(dot(gradient, gradient));
+
+  info = rcp_gradient_length * vec4(coord, gradient);
+}
+
+// FRAGMENT_SHADER:
+// linear_gradient.glsl
+
+#define MAX_COLOR_STOPS 6
+
+#ifdef GSK_LEGACY
+uniform int u_num_color_stops;
+#else
+uniform highp int u_num_color_stops; // Why? Because it works like this.
+#endif
+
+uniform float u_color_stops[MAX_COLOR_STOPS * 5];
+uniform bool u_repeat;
+
+_IN_ vec4 info;
+
+float get_offset(int index) {
+  // u_color_stops[5 * index] makes Intel Windows driver crash.
+  // See https://gitlab.gnome.org/GNOME/gtk/-/issues/3783
+  int base = 5 * index;
+  return u_color_stops[base];
+}
+
+vec4 get_color(int index) {
+  int base = 5 * index + 1;
+
+  return vec4(u_color_stops[base],
+              u_color_stops[base + 1],
+              u_color_stops[base + 2],
+              u_color_stops[base + 3]);
+}
+
+void main() {
+  float offset = dot(info.xy, info.zw);
+  float curr_offset;
+  float next_offset;
+
+  if (u_repeat) {
+    offset = fract(offset);
+  }
+
+  next_offset = get_offset(0);
+  if (offset < next_offset) {
+    gskSetOutputColor(gsk_scaled_premultiply(get_color(0), u_alpha));
+    return;
+  }
+
+  if (offset >= get_offset(u_num_color_stops - 1)) {
+    gskSetOutputColor(gsk_scaled_premultiply(get_color(u_num_color_stops - 1), u_alpha));
+    return;
+  }
+
+  for (int i = 0; i < MAX_COLOR_STOPS; i++) {
+    curr_offset = next_offset;
+    next_offset = get_offset(i + 1);
+
+    if (offset < next_offset) {
+      float f = (offset - curr_offset) / (next_offset - curr_offset);
+      vec4 curr_color = gsk_scaled_premultiply (get_color(i), u_alpha);
+      vec4 next_color = gsk_scaled_premultiply (get_color(i + 1), u_alpha);
+      vec4 color = mix(curr_color, next_color, f);
+      gskSetOutputColor(color);
+      return;
+    }
+  }
+}
diff --git gsk/gl/resources/mask.glsl gsk/gl/resources/mask.glsl
new file mode 100644
index 0000000000..0c2814be9f
--- /dev/null
+++ gsk/gl/resources/mask.glsl
@@ -0,0 +1,39 @@
+// VERTEX_SHADER:
+// mask.glsl
+
+void main() {
+  gl_Position = u_projection * u_modelview * vec4(aPosition, 0.0, 1.0);
+
+  vUv = vec2(aUv.x, aUv.y);
+}
+
+// FRAGMENT_SHADER:
+// mask.glsl
+
+uniform int u_mode;
+uniform sampler2D u_mask;
+
+float
+luminance (vec3 color)
+{
+  return dot (vec3 (0.2126, 0.7152, 0.0722), color);
+}
+
+void main() {
+  vec4 source = GskTexture(u_source, vUv);
+  vec4 mask = GskTexture(u_mask, vUv);
+  float mask_value;
+
+  if (u_mode == 0)
+    mask_value = mask.a;
+  else if (u_mode == 1)
+    mask_value = 1.0 - mask.a;
+  else if (u_mode == 2)
+    mask_value = luminance (mask.rgb);
+  else if (u_mode == 3)
+    mask_value = mask.a - luminance (mask.rgb);
+  else
+    mask_value = 0.0;
+
+  gskSetOutputColor(vec4 (source * mask_value));
+}
diff --git gsk/gl/resources/outset_shadow.glsl gsk/gl/resources/outset_shadow.glsl
new file mode 100644
index 0000000000..03b0a80b2d
--- /dev/null
+++ gsk/gl/resources/outset_shadow.glsl
@@ -0,0 +1,39 @@
+// VERTEX_SHADER:
+// outset_shadow.glsl
+
+#if defined(GSK_LEGACY) || defined(GSK_GLES)
+uniform vec4 u_outline_rect[3];
+#else
+uniform vec4[3] u_outline_rect;
+#endif
+
+_OUT_ vec4 final_color;
+_GSK_ROUNDED_RECT_OUT_(transformed_outline);
+
+void main() {
+  gl_Position = u_projection * u_modelview * vec4(aPosition, 0.0, 1.0);
+
+  vUv = vec2(aUv.x, aUv.y);
+
+  final_color = gsk_scaled_premultiply(aColor, u_alpha);
+
+  GskRoundedRect outline = gsk_create_rect(u_outline_rect);
+  gsk_rounded_rect_transform(outline, u_modelview);
+  gsk_rounded_rect_normalize(outline);
+  gsk_rounded_rect_encode(outline, transformed_outline);
+}
+
+// FRAGMENT_SHADER:
+// outset_shadow.glsl
+
+_IN_ vec4 final_color;
+_GSK_ROUNDED_RECT_IN_(transformed_outline);
+
+void main() {
+  vec2 frag = gsk_get_frag_coord();
+  float alpha = GskTexture(u_source, vUv).a;
+
+  alpha *= (1.0 -  clamp(gsk_rounded_rect_coverage(gsk_decode_rect(transformed_outline), frag), 0.0, 1.0));
+
+  gskSetScaledOutputColor(final_color, alpha);
+}
diff --git gsk/gl/resources/preamble.fs.glsl gsk/gl/resources/preamble.fs.glsl
new file mode 100644
index 0000000000..6f8841e955
--- /dev/null
+++ gsk/gl/resources/preamble.fs.glsl
@@ -0,0 +1,173 @@
+uniform sampler2D u_source;
+uniform mat4 u_projection;
+uniform mat4 u_modelview;
+uniform float u_alpha;
+uniform vec4 u_viewport;
+#if defined(GSK_LEGACY) || defined(GSK_GLES)
+uniform vec4 u_clip_rect[3];
+#else
+uniform vec4[3] u_clip_rect;
+#endif
+
+#if defined(GSK_LEGACY)
+_OUT_ vec4 outputColor;
+#elif !defined(GSK_GLES)
+_OUT_ vec4 outputColor;
+#endif
+
+_IN_ vec2 vUv;
+
+#if defined(GSK_LEGACY) || defined(GSK_GLES)
+GskRoundedRect gsk_decode_rect(vec4 r[3])
+#else
+GskRoundedRect gsk_decode_rect(_GSK_ROUNDED_RECT_UNIFORM_ r)
+#endif
+{
+  GskRoundedRect rect;
+#if defined(GSK_GLES) || defined(GSK_LEGACY)
+  rect = GskRoundedRect(r[0], r[1], r[2]);
+#else
+  rect = r;
+#endif
+  gsk_rounded_rect_normalize (rect);
+
+  return rect;
+}
+
+float
+gsk_ellipsis_dist (vec2 p, vec2 radius)
+{
+  if (radius == vec2(0, 0))
+    return 0.0;
+
+  vec2 p0 = p / radius;
+  vec2 p1 = 2.0 * p0 / radius;
+
+  return (dot(p0, p0) - 1.0) / length (p1);
+}
+
+float
+gsk_ellipsis_coverage (vec2 point, vec2 center, vec2 radius)
+{
+  float d = gsk_ellipsis_dist (point - center, radius);
+  return clamp (0.5 - d, 0.0, 1.0);
+}
+
+float
+gsk_rounded_rect_coverage (GskRoundedRect r, vec2 p)
+{
+  if (p.x < r.bounds.x || p.y < r.bounds.y ||
+      p.x >= r.bounds.z || p.y >= r.bounds.w)
+    return 0.0;
+
+  vec2 ref_tl = r.corner_points1.xy;
+  vec2 ref_tr = r.corner_points1.zw;
+  vec2 ref_br = r.corner_points2.xy;
+  vec2 ref_bl = r.corner_points2.zw;
+
+  if (p.x >= ref_tl.x && p.x >= ref_bl.x &&
+      p.x <= ref_tr.x && p.x <= ref_br.x)
+    return 1.0;
+
+  if (p.y >= ref_tl.y && p.y >= ref_tr.y &&
+      p.y <= ref_bl.y && p.y <= ref_br.y)
+    return 1.0;
+
+  vec2 rad_tl = r.corner_points1.xy - r.bounds.xy;
+  vec2 rad_tr = r.corner_points1.zw - r.bounds.zy;
+  vec2 rad_br = r.corner_points2.xy - r.bounds.zw;
+  vec2 rad_bl = r.corner_points2.zw - r.bounds.xw;
+
+  float d_tl = gsk_ellipsis_coverage(p, ref_tl, rad_tl);
+  float d_tr = gsk_ellipsis_coverage(p, ref_tr, rad_tr);
+  float d_br = gsk_ellipsis_coverage(p, ref_br, rad_br);
+  float d_bl = gsk_ellipsis_coverage(p, ref_bl, rad_bl);
+
+  vec4 corner_coverages = 1.0 - vec4(d_tl, d_tr, d_br, d_bl);
+
+  bvec4 is_out = bvec4(p.x < ref_tl.x && p.y < ref_tl.y,
+                       p.x > ref_tr.x && p.y < ref_tr.y,
+                       p.x > ref_br.x && p.y > ref_br.y,
+                       p.x < ref_bl.x && p.y > ref_bl.y);
+
+  return 1.0 - dot(vec4(is_out), corner_coverages);
+}
+
+float
+gsk_rect_coverage (vec4 r, vec2 p)
+{
+  if (p.x < r.x || p.y < r.y ||
+      p.x >= r.z || p.y >= r.w)
+    return 0.0;
+
+  return 1.0;
+}
+
+vec4 GskTexture(sampler2D sampler, vec2 texCoords) {
+#if defined(GSK_GLES) || defined(GSK_LEGACY)
+  return texture2D(sampler, texCoords);
+#else
+  return texture(sampler, texCoords);
+#endif
+}
+
+#ifdef GSK_GL3
+layout(origin_upper_left) in vec4 gl_FragCoord;
+#endif
+
+vec2 gsk_get_frag_coord() {
+  vec2 fc = gl_FragCoord.xy;
+
+#ifdef GSK_GL3
+  fc += u_viewport.xy;
+#else
+  fc.x += u_viewport.x;
+  fc.y = (u_viewport.y + u_viewport.w) - fc.y;
+#endif
+
+  return fc;
+}
+
+void gskSetOutputColor(vec4 color) {
+  vec4 result;
+
+#if defined(NO_CLIP)
+  result = color;
+#elif defined(RECT_CLIP)
+  float coverage = gsk_rect_coverage(gsk_get_bounds(u_clip_rect),
+                                     gsk_get_frag_coord());
+  result = color * coverage;
+#else
+  float coverage = gsk_rounded_rect_coverage(gsk_create_rect(u_clip_rect),
+                                             gsk_get_frag_coord());
+  result = color * coverage;
+#endif
+
+#if defined(GSK_GLES) || defined(GSK_LEGACY)
+  gl_FragColor = result;
+#else
+  outputColor = result;
+#endif
+}
+
+void gskSetScaledOutputColor(vec4 color, float alpha) {
+  vec4 result;
+
+#if defined(NO_CLIP)
+  result = color * alpha;
+#elif defined(RECT_CLIP)
+  float coverage = gsk_rect_coverage(gsk_get_bounds(u_clip_rect),
+                                     gsk_get_frag_coord());
+  result = color * (alpha * coverage);
+#else
+  float coverage = gsk_rounded_rect_coverage(gsk_create_rect(u_clip_rect),
+                                             gsk_get_frag_coord());
+  result = color * (alpha * coverage);
+#endif
+
+#if defined(GSK_GLES) || defined(GSK_LEGACY)
+  gl_FragColor = result;
+#else
+  outputColor = result;
+#endif
+}
diff --git gsk/gl/resources/preamble.glsl gsk/gl/resources/preamble.glsl
new file mode 100644
index 0000000000..3c4fa5af2d
--- /dev/null
+++ gsk/gl/resources/preamble.glsl
@@ -0,0 +1,133 @@
+#if defined(GSK_GLES3)
+#extension GL_OES_EGL_image_external_essl3 : require
+#elif defined (GSK_GLES)
+#extension GL_OES_EGL_image_external : require
+#endif
+
+#ifndef GSK_LEGACY
+precision highp float;
+#endif
+
+#if defined(GSK_GLES) || defined(GSK_LEGACY)
+#define _OUT_ varying
+#define _IN_ varying
+#define _GSK_ROUNDED_RECT_UNIFORM_ vec4[3]
+/* The array size has to follow the name: the "vec4[3] name" spelling is
+ * GLSL 1.20 and later only, and we want to compile as GLSL 1.10 too.
+ */
+#define _GSK_ROUNDED_RECT_OUT_(name) varying vec4 name[3]
+#define _GSK_ROUNDED_RECT_IN_(name) varying vec4 name[3]
+#else
+#define _OUT_ out
+#define _IN_ in
+#define _GSK_ROUNDED_RECT_UNIFORM_ GskRoundedRect
+#define _GSK_ROUNDED_RECT_OUT_(name) out GskRoundedRect name
+#define _GSK_ROUNDED_RECT_IN_(name) in GskRoundedRect name
+#endif
+
+
+struct GskRoundedRect
+{
+  vec4 bounds; // Top left and bottom right
+  // Look, arrays can't be in structs if you want to return the struct
+  // from a function in gles or whatever. Just kill me.
+  vec4 corner_points1; // xy = top left, zw = top right
+  vec4 corner_points2; // xy = bottom right, zw = bottom left
+};
+
+void gsk_rounded_rect_normalize(inout GskRoundedRect r)
+{
+  if (r.bounds.x > r.bounds.z)
+    {
+      float t = r.bounds.x;
+      r.bounds.x = r.bounds.z;
+      r.bounds.z = t;
+
+      vec2 c = r.corner_points1.xy;
+      r.corner_points1.xy = r.corner_points1.zw;
+      r.corner_points1.zw = c;
+
+      c = r.corner_points2.xy;
+      r.corner_points2.xy = r.corner_points2.zw;
+      r.corner_points2.zw = c;
+    }
+
+  if (r.bounds.y > r.bounds.w)
+    {
+      float t = r.bounds.y;
+      r.bounds.y = r.bounds.w;
+      r.bounds.w = t;
+
+      vec2 c = r.corner_points1.xy;
+      r.corner_points1.xy = r.corner_points2.xy;
+      r.corner_points2.xy = c;
+
+      c = r.corner_points1.zw;
+      r.corner_points1.zw = r.corner_points2.zw;
+      r.corner_points2.zw = c;
+    }
+}
+
+void gsk_bounds_normalize (inout vec4 bounds)
+{
+  if (bounds.x > bounds.z)
+    {
+      float t = bounds.x;
+      bounds.x = bounds.z;
+      bounds.z = t;
+    }
+  if (bounds.y > bounds.w)
+    {
+      float t = bounds.y;
+      bounds.y = bounds.w;
+      bounds.w = t;
+    }
+}
+
+// Transform from a C GskRoundedRect to what we need.
+GskRoundedRect
+#if defined(GSK_LEGACY) || defined(GSK_GLES)
+gsk_create_rect(vec4 data[3])
+#else
+gsk_create_rect(vec4[3] data)
+#endif
+{
+  vec4 bounds = vec4(data[0].xy, data[0].xy + data[0].zw);
+
+  vec4 corner_points1 = vec4(bounds.xy + data[1].xy,
+                             bounds.zy + vec2(data[1].zw * vec2(-1, 1)));
+  vec4 corner_points2 = vec4(bounds.zw + (data[2].xy * vec2(-1, -1)),
+                             bounds.xw + vec2(data[2].zw * vec2(1, -1)));
+
+  GskRoundedRect rect = GskRoundedRect(bounds, corner_points1, corner_points2);
+
+  gsk_rounded_rect_normalize (rect);
+
+  return rect;
+}
+
+vec4
+#if defined(GSK_LEGACY) || defined(GSK_GLES)
+gsk_get_bounds(vec4 data[3])
+#else
+gsk_get_bounds(vec4[3] data)
+#endif
+{
+  vec4 bounds = vec4(data[0].xy, data[0].xy + data[0].zw);
+
+  gsk_bounds_normalize (bounds);
+
+  return bounds;
+}
+
+vec4 gsk_premultiply(vec4 c) {
+  return vec4(c.rgb * c.a, c.a);
+}
+
+vec4 gsk_scaled_premultiply(vec4 c, float s) {
+  // Fast version of gsk_premultiply(c) * s
+  // 4 muls instead of 7
+  float a = s * c.a;
+
+  return vec4(c.rgb * a, a);
+}
diff --git gsk/gl/resources/preamble.vs.glsl gsk/gl/resources/preamble.vs.glsl
new file mode 100644
index 0000000000..ff3d5bb5a7
--- /dev/null
+++ gsk/gl/resources/preamble.vs.glsl
@@ -0,0 +1,75 @@
+uniform mat4 u_projection;
+uniform mat4 u_modelview;
+uniform float u_alpha;
+
+#if defined(GSK_GLES) || defined(GSK_LEGACY)
+attribute vec2 aPosition;
+attribute vec2 aUv;
+attribute vec4 aColor;
+attribute vec4 aColor2;
+_OUT_ vec2 vUv;
+#else
+_IN_ vec2 aPosition;
+_IN_ vec2 aUv;
+_IN_ vec4 aColor;
+_IN_ vec4 aColor2;
+_OUT_ vec2 vUv;
+#endif
+
+// amount is: top, right, bottom, left
+GskRoundedRect
+gsk_rounded_rect_shrink (GskRoundedRect r, vec4 amount)
+{
+  vec4 new_bounds = r.bounds + vec4(1.0,1.0,-1.0,-1.0) * amount.wxyz;
+  vec4 new_corner_points1 = r.corner_points1;
+  vec4 new_corner_points2 = r.corner_points2;
+
+  if (r.corner_points1.xy == r.bounds.xy) new_corner_points1.xy = new_bounds.xy;
+  if (r.corner_points1.zw == r.bounds.zy) new_corner_points1.zw = new_bounds.zy;
+  if (r.corner_points2.xy == r.bounds.zw) new_corner_points2.xy = new_bounds.zw;
+  if (r.corner_points2.zw == r.bounds.xw) new_corner_points2.zw = new_bounds.xw;
+  new_corner_points1 = clamp (new_corner_points1, new_bounds.xyxy, new_bounds.zwzw);
+  new_corner_points2 = clamp (new_corner_points2, new_bounds.xyxy, new_bounds.zwzw);
+
+  return GskRoundedRect (new_bounds, new_corner_points1, new_corner_points2);
+}
+
+void
+gsk_rounded_rect_offset(inout GskRoundedRect r, vec2 offset)
+{
+  r.bounds.xy += offset;
+  r.bounds.zw += offset;
+  r.corner_points1.xy += offset;
+  r.corner_points1.zw += offset;
+  r.corner_points2.xy += offset;
+  r.corner_points2.zw += offset;
+}
+
+void gsk_rounded_rect_transform(inout GskRoundedRect r, mat4 mat)
+{
+  r.bounds.xy = (mat * vec4(r.bounds.xy, 0.0, 1.0)).xy;
+  r.bounds.zw = (mat * vec4(r.bounds.zw, 0.0, 1.0)).xy;
+
+  r.corner_points1.xy = (mat * vec4(r.corner_points1.xy, 0.0, 1.0)).xy;
+  r.corner_points1.zw = (mat * vec4(r.corner_points1.zw, 0.0, 1.0)).xy;
+
+  r.corner_points2.xy = (mat * vec4(r.corner_points2.xy, 0.0, 1.0)).xy;
+  r.corner_points2.zw = (mat * vec4(r.corner_points2.zw, 0.0, 1.0)).xy;
+}
+
+#if defined(GSK_LEGACY)
+// Can't have out or inout array parameters...
+#define gsk_rounded_rect_encode(r, uni) uni[0] = r.bounds; uni[1] = r.corner_points1; uni[2] = r.corner_points2;
+#else
+void gsk_rounded_rect_encode(GskRoundedRect r, out _GSK_ROUNDED_RECT_UNIFORM_ out_r)
+{
+#if defined(GSK_GLES)
+  out_r[0] = r.bounds;
+  out_r[1] = r.corner_points1;
+  out_r[2] = r.corner_points2;
+#else
+  out_r = r;
+#endif
+}
+
+#endif
diff --git gsk/gl/resources/premultiply.glsl gsk/gl/resources/premultiply.glsl
new file mode 100644
index 0000000000..892c2c4453
--- /dev/null
+++ gsk/gl/resources/premultiply.glsl
@@ -0,0 +1,19 @@
+// VERTEX_SHADER:
+// premultiply.glsl
+
+void main() {
+  gl_Position = u_projection * u_modelview * vec4(aPosition, 0.0, 1.0);
+
+  vUv = vec2(aUv.x, aUv.y);
+}
+
+// FRAGMENT_SHADER:
+// premultiply.glsl
+
+void main() {
+  vec4 color = GskTexture(u_source, vUv);
+
+  color.rgb *= color.a;
+
+  gskSetOutputColor(color);
+}
diff --git gsk/gl/resources/radial_gradient.glsl gsk/gl/resources/radial_gradient.glsl
new file mode 100644
index 0000000000..df95516237
--- /dev/null
+++ gsk/gl/resources/radial_gradient.glsl
@@ -0,0 +1,87 @@
+// VERTEX_SHADER
+// radial_gradient.glsl
+
+uniform vec4 u_geometry;
+
+_OUT_ vec2 coord;
+
+void main() {
+  gl_Position = u_projection * (u_modelview * vec4(aPosition, 0.0, 1.0));
+
+  vec2 mv0 = u_modelview[0].xy;
+  vec2 mv1 = u_modelview[1].xy;
+  vec2 offset = aPosition - u_geometry.xy;
+  vec2 dir = vec2(dot(mv0, offset),
+                  dot(mv1, offset));
+
+  coord = dir * u_geometry.zw;
+}
+
+// FRAGMENT_SHADER:
+// radial_gradient.glsl
+
+#define MAX_COLOR_STOPS 6
+
+#ifdef GSK_LEGACY
+uniform int u_num_color_stops;
+#else
+uniform highp int u_num_color_stops;
+#endif
+
+uniform bool u_repeat;
+uniform vec2 u_range;
+uniform float u_color_stops[MAX_COLOR_STOPS * 5];
+
+_IN_ vec2 coord;
+
+float get_offset(int index) {
+  // u_color_stops[5 * index] makes Intel Windows driver crash.
+  // See https://gitlab.gnome.org/GNOME/gtk/-/issues/3783
+  int base = 5 * index;
+  return u_color_stops[base];
+}
+
+vec4 get_color(int index) {
+  int base = 5 * index + 1;
+
+  return vec4(u_color_stops[base],
+              u_color_stops[base + 1],
+              u_color_stops[base + 2],
+              u_color_stops[base + 3]);
+}
+
+void main() {
+  // Reverse scale
+  float offset = length(coord) * u_range.x + u_range.y;
+  float curr_offset;
+  float next_offset;
+
+  if (u_repeat) {
+    offset = fract(offset);
+  }
+
+  next_offset = get_offset(0);
+  if (offset < next_offset) {
+    gskSetOutputColor(gsk_scaled_premultiply(get_color(0), u_alpha));
+    return;
+  }
+
+  if (offset >= get_offset(u_num_color_stops - 1)) {
+    gskSetOutputColor(gsk_scaled_premultiply(get_color(u_num_color_stops - 1), u_alpha));
+    return;
+  }
+
+  for (int i = 0; i < MAX_COLOR_STOPS; i++) {
+    curr_offset = next_offset;
+    next_offset = get_offset(i + 1);
+
+    if (offset < next_offset) {
+      float f = (offset - curr_offset) / (next_offset - curr_offset);
+      vec4 curr_color = gsk_scaled_premultiply (get_color(i), u_alpha);
+      vec4 next_color = gsk_scaled_premultiply (get_color(i + 1), u_alpha);
+      vec4 color = mix(curr_color, next_color, f);
+      gskSetOutputColor(color);
+      return;
+    }
+  }
+}
diff --git gsk/gl/resources/repeat.glsl gsk/gl/resources/repeat.glsl
new file mode 100644
index 0000000000..1b811df2da
--- /dev/null
+++ gsk/gl/resources/repeat.glsl
@@ -0,0 +1,44 @@
+// VERTEX_SHADER:
+// repeat.glsl
+
+void main() {
+  gl_Position = u_projection * u_modelview * vec4(aPosition, 0.0, 1.0);
+
+  vUv = vec2(aUv.x, aUv.y);
+}
+
+// FRAGMENT_SHADER:
+// repeat.glsl
+
+uniform vec4 u_child_bounds;
+uniform vec4 u_texture_rect;
+
+float wrap(float f, float wrap_for) {
+  return mod(f, wrap_for);
+}
+
+/* We get the texture coordinates via vUv,
+ * but that might be on a texture atlas, so we need to do the
+ * wrapping ourselves.
+ */
+void main() {
+
+  /* We map the texture coordinate to [1;0], then wrap it and scale the result again */
+
+  float tw = u_texture_rect.z - u_texture_rect.x;
+  float th = u_texture_rect.w - u_texture_rect.y;
+
+  float mapped_x = (vUv.x - u_texture_rect.x) / tw;
+  float mapped_y = (vUv.y - u_texture_rect.y) / th;
+
+  float wrapped_x = wrap(u_child_bounds.x + mapped_x * u_child_bounds.z, 1.0);
+  float wrapped_y = wrap(u_child_bounds.y + mapped_y * u_child_bounds.w, 1.0);
+
+  vec2 tp;
+  tp.x = u_texture_rect.x + (wrapped_x * tw);
+  tp.y = u_texture_rect.y + (wrapped_y * th);
+
+  vec4 diffuse = GskTexture(u_source, tp);
+
+  gskSetScaledOutputColor(diffuse, u_alpha);
+}
diff --git gsk/gl/resources/unblurred_outset_shadow.glsl gsk/gl/resources/unblurred_outset_shadow.glsl
new file mode 100644
index 0000000000..6f17370a1d
--- /dev/null
+++ gsk/gl/resources/unblurred_outset_shadow.glsl
@@ -0,0 +1,47 @@
+// VERTEX_SHADER:
+// unblurred_outset_shadow.glsl
+
+uniform float u_spread;
+uniform vec2 u_offset;
+#if defined(GSK_LEGACY) || defined(GSK_GLES)
+uniform vec4 u_outline_rect[3];
+#else
+uniform vec4[3] u_outline_rect;
+#endif
+
+_OUT_ vec4 final_color;
+_GSK_ROUNDED_RECT_OUT_(transformed_outside_outline);
+_GSK_ROUNDED_RECT_OUT_(transformed_inside_outline);
+
+void main() {
+  gl_Position = u_projection * u_modelview * vec4(aPosition, 0.0, 1.0);
+
+  final_color = gsk_premultiply(aColor) * u_alpha;
+
+  GskRoundedRect inside = gsk_create_rect(u_outline_rect);
+  GskRoundedRect outside = gsk_rounded_rect_shrink(inside, vec4(- u_spread));
+
+  gsk_rounded_rect_offset(outside, u_offset);
+
+  gsk_rounded_rect_transform(outside, u_modelview);
+  gsk_rounded_rect_transform(inside, u_modelview);
+
+  gsk_rounded_rect_encode(outside, transformed_outside_outline);
+  gsk_rounded_rect_encode(inside, transformed_inside_outline);
+}
+
+// FRAGMENT_SHADER:
+// unblurred_outset_shadow.glsl
+_IN_ vec4 final_color;
+_GSK_ROUNDED_RECT_IN_(transformed_outside_outline);
+_GSK_ROUNDED_RECT_IN_(transformed_inside_outline);
+
+void main() {
+  vec2 frag = gsk_get_frag_coord();
+
+  float alpha = clamp(gsk_rounded_rect_coverage(gsk_decode_rect(transformed_outside_outline), frag) -
+                      gsk_rounded_rect_coverage(gsk_decode_rect(transformed_inside_outline), frag),
+                      0.0, 1.0);
+
+  gskSetScaledOutputColor(final_color, alpha);
+}
diff --git gsk/gl/stb_rect_pack.c gsk/gl/stb_rect_pack.c
new file mode 100644
index 0000000000..2ef31512b4
--- /dev/null
+++ gsk/gl/stb_rect_pack.c
@@ -0,0 +1,427 @@
+
+#include "stb_rect_pack.h"
+#define STB_RECT_PACK_IMPLEMENTATION
+//////////////////////////////////////////////////////////////////////////////
+//
+//     IMPLEMENTATION SECTION
+//
+
+#ifdef STB_RECT_PACK_IMPLEMENTATION
+#ifndef STBRP_SORT
+#include <stdlib.h>
+#define STBRP_SORT qsort
+#endif
+
+#ifndef STBRP_ASSERT
+#include <assert.h>
+#define STBRP_ASSERT assert
+#endif
+
+#ifdef _MSC_VER
+#define STBRP__NOTUSED(v)  (void)(v)
+#define STBRP__CDECL       __cdecl
+#else
+#define STBRP__NOTUSED(v)  (void)sizeof(v)
+#define STBRP__CDECL
+#endif
+
+enum
+{
+   STBRP__INIT_skyline = 1
+};
+
+STBRP_DEF void stbrp_setup_heuristic(stbrp_context *context, int heuristic)
+{
+   switch (context->init_mode) {
+      case STBRP__INIT_skyline:
+         STBRP_ASSERT(heuristic == STBRP_HEURISTIC_Skyline_BL_sortHeight || heuristic == STBRP_HEURISTIC_Skyline_BF_sortHeight);
+         context->heuristic = heuristic;
+         break;
+      default:
+         STBRP_ASSERT(0);
+   }
+}
+
+STBRP_DEF void stbrp_setup_allow_out_of_mem(stbrp_context *context, int allow_out_of_mem)
+{
+   if (allow_out_of_mem)
+      // if it's ok to run out of memory, then don't bother aligning them;
+      // this gives better packing, but may fail due to OOM (even though
+      // the rectangles easily fit). @TODO a smarter approach would be to only
+      // quantize once we've hit OOM, then we could get rid of this parameter.
+      context->align = 1;
+   else {
+      // if it's not ok to run out of memory, then quantize the widths
+      // so that num_nodes is always enough nodes.
+      //
+      // I.e. num_nodes * align >= width
+      //                  align >= width / num_nodes
+      //                  align = ceil(width/num_nodes)
+
+      context->align = (context->width + context->num_nodes-1) / context->num_nodes;
+   }
+}
+
+STBRP_DEF void stbrp_init_target(stbrp_context *context, int width, int height, stbrp_node *nodes, int num_nodes)
+{
+   int i;
+
+   for (i=0; i < num_nodes-1; ++i)
+      nodes[i].next = &nodes[i+1];
+   nodes[i].next = NULL;
+   context->init_mode = STBRP__INIT_skyline;
+   context->heuristic = STBRP_HEURISTIC_Skyline_default;
+   context->free_head = &nodes[0];
+   context->active_head = &context->extra[0];
+   context->width = width;
+   context->height = height;
+   context->num_nodes = num_nodes;
+   stbrp_setup_allow_out_of_mem(context, 0);
+
+   // node 0 is the full width, node 1 is the sentinel (lets us not store width explicitly)
+   context->extra[0].x = 0;
+   context->extra[0].y = 0;
+   context->extra[0].next = &context->extra[1];
+   context->extra[1].x = (stbrp_coord) width;
+   context->extra[1].y = (1<<30);
+   context->extra[1].next = NULL;
+}
+
+// find minimum y position if it starts at x1
+static int stbrp__skyline_find_min_y(stbrp_context *c, stbrp_node *first, int x0, int width, int *pwaste)
+{
+   stbrp_node *node = first;
+   int x1 = x0 + width;
+   int min_y, visited_width, waste_area;
+
+   STBRP__NOTUSED(c);
+
+   STBRP_ASSERT(first->x <= x0);
+
+   #if 0
+   // skip in case we're past the node
+   while (node->next->x <= x0)
+      ++node;
+   #else
+   STBRP_ASSERT(node->next->x > x0); // we ended up handling this in the caller for efficiency
+   #endif
+
+   STBRP_ASSERT(node->x <= x0);
+
+   min_y = 0;
+   waste_area = 0;
+   visited_width = 0;
+   while (node->x < x1) {
+      if (node->y > min_y) {
+         // raise min_y higher.
+         // we've accounted for all waste up to min_y,
+         // but we'll now add more waste for everything we've visited
+         waste_area += visited_width * (node->y - min_y);
+         min_y = node->y;
+         // the first time through, visited_width might be reduced
+         if (node->x < x0)
+            visited_width += node->next->x - x0;
+         else
+            visited_width += node->next->x - node->x;
+      } else {
+         // add waste area
+         int under_width = node->next->x - node->x;
+         if (under_width + visited_width > width)
+            under_width = width - visited_width;
+         waste_area += under_width * (min_y - node->y);
+         visited_width += under_width;
+      }
+      node = node->next;
+   }
+
+   *pwaste = waste_area;
+   return min_y;
+}
+
+typedef struct
+{
+   int x,y;
+   stbrp_node **prev_link;
+} stbrp__findresult;
+
+static stbrp__findresult stbrp__skyline_find_best_pos(stbrp_context *c, int width, int height)
+{
+   int best_waste = (1<<30), best_x, best_y = (1 << 30);
+   stbrp__findresult fr;
+   stbrp_node **prev, *node, *tail, **best = NULL;
+
+   // align to multiple of c->align
+   width = (width + c->align - 1);
+   width -= width % c->align;
+   STBRP_ASSERT(width % c->align == 0);
+
+   // if it can't possibly fit, bail immediately
+   if (width > c->width || height > c->height) {
+      fr.prev_link = NULL;
+      fr.x = fr.y = 0;
+      return fr;
+   }
+
+   node = c->active_head;
+   prev = &c->active_head;
+   while (node->x + width <= c->width) {
+      int y,waste;
+      y = stbrp__skyline_find_min_y(c, node, node->x, width, &waste);
+      if (c->heuristic == STBRP_HEURISTIC_Skyline_BL_sortHeight) { // actually just want to test BL
+         // bottom left
+         if (y < best_y) {
+            best_y = y;
+            best = prev;
+         }
+      } else {
+         // best-fit
+         if (y + height <= c->height) {
+            // can only use it if it first vertically
+            if (y < best_y || (y == best_y && waste < best_waste)) {
+               best_y = y;
+               best_waste = waste;
+               best = prev;
+            }
+         }
+      }
+      prev = &node->next;
+      node = node->next;
+   }
+
+   best_x = (best == NULL) ? 0 : (*best)->x;
+
+   // if doing best-fit (BF), we also have to try aligning right edge to each node position
+   //
+   // e.g, if fitting
+   //
+   //     ____________________
+   //    |____________________|
+   //
+   //            into
+   //
+   //   |                         |
+   //   |             ____________|
+   //   |____________|
+   //
+   // then right-aligned reduces waste, but bottom-left BL is always chooses left-aligned
+   //
+   // This makes BF take about 2x the time
+
+   if (c->heuristic == STBRP_HEURISTIC_Skyline_BF_sortHeight) {
+      tail = c->active_head;
+      node = c->active_head;
+      prev = &c->active_head;
+      // find first node that's admissible
+      while (tail->x < width)
+         tail = tail->next;
+      while (tail) {
+         int xpos = tail->x - width;
+         int y,waste;
+         STBRP_ASSERT(xpos >= 0);
+         // find the left position that matches this
+         while (node->next->x <= xpos) {
+            prev = &node->next;
+            node = node->next;
+         }
+         STBRP_ASSERT(node->next->x > xpos && node->x <= xpos);
+         y = stbrp__skyline_find_min_y(c, node, xpos, width, &waste);
+         if (y + height <= c->height) {
+            if (y <= best_y) {
+               if (y < best_y || waste < best_waste || (waste==best_waste && xpos < best_x)) {
+                  best_x = xpos;
+                  STBRP_ASSERT(y <= best_y);
+                  best_y = y;
+                  best_waste = waste;
+                  best = prev;
+               }
+            }
+         }
+         tail = tail->next;
+      }
+   }
+
+   fr.prev_link = best;
+   fr.x = best_x;
+   fr.y = best_y;
+   return fr;
+}
+
+static stbrp__findresult stbrp__skyline_pack_rectangle(stbrp_context *context, int width, int height)
+{
+   // find best position according to heuristic
+   stbrp__findresult res = stbrp__skyline_find_best_pos(context, width, height);
+   stbrp_node *node, *cur;
+
+   // bail if:
+   //    1. it failed
+   //    2. the best node doesn't fit (we don't always check this)
+   //    3. we're out of memory
+   if (res.prev_link == NULL || res.y + height > context->height || context->free_head == NULL) {
+      res.prev_link = NULL;
+      return res;
+   }
+
+   // on success, create new node
+   node = context->free_head;
+   node->x = (stbrp_coord) res.x;
+   node->y = (stbrp_coord) (res.y + height);
+
+   context->free_head = node->next;
+
+   // insert the new node into the right starting point, and
+   // let 'cur' point to the remaining nodes needing to be
+   // stiched back in
+
+   cur = *res.prev_link;
+   if (cur->x < res.x) {
+      // preserve the existing one, so start testing with the next one
+      stbrp_node *next = cur->next;
+      cur->next = node;
+      cur = next;
+   } else {
+      *res.prev_link = node;
+   }
+
+   // from here, traverse cur and free the nodes, until we get to one
+   // that shouldn't be freed
+   while (cur->next && cur->next->x <= res.x + width) {
+      stbrp_node *next = cur->next;
+      // move the current node to the free list
+      cur->next = context->free_head;
+      context->free_head = cur;
+      cur = next;
+   }
+
+   // stitch the list back in
+   node->next = cur;
+
+   if (cur->x < res.x + width)
+      cur->x = (stbrp_coord) (res.x + width);
+
+#ifdef _DEBUG
+   cur = context->active_head;
+   while (cur->x < context->width) {
+      STBRP_ASSERT(cur->x < cur->next->x);
+      cur = cur->next;
+   }
+   STBRP_ASSERT(cur->next == NULL);
+
+   {
+      int count=0;
+      cur = context->active_head;
+      while (cur) {
+         cur = cur->next;
+         ++count;
+      }
+      cur = context->free_head;
+      while (cur) {
+         cur = cur->next;
+         ++count;
+      }
+      STBRP_ASSERT(count == context->num_nodes+2);
+   }
+#endif
+
+   return res;
+}
+
+static int STBRP__CDECL rect_height_compare(const void *a, const void *b)
+{
+   const stbrp_rect *p = (const stbrp_rect *) a;
+   const stbrp_rect *q = (const stbrp_rect *) b;
+   if (p->h > q->h)
+      return -1;
+   if (p->h < q->h)
+      return  1;
+   return (p->w > q->w) ? -1 : (p->w < q->w);
+}
+
+static int STBRP__CDECL rect_original_order(const void *a, const void *b)
+{
+   const stbrp_rect *p = (const stbrp_rect *) a;
+   const stbrp_rect *q = (const stbrp_rect *) b;
+   return (p->was_packed < q->was_packed) ? -1 : (p->was_packed > q->was_packed);
+}
+
+STBRP_DEF int stbrp_pack_rects(stbrp_context *context, stbrp_rect *rects, int num_rects)
+{
+   int i, all_rects_packed = 1;
+
+   // we use the 'was_packed' field internally to allow sorting/unsorting
+   for (i=0; i < num_rects; ++i) {
+      rects[i].was_packed = i;
+   }
+
+   // sort according to heuristic
+   STBRP_SORT(rects, num_rects, sizeof(rects[0]), rect_height_compare);
+
+   for (i=0; i < num_rects; ++i) {
+      if (rects[i].w == 0 || rects[i].h == 0) {
+         rects[i].x = rects[i].y = 0;  // empty rect needs no space
+      } else {
+         stbrp__findresult fr = stbrp__skyline_pack_rectangle(context, rects[i].w, rects[i].h);
+         if (fr.prev_link) {
+            rects[i].x = (stbrp_coord) fr.x;
+            rects[i].y = (stbrp_coord) fr.y;
+         } else {
+            rects[i].x = rects[i].y = STBRP__MAXVAL;
+         }
+      }
+   }
+
+   // unsort
+   STBRP_SORT(rects, num_rects, sizeof(rects[0]), rect_original_order);
+
+   // set was_packed flags and all_rects_packed status
+   for (i=0; i < num_rects; ++i) {
+      rects[i].was_packed = !(rects[i].x == STBRP__MAXVAL && rects[i].y == STBRP__MAXVAL);
+      if (!rects[i].was_packed)
+         all_rects_packed = 0;
+   }
+
+   // return the all_rects_packed status
+   return all_rects_packed;
+}
+#endif
+
+/*
+------------------------------------------------------------------------------
+This software is available under 2 licenses -- choose whichever you prefer.
+------------------------------------------------------------------------------
+ALTERNATIVE A - MIT License
+Copyright (c) 2017 Sean Barrett
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+------------------------------------------------------------------------------
+ALTERNATIVE B - Public Domain (www.unlicense.org)
+This is free and unencumbered software released into the public domain.
+Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
+software, either in source code form or as a compiled binary, for any purpose,
+commercial or non-commercial, and by any means.
+In jurisdictions that recognize copyright laws, the author or authors of this
+software dedicate any and all copyright interest in the software to the public
+domain. We make this dedication for the benefit of the public at large and to
+the detriment of our heirs and successors. We intend this dedication to be an
+overt act of relinquishment in perpetuity of all present and future rights to
+this software under copyright law.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+------------------------------------------------------------------------------
+*/
diff --git gsk/gl/stb_rect_pack.h gsk/gl/stb_rect_pack.h
new file mode 100644
index 0000000000..2a3833ec80
--- /dev/null
+++ gsk/gl/stb_rect_pack.h
@@ -0,0 +1,197 @@
+// stb_rect_pack.h - v1.01 - public domain - rectangle packing
+// Sean Barrett 2014
+//
+// Useful for e.g. packing rectangular textures into an atlas.
+// Does not do rotation.
+//
+// Before #including,
+//
+//    #define STB_RECT_PACK_IMPLEMENTATION
+//
+// in the file that you want to have the implementation.
+//
+// Not necessarily the awesomest packing method, but better than
+// the totally naive one in stb_truetype (which is primarily what
+// this is meant to replace).
+//
+// Has only had a few tests run, may have issues.
+//
+// More docs to come.
+//
+// No memory allocations; uses qsort() and assert() from stdlib.
+// Can override those by defining STBRP_SORT and STBRP_ASSERT.
+//
+// This library currently uses the Skyline Bottom-Left algorithm.
+//
+// Please note: better rectangle packers are welcome! Please
+// implement them to the same API, but with a different init
+// function.
+//
+// Credits
+//
+//  Library
+//    Sean Barrett
+//  Minor features
+//    Martins Mozeiko
+//    github:IntellectualKitty
+//
+//  Bugfixes / warning fixes
+//    Jeremy Jaussaud
+//    Fabian Giesen
+//
+// Version history:
+//
+//     1.01  (2021-07-11)  always use large rect mode, expose STBRP__MAXVAL in public section
+//     1.00  (2019-02-25)  avoid small space waste; gracefully fail too-wide rectangles
+//     0.99  (2019-02-07)  warning fixes
+//     0.11  (2017-03-03)  return packing success/fail result
+//     0.10  (2016-10-25)  remove cast-away-const to avoid warnings
+//     0.09  (2016-08-27)  fix compiler warnings
+//     0.08  (2015-09-13)  really fix bug with empty rects (w=0 or h=0)
+//     0.07  (2015-09-13)  fix bug with empty rects (w=0 or h=0)
+//     0.06  (2015-04-15)  added STBRP_SORT to allow replacing qsort
+//     0.05:  added STBRP_ASSERT to allow replacing assert
+//     0.04:  fixed minor bug in STBRP_LARGE_RECTS support
+//     0.01:  initial release
+//
+// LICENSE
+//
+//   See end of file for license information.
+
+//////////////////////////////////////////////////////////////////////////////
+//
+//       INCLUDE SECTION
+//
+
+#pragma once
+
+#define STB_RECT_PACK_VERSION  1
+
+#ifdef STBRP_STATIC
+#define STBRP_DEF static
+#else
+#define STBRP_DEF extern
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct stbrp_context stbrp_context;
+typedef struct stbrp_node    stbrp_node;
+typedef struct stbrp_rect    stbrp_rect;
+
+typedef int            stbrp_coord;
+
+#define STBRP__MAXVAL  0x7fffffff
+// Mostly for internal use, but this is the maximum supported coordinate value.
+
+STBRP_DEF int stbrp_pack_rects (stbrp_context *context, stbrp_rect *rects, int num_rects);
+// Assign packed locations to rectangles. The rectangles are of type
+// 'stbrp_rect' defined below, stored in the array 'rects', and there
+// are 'num_rects' many of them.
+//
+// Rectangles which are successfully packed have the 'was_packed' flag
+// set to a non-zero value and 'x' and 'y' store the minimum location
+// on each axis (i.e. bottom-left in cartesian coordinates, top-left
+// if you imagine y increasing downwards). Rectangles which do not fit
+// have the 'was_packed' flag set to 0.
+//
+// You should not try to access the 'rects' array from another thread
+// while this function is running, as the function temporarily reorders
+// the array while it executes.
+//
+// To pack into another rectangle, you need to call stbrp_init_target
+// again. To continue packing into the same rectangle, you can call
+// this function again. Calling this multiple times with multiple rect
+// arrays will probably produce worse packing results than calling it
+// a single time with the full rectangle array, but the option is
+// available.
+//
+// The function returns 1 if all of the rectangles were successfully
+// packed and 0 otherwise.
+
+struct stbrp_rect
+{
+   // reserved for your use:
+   int            id;
+
+   // input:
+   stbrp_coord    w, h;
+
+   // output:
+   stbrp_coord    x, y;
+   int            was_packed;  // non-zero if valid packing
+
+}; // 16 bytes, nominally
+
+
+STBRP_DEF void stbrp_init_target (stbrp_context *context, int width, int height, stbrp_node *nodes, int num_nodes);
+// Initialize a rectangle packer to:
+//    pack a rectangle that is 'width' by 'height' in dimensions
+//    using temporary storage provided by the array 'nodes', which is 'num_nodes' long
+//
+// You must call this function every time you start packing into a new target.
+//
+// There is no "shutdown" function. The 'nodes' memory must stay valid for
+// the following stbrp_pack_rects() call (or calls), but can be freed after
+// the call (or calls) finish.
+//
+// Note: to guarantee best results, either:
+//       1. make sure 'num_nodes' >= 'width'
+//   or  2. call stbrp_allow_out_of_mem() defined below with 'allow_out_of_mem = 1'
+//
+// If you don't do either of the above things, widths will be quantized to multiples
+// of small integers to guarantee the algorithm doesn't run out of temporary storage.
+//
+// If you do #2, then the non-quantized algorithm will be used, but the algorithm
+// may run out of temporary storage and be unable to pack some rectangles.
+
+STBRP_DEF void stbrp_setup_allow_out_of_mem (stbrp_context *context, int allow_out_of_mem);
+// Optionally call this function after init but before doing any packing to
+// change the handling of the out-of-temp-memory scenario, described above.
+// If you call init again, this will be reset to the default (false).
+
+
+STBRP_DEF void stbrp_setup_heuristic (stbrp_context *context, int heuristic);
+// Optionally select which packing heuristic the library should use. Different
+// heuristics will produce better/worse results for different data sets.
+// If you call init again, this will be reset to the default.
+
+enum
+{
+   STBRP_HEURISTIC_Skyline_default=0,
+   STBRP_HEURISTIC_Skyline_BL_sortHeight = STBRP_HEURISTIC_Skyline_default,
+   STBRP_HEURISTIC_Skyline_BF_sortHeight
+};
+
+
+//////////////////////////////////////////////////////////////////////////////
+//
+// the details of the following structures don't matter to you, but they must
+// be visible so you can handle the memory allocations for them
+
+struct stbrp_node
+{
+   stbrp_coord  x,y;
+   stbrp_node  *next;
+};
+
+struct stbrp_context
+{
+   int width;
+   int height;
+   int align;
+   int init_mode;
+   int heuristic;
+   int num_nodes;
+   stbrp_node *active_head;
+   stbrp_node *free_head;
+   stbrp_node extra[2]; // we allocate two extra nodes so optimal user-node-count is 'width' not 'width+2'
+};
+
+#ifdef __cplusplus
+}
+#endif
+
+
diff --git gsk/gpu/gskglframe.c gsk/gpu/gskglframe.c
index 7ecad85d9c..50a1ea36ea 100644
--- gsk/gpu/gskglframe.c
+++ gsk/gpu/gskglframe.c
@@ -322,7 +322,8 @@ gsk_gl_frame_submit (GskGpuFrame       *frame,
       op = gsk_gpu_op_gl_command (op, frame, &state);
     }
 
-  self->sync = glFenceSync (GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
+  if (gdk_gl_context_has_feature (GDK_GL_CONTEXT(gsk_gpu_frame_get_context (frame)), GDK_GL_FEATURE_SYNC))
+    self->sync = glFenceSync (GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
 }
 
 static void
diff --git gsk/gpu/gskgpudownloadop.c gsk/gpu/gskgpudownloadop.c
index 836f63402d..f60773ec97 100644
--- gsk/gpu/gskgpudownloadop.c
+++ gsk/gpu/gskgpudownloadop.c
@@ -358,7 +358,10 @@ gsk_gpu_download_op_gl_command (GskGpuOp          *op,
    * the texture might survive the frame and its surface */
   data->context = g_object_ref (gdk_display_get_gl_context (gsk_gpu_device_get_display (gsk_gpu_frame_get_device (frame))));
   data->texture_id = texture_id;
-  data->sync = glFenceSync (GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
+  if (gdk_gl_context_has_feature (data->context, GDK_GL_FEATURE_SYNC))
+    data->sync = glFenceSync (GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
+  else
+    data->sync = NULL;
 
   builder = gdk_gl_texture_builder_new ();
   gdk_gl_texture_builder_set_context (builder, data->context);
diff --git gsk/gsk.h gsk/gsk.h
index da1b3311f3..426639465a 100644
--- gsk/gsk.h
+++ gsk/gsk.h
@@ -72,6 +72,7 @@
 #include <gsk/gskcairorenderer.h>
 #include <gsk/gpu/gskvulkanrenderer.h>
 #include <gsk/gpu/gskglrenderer.h>
+#include <gsk/gl/gskglrenderer.h>
 
 #include <gsk/gsktypes.h>
 #include <gsk/gskenumtypes.h>
diff --git gsk/gskcairorenderer.c gsk/gskcairorenderer.c
index e0f27abf68..aab13a132d 100644
--- gsk/gskcairorenderer.c
+++ gsk/gskcairorenderer.c
@@ -85,6 +85,27 @@ gsk_cairo_renderer_unrealize (GskRenderer *renderer)
     }
 }
 
+static void
+gsk_cairo_renderer_do_render (GskRenderer   *renderer,
+                              cairo_t       *cr,
+                              GdkColorState *ccs,
+                              GskRenderNode *root)
+{
+  GskCairoRenderer *self = GSK_CAIRO_RENDERER (renderer);
+  GskProfiler *profiler;
+  gint64 cpu_time;
+
+  profiler = gsk_renderer_get_profiler (renderer);
+  gsk_profiler_timer_begin (profiler, self->profile_timers.cpu_time);
+
+  gsk_render_node_draw_with_color_state (root, cr, ccs);
+
+  cpu_time = gsk_profiler_timer_end (profiler, self->profile_timers.cpu_time);
+  gsk_profiler_timer_set (profiler, self->profile_timers.cpu_time, cpu_time);
+
+  gsk_profiler_push_samples (profiler);
+}
+
 static GdkTexture *
 gsk_cairo_renderer_render_texture (GskRenderer           *renderer,
                                    GskRenderNode         *root,
@@ -135,7 +156,7 @@ gsk_cairo_renderer_render_texture (GskRenderer           *renderer,
 
   cairo_translate (cr, - viewport->origin.x, - viewport->origin.y);
 
-  gsk_render_node_draw_with_color_state (root, cr, GDK_COLOR_STATE_SRGB);
+  gsk_cairo_renderer_do_render (renderer, cr, GDK_COLOR_STATE_SRGB, root);
 
   cairo_destroy (cr);
 
@@ -177,7 +198,10 @@ G_GNUC_END_IGNORE_DEPRECATIONS
       cairo_restore (cr);
     }
 
-  gsk_render_node_draw_with_color_state (root, cr, gdk_draw_context_get_color_state (GDK_DRAW_CONTEXT (self->cairo_context)));
+  gsk_cairo_renderer_do_render (renderer,
+                                cr,
+                                gdk_draw_context_get_color_state (GDK_DRAW_CONTEXT (self->cairo_context)),
+                                root);
 
   cairo_destroy (cr);
 
@@ -198,6 +222,9 @@ gsk_cairo_renderer_class_init (GskCairoRendererClass *klass)
 static void
 gsk_cairo_renderer_init (GskCairoRenderer *self)
 {
+  GskProfiler *profiler = gsk_renderer_get_profiler (GSK_RENDERER (self));
+
+  self->profile_timers.cpu_time = gsk_profiler_add_timer (profiler, "cpu-time", "CPU time", FALSE, TRUE);
 }
 
 /**
diff --git gsk/gskprofiler.c gsk/gskprofiler.c
new file mode 100644
index 0000000000..5344b01ffb
--- /dev/null
+++ gsk/gskprofiler.c
@@ -0,0 +1,525 @@
+#include "config.h"
+
+#include "gskprofilerprivate.h"
+
+#define MAX_SAMPLES     32
+
+typedef struct {
+  GQuark id;
+  char *description;
+  gint64 value;
+  gint64 n_samples;
+  unsigned int can_reset : 1;
+} NamedCounter;
+
+typedef struct {
+  GQuark id;
+  char *description;
+  gint64 value;
+  gint64 start_time;
+  gint64 min_value;
+  gint64 max_value;
+  gint64 avg_value;
+  gint64 n_samples;
+  unsigned int in_flight : 1;
+  unsigned int can_reset : 1;
+  unsigned int invert : 1;
+} NamedTimer;
+
+typedef struct {
+  GQuark id;
+  gint64 value;
+} Sample;
+
+struct _GskProfiler
+{
+  GObject parent_instance;
+
+  GHashTable *counters;
+  GHashTable *timers;
+
+  Sample timer_samples[MAX_SAMPLES];
+  guint last_sample;
+};
+
+G_DEFINE_TYPE (GskProfiler, gsk_profiler, G_TYPE_OBJECT)
+
+static void
+named_counter_free (gpointer data)
+{
+  NamedCounter *counter = data;
+
+  if (data == NULL)
+    return;
+
+  g_free (counter->description);
+  g_free (counter);
+}
+
+static void
+named_timer_free (gpointer data)
+{
+  NamedTimer *timer = data;
+
+  if (data == NULL)
+    return;
+
+  g_free (timer->description);
+  g_free (timer);
+}
+
+static void
+gsk_profiler_finalize (GObject *gobject)
+{
+  GskProfiler *self = GSK_PROFILER (gobject);
+
+  g_clear_pointer (&self->counters, g_hash_table_unref);
+  g_clear_pointer (&self->timers, g_hash_table_unref);
+
+  G_OBJECT_CLASS (gsk_profiler_parent_class)->finalize (gobject);
+}
+
+static void
+gsk_profiler_class_init (GskProfilerClass *klass)
+{
+  G_OBJECT_CLASS (klass)->finalize = gsk_profiler_finalize;
+}
+
+static void
+gsk_profiler_init (GskProfiler *self)
+{
+  self->counters = g_hash_table_new_full (g_direct_hash, g_direct_equal,
+                                          NULL,
+                                          named_counter_free);
+  self->timers = g_hash_table_new_full (g_direct_hash, g_direct_equal,
+                                        NULL,
+                                        named_timer_free);
+}
+
+GskProfiler *
+gsk_profiler_new (void)
+{
+  return g_object_new (GSK_TYPE_PROFILER, NULL);
+}
+
+static NamedCounter *
+named_counter_new (GQuark      id,
+                   const char *description,
+                   gboolean    can_reset)
+{
+  NamedCounter *res = g_new0 (NamedCounter, 1);
+
+  res->id = id;
+  res->description = g_strdup (description);
+  res->can_reset = can_reset;
+
+  return res;
+}
+
+static NamedCounter *
+gsk_profiler_get_counter (GskProfiler *profiler,
+                          GQuark       id)
+{
+  return g_hash_table_lookup (profiler->counters, GINT_TO_POINTER (id));
+}
+
+GQuark
+gsk_profiler_add_counter (GskProfiler *profiler,
+                          const char  *counter_name,
+                          const char  *description,
+                          gboolean     can_reset)
+{
+  NamedCounter *counter;
+  GQuark id;
+
+  g_return_val_if_fail (GSK_IS_PROFILER (profiler), 0);
+
+  id = g_quark_from_string (counter_name);
+  counter = gsk_profiler_get_counter (profiler, id);
+  if (counter != NULL)
+    {
+      g_critical ("Cannot add a counter '%s' as one already exists.", counter_name);
+      return counter->id;
+    }
+
+  counter = named_counter_new (id, description, can_reset);
+  g_hash_table_insert (profiler->counters, GINT_TO_POINTER (id), counter);
+
+  return counter->id;
+}
+
+static NamedTimer *
+named_timer_new (GQuark      id,
+                 const char *description,
+                 gboolean    invert,
+                 gboolean    can_reset)
+{
+  NamedTimer *res = g_new0 (NamedTimer, 1);
+
+  res->id = id;
+  res->description = g_strdup (description);
+  res->invert = invert;
+  res->can_reset = can_reset;
+
+  return res;
+}
+
+static NamedTimer *
+gsk_profiler_get_timer (GskProfiler *profiler,
+                        GQuark       id)
+{
+  return g_hash_table_lookup (profiler->timers, GINT_TO_POINTER (id));
+}
+
+GQuark
+gsk_profiler_add_timer (GskProfiler *profiler,
+                        const char  *timer_name,
+                        const char  *description,
+                        gboolean     invert,
+                        gboolean     can_reset)
+{
+  NamedTimer *timer;
+  GQuark id;
+
+  g_return_val_if_fail (GSK_IS_PROFILER (profiler), 0);
+
+  id = g_quark_from_string (timer_name);
+  timer = gsk_profiler_get_timer (profiler, id);
+  if (timer != NULL)
+    {
+      g_critical ("Cannot add a timer '%s' as one already exists.", timer_name);
+      return timer->id;
+    }
+
+  timer = named_timer_new (id, description, invert, can_reset);
+  g_hash_table_insert (profiler->timers, GINT_TO_POINTER (id), timer);
+
+  return timer->id;
+}
+
+void
+gsk_profiler_counter_inc (GskProfiler *profiler,
+                          GQuark       counter_id)
+{
+  gsk_profiler_counter_add (profiler, counter_id, 1);
+}
+
+void
+gsk_profiler_counter_set (GskProfiler *profiler,
+                          GQuark       counter_id,
+                          gint64       value)
+{
+  NamedCounter *counter;
+
+  g_return_if_fail (GSK_IS_PROFILER (profiler));
+
+  counter = gsk_profiler_get_counter (profiler, counter_id);
+  if (counter == NULL)
+    {
+      g_critical ("No counter '%s' (id:%d) found; did you forget to call gsk_profiler_add_counter()?",
+                  g_quark_to_string (counter_id), counter_id);
+      return;
+    }
+
+  counter->value = value;
+}
+
+void
+gsk_profiler_counter_add (GskProfiler *profiler,
+                          GQuark       counter_id,
+                          gint64       increment)
+{
+  NamedCounter *counter;
+
+  g_return_if_fail (GSK_IS_PROFILER (profiler));
+
+  counter = gsk_profiler_get_counter (profiler, counter_id);
+  if (counter == NULL)
+    {
+      g_critical ("No counter '%s' (id:%d) found; did you forget to call gsk_profiler_add_counter()?",
+                  g_quark_to_string (counter_id), counter_id);
+      return;
+    }
+
+  counter->value += increment;
+}
+
+void
+gsk_profiler_timer_begin (GskProfiler *profiler,
+                          GQuark       timer_id)
+{
+  NamedTimer *timer;
+
+  g_return_if_fail (GSK_IS_PROFILER (profiler));
+
+  timer = gsk_profiler_get_timer (profiler, timer_id);
+  if (timer == NULL)
+    return;
+
+  if (timer->in_flight)
+    return;
+
+  timer->in_flight = TRUE;
+  timer->start_time = g_get_monotonic_time ();
+}
+
+gint64
+gsk_profiler_timer_end (GskProfiler *profiler,
+                        GQuark       timer_id)
+{
+  NamedTimer *timer;
+  gint64 diff;
+
+  g_return_val_if_fail (GSK_IS_PROFILER (profiler), 0);
+
+  timer = gsk_profiler_get_timer (profiler, timer_id);
+  if (timer == NULL)
+    {
+      g_critical ("No timer '%s' (id:%d) found; did you forget to call gsk_profiler_add_timer()?",
+                  g_quark_to_string (timer_id), timer_id);
+      return 0;
+    }
+
+  if (!timer->in_flight)
+    {
+      g_critical ("Timer '%s' (id:%d) is not running; did you forget to call gsk_profiler_timer_begin()?",
+                  g_quark_to_string (timer->id), timer->id);
+      return 0;
+    }
+
+  diff = g_get_monotonic_time () - timer->start_time;
+
+  timer->in_flight = FALSE;
+  timer->value += diff;
+
+  return diff;
+}
+
+void
+gsk_profiler_timer_set (GskProfiler *profiler,
+                        GQuark       timer_id,
+                        gint64       value)
+{
+  NamedTimer *timer;
+
+  g_return_if_fail (GSK_IS_PROFILER (profiler));
+
+  timer = gsk_profiler_get_timer (profiler, timer_id);
+  if (timer == NULL)
+    {
+      g_critical ("No timer '%s' (id:%d) found; did you forget to call gsk_profiler_add_timer()?",
+                  g_quark_to_string (timer_id), timer_id);
+      return;
+    }
+
+  if (timer->in_flight)
+    {
+      g_critical ("Timer '%s' (id:%d) is running; are you sure you don't want to call "
+                  "gsk_profiler_timer_end() instead of gsk_profiler_timer_set()?",
+                  g_quark_to_string (timer_id), timer_id);
+    }
+
+  timer->value = value;
+}
+
+gint64
+gsk_profiler_counter_get (GskProfiler *profiler,
+                          GQuark       counter_id)
+{
+  NamedCounter *counter;
+
+  g_return_val_if_fail (GSK_IS_PROFILER (profiler), 0);
+
+  counter = gsk_profiler_get_counter (profiler, counter_id);
+  if (counter == NULL)
+    {
+      g_critical ("No counter '%s' (id:%d) found; did you forget to call gsk_profiler_add_counter()?",
+                  g_quark_to_string (counter_id), counter_id);
+      return 0;
+    }
+
+  return counter->value;
+}
+
+gint64
+gsk_profiler_timer_get (GskProfiler *profiler,
+                        GQuark       timer_id)
+{
+  NamedTimer *timer;
+
+  g_return_val_if_fail (GSK_IS_PROFILER (profiler), 0);
+
+  timer = gsk_profiler_get_timer (profiler, timer_id);
+  if (timer == NULL)
+    {
+      g_critical ("No timer '%s' (id:%d) found; did you forget to call gsk_profiler_add_timer()?",
+                  g_quark_to_string (timer_id), timer_id);
+      return 0;
+    }
+
+  if (timer->invert)
+    return (gint64) (1000000.0 / (double) timer->value);
+
+  return timer->value;
+}
+
+gint64
+gsk_profiler_timer_get_start (GskProfiler *profiler,
+                              GQuark       timer_id)
+{
+  NamedTimer *timer;
+
+  timer = gsk_profiler_get_timer (profiler, timer_id);
+  if (timer == NULL)
+    return 0;
+
+  return timer->start_time;
+}
+
+void
+gsk_profiler_reset (GskProfiler *profiler)
+{
+  GHashTableIter iter;
+  gpointer value_p = NULL;
+
+  g_return_if_fail (GSK_IS_PROFILER (profiler));
+
+  g_hash_table_iter_init (&iter, profiler->counters);
+  while (g_hash_table_iter_next (&iter, NULL, &value_p))
+    {
+      NamedCounter *counter = value_p;
+
+      if (counter->can_reset)
+        counter->value = 0;
+    }
+
+  g_hash_table_iter_init (&iter, profiler->timers);
+  while (g_hash_table_iter_next (&iter, NULL, &value_p))
+    {
+      NamedTimer *timer = value_p;
+
+      if (timer->can_reset)
+        {
+          timer->value = 0;
+          timer->min_value = 0;
+          timer->max_value = 0;
+          timer->avg_value = 0;
+          timer->n_samples = 0;
+        }
+    }
+
+  profiler->last_sample = 0;
+}
+
+void
+gsk_profiler_push_samples (GskProfiler *profiler)
+{
+  GHashTableIter iter;
+  gpointer value_p = NULL;
+  guint last_sample;
+
+  g_return_if_fail (GSK_IS_PROFILER (profiler));
+
+  g_hash_table_iter_init (&iter, profiler->timers);
+  while (g_hash_table_iter_next (&iter, NULL, &value_p))
+    {
+      NamedTimer *timer = value_p;
+      Sample *s;
+
+      last_sample = profiler->last_sample;
+      profiler->last_sample += 1;
+      if (profiler->last_sample == MAX_SAMPLES)
+        profiler->last_sample = 0;
+
+      s = &(profiler->timer_samples[last_sample]);
+      s->id = timer->id;
+
+      if (timer->invert)
+        s->value = (gint64) (1000000.0 / (double) timer->value);
+      else
+        s->value = timer->value;
+    }
+}
+
+void
+gsk_profiler_append_counters (GskProfiler *profiler,
+                              GString     *buffer)
+{
+  GHashTableIter iter;
+  gpointer value_p = NULL;
+
+  g_return_if_fail (GSK_IS_PROFILER (profiler));
+  g_return_if_fail (buffer != NULL);
+
+  g_hash_table_iter_init (&iter, profiler->counters);
+  while (g_hash_table_iter_next (&iter, NULL, &value_p))
+    {
+      NamedCounter *counter = value_p;
+
+      g_string_append_printf (buffer, "%s: %" G_GINT64_FORMAT "\n",
+                              counter->description,
+                              counter->value);
+    }
+}
+
+void
+gsk_profiler_append_timers (GskProfiler *profiler,
+                            GString     *buffer)
+{
+  GHashTableIter iter;
+  gpointer value_p = NULL;
+  int i;
+
+  g_return_if_fail (GSK_IS_PROFILER (profiler));
+  g_return_if_fail (buffer != NULL);
+
+  g_hash_table_iter_init (&iter, profiler->timers);
+  while (g_hash_table_iter_next (&iter, NULL, &value_p))
+    {
+      NamedTimer *timer = value_p;
+
+      timer->min_value = G_MAXINT64;
+      timer->max_value = G_MININT64;
+      timer->avg_value = 0;
+      timer->n_samples = 0;
+    }
+
+  for (i = 0; i < profiler->last_sample; i++)
+    {
+      Sample *s = &(profiler->timer_samples[i]);
+      NamedTimer *timer;
+
+      if (s->id == 0)
+        continue;
+
+      timer = gsk_profiler_get_timer (profiler, s->id);
+      timer->min_value = MIN (timer->min_value, s->value);
+      timer->max_value = MAX (timer->max_value, s->value);
+      timer->avg_value += s->value;
+      timer->n_samples += 1;
+    }
+
+  g_hash_table_iter_init (&iter, profiler->timers);
+  while (g_hash_table_iter_next (&iter, NULL, &value_p))
+    {
+      NamedTimer *timer = value_p;
+      const char *unit = timer->invert ? "" : "usec";
+
+      g_string_append_printf (buffer, "%s (%s): %.2f",
+                              timer->description,
+                              unit,
+                              (double) timer->value);
+
+      if (timer->n_samples > 1)
+        {
+          timer->avg_value = timer->avg_value / timer->n_samples;
+          g_string_append_printf (buffer, " Min: %.2f Avg: %.2f Max: %.2f (%" G_GINT64_FORMAT " samples)",
+                                  (double) timer->min_value,
+                                  (double) timer->avg_value,
+                                  (double) timer->max_value,
+                                  timer->n_samples);
+        }
+
+      g_string_append (buffer, "\n");
+    }
+}
diff --git gsk/gskprofilerprivate.h gsk/gskprofilerprivate.h
new file mode 100644
index 0000000000..7444ce0c68
--- /dev/null
+++ gsk/gskprofilerprivate.h
@@ -0,0 +1,53 @@
+#pragma once
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define GSK_TYPE_PROFILER (gsk_profiler_get_type ())
+G_DECLARE_FINAL_TYPE (GskProfiler, gsk_profiler, GSK, PROFILER, GObject)
+
+GskProfiler *   gsk_profiler_new                (void);
+
+GQuark          gsk_profiler_add_counter        (GskProfiler *profiler,
+                                                 const char  *counter_name,
+                                                 const char  *description,
+                                                 gboolean     can_reset);
+GQuark          gsk_profiler_add_timer          (GskProfiler *profiler,
+                                                 const char  *timer_name,
+                                                 const char  *description,
+                                                 gboolean     invert,
+                                                 gboolean     can_reset);
+
+void            gsk_profiler_counter_inc        (GskProfiler *profiler,
+                                                 GQuark       counter_id);
+void            gsk_profiler_counter_add        (GskProfiler *profiler,
+                                                 GQuark       counter_id,
+                                                 gint64       increment);
+void            gsk_profiler_counter_set        (GskProfiler *profiler,
+                                                 GQuark       counter_id,
+                                                 gint64       value);
+void            gsk_profiler_timer_begin        (GskProfiler *profiler,
+                                                 GQuark       timer_id);
+gint64          gsk_profiler_timer_end          (GskProfiler *profiler,
+                                                 GQuark       timer_id);
+void            gsk_profiler_timer_set          (GskProfiler *profiler,
+                                                 GQuark       timer_id,
+                                                 gint64       value);
+
+gint64          gsk_profiler_counter_get        (GskProfiler *profiler,
+                                                 GQuark       counter_id);
+gint64          gsk_profiler_timer_get          (GskProfiler *profiler,
+                                                 GQuark       timer_id);
+gint64          gsk_profiler_timer_get_start    (GskProfiler *profiler,
+                                                 GQuark       timer_id);
+
+void            gsk_profiler_reset              (GskProfiler *profiler);
+
+void            gsk_profiler_push_samples       (GskProfiler *profiler);
+void            gsk_profiler_append_counters    (GskProfiler *profiler,
+                                                 GString     *buffer);
+void            gsk_profiler_append_timers      (GskProfiler *profiler,
+                                                 GString     *buffer);
+
+G_END_DECLS
diff --git gsk/gskrenderer.c gsk/gskrenderer.c
index 458792d6cc..34d36b0ce2 100644
--- gsk/gskrenderer.c
+++ gsk/gskrenderer.c
@@ -41,6 +41,7 @@
 
 #include "gskenumtypes.h"
 
+#include "gl/gskglrenderer.h"
 #include "gpu/gskglrenderer.h"
 #include "gpu/gskvulkanrenderer.h"
 #include "gdk/gdkvulkancontextprivate.h"
@@ -62,6 +63,8 @@ typedef struct
   GdkSurface *surface;
   GskRenderNode *prev_node;
 
+  GskProfiler *profiler;
+
   GskDebugFlags debug_flags;
 
   unsigned int is_realized : 1;
@@ -120,12 +123,14 @@ static void
 gsk_renderer_dispose (GObject *gobject)
 {
   GskRenderer *self = GSK_RENDERER (gobject);
-  G_GNUC_UNUSED GskRendererPrivate *priv = gsk_renderer_get_instance_private (self);
+  GskRendererPrivate *priv = gsk_renderer_get_instance_private (self);
 
   /* We can't just unrealize here because superclasses have already run dispose.
    * So we insist that unrealize must be called before unreffing. */
   g_assert (!priv->is_realized);
 
+  g_clear_object (&priv->profiler);
+
   G_OBJECT_CLASS (gsk_renderer_parent_class)->dispose (gobject);
 }
 
@@ -195,6 +200,7 @@ gsk_renderer_init (GskRenderer *self)
 {
   GskRendererPrivate *priv = gsk_renderer_get_instance_private (self);
 
+  priv->profiler = gsk_profiler_new ();
   priv->debug_flags = gsk_get_debug_flags ();
 }
 
@@ -481,6 +487,24 @@ gsk_renderer_render (GskRenderer          *renderer,
   priv->prev_node = gsk_render_node_ref (root);
 }
 
+/*< private >
+ * gsk_renderer_get_profiler:
+ * @renderer: a renderer
+ *
+ * Retrieves a pointer to the `GskProfiler` instance of the renderer.
+ *
+ * Returns: (transfer none): the profiler
+ */
+GskProfiler *
+gsk_renderer_get_profiler (GskRenderer *renderer)
+{
+  GskRendererPrivate *priv = gsk_renderer_get_instance_private (renderer);
+
+  g_return_val_if_fail (GSK_IS_RENDERER (renderer), NULL);
+
+  return priv->profiler;
+}
+
 static GType
 get_renderer_for_name (const char *renderer_name)
 {
@@ -492,6 +516,8 @@ get_renderer_for_name (const char *renderer_name)
 #endif
   else if (g_ascii_strcasecmp (renderer_name, "cairo") == 0)
     return GSK_TYPE_CAIRO_RENDERER;
+  else if (g_ascii_strcasecmp (renderer_name, "legacy") == 0)
+    return GSK_TYPE_GL_LEGACY_RENDERER;
   else if (g_ascii_strcasecmp (renderer_name, "gl") == 0 ||
            g_ascii_strcasecmp (renderer_name, "opengl") == 0)
     return GSK_TYPE_GL_RENDERER;
@@ -513,6 +539,7 @@ get_renderer_for_name (const char *renderer_name)
                         "  broadway - Disabled during GTK build\n"
 #endif
                         "     cairo - Use the Cairo fallback renderer\n"
+                        "    legacy - Use the legacy GL renderer (supports OpenGL 2.0+)\n"
                         "    opengl - Use the OpenGL renderer\n"
                         "        gl - Use the OpenGL renderer\n"
 #ifdef GDK_RENDERING_VULKAN
@@ -521,8 +548,8 @@ get_renderer_for_name (const char *renderer_name)
                         "    vulkan - Disabled during GTK build\n"
 #endif
                         "      help - Print this help\n\n"
-                        "The old OpenGL renderer has been removed in GTK 4.18, so using\n"
-                        "GSK_RENDERER=gl will cause a warning and use the new OpenGL renderer.\n\n"
+                        "The GPU renderer (gl/opengl) requires OpenGL 3.3+ or GLES 3.0+.\n"
+                        "For older OpenGL versions (2.0-3.2), use GSK_RENDERER=legacy.\n\n"
                         "Other arguments will cause a warning and be ignored.");
     }
   else
@@ -690,6 +717,15 @@ get_renderer_for_vulkan_fallback (GdkSurface *surface)
 }
 #endif
 
+static GType
+get_renderer_for_gl_legacy (GdkSurface *surface)
+{
+  if (!gl_supported_platform (surface, TRUE))
+    return G_TYPE_INVALID;
+
+  return GSK_TYPE_GL_LEGACY_RENDERER;
+}
+
 static GType
 get_renderer_fallback (GdkSurface *surface)
 {
@@ -711,6 +747,10 @@ static struct {
   { FALSE, get_renderer_for_vulkan_fallback },
 #endif
   { FALSE, get_renderer_for_gl_fallback },
+  /* Last chance before falling back to software: the legacy GL renderer
+   * still works on the OpenGL 2.x hardware the GPU renderer rejects.
+   */
+  { FALSE, get_renderer_for_gl_legacy },
   { FALSE, get_renderer_fallback },
 };
 
diff --git gsk/gskrendererprivate.h gsk/gskrendererprivate.h
index dec6dcb318..b32cef44e6 100644
--- gsk/gskrendererprivate.h
+++ gsk/gskrendererprivate.h
@@ -19,6 +19,7 @@
 #pragma once
 
 #include "gskrenderer.h"
+#include "gskprofilerprivate.h"
 #include "gskdebugprivate.h"
 #include "gskoffloadprivate.h"
 
@@ -57,6 +58,8 @@ struct _GskRendererClass
 GskRenderer *           gsk_renderer_new_for_surface_full       (GdkSurface             *surface,
                                                                  gboolean                attach);
 
+GskProfiler *           gsk_renderer_get_profiler               (GskRenderer    *renderer);
+
 GskDebugFlags           gsk_renderer_get_debug_flags            (GskRenderer    *renderer);
 void                    gsk_renderer_set_debug_flags            (GskRenderer    *renderer,
                                                                  GskDebugFlags   flags);
diff --git gsk/meson.build gsk/meson.build
index e1cb918394..e9e9d77905 100644
--- gsk/meson.build
+++ gsk/meson.build
@@ -1,3 +1,29 @@
+gsk_private_gl_shaders = [
+  'gl/resources/preamble.glsl',
+  'gl/resources/preamble.fs.glsl',
+  'gl/resources/preamble.vs.glsl',
+  'gl/resources/border.glsl',
+  'gl/resources/blit.glsl',
+  'gl/resources/coloring.glsl',
+  'gl/resources/color.glsl',
+  'gl/resources/linear_gradient.glsl',
+  'gl/resources/radial_gradient.glsl',
+  'gl/resources/conic_gradient.glsl',
+  'gl/resources/color_matrix.glsl',
+  'gl/resources/blur.glsl',
+  'gl/resources/inset_shadow.glsl',
+  'gl/resources/outset_shadow.glsl',
+  'gl/resources/unblurred_outset_shadow.glsl',
+  'gl/resources/cross_fade.glsl',
+  'gl/resources/blend.glsl',
+  'gl/resources/repeat.glsl',
+  'gl/resources/custom.glsl',
+  'gl/resources/filled_border.glsl',
+  'gl/resources/mask.glsl',
+  'gl/resources/external.glsl',
+  'gl/resources/premultiply.glsl',
+]
+
 gsk_public_sources = files([
   'gskarithmeticnode.c',
   'gskblendnode.c',
@@ -59,6 +85,7 @@ gsk_public_sources = files([
   'gsktransform.c',
   'gsktransformnode.c',
   'gskturbulencenode.c',
+  'gl/gskglrenderer.c',
   'gpu/gskglrenderer.c',
   'gpu/gskvulkanrenderer.c',
 ])
@@ -72,7 +99,23 @@ gsk_private_sources = files([
   'gskcurveintersect.c',
   'gskdebug.c',
   'gskprivate.c',
+  'gskprofiler.c',
   'gl/fp16.c',
+  'gl/gskglattachmentstate.c',
+  'gl/gskglbuffer.c',
+  'gl/gskglcommandqueue.c',
+  'gl/gskglcompiler.c',
+  'gl/gskgldriver.c',
+  'gl/gskglglyphlibrary.c',
+  'gl/gskgliconlibrary.c',
+  'gl/gskglprogram.c',
+  'gl/gskglrenderjob.c',
+  'gl/gskglshadowlibrary.c',
+  'gl/gskgltexturelibrary.c',
+  'gl/gskgluniformstate.c',
+  'gl/gskgltexture.c',
+  'gl/gskglprofiler.c',
+  'gl/stb_rect_pack.c',
   'gpu/gskatlasallocator.c',
   'gpu/gskglbuffer.c',
   'gpu/gskgldevice.c',
@@ -217,6 +260,7 @@ gsk_resources_xml = custom_target(output: 'gsk.resources.xml',
   command: [
     find_program('gen-gsk-gresources-xml.py'),
     '@OUTPUT@',
+    gsk_private_gl_shaders,
     gsk_private_gpu_gl_shaders,
     gsk_private_gpu_vulkan_shaders,
   ],
diff --git gtk/gtkglarea.c gtk/gtkglarea.c
index 8962f0a9e5..90229a18e8 100644
--- gtk/gtkglarea.c
+++ gtk/gtkglarea.c
@@ -815,8 +815,11 @@ gtk_gl_area_snapshot (GtkWidget   *widget,
       priv->texture = NULL;
       priv->textures = g_list_prepend (priv->textures, texture);
 
-      sync = glFenceSync (GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
-      gdk_gl_texture_builder_set_sync (texture->builder, sync);
+      if (gdk_gl_context_has_feature (priv->context, GDK_GL_FEATURE_SYNC))
+        {
+          sync = glFenceSync (GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
+          gdk_gl_texture_builder_set_sync (texture->builder, sync);
+        }
 
       texture->gl_texture = gdk_gl_texture_builder_build (texture->builder,
                                                           release_gl_texture,
diff --git gtk/inspector/eventrecording.h gtk/inspector/eventrecording.h
index f67bceabcf..04fc0eca0d 100644
--- gtk/inspector/eventrecording.h
+++ gtk/inspector/eventrecording.h
@@ -19,6 +19,7 @@
 
 #include <gdk/gdk.h>
 #include <gsk/gsk.h>
+#include "gsk/gskprofilerprivate.h"
 #include "gtk/gtkenums.h"
 #include "gtk/gtkwidget.h"
 #include "gtk/gtkeventcontroller.h"
diff --git gtk/inspector/recorder.c gtk/inspector/recorder.c
index cd044ac881..4c5e294487 100644
--- gtk/inspector/recorder.c
+++ gtk/inspector/recorder.c
@@ -2826,6 +2826,7 @@ gtk_inspector_recorder_record_render (GtkInspectorRecorder *recorder,
     }
 
   recording = gtk_inspector_render_recording_new (frame_time,
+                                                  gsk_renderer_get_profiler (renderer),
                                                   &(GdkRectangle) { 0, 0,
                                                     gdk_surface_get_width (surface),
                                                     gdk_surface_get_height (surface) },
diff --git gtk/inspector/renderrecording.c gtk/inspector/renderrecording.c
index bda2afe4da..56fc29c1aa 100644
--- gtk/inspector/renderrecording.c
+++ gtk/inspector/renderrecording.c
@@ -30,6 +30,7 @@ gtk_inspector_render_recording_finalize (GObject *object)
   g_clear_pointer (&recording->clip_region, cairo_region_destroy);
   g_clear_pointer (&recording->node, gsk_render_node_unref);
   g_clear_pointer (&recording->profile_node, gsk_render_node_unref);
+  g_clear_pointer (&recording->profiler_info, g_free);
 
   G_OBJECT_CLASS (gtk_inspector_render_recording_parent_class)->finalize (object);
 }
@@ -47,8 +48,21 @@ gtk_inspector_render_recording_init (GtkInspectorRenderRecording *vis)
 {
 }
 
+static void
+collect_profiler_info (GtkInspectorRenderRecording *recording,
+                       GskProfiler                 *profiler)
+{
+  GString *string;
+
+  string = g_string_new (NULL);
+  gsk_profiler_append_timers (profiler, string);
+  gsk_profiler_append_counters (profiler, string);
+  recording->profiler_info = g_string_free (string, FALSE);
+}
+
 GtkInspectorRecording *
 gtk_inspector_render_recording_new (gint64                timestamp,
+                                    GskProfiler          *profiler,
                                     const GdkRectangle   *area,
                                     const cairo_region_t *clip_region,
                                     GskRenderNode        *node,
@@ -60,6 +74,7 @@ gtk_inspector_render_recording_new (gint64                timestamp,
                             "timestamp", timestamp,
                             NULL);
 
+  collect_profiler_info (recording, profiler);
   recording->area = *area;
   recording->clip_region = cairo_region_copy (clip_region);
   recording->node = gsk_render_node_ref (node);
@@ -107,4 +122,10 @@ gtk_inspector_render_recording_get_surface (GtkInspectorRenderRecording *recordi
   return recording->surface;
 }
 
+const char *
+gtk_inspector_render_recording_get_profiler_info (GtkInspectorRenderRecording *recording)
+{
+  return recording->profiler_info;
+}
+
 // vim: set et sw=2 ts=2:
diff --git gtk/inspector/renderrecording.h gtk/inspector/renderrecording.h
index 9448508697..63e0bec5b8 100644
--- gtk/inspector/renderrecording.h
+++ gtk/inspector/renderrecording.h
@@ -19,6 +19,7 @@
 
 #include <gdk/gdk.h>
 #include <gsk/gsk.h>
+#include "gsk/gskprofilerprivate.h"
 
 #include "inspector/recording.h"
 
@@ -43,6 +44,7 @@ typedef struct _GtkInspectorRenderRecording
   GskRenderNode *node;
   GskRenderNode *profile_node;
   gpointer surface;
+  char *profiler_info;
 } GtkInspectorRenderRecording;
 
 typedef struct _GtkInspectorRenderRecordingClass
@@ -54,6 +56,7 @@ GType           gtk_inspector_render_recording_get_type         (void);
 
 GtkInspectorRecording *
                 gtk_inspector_render_recording_new              (gint64                             timestamp,
+                                                                 GskProfiler                       *profiler,
                                                                  const GdkRectangle                *area,
                                                                  const cairo_region_t              *clip_region,
                                                                  GskRenderNode                     *node,
@@ -71,6 +74,9 @@ const cairo_rectangle_int_t *
 gpointer
                 gtk_inspector_render_recording_get_surface      (GtkInspectorRenderRecording       *recording);
 
+const char *    gtk_inspector_render_recording_get_profiler_info
+                                                                (GtkInspectorRenderRecording       *recording);
+
 G_END_DECLS
 
 
diff --git tools/gtk-rendernode-tool-benchmark.c tools/gtk-rendernode-tool-benchmark.c
index 50e1cf7872..5e085170e7 100644
--- tools/gtk-rendernode-tool-benchmark.c
+++ tools/gtk-rendernode-tool-benchmark.c
@@ -138,7 +138,7 @@ do_benchmark (int          *argc,
     }
 
   if (renderers == NULL || renderers[0] == NULL)
-    renderers = g_strdupv ((char **) (const char *[]) { "gl", "vulkan", "cairo", NULL });
+    renderers = g_strdupv ((char **) (const char *[]) { "legacy", "gl", "vulkan", "cairo", NULL });
   
   node = load_node_file (filenames[0]);
 
-- 
2.43.0

