diff --git a/test/tutorial/tut03.cpp b/test/tutorial/tut03.cpp
index 93b8fd25..6cbb546a 100644
--- a/test/tutorial/tut03.cpp
+++ b/test/tutorial/tut03.cpp
@@ -56,8 +56,8 @@ public:
         m_size = size;
         m_size.x = (m_size.x + 15) & ~15;
         m_size.y = (m_size.y + 15) & ~15;
-        m_texel_settings = vec4(vec2(1.0, 1.0) / (vec2)m_size,
-                                vec2(0.5, 0.5) * (vec2)m_size);
+        m_texel_settings = vec4(1.0, 1.0, 2.0, 2.0) / (vec4)m_size.xyxy();
+        m_screen_settings = vec4(1.0, 1.0, 0.5, 0.5) * (vec4)m_size.xyxy();
 
         /* Window size decides the world aspect ratio. For instance, 640×480
          * will be mapped to (-0.66,-0.5) - (0.66,0.5). */
@@ -78,6 +78,7 @@ public:
         m_pixels = new u8vec4[m_size.x * m_size.y];
         m_tmppixels = new u8vec4[m_size.x / 2 * m_size.y / 2];
         m_frame = -1;
+        m_slices = 4;
         for (int i = 0; i < 4; i++)
         {
             m_deltashift[i] = 0.0;
@@ -96,6 +97,7 @@ public:
         m_translate = 0;
         m_radius = 5.0;
         m_ready = false;
+        m_drag = false;
 
         m_palette = new u8vec4[(MAX_ITERATIONS + 1) * PALETTE_STEP];
         for (int i = 0; i < (MAX_ITERATIONS + 1) * PALETTE_STEP; i++)
@@ -157,14 +159,14 @@ public:
         delete m_palette;
     }
 
-    inline f64cmplx TexelToWorldOffset(ivec2 texel)
+    inline f64cmplx TexelToWorldOffset(vec2 texel)
     {
         double dx = (0.5 + texel.x - m_size.x / 2) * m_texel2world.x;
         double dy = (0.5 + m_size.y / 2 - texel.y) * m_texel2world.y;
         return m_radius * f64cmplx(dx, dy);
     }
 
-    inline f64cmplx ScreenToWorldOffset(ivec2 pixel)
+    inline f64cmplx ScreenToWorldOffset(vec2 pixel)
     {
         /* No 0.5 offset here, because we want to be able to position the
          * mouse at (0,0) exactly. */
@@ -186,17 +188,30 @@ public:
 #if !defined __CELLOS_LV2__
         if (buttons[1])
         {
-            if (clicked[1])
+            if (!m_drag)
+            {
                 m_oldmouse = mousepos;
+                m_drag = true;
+            }
             m_translate = ScreenToWorldOffset(m_oldmouse)
                         - ScreenToWorldOffset(mousepos);
+            /* XXX: the purpose of this hack is to avoid translating by
+             * an exact number of pixels. If this were to happen, the step()
+             * optimisation for i915 cards in our shader would behave
+             * incorrectly because a quarter of the pixels in the image
+             * would have tie rankings in the distance calculation. */
+            m_translate *= 1023.0 / 1024.0;
             m_oldmouse = mousepos;
         }
-        else if (m_translate != 0.0)
+        else
         {
-            m_translate *= pow(2.0, -deltams * 0.005);
-            if (m_translate.norm() / m_radius < 1e-4)
-                m_translate = 0.0;
+            m_drag = false;
+            if (m_translate != 0.0)
+            {
+                m_translate *= pow(2.0, -deltams * 0.005);
+                if (m_translate.norm() / m_radius < 1e-4)
+                    m_translate = 0.0;
+            }
         }
 
         if ((buttons[0] || buttons[2]) && mousepos.x != -1)
@@ -209,7 +224,7 @@ public:
         else if (m_zoom_speed)
         {
             m_zoom_speed *= pow(2.0, -deltams * 0.005);
-            if (abs(m_zoom_speed) < 1e-5)
+            if (abs(m_zoom_speed) < 1e-5 || m_drag)
                 m_zoom_speed = 0.0;
         }
 #endif
@@ -286,54 +301,88 @@ public:
         m_zoomtext->SetText(buf);
 #endif
 
-        u8vec4 *m_pixelstart = m_pixels + m_size.x * m_size.y / 4 * m_frame;
-
         if (m_dirty[m_frame])
         {
-            double const maxsqlen = 1024;
-            double const k1 = 1.0 / (1 << 10) / log2(maxsqlen);
-
             m_dirty[m_frame]--;
 
-            for (int j = ((m_frame + 1) % 4) / 2; j < m_size.y; j += 2)
-            for (int i = m_frame % 2; i < m_size.x; i += 2)
+            /* FIXME: this is the ugliest, most pathetic excuse for a
+             * threading system that I have seen in a while. */
+            DoWorkHelper helpers[m_slices];
+            for (int slice = 0; slice < m_slices; slice++)
             {
+                helpers[slice].fractal = this;
+                helpers[slice].slice = slice;
+                helpers[slice].thread = new Thread(DoWorkHelper::Help,
+                                                   &helpers[slice]);
+            }
+            for (int slice = 0; slice < m_slices; slice++)
+            {
+                delete helpers[slice].thread;
+            }
+        }
+    }
+
+    struct DoWorkHelper
+    {
+        Fractal *fractal;
+        Thread *thread;
+        int slice;
+
+        static void *Help(void *data)
+        {
+            DoWorkHelper *helper = (DoWorkHelper *)data;
+            helper->fractal->DoWork(helper->slice);
+            return NULL;
+        }
+    };
+
+    void DoWork(int slice)
+    {
+        double const maxsqlen = 1024;
+        double const k1 = 1.0 / (1 << 10) / log2(maxsqlen);
+
+        int jmin = m_size.y * slice / m_slices;
+        int jmax = m_size.y * (slice + 1) / m_slices;
+        u8vec4 *m_pixelstart = m_pixels
+                             + m_size.x * (m_size.y / 4 * m_frame + jmin / 4);
 
-                f64cmplx z0 = m_center + TexelToWorldOffset(ivec2(i, j));
-                f64cmplx r0 = z0;
-                //f64cmplx r0(0.28693186889504513, 0.014286693904085048);
-                //f64cmplx r0(0.001643721971153, 0.822467633298876);
-                //f64cmplx r0(-1.207205434596, 0.315432814901);
-                //f64cmplx r0(-0.79192956889854, -0.14632423080102);
-                //f64cmplx r0(0.3245046418497685, 0.04855101129280834);
-                f64cmplx z;
-                int iter = MAX_ITERATIONS;
-                for (z = z0; iter && z.sqlen() < maxsqlen; z = z * z + r0)
-                    --iter;
-
-                if (iter)
-                {
-                    double f = iter;
-                    double n = z.sqlen();
-                    if (n > maxsqlen * maxsqlen)
-                        n = maxsqlen * maxsqlen;
-
-                    /* Approximate log(sqrt(n))/log(sqrt(maxsqlen)) */
-                    union { double n; uint64_t x; } u = { n };
-                    double k = (u.x >> 42) - (((1 << 10) - 1) << 10);
-                    k *= k1;
-
-                    /* Approximate log2(k) in [1,2]. */
-                    f += (- 0.344847817623168308695977510213252644185 * k
-                          + 2.024664188044341212602376988171727038739) * k
-                          - 1.674876738008591047163498125918330313237;
-
-                    *m_pixelstart++ = m_palette[(int)(f * PALETTE_STEP)];
-                }
-                else
-                {
-                    *m_pixelstart++ = u8vec4(0, 0, 0, 255);
-                }
+        for (int j = ((m_frame + 1) % 4) / 2 + jmin; j < jmax; j += 2)
+        for (int i = m_frame % 2; i < m_size.x; i += 2)
+        {
+            f64cmplx z0 = m_center + TexelToWorldOffset(ivec2(i, j));
+            f64cmplx r0 = z0;
+            //f64cmplx r0(0.28693186889504513, 0.014286693904085048);
+            //f64cmplx r0(0.001643721971153, 0.822467633298876);
+            //f64cmplx r0(-1.207205434596, 0.315432814901);
+            //f64cmplx r0(-0.79192956889854, -0.14632423080102);
+            //f64cmplx r0(0.3245046418497685, 0.04855101129280834);
+            f64cmplx z;
+            int iter = MAX_ITERATIONS;
+            for (z = z0; iter && z.sqlen() < maxsqlen; z = z * z + r0)
+                --iter;
+
+            if (iter)
+            {
+                double f = iter;
+                double n = z.sqlen();
+                if (n > maxsqlen * maxsqlen)
+                    n = maxsqlen * maxsqlen;
+
+                /* Approximate log(sqrt(n))/log(sqrt(maxsqlen)) */
+                union { double n; uint64_t x; } u = { n };
+                double k = (u.x >> 42) - (((1 << 10) - 1) << 10);
+                k *= k1;
+
+                /* Approximate log2(k) in [1,2]. */
+                f += (- 0.344847817623168308695977510213252644185 * k
+                      + 2.024664188044341212602376988171727038739) * k
+                      - 1.674876738008591047163498125918330313237;
+
+                *m_pixelstart++ = m_palette[(int)(f * PALETTE_STEP)];
+            }
+            else
+            {
+                *m_pixelstart++ = u8vec4(0, 0, 0, 255);
             }
         }
     }
@@ -390,6 +439,7 @@ public:
                 ""
                 "uniform mat4 u_ZoomSettings;"
                 "uniform vec4 u_TexelSize;"
+                "uniform vec4 u_ScreenSize;"
                 ""
                 "attribute vec2 a_TexCoord;"
                 "attribute vec2 a_Vertex;"
@@ -418,7 +468,6 @@ public:
                 "                       u_ZoomSettings[1][1],"
                 "                       u_ZoomSettings[2][1],"
                 "                       u_ZoomSettings[3][1]);"
-                     /* Pass all this to the fragment shader */
                 "    v_CenterX = zoomscale * a_TexCoord.x + zoomtx"
                 "              + offsets.xyxy * u_TexelSize.x;"
                 "    v_CenterY = zoomscale * a_TexCoord.y - zoomty"
@@ -427,8 +476,8 @@ public:
                       * point lies. The fragment shader will call floor() on
                       * this value. We add or remove a slight offset to avoid
                       * rounding issues at the image's edges. */
-                "    v_IndexX = v_CenterX * u_TexelSize.z - offsets.zwzw;"
-                "    v_IndexY = v_CenterY * u_TexelSize.w - offsets.zwwz;"
+                "    v_IndexX = v_CenterX * u_ScreenSize.z - (offsets.zwzw + vec4(0.001, 0.002, 0.003, 0.004));"
+                "    v_IndexY = v_CenterY * u_ScreenSize.w - (offsets.zwwz + vec4(0.0015, 0.0025, 0.0035, 0.0045));"
                 "}",
 
 #if !defined HAVE_GLES_2X
@@ -444,33 +493,59 @@ public:
                 ""
                 "void main(void)"
                 "{"
-                     /* Get a pixel coordinate from each slice into rx & ry */
-                "    vec4 rx = u_TexelSize.x * (1.0 + 2.0 * floor(v_IndexX));"
-                "    vec4 ry = u_TexelSize.y * (1.0 + 2.0 * floor(v_IndexY));"
-                     /* Compute distance to expected pixel in dd */
                 "    vec4 v05 = vec4(0.5, 0.5, 0.5, 0.5);"
-                "    vec4 t0 = step(abs(rx - v05), v05)"
-                "            * step(abs(ry - v05), v05);"
-                "    vec4 dx = rx - v_CenterX;"
-                "    vec4 dy = ry - v_CenterY;"
+                "    vec4 rx, ry, t0, dx, dy, dd;"
+                     /* Get a pixel coordinate from each slice into rx & ry */
+                "    rx = u_TexelSize.x + u_TexelSize.z * floor(v_IndexX);"
+                "    ry = u_TexelSize.y + u_TexelSize.w * floor(v_IndexY);"
+                     /* Compute inverse distance to expected pixel in dd,
+                      * and put zero if we fall outside the texture. */
+                "    t0 = step(abs(rx - v05), v05) * step(abs(ry - v05), v05);"
+                "    dx = rx - v_CenterX;"
+                "    dy = ry - v_CenterY;"
                 //"    vec4 dd = t0 * (abs(dx) + abs(dy));"
                 //"    vec4 dd = t0 / (0.001 + sqrt((dx * dx) + (dy * dy)));"
-                "    vec4 dd = t0 / (0.000001 + (dx * dx) + (dy * dy));"
+                "    dd = t0 / (0.000001 + (dx * dx) + (dy * dy));"
                      /* Modify Y coordinate to select proper quarter. */
                 "    ry = ry * 0.25 + vec4(0.0, 0.25, 0.5, 0.75);"
                 ""
 #if 1
-                     /* Put min(.x,.y) in .x and min(.z,.w) in .z */
-                "    vec4 t1 = step(dd, dd.yyww);"
-                "    rx = mix(rx, rx.yyww, t1);"
-                "    ry = mix(ry, ry.yyww, t1);"
-                "    dd = mix(dd, dd.yyww, t1);"
-                     /* Put min(x,z) in x */
-                "    vec4 t2 = step(dd, dd.zzzz);"
-                "    rx = mix(rx, rx.zzzz, t2);"
-                "    ry = mix(ry, ry.zzzz, t2);"
+                "\n#if 0\n" /* XXX: disabled until we can autodetect i915 */
+                     /* t1.x <-- dd.x > dd.y */
+                     /* t1.y <-- dd.z > dd.w */
+                "    vec2 t1 = step(dd.xz, dd.yw);"
+                     /* ret.x <-- max(rx.x, rx.y) wrt. t1.x */
+                     /* ret.y <-- max(rx.z, rx.w) wrt. t1.y */
+                     /* ret.z <-- max(ry.x, ry.y) wrt. t1.x */
+                     /* ret.w <-- max(ry.z, ry.w) wrt. t1.y */
+                "    vec4 ret = mix(vec4(rx.xz, ry.xz),"
+                "                   vec4(rx.yw, ry.yw), t1.xyxy);"
+                     /* dd.x <-- max(dd.x, dd.y) */
+                     /* dd.z <-- max(dd.z, dd.w) */
+                "    dd.xy = mix(dd.xz, dd.yw, t1);"
+                     /* t2 <-- dd.x > dd.z */
+                "    float t2 = step(dd.x, dd.y);"
+                     /* ret.x <-- max(ret.x, ret.y); */
+                     /* ret.y <-- max(ret.z, ret.yw; */
+                "    ret.xy = mix(ret.xz, ret.yw, t2);"
+                "\n#else\n"
+                     /* Fallback for i915 cards -- the trick to reduce the
+                      * number of operations is to compute both step(a,b)
+                      * and step(b,a) and hope that their sum is 1. This is
+                      * almost always the case, and when it isn't we can
+                      * afford to have a few wrong pixels. However, a real
+                      * problem is when panning the image, because half the
+                      * screen is likely to flicker. To avoid this problem,
+                      * we cheat a little (see m_translate comment above). */
+                "    vec4 t1 = step(dd.xzyw, dd.ywxz);"
+                "    vec4 ret = vec4(rx.xz, ry.xz) * t1.zwzw"
+                "             + vec4(rx.yw, ry.yw) * t1.xyxy;"
+                "    dd.xy = dd.xz * t1.zw + dd.yw * t1.xy;"
+                "    vec2 t2 = step(dd.xy, dd.yx);"
+                "    ret.xy = ret.xz * t2.yy + ret.yw * t2.xx;"
+                "\n#endif\n"
                      /* Nearest neighbour */
-                "    gl_FragColor = texture2D(in_Texture, vec2(rx.x, ry.x));"
+                "    gl_FragColor = texture2D(in_Texture, ret.xy);"
 #else
                      /* Alternate version: some kind of linear interpolation */
                 "    vec4 p0 = texture2D(in_Texture, vec2(rx.x, ry.x));"
@@ -545,6 +620,7 @@ public:
             m_vertexattrib = m_shader->GetAttribLocation("a_Vertex");
             m_texattrib = m_shader->GetAttribLocation("a_TexCoord");
             m_texeluni = m_shader->GetUniformLocation("u_TexelSize");
+            m_screenuni = m_shader->GetUniformLocation("u_ScreenSize");
             m_zoomuni = m_shader->GetUniformLocation("u_ZoomSettings");
             m_ready = true;
 
@@ -591,6 +667,7 @@ public:
 
         m_shader->Bind();
         m_shader->SetUniform(m_texeluni, m_texel_settings);
+        m_shader->SetUniform(m_screenuni, m_screen_settings);
         m_shader->SetUniform(m_zoomuni, m_zoom_settings);
 #if !defined __CELLOS_LV2__ && !defined __ANDROID__
         glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
@@ -641,13 +718,13 @@ private:
     GLuint m_vbo, m_tbo;
     GLuint m_tco;
 #endif
-    int m_vertexattrib, m_texattrib, m_texeluni, m_zoomuni;
-    int m_frame, m_dirty[4];
-    bool m_ready;
+    int m_vertexattrib, m_texattrib, m_texeluni, m_screenuni, m_zoomuni;
+    int m_frame, m_slices, m_dirty[4];
+    bool m_ready, m_drag;
 
     f64cmplx m_center, m_translate;
     double m_zoom_speed, m_radius;
-    vec4 m_texel_settings;
+    vec4 m_texel_settings, m_screen_settings;
     mat4 m_zoom_settings;
     f64cmplx m_deltashift[4];
     double m_deltascale[4];