From http://www.jwz.org/xscreensaver/xscreensaver-5.40.tar.gz
[xscreensaver] / jwxyz / jwxyz-gl.c
index 9bf2661e6982592ea1748c3373f2012da5e5271e..369d16fbbc1ff73763668a10cc41f6245d3fcfbd 100644 (file)
    Xlib and that do OpenGL-ish things that bear some resemblance to the
    things that Xlib might have done.
 
-   This is the version of jwxyz for Android.  The version used by MacOS
+   This is the version of jwxyz for Android.  The version used by macOS
    and iOS is in jwxyz.m.
  */
 
 /* Be advised, this is all very much a work in progress. */
 
-/* FWIW, at one point macOS 10.x could actually run with 256 colors. It might
-   not be able to do this anymore, though.
+/* There is probably no reason to ever implement indexed-color rendering here,
+   even if many screenhacks still work with PseudoColor.
+   - OpenGL ES 1.1 (Android, iOS) doesn't support indexed color.
+   - macOS only provides indexed color via AGL (now deprecated), not
+     NSOpenGLPixelFormat.
  */
 
 /* TODO:
@@ -134,6 +137,8 @@ struct jwxyz_Display {
 
   int gc_function;
   Bool gc_alpha_allowed_p;
+  int gc_clip_x_origin, gc_clip_y_origin;
+  GLuint gc_clip_mask;
 
   // Alternately, there could be one queue per pixmap.
   size_t queue_size, queue_capacity;
@@ -147,6 +152,8 @@ struct jwxyz_Display {
 struct jwxyz_GC {
   XGCValues gcv;
   unsigned int depth;
+  GLuint clip_mask;
+  unsigned clip_mask_width, clip_mask_height;
 };
 
 struct jwxyz_linked_point {
@@ -220,6 +227,62 @@ gl_check_ver (const struct gl_version *caps,
 #endif
 
 
+static void
+tex_parameters (Display *d, GLuint texture)
+{
+  // TODO: Check for (ARB|EXT|NV)_texture_rectangle. (All three are alike.)
+  // Rectangle textures should be present on OS X with the following exceptions:
+  // - Generic renderer on PowerPC OS X 10.4 and earlier
+  // - ATI Rage 128
+  glBindTexture (d->gl_texture_target, texture);
+  // TODO: This is all somewhere else. Refactor.
+  glTexParameteri (d->gl_texture_target, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+  glTexParameteri (d->gl_texture_target, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+
+  // This might be redundant for rectangular textures.
+# ifndef HAVE_JWZGLES
+  const GLint wrap = GL_CLAMP;
+# else  // HAVE_JWZGLES
+  const GLint wrap = GL_CLAMP_TO_EDGE;
+# endif // HAVE_JWZGLES
+
+  // In OpenGL, CLAMP_TO_EDGE is OpenGL 1.2 or GL_SGIS_texture_edge_clamp.
+  // This is always present with OpenGL ES.
+  glTexParameteri (d->gl_texture_target, GL_TEXTURE_WRAP_S, wrap);
+  glTexParameteri (d->gl_texture_target, GL_TEXTURE_WRAP_T, wrap);
+}
+
+static void
+tex_size (Display *dpy, unsigned *tex_w, unsigned *tex_h)
+{
+  if (!dpy->gl_texture_npot_p) {
+    *tex_w = to_pow2(*tex_w);
+    *tex_h = to_pow2(*tex_h);
+  }
+}
+
+static void
+tex_image (Display *dpy, GLenum internalformat,
+           unsigned *tex_w, unsigned *tex_h, GLenum format, GLenum type,
+           const void *data)
+{
+  unsigned w = *tex_w, h = *tex_h;
+  tex_size (dpy, tex_w, tex_h);
+
+  // TODO: Would using glTexSubImage2D exclusively be faster?
+  if (*tex_w == w && *tex_h == h) {
+    glTexImage2D (dpy->gl_texture_target, 0, internalformat, *tex_w, *tex_h,
+                  0, format, type, data);
+  } else {
+    // TODO: Sampling the last row might be a problem if src_x != 0.
+    glTexImage2D (dpy->gl_texture_target, 0, internalformat, *tex_w, *tex_h,
+                  0, format, type, NULL);
+    glTexSubImage2D (dpy->gl_texture_target, 0, 0, 0, w, h,
+                     format, type, data);
+  }
+}
+
+
 extern const struct jwxyz_vtbl gl_vtbl;
 
 Display *
@@ -301,19 +364,24 @@ jwxyz_gl_make_display (Window w)
   Visual *v = &d->visual;
   v->class      = TrueColor;
   if (d->pixel_format == GL_BGRA_EXT) {
-    v->rgba_masks[0] = 0x00ff0000;
-    v->rgba_masks[1] = 0x0000ff00;
-    v->rgba_masks[2] = 0x000000ff;
-    v->rgba_masks[3] = 0xff000000;
+    v->red_mask   = 0x00ff0000;
+    v->green_mask = 0x0000ff00;
+    v->blue_mask  = 0x000000ff;
+    v->alpha_mask = 0xff000000;
   } else {
     Assert(d->pixel_format == GL_RGBA,
            "jwxyz_gl_make_display: Unknown pixel_format");
+    unsigned long masks[4];
     for (unsigned i = 0; i != 4; ++i) {
       union color_bytes color;
       color.pixel = 0;
       color.bytes[i] = 0xff;
-      v->rgba_masks[i] = color.pixel;
+      masks[i] = color.pixel;
     }
+    v->red_mask   = masks[0];
+    v->green_mask = masks[1];
+    v->blue_mask  = masks[2];
+    v->alpha_mask = masks[3];
   }
 
   d->timers_data = jwxyz_sources_init (XtDisplayToApplicationContext (d));
@@ -321,39 +389,36 @@ jwxyz_gl_make_display (Window w)
   d->window_background = BlackPixel(d,0);
 
   d->main_window = w;
+
   {
-    GLint max_texture_size;
+    GLint max_texture_size, max_texture_units;
     glGetIntegerv (GL_MAX_TEXTURE_SIZE, &max_texture_size);
-    Log ("GL_MAX_TEXTURE_SIZE: %d\n", max_texture_size);
+    glGetIntegerv (GL_MAX_TEXTURE_UNITS, &max_texture_units);
+    Log ("GL_MAX_TEXTURE_SIZE: %d, GL_MAX_TEXTURE_UNITS: %d\n",
+         max_texture_size, max_texture_units);
+
+    // OpenGL ES 1.1 and OpenGL 1.3 both promise at least 2 texture units:
+    // OpenGL (R) ES Common/Common-Lite Profile Specification, Version 1.1.12 (Full Specification)
+    // https://www.khronos.org/registry/OpenGL/specs/es/1.1/es_full_spec_1.1.pdf
+    // * Table 6.22. Implementation Dependent Values
+    // * D.2 Enhanced Texture Processing
+    // (OpenGL 1.2 provides multitexturing as an ARB extension, and requires 1
+    // texture unit only.)
+
+    // ...but the glGet reference page contradicts this, and says there can be
+    // just one.
+    // https://www.khronos.org/registry/OpenGL-Refpages/es1.1/xhtml/glGet.xml
   }
+
   glGenTextures (countof (d->textures), d->textures);
 
   for (unsigned i = 0; i != countof (d->textures); i++) {
-    // TODO: Check for (ARB|EXT|NV)_texture_rectangle. (All three are alike.)
-    // Rectangle textures should be present on OS X with the following exceptions:
-    // - Generic renderer on PowerPC OS X 10.4 and earlier
-    // - ATI Rage 128
-    glBindTexture (d->gl_texture_target, d->textures[i]);
-    // TODO: This is all somewhere else. Refactor.
-    glTexParameteri (d->gl_texture_target, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
-    glTexParameteri (d->gl_texture_target, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
-
-    // This might be redundant for rectangular textures.
-# ifndef HAVE_JWZGLES
-    const GLint wrap = GL_CLAMP;
-# else  // HAVE_JWZGLES
-    const GLint wrap = GL_CLAMP_TO_EDGE;
-# endif // HAVE_JWZGLES
-
-    // In OpenGL, CLAMP_TO_EDGE is OpenGL 1.2 or GL_SGIS_texture_edge_clamp.
-    // This is always present with OpenGL ES.
-    glTexParameteri (d->gl_texture_target, GL_TEXTURE_WRAP_S, wrap);
-    glTexParameteri (d->gl_texture_target, GL_TEXTURE_WRAP_T, wrap);
+    tex_parameters (d, d->textures[i]);
   }
 
   d->gc_function = GXcopy;
   d->gc_alpha_allowed_p = False;
+  d->gc_clip_mask = 0;
 
   jwxyz_assert_display(d);
   return d;
@@ -446,7 +511,7 @@ visual (Display *dpy)
 
    All drawing functions:
    function                                | glLogicOp w/ GL_COLOR_LOGIC_OP
-   clip_x_origin, clip_y_origin, clip_mask | Stencil mask
+   clip_x_origin, clip_y_origin, clip_mask | Multitexturing w/ GL_TEXTURE1
 
    Shape drawing functions:
    foreground, background                  | glColor*
@@ -483,8 +548,12 @@ enqueue (Display *dpy, Drawable d, GC gc, int mode, size_t count,
          unsigned long pixel)
 {
   if (dpy->queue_size &&
-      (dpy->gc_function != gc->gcv.function ||
+      (!gc || /* Could allow NULL GCs here... */
+       dpy->gc_function != gc->gcv.function ||
        dpy->gc_alpha_allowed_p != gc->gcv.alpha_allowed_p ||
+       dpy->gc_clip_mask != gc->clip_mask ||
+       dpy->gc_clip_x_origin != gc->gcv.clip_x_origin ||
+       dpy->gc_clip_y_origin != gc->gcv.clip_y_origin ||
        dpy->queue_mode != mode ||
        dpy->queue_drawable != d)) {
     jwxyz_gl_flush (dpy);
@@ -528,8 +597,21 @@ enqueue (Display *dpy, Drawable d, GC gc, int mode, size_t count,
   dpy->queue_drawable = d;
 
   union color_bytes color;
-  // TODO: validate color
-  JWXYZ_QUERY_COLOR (dpy, pixel, 0xffull, color.bytes);
+
+  // Like query_color, but for bytes.
+  jwxyz_validate_pixel (dpy, pixel, jwxyz_drawable_depth (d),
+                        gc ? gc->gcv.alpha_allowed_p : False);
+
+  if (jwxyz_drawable_depth (d) == 1) {
+    uint8_t b = pixel ? 0xff : 0;
+    color.bytes[0] = b;
+    color.bytes[1] = b;
+    color.bytes[2] = b;
+    color.bytes[3] = 0xff;
+  } else {
+    JWXYZ_QUERY_COLOR (dpy, pixel, 0xffull, color.bytes);
+  }
+
   for (size_t i = 0; i != count; ++i) // TODO: wmemset when applicable.
     dpy->queue_color[i + old_size] = color.pixel;
 
@@ -556,44 +638,49 @@ finish_triangle_strip (Display *dpy, GLfloat *enqueue_result)
 
 
 static void
-set_clip_mask (GC gc)
-{
-  Assert (!gc->gcv.clip_mask, "set_clip_mask: TODO");
-}
-
-
-static void
-set_color (Display *dpy, unsigned long pixel, unsigned int depth,
-           Bool alpha_allowed_p)
+query_color (Display *dpy, unsigned long pixel, unsigned int depth,
+             Bool alpha_allowed_p, GLfloat *rgba)
 {
   jwxyz_validate_pixel (dpy, pixel, depth, alpha_allowed_p);
 
   if (depth == 1) {
     GLfloat f = pixel;
-    glColor4f (f, f, f, 1);
+    rgba[0] = f;
+    rgba[1] = f;
+    rgba[2] = f;
+    rgba[3] = 1;
   } else {
-    GLfloat rgba[4];
     JWXYZ_QUERY_COLOR (dpy, pixel, 1.0f, rgba);
-    glColor4f (rgba[0], rgba[1], rgba[2], rgba[3]);
   }
 }
 
+
+static void
+set_color (Display *dpy, unsigned long pixel, unsigned int depth,
+           Bool alpha_allowed_p)
+{
+  GLfloat rgba[4];
+  query_color (dpy, pixel, depth, alpha_allowed_p, rgba);
+  glColor4f (rgba[0], rgba[1], rgba[2], rgba[3]);
+}
+
 /* Pushes a GC context; sets Function and ClipMask. */
 void
 jwxyz_gl_set_gc (Display *dpy, GC gc)
 {
   int function;
   Bool alpha_allowed_p;
+  GLuint clip_mask;
 
   // GC is NULL for XClearArea and XClearWindow.
   if (gc) {
     function = gc->gcv.function;
-    alpha_allowed_p = gc->gcv.alpha_allowed_p;
-    set_clip_mask (gc);
+    alpha_allowed_p = gc->gcv.alpha_allowed_p || gc->clip_mask;
+    clip_mask = gc->clip_mask;
   } else {
     function = GXcopy;
     alpha_allowed_p = False;
-    // TODO: Set null clip mask for NULL GC here.
+    clip_mask = 0;
   }
 
   /* GL_COLOR_LOGIC_OP: OpenGL 1.1. */
@@ -616,16 +703,54 @@ jwxyz_gl_set_gc (Display *dpy, GC gc)
      widespread.
    */
 
-  if (alpha_allowed_p != dpy->gc_alpha_allowed_p) {
-    dpy->gc_alpha_allowed_p = alpha_allowed_p;
-    if (gc && gc->gcv.alpha_allowed_p) {
-      // TODO: Maybe move glBlendFunc to XCreatePixmap?
-      glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
-      glEnable (GL_BLEND);
-    } else {
-      glDisable (GL_BLEND);
+  dpy->gc_alpha_allowed_p = alpha_allowed_p;
+  if (alpha_allowed_p || clip_mask) {
+    // TODO: Maybe move glBlendFunc to XCreatePixmap?
+    glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+    glEnable (GL_BLEND);
+  } else {
+    glDisable (GL_BLEND);
+  }
+
+  /* Texture units:
+     GL_TEXTURE0: Texture for XPutImage/XCopyArea (if applicable)
+     GL_TEXTURE1: Texture for clip masks (if applicable)
+   */
+  dpy->gc_clip_mask = clip_mask;
+
+  glActiveTexture (GL_TEXTURE1);
+  if (clip_mask) {
+    glEnable (dpy->gl_texture_target);
+    glBindTexture (dpy->gl_texture_target, gc->clip_mask);
+
+    glTexEnvi (GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE,
+               alpha_allowed_p ? GL_MODULATE : GL_REPLACE);
+
+    glMatrixMode (GL_TEXTURE);
+    glLoadIdentity ();
+
+    unsigned
+      tex_w = gc->clip_mask_width + 2, tex_h = gc->clip_mask_height + 2;
+    tex_size (dpy, &tex_w, &tex_h);
+
+# ifndef HAVE_JWZGLES
+    if (dpy->gl_texture_target == GL_TEXTURE_RECTANGLE_EXT)
+    {
+      glScalef (1, -1, 1);
     }
+    else
+# endif
+    {
+      glScalef (1.0f / tex_w, -1.0f / tex_h, 1);
+    }
+
+    glTranslatef (1 - gc->gcv.clip_x_origin,
+                  1 - gc->gcv.clip_y_origin - (int)gc->clip_mask_height - 2,
+                  0);
+  } else {
+    glDisable (dpy->gl_texture_target);
   }
+  glActiveTexture (GL_TEXTURE0);
 }
 
 
@@ -720,14 +845,18 @@ clear_texture (Display *dpy)
                 0, dpy->pixel_format, gl_pixel_type (dpy), NULL);
 }
 
+
 static void
-set_white (void)
+vertex_pointer (Display *dpy, GLenum type, GLsizei stride,
+                const void *pointer)
 {
-#ifdef HAVE_JWZGLES
-  glColor4f (1, 1, 1, 1);
-#else
-  glColor3ub (0xff, 0xff, 0xff);
-#endif
+  glVertexPointer(2, type, stride, pointer);
+  if (dpy->gc_clip_mask) {
+    glClientActiveTexture (GL_TEXTURE1);
+    glEnableClientState (GL_TEXTURE_COORD_ARRAY);
+    glTexCoordPointer (2, type, stride, pointer);
+    glClientActiveTexture (GL_TEXTURE0);
+  }
 }
 
 
@@ -747,7 +876,8 @@ jwxyz_gl_flush (Display *dpy)
 
   // TODO: Use glColor instead of glColorPointer if there's just one color.
   // TODO: Does OpenGL use both GL_COLOR_ARRAY and glColor at the same time?
-  set_white();
+  //       (Probably not.)
+  glColor4f (1, 1, 1, 1);
 
   Bool shifted = dpy->queue_mode == GL_POINTS || dpy->queue_mode == GL_LINES;
   if (shifted) {
@@ -756,9 +886,9 @@ jwxyz_gl_flush (Display *dpy)
   }
 
   glColorPointer (4, GL_UNSIGNED_BYTE, 0, dpy->queue_color);
-  glVertexPointer (2,
-                   dpy->queue_mode == GL_TRIANGLE_STRIP ? GL_FLOAT : GL_SHORT,
-                   0, dpy->queue_vertex);
+  vertex_pointer (dpy,
+                  dpy->queue_mode == GL_TRIANGLE_STRIP ? GL_FLOAT : GL_SHORT,
+                  0, dpy->queue_vertex);
   glDrawArrays (dpy->queue_mode, 0, dpy->queue_size);
 
   // TODO: This is right, right?
@@ -766,8 +896,8 @@ jwxyz_gl_flush (Display *dpy)
     Assert (!(dpy->queue_size % 2), "bad count for GL_LINES");
     glColorPointer (4, GL_UNSIGNED_BYTE, sizeof(GLubyte) * 8,
                     dpy->queue_color);
-    glVertexPointer (2, GL_SHORT, sizeof(GLshort) * 4,
-                     (GLshort *)dpy->queue_vertex + 2);
+    vertex_pointer (dpy, GL_SHORT, sizeof(GLshort) * 4,
+                    (GLshort *)dpy->queue_vertex + 2);
     glDrawArrays (GL_POINTS, 0, dpy->queue_size / 2);
   }
 
@@ -833,30 +963,44 @@ jwxyz_gl_copy_area_write_tex_image (Display *dpy, GC gc, int src_x, int src_y,
   /* Must match what's in jwxyz_gl_copy_area_read_tex_image. */
   glBindTexture (dpy->gl_texture_target, dpy->textures[texture_rgba]);
 
-  jwxyz_gl_draw_image (dpy->gl_texture_target, tex_w, tex_h, 0, 0,
-                       width, height, dst_x, dst_y);
+  jwxyz_gl_draw_image (dpy, gc, dpy->gl_texture_target, tex_w, tex_h,
+                       0, 0, gc->depth, width, height, dst_x, dst_y, False);
 
   clear_texture (dpy);
 }
 
 
 void
-jwxyz_gl_draw_image (GLenum gl_texture_target,
+jwxyz_gl_draw_image (Display *dpy, GC gc, GLenum gl_texture_target,
                      unsigned int tex_w, unsigned int tex_h,
-                     int src_x, int src_y,
+                     int src_x, int src_y, int src_depth,
                      unsigned int width, unsigned int height,
-                     int dst_x, int dst_y)
+                     int dst_x, int dst_y, Bool flip_y)
 {
-  set_white ();
-  glEnable (gl_texture_target);
+  if (!gc || src_depth == gc->depth) {
+    glTexEnvi (GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
+  } else {
+    Assert (src_depth == 1 && gc->depth == 32,
+            "jwxyz_gl_draw_image: bad depths");
+
+    set_color (dpy, gc->gcv.background, gc->depth, gc->gcv.alpha_allowed_p);
+
+    GLfloat rgba[4];
+    query_color (dpy, gc->gcv.foreground, gc->depth, gc->gcv.alpha_allowed_p,
+                 rgba);
+    glTexEnvi (GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_BLEND);
+    glTexEnvfv (GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, rgba);
+  }
 
+  glEnable (gl_texture_target);
   glEnableClientState (GL_TEXTURE_COORD_ARRAY);
   glEnableClientState (GL_VERTEX_ARRAY);
-    
-  /* TODO: Copied from XPutImage. Refactor. */
+
+  Assert (!glIsEnabled (GL_COLOR_ARRAY), "glIsEnabled (GL_COLOR_ARRAY)");
+  Assert (!glIsEnabled (GL_NORMAL_ARRAY), "glIsEnabled (GL_NORMAL_ARRAY)");
+
   /* TODO: EXT_draw_texture or whatever it's called. */
-  GLfloat vertices[4][2] =
-  {
+  GLfloat vertices[4][2] = {
     {dst_x, dst_y},
     {dst_x, dst_y + height},
     {dst_x + width, dst_y + height},
@@ -864,9 +1008,14 @@ jwxyz_gl_draw_image (GLenum gl_texture_target,
   };
 
   GLfloat
-    tex_x0 = src_x, tex_y0 = src_y + height,
+    tex_x0 = src_x, tex_y0 = src_y,
     tex_x1 = src_x + width, tex_y1 = src_y;
 
+  if (flip_y)
+    tex_y1 += height;
+  else
+    tex_y0 += height;
+
 # ifndef HAVE_JWZGLES
   if (gl_texture_target != GL_TEXTURE_RECTANGLE_EXT)
 # endif
@@ -878,21 +1027,22 @@ jwxyz_gl_draw_image (GLenum gl_texture_target,
     tex_y1 *= my;
   }
 
-  GLfloat tex_coords[4][2] =
-  {
+  GLfloat tex_coords[4][2] = {
     {tex_x0, tex_y0},
     {tex_x0, tex_y1},
     {tex_x1, tex_y1},
     {tex_x1, tex_y0}
   };
 
-  glVertexPointer (2, GL_FLOAT, 0, vertices);
+  vertex_pointer (dpy, GL_FLOAT, 0, vertices);
   glTexCoordPointer (2, GL_FLOAT, 0, tex_coords);
   glDrawArrays (GL_TRIANGLE_FAN, 0, 4);
 
+//clear_texture();
   glDisable (gl_texture_target);
 }
 
+#if 0
 void
 jwxyz_gl_copy_area_read_pixels (Display *dpy, Drawable src, Drawable dst,
                                 GC gc, int src_x, int src_y,
@@ -903,6 +1053,7 @@ jwxyz_gl_copy_area_read_pixels (Display *dpy, Drawable src, Drawable dst,
   XPutImage (dpy, dst, gc, img, 0, 0, dst_x, dst_y, width, height);
   XDestroyImage (img);
 }
+#endif
 
 
 static int
@@ -934,7 +1085,7 @@ DrawLines (Display *dpy, Drawable d, GC gc, XPoint *points, int count,
   
   glEnableClientState (GL_VERTEX_ARRAY);
   glDisableClientState (GL_TEXTURE_COORD_ARRAY);
-  glVertexPointer (2, GL_SHORT, 0, vertices);
+  vertex_pointer (dpy, GL_SHORT, 0, vertices);
   glDrawArrays (GL_LINE_STRIP, 0, count);
   
   free (vertices);
@@ -942,7 +1093,7 @@ DrawLines (Display *dpy, Drawable d, GC gc, XPoint *points, int count,
   if (gc->gcv.cap_style != CapNotLast) {
     // TODO: How does this look with multisampling?
     // TODO: Disable me for closed loops.
-    glVertexPointer (2, GL_SHORT, 0, p);
+    vertex_pointer (dpy, GL_SHORT, 0, p);
     glDrawArrays (GL_POINTS, 0, 1);
   }
 
@@ -1114,7 +1265,7 @@ FillPolygon (Display *dpy, Drawable d, GC gc,
     glEnableClientState (GL_VERTEX_ARRAY);
     glDisableClientState (GL_TEXTURE_COORD_ARRAY);
 
-    glVertexPointer (2, GL_SHORT, 0, vertices);
+    vertex_pointer (dpy, GL_SHORT, 0, vertices);
     glDrawArrays (GL_TRIANGLE_FAN, 0, npoints);
 
     free(vertices);
@@ -1126,7 +1277,7 @@ FillPolygon (Display *dpy, Drawable d, GC gc,
     linked_point *root;
     root = (linked_point *) malloc( sizeof(linked_point) );
     set_points_list(points,npoints,root);
-    traverse_points_list(root);
+    traverse_points_list(dpy, root);
 
   } else {
     Assert((shape == Convex || shape == Nonconvex),
@@ -1279,7 +1430,7 @@ draw_arc_gl (Display *dpy, Drawable d, GC gc, int x, int y,
   glDisableClientState (GL_TEXTURE_COORD_ARRAY);
   glEnableClientState (GL_VERTEX_ARRAY);
   
-  glVertexPointer (2, GL_FLOAT, 0, data);
+  vertex_pointer (dpy, GL_FLOAT, 0, data);
   glDrawArrays (fill_p ? GL_TRIANGLE_FAN : GL_LINE_STRIP,
                 0,
                 (GLsizei)((data_ptr - data) / 2));
@@ -1340,21 +1491,26 @@ CreateGC (Display *dpy, Drawable d, unsigned long mask, XGCValues *xgcv)
 }
 
 
+static void
+free_clip_mask (Display *dpy, GC gc)
+{
+  if (gc->gcv.clip_mask) {
+    if (dpy->gc_clip_mask == gc->clip_mask) {
+      jwxyz_gl_flush (dpy);
+      dpy->gc_clip_mask = 0;
+    }
+    glDeleteTextures (1, &gc->clip_mask);
+  }
+}
+
+
 static int
 FreeGC (Display *dpy, GC gc)
 {
   if (gc->gcv.font)
     XUnloadFont (dpy, gc->gcv.font);
 
-  // TODO
-/*
-  Assert (!!gc->gcv.clip_mask == !!gc->clip_mask, "GC clip mask mixup");
-
-  if (gc->gcv.clip_mask) {
-    XFreePixmap (dpy, gc->gcv.clip_mask);
-    CGImageRelease (gc->clip_mask);
-  }
-*/
+  free_clip_mask (dpy, gc);
   free (gc);
   return 0;
 }
@@ -1447,8 +1603,6 @@ PutImage (Display *dpy, Drawable d, GC gc, XImage *ximage,
 # endif
 
     // glPixelStorei (GL_UNPACK_ALIGNMENT, 4); // Probably unnecessary.
-
-    set_white ();
   } else {
     Assert (bpp == 1, "expected 1 or 32 bpp");
     Assert ((src_x % 8) == 0,
@@ -1526,9 +1680,6 @@ PutImage (Display *dpy, Drawable d, GC gc, XImage *ximage,
     tex_index = texture_mono;
 
     // glPixelStorei (GL_UNPACK_ALIGNMENT, 1);
-
-    set_color (dpy, gc->gcv.foreground, gc->depth, gc->gcv.alpha_allowed_p);
-    // TODO: Deal with the background color.
   }
 
 # if 1 // defined HAVE_JWZGLES
@@ -1536,80 +1687,19 @@ PutImage (Display *dpy, Drawable d, GC gc, XImage *ximage,
   // TODO: Make use of OES_draw_texture.
 
   unsigned tex_w = src_w, tex_h = h;
-  if (!dpy->gl_texture_npot_p) {
-    tex_w = to_pow2(tex_w);
-    tex_h = to_pow2(tex_h);
-  }
-
   glBindTexture (dpy->gl_texture_target, dpy->textures[tex_index]);
 
   // A fun project: reimplement xshm.c by means of a PBO using
   // GL_MAP_UNSYNCHRONIZED_BIT.
 
-  // TODO: Would using glTexSubImage2D exclusively be faster?
-  if (tex_w == src_w && tex_h == h) {
-    glTexImage2D (dpy->gl_texture_target, 0, tex_internalformat, tex_w, tex_h,
-                  0, tex_format, tex_type, tex_data);
-  } else {
-    // TODO: Sampling the last row might be a problem if src_x != 0.
-    glTexImage2D (dpy->gl_texture_target, 0, tex_internalformat, tex_w, tex_h,
-                  0, tex_format, tex_type, NULL);
-    glTexSubImage2D (dpy->gl_texture_target, 0, 0, 0, src_w, h,
-                     tex_format, tex_type, tex_data);
-  }
+  tex_image (dpy, tex_internalformat, &tex_w, &tex_h, tex_format, tex_type,
+             tex_data);
 
   if (bpp == 1)
     free(tex_data);
 
-  // TODO: This looks a lot like jwxyz_gl_draw_image. Refactor.
-
-  // glEnable (dpy->gl_texture_target);
-  // glColor4f (0.5, 0, 1, 1);
-  glEnable (dpy->gl_texture_target);
-  glEnableClientState (GL_VERTEX_ARRAY);
-  glEnableClientState (GL_TEXTURE_COORD_ARRAY);
-
-  // TODO: Why are these ever turned on in the first place?
-  glDisableClientState (GL_COLOR_ARRAY);
-  glDisableClientState (GL_NORMAL_ARRAY);
-  // glDisableClientState (GL_TEXTURE_COORD_ARRAY);
-
-  GLfloat vertices[4][2] =
-  {
-    {dest_x, dest_y},
-    {dest_x, dest_y + h},
-    {dest_x + w, dest_y + h},
-    {dest_x + w, dest_y}
-  };
-
-  GLfloat texcoord_w, texcoord_h;
-#  ifndef HAVE_JWZGLES
-  if (dpy->gl_texture_target == GL_TEXTURE_RECTANGLE_EXT) {
-    texcoord_w = w;
-    texcoord_h = h;
-  } else
-#  endif /* HAVE_JWZGLES */
-  {
-    texcoord_w = (double)w / tex_w;
-    texcoord_h = (double)h / tex_h;
-  }
-
-  GLfloat tex_coords[4][2];
-  tex_coords[0][0] = 0;
-  tex_coords[0][1] = 0;
-  tex_coords[1][0] = 0;
-  tex_coords[1][1] = texcoord_h;
-  tex_coords[2][0] = texcoord_w;
-  tex_coords[2][1] = texcoord_h;
-  tex_coords[3][0] = texcoord_w;
-  tex_coords[3][1] = 0;
-
-  glVertexPointer (2, GL_FLOAT, 0, vertices);
-  glTexCoordPointer (2, GL_FLOAT, 0, tex_coords);
-  glDrawArrays (GL_TRIANGLE_FAN, 0, 4);
-
-//clear_texture();
-  glDisable (dpy->gl_texture_target);
+  jwxyz_gl_draw_image (dpy, gc, dpy->gl_texture_target, tex_w, tex_h,
+                       0, 0, bpp, w, h, dest_x, dest_y, True);
 # else
   glRasterPos2i (dest_x, dest_y);
   glPixelZoom (1, -1);
@@ -1738,47 +1828,101 @@ GetSubImage (Display *dpy, Drawable d, int x, int y,
 }
 
 
-#if 0
-static Pixmap
-copy_pixmap (Display *dpy, Pixmap p)
+static int
+SetClipMask (Display *dpy, GC gc, Pixmap m)
 {
-  if (!p) return 0;
-  Assert (p->type == PIXMAP, "not a pixmap");
-
-  Pixmap p2 = 0;
-
-  Window root;
-  int x, y;
-  unsigned int width, height, border_width, depth;
-  if (XGetGeometry (dpy, p, &root,
-                    &x, &y, &width, &height, &border_width, &depth)) {
-    XGCValues gcv;
-    gcv.function = GXcopy;
-    GC gc = XCreateGC (dpy, p, GCFunction, &gcv);
-    if (gc) {
-      p2 = XCreatePixmap (dpy, p, width, height, depth);
-      if (p2)
-        XCopyArea (dpy, p, p2, gc, 0, 0, width, height, 0, 0);
-      XFreeGC (dpy, gc);
-    }
+  Assert (m != dpy->main_window, "not a pixmap");
+
+  free_clip_mask (dpy, gc);
+
+  if (!m) {
+    gc->clip_mask = 0;
+    return 0;
   }
 
-  Assert (p2, "could not copy pixmap");
+  Assert (jwxyz_drawable_depth (m) == 1, "wrong depth for clip mask");
 
-  return p2;
-}
-#endif
+  const XRectangle *frame = jwxyz_frame (m);
+  gc->clip_mask_width = frame->width;
+  gc->clip_mask_height = frame->height;
 
+  /* Apparently GL_ALPHA isn't a valid format for a texture used in an FBO:
 
-static int
-SetClipMask (Display *dpy, GC gc, Pixmap m)
-{
-  Log ("TODO: No clip masks yet");
-  /* Protip: Do glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
-     clearing just the stencil buffer in a packed depth/stencil arrangement is
-     slower than the above. Adreno recommends this, but other GPUs probably
-     benefit as well.
+    (86) Are any one- or two- component formats color-renderable?
+
+            Presently none of the one- or two- component texture formats
+            defined in unextended OpenGL is color-renderable.  The R
+            and RG float formats defined by the NV_float_buffer
+            extension are color-renderable.
+
+            Although an early draft of the FBO specification permitted
+            rendering into alpha, luminance, and intensity formats, this
+            this capability was pulled when it was realized that it was
+            under-specified exactly how rendering into these formats
+            would work.  (specifically, how R/G/B/A map to I/L/A)
+
+     <https://www.khronos.org/registry/OpenGL/extensions/EXT/EXT_framebuffer_object.txt>
+
+     ...Therefore, 1-bit drawables get to have wasted color channels.
+     Currently, R=G=B=grey, and the alpha channel is unused.
    */
+
+  /* There doesn't seem to be any way to move what's on one of the color
+     channels to the alpha channel in GLES 1.1 without leaving the GPU.
+   */
+
+  /* GLES 1.1 only supports GL_CLAMP_TO_EDGE or GL_REPEAT for
+     GL_TEXTURE_WRAP_S and GL_TEXTURE_WRAP_T, so to prevent drawing outside
+     the clip mask pixmap, there's two options:
+     1. Use glScissor.
+     2. Add a black border.
+
+     Option #2 is in use here.
+
+     The following converts in-place an RGBA image to alpha-only image with a
+     1px black border.
+   */
+
+  // Prefix/suffix: 1 row+1 pixel, rounded to nearest int32.
+  size_t pad0 = frame->width + 3, pad = (pad0 + 3) & ~3;
+  uint8_t *data = malloc(2 * pad + frame->width * frame->height * 4);
+
+  uint8_t *rgba = data + pad;
+  uint8_t *alpha = rgba;
+  uint8_t *alpha0 = alpha - pad0;
+  memset (alpha0, 0, pad0); // Top row.
+
+  jwxyz_gl_flush (dpy);
+  jwxyz_bind_drawable (dpy, dpy->main_window, m);
+  glReadPixels (0, 0, frame->width, frame->height, GL_RGBA, GL_UNSIGNED_BYTE,
+                rgba);
+
+  for (unsigned y = 0; y != frame->height; ++y) {
+    for (unsigned x = 0; x != frame->width; ++x)
+      alpha[x] = rgba[x * 4];
+
+    rgba += frame->width * 4;
+
+    alpha += frame->width;
+    alpha[0] = 0;
+    alpha[1] = 0;
+    alpha += 2;
+  }
+
+  alpha -= 2;
+  memset (alpha, 0, pad0); // Bottom row.
+
+  glGenTextures (1, &gc->clip_mask);
+  tex_parameters (dpy, gc->clip_mask);
+
+  glPixelStorei (GL_UNPACK_ALIGNMENT, 1);
+
+  unsigned tex_w = frame->width + 2, tex_h = frame->height + 2;
+  tex_image (dpy, GL_ALPHA, &tex_w, &tex_h, GL_ALPHA, GL_UNSIGNED_BYTE,
+             alpha0);
+
+  free(data);
+
   return 0;
 }
 
@@ -1859,7 +2003,7 @@ Bool is_same_slope(linked_point * a)
     return aa == bb;
 }
 
-void draw_three_vertices(linked_point * a, Bool triangle)
+void draw_three_vertices(Display *dpy, linked_point * a, Bool triangle)
 {
 
     linked_point *b;
@@ -1882,7 +2026,7 @@ void draw_three_vertices(linked_point * a, Bool triangle)
     }
 
     glEnableClientState(GL_VERTEX_ARRAY);
-    glVertexPointer(2, GL_FLOAT, 0, vertices);
+    vertex_pointer(dpy, GL_FLOAT, 0, vertices);
     glDrawArrays(drawType, 0, 3);
 
     free(b);  // cut midpoint off from remaining polygon vertex list
@@ -1919,16 +2063,16 @@ Bool is_three_point_loop(linked_point * head)
 }
 
 
-void traverse_points_list(linked_point * root)
+void traverse_points_list(Display *dpy, linked_point * root)
 {
     linked_point *head;
     head = root;
 
     while (!is_three_point_loop(head)) {
         if (is_an_ear(head)) {
-            draw_three_vertices(head, True);
+            draw_three_vertices(dpy, head, True);
         } else if (is_same_slope(head)) {
-            draw_three_vertices(head, False);
+            draw_three_vertices(dpy, head, False);
         } else {
             head = head->next;
         }
@@ -1936,9 +2080,9 @@ void traverse_points_list(linked_point * root)
 
     // handle final three vertices in polygon
     if (is_an_ear(head)) {
-        draw_three_vertices(head, True);
+        draw_three_vertices(dpy, head, True);
     } else if (is_same_slope(head)) {
-        draw_three_vertices(head, False);
+        draw_three_vertices(dpy, head, False);
     } else {
         free(head->next->next);
         free(head->next);