diff --git a/js/data/symbol_bucket.js b/js/data/symbol_bucket.js
index 70954647980..e8d49dc8552 100644
--- a/js/data/symbol_bucket.js
+++ b/js/data/symbol_bucket.js
@@ -333,7 +333,7 @@ SymbolBucket.prototype.placeFeatures = function(collisionTile, buffers, collisio
         this.symbolInstances.sort(function(a, b) {
             var aRotated = sin * a.x + cos * a.y;
             var bRotated = sin * b.x + cos * b.y;
-            return bRotated - aRotated;
+            return aRotated - bRotated;
         });
     }
 
diff --git a/js/render/draw_background.js b/js/render/draw_background.js
index 3dca5578506..d5cc4bc89d4 100644
--- a/js/render/draw_background.js
+++ b/js/render/draw_background.js
@@ -1,11 +1,13 @@
 'use strict';
 
-var mat3 = require('gl-matrix').mat3;
+var TilePyramid = require('../source/tile_pyramid');
+var pyramid = new TilePyramid({ tileSize: 512 });
 
 module.exports = drawBackground;
 
-function drawBackground(painter, layer, posMatrix) {
+function drawBackground(painter, source, layer) {
     var gl = painter.gl;
+    var transform = painter.transform;
     var color = layer.paint['background-color'];
     var image = layer.paint['background-pattern'];
     var opacity = layer.paint['background-opacity'];
@@ -14,10 +16,14 @@ function drawBackground(painter, layer, posMatrix) {
     var imagePosA = image ? painter.spriteAtlas.getPosition(image.from, true) : null;
     var imagePosB = image ? painter.spriteAtlas.getPosition(image.to, true) : null;
 
+    painter.setDepthSublayer(0);
     if (imagePosA && imagePosB) {
+
+        if (painter.isOpaquePass) return;
+
         // Draw texture fill
         shader = painter.patternShader;
-        gl.switchShader(shader, posMatrix);
+        gl.switchShader(shader);
         gl.uniform1i(shader.u_image, 0);
         gl.uniform2fv(shader.u_pattern_tl_a, imagePosA.tl);
         gl.uniform2fv(shader.u_pattern_br_a, imagePosA.br);
@@ -25,62 +31,48 @@ function drawBackground(painter, layer, posMatrix) {
         gl.uniform2fv(shader.u_pattern_br_b, imagePosB.br);
         gl.uniform1f(shader.u_opacity, opacity);
 
-        var transform = painter.transform;
-        var sizeA = imagePosA.size;
-        var sizeB = imagePosB.size;
-        var center = transform.locationCoordinate(transform.center);
-        var scale = 1 / Math.pow(2, transform.zoomFraction);
-
         gl.uniform1f(shader.u_mix, image.t);
 
-        var matrixA = mat3.create();
-        mat3.scale(matrixA, matrixA, [
-            1 / (sizeA[0] * image.fromScale),
-            1 / (sizeA[1] * image.fromScale)
-        ]);
-        mat3.translate(matrixA, matrixA, [
-            (center.column * transform.tileSize) % (sizeA[0] * image.fromScale),
-            (center.row    * transform.tileSize) % (sizeA[1] * image.fromScale)
-        ]);
-        mat3.rotate(matrixA, matrixA, -transform.angle);
-        mat3.scale(matrixA, matrixA, [
-            scale * transform.width  / 2,
-            -scale * transform.height / 2
-        ]);
+        var factor = (4096 / transform.tileSize) / Math.pow(2, 0);
 
-        var matrixB = mat3.create();
-        mat3.scale(matrixB, matrixB, [
-            1 / (sizeB[0] * image.toScale),
-            1 / (sizeB[1] * image.toScale)
-        ]);
-        mat3.translate(matrixB, matrixB, [
-            (center.column * transform.tileSize) % (sizeB[0] * image.toScale),
-            (center.row    * transform.tileSize) % (sizeB[1] * image.toScale)
-        ]);
-        mat3.rotate(matrixB, matrixB, -transform.angle);
-        mat3.scale(matrixB, matrixB, [
-            scale * transform.width  / 2,
-            -scale * transform.height / 2
+        gl.uniform2fv(shader.u_patternscale_a, [
+            1 / (imagePosA.size[0] * factor * image.fromScale),
+            1 / (imagePosA.size[1] * factor * image.fromScale)
         ]);
 
-        gl.uniformMatrix3fv(shader.u_patternmatrix_a, false, matrixA);
-        gl.uniformMatrix3fv(shader.u_patternmatrix_b, false, matrixB);
+        gl.uniform2fv(shader.u_patternscale_b, [
+            1 / (imagePosB.size[0] * factor * image.toScale),
+            1 / (imagePosB.size[1] * factor * image.toScale)
+        ]);
 
         painter.spriteAtlas.bind(gl, true);
 
     } else {
         // Draw filling rectangle.
+        if (painter.isOpaquePass !== (color[3] === 1)) return;
+
         shader = painter.fillShader;
-        gl.switchShader(shader, posMatrix);
+        gl.switchShader(shader);
         gl.uniform4fv(shader.u_color, color);
     }
 
     gl.disable(gl.STENCIL_TEST);
-    gl.bindBuffer(gl.ARRAY_BUFFER, painter.backgroundBuffer);
-    gl.vertexAttribPointer(shader.a_pos, painter.backgroundBuffer.itemSize, gl.SHORT, false, 0, 0);
-    gl.drawArrays(gl.TRIANGLE_STRIP, 0, painter.backgroundBuffer.itemCount);
-    gl.enable(gl.STENCIL_TEST);
 
+    gl.bindBuffer(gl.ARRAY_BUFFER, painter.tileExtentBuffer);
+    gl.vertexAttribPointer(shader.a_pos, painter.tileExtentBuffer.itemSize, gl.SHORT, false, 0, 0);
+
+    // We need to draw the background in tiles in order to use calculatePosMatrix
+    // which applies the projection matrix (transform.projMatrix). Otherwise
+    // the depth and stencil buffers get into a bad state.
+    // This can be refactored into a single draw call once earcut lands and
+    // we don't have so much going on in the stencil buffer.
+    var coords = pyramid.coveringTiles(transform);
+    for (var c = 0; c < coords.length; c++) {
+        gl.setPosMatrix(painter.calculatePosMatrix(coords[c]));
+        gl.drawArrays(gl.TRIANGLE_STRIP, 0, painter.tileExtentBuffer.itemCount);
+    }
+
+    gl.enable(gl.STENCIL_TEST);
     gl.stencilMask(0x00);
     gl.stencilFunc(gl.EQUAL, 0x80, 0x80);
 }
diff --git a/js/render/draw_circle.js b/js/render/draw_circle.js
index 02ba098d686..bf6f1ff63cc 100644
--- a/js/render/draw_circle.js
+++ b/js/render/draw_circle.js
@@ -4,27 +4,21 @@ var browser = require('../util/browser.js');
 
 module.exports = drawCircles;
 
-function drawCircles(painter, layer, posMatrix, tile) {
-    // short-circuit if tile is empty
-    if (!tile.buffers) return;
+function drawCircles(painter, source, layer, coords) {
+    if (painter.isOpaquePass) return;
 
-    posMatrix = painter.translateMatrix(posMatrix, tile, layer.paint['circle-translate'], layer.paint['circle-translate-anchor']);
+    var gl = painter.gl;
 
-    if (!tile.elementGroups[layer.ref || layer.id]) return;
-    var elementGroups = tile.elementGroups[layer.ref || layer.id].circle;
+    var shader = painter.circleShader;
+    painter.gl.switchShader(shader);
 
-    var gl = painter.gl;
+    painter.setDepthSublayer(0);
+    painter.depthMask(false);
 
     // Allow circles to be drawn across boundaries, so that
     // large circles are not clipped to tiles
     gl.disable(gl.STENCIL_TEST);
 
-    gl.switchShader(painter.circleShader, posMatrix, tile.exMatrix);
-
-    var vertex = tile.buffers.circleVertex;
-    var shader = painter.circleShader;
-    var elements = tile.buffers.circleElement;
-
     // antialiasing factor: this is a minimum blur distance that serves as
     // a faux-antialiasing for the circle. since blur is a ratio of the circle's
     // size and the intent is to keep the blur at roughly 1px, the two
@@ -35,18 +29,38 @@ function drawCircles(painter, layer, posMatrix, tile) {
     gl.uniform1f(shader.u_blur, Math.max(layer.paint['circle-blur'], antialias));
     gl.uniform1f(shader.u_size, layer.paint['circle-radius']);
 
-    for (var k = 0; k < elementGroups.groups.length; k++) {
-        var group = elementGroups.groups[k];
-        var offset = group.vertexStartIndex * vertex.itemSize;
+    for (var i = 0; i < coords.length; i++) {
+        var coord = coords[i];
+
+        var tile = source.getTile(coord);
+        if (!tile.buffers) continue;
+        if (!tile.elementGroups[layer.ref || layer.id].circle) continue;
+
+        var elementGroups = tile.elementGroups[layer.ref || layer.id].circle;
+        var vertex = tile.buffers.circleVertex;
+        var elements = tile.buffers.circleElement;
+
+        gl.setPosMatrix(painter.translatePosMatrix(
+            painter.calculatePosMatrix(coord, source.maxzoom),
+            tile,
+            layer.paint['circle-translate'],
+            layer.paint['circle-translate-anchor']
+        ));
+        gl.setExMatrix(painter.transform.exMatrix);
+
+        for (var k = 0; k < elementGroups.groups.length; k++) {
+            var group = elementGroups.groups[k];
+            var offset = group.vertexStartIndex * vertex.itemSize;
 
-        vertex.bind(gl);
-        vertex.setAttribPointers(gl, shader, offset);
+            vertex.bind(gl);
+            vertex.setAttribPointers(gl, shader, offset);
 
-        elements.bind(gl);
+            elements.bind(gl);
 
-        var count = group.elementLength * 3;
-        var elementOffset = group.elementStartIndex * elements.itemSize;
-        gl.drawElements(gl.TRIANGLES, count, gl.UNSIGNED_SHORT, elementOffset);
+            var count = group.elementLength * 3;
+            var elementOffset = group.elementStartIndex * elements.itemSize;
+            gl.drawElements(gl.TRIANGLES, count, gl.UNSIGNED_SHORT, elementOffset);
+        }
     }
 
     gl.enable(gl.STENCIL_TEST);
diff --git a/js/render/draw_collision_debug.js b/js/render/draw_collision_debug.js
index 01f3b34edbd..986956fe492 100644
--- a/js/render/draw_collision_debug.js
+++ b/js/render/draw_collision_debug.js
@@ -1,19 +1,23 @@
 'use strict';
 
-module.exports = drawPlacementDebug;
-
-function drawPlacementDebug(painter, layer, posMatrix, tile) {
+module.exports = drawCollisionDebug;
 
+function drawCollisionDebug(painter, layer, coord, tile) {
+    if (!tile.elementGroups[layer.ref || layer.id]) return;
     var elementGroups = tile.elementGroups[layer.ref || layer.id].collisionBox;
     if (!elementGroups) return;
+    if (!tile.buffers) return;
 
     var gl = painter.gl;
     var buffer = tile.buffers.collisionBoxVertex;
     var shader = painter.collisionBoxShader;
+    var posMatrix = painter.calculatePosMatrix(coord);
 
     gl.enable(gl.STENCIL_TEST);
+    painter.enableTileClippingMask(coord);
 
     gl.switchShader(shader, posMatrix);
+
     buffer.bind(gl);
     buffer.setAttribPointers(gl, shader, 0);
 
diff --git a/js/render/draw_debug.js b/js/render/draw_debug.js
index 8d1d6155ab3..9544a8fe405 100644
--- a/js/render/draw_debug.js
+++ b/js/render/draw_debug.js
@@ -5,33 +5,37 @@ var browser = require('../util/browser');
 
 module.exports = drawDebug;
 
-function drawDebug(painter, tile) {
-    var gl = painter.gl;
+function drawDebug(painter, coords) {
+    if (painter.isOpaquePass) return;
+    if (!painter.options.debug) return;
 
-    // Blend to the front, not the back.
-    gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
+    for (var i = 0; i < coords.length; i++) {
+        drawDebugTile(painter, coords[i]);
+    }
+}
 
-    gl.switchShader(painter.debugShader, tile.posMatrix);
+function drawDebugTile(painter, coord) {
+    var gl = painter.gl;
+
+    var shader = painter.debugShader;
+    gl.switchShader(shader, painter.calculatePosMatrix(coord));
 
     // draw bounding rectangle
     gl.bindBuffer(gl.ARRAY_BUFFER, painter.debugBuffer);
-    gl.vertexAttribPointer(painter.debugShader.a_pos, painter.debugBuffer.itemSize, gl.SHORT, false, 0, 0);
-    gl.uniform4f(painter.debugShader.u_color, 1, 0, 0, 1);
+    gl.vertexAttribPointer(shader.a_pos, painter.debugBuffer.itemSize, gl.SHORT, false, 0, 0);
+    gl.uniform4f(shader.u_color, 1, 0, 0, 1);
     gl.lineWidth(4);
     gl.drawArrays(gl.LINE_STRIP, 0, painter.debugBuffer.itemCount);
 
-    var vertices = textVertices(tile.coord.toString(), 50, 200, 5);
+    var vertices = textVertices(coord.toString(), 50, 200, 5);
 
     gl.bindBuffer(gl.ARRAY_BUFFER, painter.debugTextBuffer);
     gl.bufferData(gl.ARRAY_BUFFER, new Int16Array(vertices), gl.STREAM_DRAW);
-    gl.vertexAttribPointer(painter.debugShader.a_pos, painter.debugTextBuffer.itemSize, gl.SHORT, false, 0, 0);
+    gl.vertexAttribPointer(shader.a_pos, painter.debugTextBuffer.itemSize, gl.SHORT, false, 0, 0);
     gl.lineWidth(8 * browser.devicePixelRatio);
-    gl.uniform4f(painter.debugShader.u_color, 1, 1, 1, 1);
+    gl.uniform4f(shader.u_color, 1, 1, 1, 1);
     gl.drawArrays(gl.LINES, 0, vertices.length / painter.debugTextBuffer.itemSize);
     gl.lineWidth(2 * browser.devicePixelRatio);
-    gl.uniform4f(painter.debugShader.u_color, 0, 0, 0, 1);
+    gl.uniform4f(shader.u_color, 0, 0, 0, 1);
     gl.drawArrays(gl.LINES, 0, vertices.length / painter.debugTextBuffer.itemSize);
-
-    // Revert blending mode to blend to the back.
-    gl.blendFunc(gl.ONE_MINUS_DST_ALPHA, gl.ONE);
 }
diff --git a/js/render/draw_fill.js b/js/render/draw_fill.js
index 92fc1748ed1..ac3b6d56c2e 100644
--- a/js/render/draw_fill.js
+++ b/js/render/draw_fill.js
@@ -1,119 +1,122 @@
 'use strict';
 
 var browser = require('../util/browser');
-var mat3 = require('gl-matrix').mat3;
 
-module.exports = drawFill;
+module.exports = draw;
 
-function drawFill(painter, layer, posMatrix, tile) {
-    // No data
+function draw(painter, source, layer, coords) {
+    var gl = painter.gl;
+
+    var color = layer.paint['fill-color'];
+    var image = layer.paint['fill-pattern'];
+    var strokeColor = layer.paint['fill-outline-color'];
+
+    // Draw fill
+    if (image ? !painter.isOpaquePass : painter.isOpaquePass === (color[3] === 1)) {
+        // Once we switch to earcut drawing we can pull most of the WebGL setup
+        // outside of this coords loop.
+        for (var i = 0; i < coords.length; i++) {
+            drawFill(painter, source, layer, coords[i]);
+        }
+    }
+
+    // Draw stroke
+    if (!painter.isOpaquePass && layer.paint['fill-antialias'] && !(layer.paint['fill-pattern'] && !strokeColor)) {
+        gl.switchShader(painter.outlineShader);
+        gl.lineWidth(2 * browser.devicePixelRatio * 10);
+
+        if (strokeColor) {
+            // If we defined a different color for the fill outline, we are
+            // going to ignore the bits in 0x07 and just care about the global
+            // clipping mask.
+            painter.setDepthSublayer(2);
+
+        } else {
+            // Otherwise, we only want to drawFill the antialiased parts that are
+            // *outside* the current shape. This is important in case the fill
+            // or stroke color is translucent. If we wouldn't clip to outside
+            // the current shape, some pixels from the outline stroke overlapped
+            // the (non-antialiased) fill.
+            painter.setDepthSublayer(0);
+        }
+
+        gl.uniform2f(painter.outlineShader.u_world, gl.drawingBufferWidth, gl.drawingBufferHeight);
+        gl.uniform4fv(painter.outlineShader.u_color, strokeColor ? strokeColor : color);
+
+        for (var j = 0; j < coords.length; j++) {
+            drawStroke(painter, source, layer, coords[j]);
+        }
+    }
+}
+
+function drawFill(painter, source, layer, coord) {
+    var tile = source.getTile(coord);
     if (!tile.buffers) return;
     if (!tile.elementGroups[layer.ref || layer.id]) return;
     var elementGroups = tile.elementGroups[layer.ref || layer.id].fill;
 
     var gl = painter.gl;
-    var translatedPosMatrix = painter.translateMatrix(posMatrix, tile, layer.paint['fill-translate'], layer.paint['fill-translate-anchor']);
 
     var color = layer.paint['fill-color'];
+    var image = layer.paint['fill-pattern'];
+    var opacity = layer.paint['fill-opacity'];
 
-    var vertex, elements, group, count;
+    var posMatrix = painter.calculatePosMatrix(coord, source.maxzoom);
+    var translatedPosMatrix = painter.translatePosMatrix(posMatrix, tile, layer.paint['fill-translate'], layer.paint['fill-translate-anchor']);
 
     // Draw the stencil mask.
+    painter.setDepthSublayer(1);
 
-    // We're only drawing to the first seven bits (== support a maximum of
-    // 127 overlapping polygons in one place before we get rendering errors).
-    gl.stencilMask(0x3F);
+    // We're only drawFilling to the first seven bits (== support a maximum of
+    // 8 overlapping polygons in one place before we get rendering errors).
+    gl.stencilMask(0x07);
     gl.clear(gl.STENCIL_BUFFER_BIT);
 
     // Draw front facing triangles. Wherever the 0x80 bit is 1, we are
     // increasing the lower 7 bits by one if the triangle is a front-facing
     // triangle. This means that all visible polygons should be in CCW
     // orientation, while all holes (see below) are in CW orientation.
-    gl.stencilFunc(gl.NOTEQUAL, 0x80, 0x80);
+    painter.enableTileClippingMask(coord);
 
     // When we do a nonzero fill, we count the number of times a pixel is
     // covered by a counterclockwise polygon, and subtract the number of
     // times it is "uncovered" by a clockwise polygon.
-    gl.stencilOpSeparate(gl.FRONT, gl.INCR_WRAP, gl.KEEP, gl.KEEP);
-    gl.stencilOpSeparate(gl.BACK, gl.DECR_WRAP, gl.KEEP, gl.KEEP);
+    gl.stencilOpSeparate(gl.FRONT, gl.KEEP, gl.KEEP, gl.INCR_WRAP);
+    gl.stencilOpSeparate(gl.BACK, gl.KEEP, gl.KEEP, gl.DECR_WRAP);
 
-    // When drawing a shape, we first draw all shapes to the stencil buffer
+    // When drawFilling a shape, we first drawFill all shapes to the stencil buffer
     // and incrementing all areas where polygons are
     gl.colorMask(false, false, false, false);
+    painter.depthMask(false);
 
     // Draw the actual triangle fan into the stencil buffer.
     gl.switchShader(painter.fillShader, translatedPosMatrix);
 
     // Draw all buffers
-    vertex = tile.buffers.fillVertex;
+    var vertex = tile.buffers.fillVertex;
     vertex.bind(gl);
 
-    elements = tile.buffers.fillElement;
+    var elements = tile.buffers.fillElement;
     elements.bind(gl);
 
-    var offset, elementOffset;
-
     for (var i = 0; i < elementGroups.groups.length; i++) {
-        group = elementGroups.groups[i];
-        offset = group.vertexStartIndex * vertex.itemSize;
+        var group = elementGroups.groups[i];
+        var offset = group.vertexStartIndex * vertex.itemSize;
         vertex.setAttribPointers(gl, painter.fillShader, offset);
 
-        count = group.elementLength * 3;
-        elementOffset = group.elementStartIndex * elements.itemSize;
+        var count = group.elementLength * 3;
+        var elementOffset = group.elementStartIndex * elements.itemSize;
         gl.drawElements(gl.TRIANGLES, count, gl.UNSIGNED_SHORT, elementOffset);
     }
 
     // Now that we have the stencil mask in the stencil buffer, we can start
     // writing to the color buffer.
     gl.colorMask(true, true, true, true);
+    painter.depthMask(true);
 
     // From now on, we don't want to update the stencil buffer anymore.
     gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP);
     gl.stencilMask(0x0);
-
-    var strokeColor = layer.paint['fill-outline-color'];
-
-    // Because we're drawing top-to-bottom, and we update the stencil mask
-    // below, we have to draw the outline first (!)
-    if (layer.paint['fill-antialias'] === true && !(layer.paint['fill-pattern'] && !strokeColor)) {
-        gl.switchShader(painter.outlineShader, translatedPosMatrix);
-        gl.lineWidth(2 * browser.devicePixelRatio);
-
-        if (strokeColor) {
-            // If we defined a different color for the fill outline, we are
-            // going to ignore the bits in 0x3F and just care about the global
-            // clipping mask.
-            gl.stencilFunc(gl.EQUAL, 0x80, 0x80);
-        } else {
-            // Otherwise, we only want to draw the antialiased parts that are
-            // *outside* the current shape. This is important in case the fill
-            // or stroke color is translucent. If we wouldn't clip to outside
-            // the current shape, some pixels from the outline stroke overlapped
-            // the (non-antialiased) fill.
-            gl.stencilFunc(gl.EQUAL, 0x80, 0xBF);
-        }
-
-        gl.uniform2f(painter.outlineShader.u_world, gl.drawingBufferWidth, gl.drawingBufferHeight);
-        gl.uniform4fv(painter.outlineShader.u_color, strokeColor ? strokeColor : color);
-
-        // Draw all buffers
-        vertex = tile.buffers.fillVertex;
-        elements = tile.buffers.fillSecondElement;
-        elements.bind(gl);
-
-        for (var k = 0; k < elementGroups.groups.length; k++) {
-            group = elementGroups.groups[k];
-            offset = group.vertexStartIndex * vertex.itemSize;
-            vertex.setAttribPointers(gl, painter.outlineShader, offset);
-
-            count = group.secondElementLength * 2;
-            elementOffset = group.secondElementStartIndex * elements.itemSize;
-            gl.drawElements(gl.LINES, count, gl.UNSIGNED_SHORT, elementOffset);
-        }
-    }
-
-    var image = layer.paint['fill-pattern'];
-    var opacity = layer.paint['fill-opacity'] || 1;
     var shader;
 
     if (image) {
@@ -132,23 +135,18 @@ function drawFill(painter, layer, posMatrix, tile) {
         gl.uniform1f(shader.u_opacity, opacity);
         gl.uniform1f(shader.u_mix, image.t);
 
-        var factor = (tile.tileExtent / tile.tileSize) / Math.pow(2, painter.transform.tileZoom - tile.coord.z);
+        var factor = (4096 / tile.tileSize) / Math.pow(2, painter.transform.tileZoom - tile.coord.z);
 
-        var matrixA = mat3.create();
-        mat3.scale(matrixA, matrixA, [
+        gl.uniform2fv(shader.u_patternscale_a, [
             1 / (imagePosA.size[0] * factor * image.fromScale),
             1 / (imagePosA.size[1] * factor * image.fromScale)
         ]);
 
-        var matrixB = mat3.create();
-        mat3.scale(matrixB, matrixB, [
+        gl.uniform2fv(shader.u_patternscale_b, [
             1 / (imagePosB.size[0] * factor * image.toScale),
             1 / (imagePosB.size[1] * factor * image.toScale)
         ]);
 
-        gl.uniformMatrix3fv(shader.u_patternmatrix_a, false, matrixA);
-        gl.uniformMatrix3fv(shader.u_patternmatrix_b, false, matrixB);
-
         painter.spriteAtlas.bind(gl, true);
 
     } else {
@@ -159,11 +157,44 @@ function drawFill(painter, layer, posMatrix, tile) {
     }
 
     // Only draw regions that we marked
-    gl.stencilFunc(gl.NOTEQUAL, 0x0, 0x3F);
+    gl.stencilFunc(gl.NOTEQUAL, 0x0, 0x07);
     gl.bindBuffer(gl.ARRAY_BUFFER, painter.tileExtentBuffer);
     gl.vertexAttribPointer(shader.a_pos, painter.tileExtentBuffer.itemSize, gl.SHORT, false, 0, 0);
     gl.drawArrays(gl.TRIANGLE_STRIP, 0, painter.tileExtentBuffer.itemCount);
 
     gl.stencilMask(0x00);
-    gl.stencilFunc(gl.EQUAL, 0x80, 0x80);
+}
+
+function drawStroke(painter, source, layer, coord) {
+    var tile = source.getTile(coord);
+    if (!tile.buffers) return;
+    if (!tile.elementGroups[layer.ref || layer.id]) return;
+
+    var gl = painter.gl;
+    var elementGroups = tile.elementGroups[layer.ref || layer.id].fill;
+
+    gl.setPosMatrix(painter.translatePosMatrix(
+        painter.calculatePosMatrix(coord, source.maxzoom),
+        tile,
+        layer.paint['fill-translate'],
+        layer.paint['fill-translate-anchor']
+    ));
+
+    // Draw all buffers
+    var vertex = tile.buffers.fillVertex;
+    var elements = tile.buffers.fillSecondElement;
+    vertex.bind(gl);
+    elements.bind(gl);
+
+    painter.enableTileClippingMask(coord);
+
+    for (var k = 0; k < elementGroups.groups.length; k++) {
+        var group = elementGroups.groups[k];
+        var offset = group.vertexStartIndex * vertex.itemSize;
+        vertex.setAttribPointers(gl, painter.outlineShader, offset);
+
+        var count = group.secondElementLength * 2;
+        var elementOffset = group.secondElementStartIndex * elements.itemSize;
+        gl.drawElements(gl.LINES, count, gl.UNSIGNED_SHORT, elementOffset);
+    }
 }
diff --git a/js/render/draw_line.js b/js/render/draw_line.js
index f9fc0f36f03..f4e87d9c30f 100644
--- a/js/render/draw_line.js
+++ b/js/render/draw_line.js
@@ -13,11 +13,19 @@ var mat2 = require('gl-matrix').mat2;
  * @returns {undefined} draws with the painter
  * @private
  */
-module.exports = function drawLine(painter, layer, posMatrix, tile) {
-    // No data
-    if (!tile.buffers) return;
-    if (!tile.elementGroups[layer.ref || layer.id]) return;
-    var elementGroups = tile.elementGroups[layer.ref || layer.id].line;
+module.exports = function drawLine(painter, source, layer, coords) {
+    if (painter.isOpaquePass) return;
+    painter.setDepthSublayer(0);
+    painter.depthMask(false);
+
+    var hasData = false;
+    for (var j = 0; j < coords.length; j++) {
+        if (source.getTile(coords[j]).hasLayerData(layer)) {
+            hasData = true;
+            break;
+        }
+    }
+    if (!hasData) return;
 
     var gl = painter.gl;
 
@@ -43,14 +51,10 @@ module.exports = function drawLine(painter, layer, posMatrix, tile) {
     }
 
     var outset = offset + edgeWidth + antialiasing / 2 + shift;
-
     var color = layer.paint['line-color'];
-    var ratio = painter.transform.scale / (1 << tile.coord.z) / (tile.tileExtent / tile.tileSize);
-    var vtxMatrix = painter.translateMatrix(posMatrix, tile, layer.paint['line-translate'], layer.paint['line-translate-anchor']);
 
     var tr = painter.transform;
 
-
     var antialiasingMatrix = mat2.create();
     mat2.scale(antialiasingMatrix, antialiasingMatrix, [1, Math.cos(tr._pitch)]);
     mat2.rotate(antialiasingMatrix, antialiasingMatrix, painter.transform.angle);
@@ -61,42 +65,25 @@ module.exports = function drawLine(painter, layer, posMatrix, tile) {
     var x = tr.height / 2 * Math.tan(tr._pitch);
     var extra = (topedgelength + x) / topedgelength - 1;
 
-    // how much the tile is overscaled by
-    var overscaling = tile.tileSize / painter.transform.tileSize;
-
-    var shader;
-
-
     var dasharray = layer.paint['line-dasharray'];
     var image = layer.paint['line-pattern'];
+    var shader, posA, posB, imagePosA, imagePosB;
 
     if (dasharray) {
-
         shader = painter.linesdfpatternShader;
-        gl.switchShader(shader, vtxMatrix, tile.exMatrix);
+        gl.switchShader(shader);
 
         gl.uniform2fv(shader.u_linewidth, [ outset, inset ]);
-        gl.uniform1f(shader.u_ratio, ratio);
         gl.uniform1f(shader.u_blur, blur);
         gl.uniform4fv(shader.u_color, color);
 
-        var posA = painter.lineAtlas.getDash(dasharray.from, layer.layout['line-cap'] === 'round');
-        var posB = painter.lineAtlas.getDash(dasharray.to, layer.layout['line-cap'] === 'round');
+        posA = painter.lineAtlas.getDash(dasharray.from, layer.layout['line-cap'] === 'round');
+        posB = painter.lineAtlas.getDash(dasharray.to, layer.layout['line-cap'] === 'round');
         painter.lineAtlas.bind(gl);
 
-        var patternratio = Math.pow(2, Math.floor(Math.log(painter.transform.scale) / Math.LN2) - tile.coord.z) / 8 * overscaling;
-        var scaleA = [patternratio / posA.width / dasharray.fromScale, -posA.height / 2];
-        var gammaA = painter.lineAtlas.width / (dasharray.fromScale * posA.width * 256 * browser.devicePixelRatio) / 2;
-        var scaleB = [patternratio / posB.width / dasharray.toScale, -posB.height / 2];
-        var gammaB = painter.lineAtlas.width / (dasharray.toScale * posB.width * 256 * browser.devicePixelRatio) / 2;
-
-        gl.uniform2fv(shader.u_patternscale_a, scaleA);
         gl.uniform1f(shader.u_tex_y_a, posA.y);
-        gl.uniform2fv(shader.u_patternscale_b, scaleB);
         gl.uniform1f(shader.u_tex_y_b, posB.y);
-
         gl.uniform1i(shader.u_image, 0);
-        gl.uniform1f(shader.u_sdfgamma, Math.max(gammaA, gammaB));
         gl.uniform1f(shader.u_mix, dasharray.t);
 
         gl.uniform1f(shader.u_extra, extra);
@@ -104,22 +91,17 @@ module.exports = function drawLine(painter, layer, posMatrix, tile) {
         gl.uniformMatrix2fv(shader.u_antialiasingmatrix, false, antialiasingMatrix);
 
     } else if (image) {
-        var imagePosA = painter.spriteAtlas.getPosition(image.from, true);
-        var imagePosB = painter.spriteAtlas.getPosition(image.to, true);
+        imagePosA = painter.spriteAtlas.getPosition(image.from, true);
+        imagePosB = painter.spriteAtlas.getPosition(image.to, true);
         if (!imagePosA || !imagePosB) return;
-        var factor = tile.tileExtent / tile.tileSize / Math.pow(2, painter.transform.tileZoom - tile.coord.z) * overscaling;
 
         painter.spriteAtlas.bind(gl, true);
 
         shader = painter.linepatternShader;
-        gl.switchShader(shader, vtxMatrix, tile.exMatrix);
+        gl.switchShader(shader);
 
         gl.uniform2fv(shader.u_linewidth, [ outset, inset ]);
-        gl.uniform1f(shader.u_ratio, ratio);
         gl.uniform1f(shader.u_blur, blur);
-
-        gl.uniform2fv(shader.u_pattern_size_a, [imagePosA.size[0] * factor * image.fromScale, imagePosB.size[1] ]);
-        gl.uniform2fv(shader.u_pattern_size_b, [imagePosB.size[0] * factor * image.toScale, imagePosB.size[1] ]);
         gl.uniform2fv(shader.u_pattern_tl_a, imagePosA.tl);
         gl.uniform2fv(shader.u_pattern_br_a, imagePosA.br);
         gl.uniform2fv(shader.u_pattern_tl_b, imagePosB.tl);
@@ -133,31 +115,71 @@ module.exports = function drawLine(painter, layer, posMatrix, tile) {
 
     } else {
         shader = painter.lineShader;
-        gl.switchShader(shader, vtxMatrix, tile.exMatrix);
+        gl.switchShader(shader);
 
         gl.uniform2fv(shader.u_linewidth, [ outset, inset ]);
-        gl.uniform1f(shader.u_ratio, ratio);
         gl.uniform1f(shader.u_blur, blur);
         gl.uniform1f(shader.u_extra, extra);
         gl.uniform1f(shader.u_offset, -layer.paint['line-offset']);
         gl.uniformMatrix2fv(shader.u_antialiasingmatrix, false, antialiasingMatrix);
-
         gl.uniform4fv(shader.u_color, color);
     }
 
-    var vertex = tile.buffers.lineVertex;
-    vertex.bind(gl);
-    var element = tile.buffers.lineElement;
-    element.bind(gl);
-
-    for (var i = 0; i < elementGroups.groups.length; i++) {
-        var group = elementGroups.groups[i];
-        var vtxOffset = group.vertexStartIndex * vertex.itemSize;
-        vertex.setAttribPointers(gl, shader, vtxOffset);
-
-        var count = group.elementLength * 3;
-        var elementOffset = group.elementStartIndex * element.itemSize;
-        gl.drawElements(gl.TRIANGLES, count, gl.UNSIGNED_SHORT, elementOffset);
+    for (var k = 0; k < coords.length; k++) {
+        var coord = coords[k];
+        var tile = source.getTile(coord);
+
+        var elementGroups = tile.buffers && tile.elementGroups[layer.ref || layer.id] && tile.elementGroups[layer.ref || layer.id].line;
+        if (!elementGroups) continue;
+
+        painter.enableTileClippingMask(coord);
+
+        // set uniforms that are different for each tile
+        var posMatrix = painter.translatePosMatrix(painter.calculatePosMatrix(coord, source.maxzoom), tile, layer.paint['line-translate'], layer.paint['line-translate-anchor']);
+
+        gl.setPosMatrix(posMatrix);
+        gl.setExMatrix(painter.transform.exMatrix);
+        var ratio = painter.transform.scale / (1 << coord.z) / (4096 / tile.tileSize);
+
+        // how much the tile is overscaled by
+        var overscaling = tile.tileSize / painter.transform.tileSize;
+
+        if (dasharray) {
+            var patternratio = Math.pow(2, Math.floor(Math.log(painter.transform.scale) / Math.LN2) - coord.z) / 8 * overscaling;
+            var scaleA = [patternratio / posA.width / dasharray.fromScale, -posA.height / 2];
+            var gammaA = painter.lineAtlas.width / (dasharray.fromScale * posA.width * 256 * browser.devicePixelRatio) / 2;
+            var scaleB = [patternratio / posB.width / dasharray.toScale, -posB.height / 2];
+            var gammaB = painter.lineAtlas.width / (dasharray.toScale * posB.width * 256 * browser.devicePixelRatio) / 2;
+            gl.uniform1f(shader.u_ratio, ratio);
+            gl.uniform2fv(shader.u_patternscale_a, scaleA);
+            gl.uniform2fv(shader.u_patternscale_b, scaleB);
+            gl.uniform1f(shader.u_sdfgamma, Math.max(gammaA, gammaB));
+
+        } else if (image) {
+            var factor = 4096 / tile.tileSize / Math.pow(2, painter.transform.tileZoom - coord.z) * overscaling;
+            gl.uniform1f(shader.u_ratio, ratio);
+            gl.uniform2fv(shader.u_pattern_size_a, [imagePosA.size[0] * factor * image.fromScale, imagePosB.size[1] ]);
+            gl.uniform2fv(shader.u_pattern_size_b, [imagePosB.size[0] * factor * image.toScale, imagePosB.size[1] ]);
+
+        } else {
+            gl.uniform1f(shader.u_ratio, ratio);
+        }
+
+        var vertex = tile.buffers.lineVertex;
+        vertex.bind(gl);
+        var element = tile.buffers.lineElement;
+        element.bind(gl);
+
+        for (var i = 0; i < elementGroups.groups.length; i++) {
+            var group = elementGroups.groups[i];
+            var vtxOffset = group.vertexStartIndex * vertex.itemSize;
+            gl.vertexAttribPointer(shader.a_pos, 2, gl.SHORT, false, 8, vtxOffset + 0);
+            gl.vertexAttribPointer(shader.a_data, 4, gl.BYTE, false, 8, vtxOffset + 4);
+
+            var count = group.elementLength * 3;
+            var elementOffset = group.elementStartIndex * element.itemSize;
+            gl.drawElements(gl.TRIANGLES, count, gl.UNSIGNED_SHORT, elementOffset);
+        }
     }
 
 };
diff --git a/js/render/draw_raster.js b/js/render/draw_raster.js
index d2f5e9ea32d..a125cba7b7b 100644
--- a/js/render/draw_raster.js
+++ b/js/render/draw_raster.js
@@ -4,11 +4,24 @@ var util = require('../util/util');
 
 module.exports = drawRaster;
 
-function drawRaster(painter, layer, posMatrix, tile) {
+function drawRaster(painter, source, layer, coords) {
+    for (var i = 0; i < coords.length; i++) {
+        drawRasterTile(painter, source, layer, coords[i]);
+    }
+}
+
+function drawRasterTile(painter, source, layer, coord) {
+    if (painter.isOpaquePass) return;
+
+    painter.setDepthSublayer(0);
+
     var gl = painter.gl;
 
     gl.disable(gl.STENCIL_TEST);
 
+    var tile = source.getTile(coord);
+    var posMatrix = painter.calculatePosMatrix(coord, source.maxzoom);
+
     var shader = painter.rasterShader;
     gl.switchShader(shader, posMatrix);
 
@@ -19,7 +32,7 @@ function drawRaster(painter, layer, posMatrix, tile) {
     gl.uniform1f(shader.u_contrast_factor, contrastFactor(layer.paint['raster-contrast']));
     gl.uniform3fv(shader.u_spin_weights, spinWeights(layer.paint['raster-hue-rotate']));
 
-    var parentTile = tile.source && tile.source._pyramid.findLoadedParent(tile.coord, 0, {}),
+    var parentTile = tile.source && tile.source._pyramid.findLoadedParent(coord, 0, {}),
         opacities = getOpacities(tile, parentTile, layer, painter.transform);
 
     var parentScaleBy, parentTL;
diff --git a/js/render/draw_symbol.js b/js/render/draw_symbol.js
index 505a6a9ceb0..4b45a7bd3dc 100644
--- a/js/render/draw_symbol.js
+++ b/js/render/draw_symbol.js
@@ -1,17 +1,14 @@
 'use strict';
 
-var browser = require('../util/browser');
 var mat4 = require('gl-matrix').mat4;
 
+var browser = require('../util/browser');
 var drawCollisionDebug = require('./draw_collision_debug');
 
 module.exports = drawSymbols;
 
-function drawSymbols(painter, layer, posMatrix, tile) {
-    // No data
-    if (!tile.buffers) return;
-    var elementGroups = tile.elementGroups[layer.ref || layer.id];
-    if (!elementGroups) return;
+function drawSymbols(painter, source, layer, coords) {
+    if (painter.isOpaquePass) return;
 
     var drawAcrossEdges = !(layer.layout['text-allow-overlap'] || layer.layout['icon-allow-overlap'] ||
         layer.layout['text-ignore-placement'] || layer.layout['icon-ignore-placement']);
@@ -27,18 +24,48 @@ function drawSymbols(painter, layer, posMatrix, tile) {
         gl.disable(gl.STENCIL_TEST);
     }
 
-    if (elementGroups.glyph.groups.length) {
-        drawSymbol(painter, layer, posMatrix, tile, elementGroups.glyph, 'text', true);
-    }
-    if (elementGroups.icon.groups.length) {
+    painter.setDepthSublayer(0);
+    painter.depthMask(false);
+    gl.disable(gl.DEPTH_TEST);
+
+    var tile, elementGroups, posMatrix;
+
+    for (var i = 0; i < coords.length; i++) {
+        tile = source.getTile(coords[i]);
+
+        if (!tile.buffers) continue;
+        elementGroups = tile.elementGroups[layer.ref || layer.id];
+        if (!elementGroups) continue;
+        if (!elementGroups.icon.groups.length) continue;
+
+        posMatrix = painter.calculatePosMatrix(coords[i], source.maxzoom);
+        painter.enableTileClippingMask(coords[i]);
         drawSymbol(painter, layer, posMatrix, tile, elementGroups.icon, 'icon', elementGroups.sdfIcons);
     }
 
-    drawCollisionDebug(painter, layer, posMatrix, tile);
+    for (var j = 0; j < coords.length; j++) {
+        tile = source.getTile(coords[j]);
+
+        if (!tile.buffers) continue;
+        elementGroups = tile.elementGroups[layer.ref || layer.id];
+        if (!elementGroups) continue;
+        if (!elementGroups.glyph.groups.length) continue;
+
+        posMatrix = painter.calculatePosMatrix(coords[j], source.maxzoom);
+        painter.enableTileClippingMask(coords[j]);
+        drawSymbol(painter, layer, posMatrix, tile, elementGroups.glyph, 'text', true);
+    }
+
+    for (var k = 0; k < coords.length; k++) {
+        tile = source.getTile(coords[k]);
+        painter.enableTileClippingMask(coords[k]);
+        drawCollisionDebug(painter, layer, coords[k], tile);
+    }
 
     if (drawAcrossEdges) {
         gl.enable(gl.STENCIL_TEST);
     }
+    gl.enable(gl.DEPTH_TEST);
 }
 
 var defaultSizes = {
@@ -49,7 +76,7 @@ var defaultSizes = {
 function drawSymbol(painter, layer, posMatrix, tile, elementGroups, prefix, sdf) {
     var gl = painter.gl;
 
-    posMatrix = painter.translateMatrix(posMatrix, tile, layer.paint[prefix + '-translate'], layer.paint[prefix + '-translate-anchor']);
+    posMatrix = painter.translatePosMatrix(posMatrix, tile, layer.paint[prefix + '-translate'], layer.paint[prefix + '-translate-anchor']);
 
     var tr = painter.transform;
     var alignedWithMap = layer.layout[prefix + '-rotation-alignment'] === 'map';
@@ -61,7 +88,7 @@ function drawSymbol(painter, layer, posMatrix, tile, elementGroups, prefix, sdf)
         s = tile.tileExtent / tile.tileSize / Math.pow(2, painter.transform.zoom - tile.coord.z);
         gammaScale = 1 / Math.cos(tr._pitch);
     } else {
-        exMatrix = mat4.clone(tile.exMatrix);
+        exMatrix = mat4.clone(painter.transform.exMatrix);
         s = painter.transform.altitude;
         gammaScale = 1;
     }
@@ -132,21 +159,6 @@ function drawSymbol(painter, layer, posMatrix, tile, elementGroups, prefix, sdf)
         var haloOffset = 6;
         var gamma = 0.105 * defaultSizes[prefix] / fontSize / browser.devicePixelRatio;
 
-        gl.uniform1f(shader.u_gamma, gamma * gammaScale);
-        gl.uniform4fv(shader.u_color, layer.paint[prefix + '-color']);
-        gl.uniform1f(shader.u_buffer, (256 - 64) / 256);
-
-        for (var i = 0; i < elementGroups.groups.length; i++) {
-            group = elementGroups.groups[i];
-            offset = group.vertexStartIndex * vertex.itemSize;
-            vertex.bind(gl);
-            vertex.setAttribPointers(gl, shader, offset);
-
-            count = group.elementLength * 3;
-            elementOffset = group.elementStartIndex * elements.itemSize;
-            gl.drawElements(gl.TRIANGLES, count, gl.UNSIGNED_SHORT, elementOffset);
-        }
-
         if (layer.paint[prefix + '-halo-width']) {
             // Draw halo underneath the text.
             gl.uniform1f(shader.u_gamma, (layer.paint[prefix + '-halo-blur'] * blurOffset / fontScale / sdfPx + gamma) * gammaScale);
@@ -164,6 +176,22 @@ function drawSymbol(painter, layer, posMatrix, tile, elementGroups, prefix, sdf)
                 gl.drawElements(gl.TRIANGLES, count, gl.UNSIGNED_SHORT, elementOffset);
             }
         }
+
+        gl.uniform1f(shader.u_gamma, gamma * gammaScale);
+        gl.uniform4fv(shader.u_color, layer.paint[prefix + '-color']);
+        gl.uniform1f(shader.u_buffer, (256 - 64) / 256);
+
+        for (var i = 0; i < elementGroups.groups.length; i++) {
+            group = elementGroups.groups[i];
+            offset = group.vertexStartIndex * vertex.itemSize;
+            vertex.bind(gl);
+            vertex.setAttribPointers(gl, shader, offset);
+
+            count = group.elementLength * 3;
+            elementOffset = group.elementStartIndex * elements.itemSize;
+            gl.drawElements(gl.TRIANGLES, count, gl.UNSIGNED_SHORT, elementOffset);
+        }
+
     } else {
         gl.uniform1f(shader.u_opacity, layer.paint['icon-opacity']);
         for (var k = 0; k < elementGroups.groups.length; k++) {
diff --git a/js/render/draw_vertices.js b/js/render/draw_vertices.js
deleted file mode 100644
index 6d3d34164fe..00000000000
--- a/js/render/draw_vertices.js
+++ /dev/null
@@ -1,51 +0,0 @@
-'use strict';
-
-var browser = require('../util/browser');
-var mat4 = require('gl-matrix').mat4;
-
-module.exports = drawVertices;
-
-function drawVertices(painter, layer, posMatrix, tile) {
-    var gl = painter.gl;
-
-    if (!tile || !tile.buffers) return;
-    var elementGroups = tile.elementGroups[layer.ref || layer.id];
-    if (!elementGroups) return;
-
-    // Blend to the front, not the back.
-    gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
-
-    // Draw all buffers
-    if (layer.type === 'fill') {
-        drawPoints(tile.buffers.fillVertex, elementGroups.groups, posMatrix);
-    } else if (layer.type === 'symbol') {
-        drawPoints(tile.buffers.iconVertex, elementGroups.icon.groups, posMatrix);
-        drawPoints(tile.buffers.glyphVertex, elementGroups.glyph.groups, posMatrix);
-    } else if (layer.type === 'line') {
-        var newPosMatrix = mat4.clone(posMatrix);
-        mat4.scale(newPosMatrix, newPosMatrix, [0.5, 0.5, 1]);
-        drawPoints(tile.buffers.lineVertex, elementGroups.groups, newPosMatrix);
-    }
-
-    function drawPoints(vertex, groups, matrix) {
-        gl.switchShader(painter.dotShader, matrix);
-
-        gl.uniform1f(painter.dotShader.u_size, 4 * browser.devicePixelRatio);
-        gl.uniform1f(painter.dotShader.u_blur, 0.25);
-        gl.uniform4fv(painter.dotShader.u_color, [0.1, 0, 0, 0.1]);
-
-        vertex.bind(gl);
-        for (var i = 0; i < groups.length; i++) {
-            var group = groups[i];
-            var begin = group.vertexStartIndex;
-            var count = group.vertexLength;
-
-            gl.vertexAttribPointer(painter.dotShader.a_pos, 2, gl.SHORT, false, vertex.itemSize, 0);
-
-            gl.drawArrays(gl.POINTS, begin, count);
-        }
-    }
-
-    // Revert blending mode to blend to the back.
-    gl.blendFunc(gl.ONE_MINUS_DST_ALPHA, gl.ONE);
-}
diff --git a/js/render/gl_util.js b/js/render/gl_util.js
index edc8cf19c71..d30629cd9a6 100644
--- a/js/render/gl_util.js
+++ b/js/render/gl_util.js
@@ -61,10 +61,6 @@ exports.extend = function(context) {
 
     // Switches to a different shader program.
     context.switchShader = function(shader, posMatrix, exMatrix) {
-        if (!posMatrix) {
-            console.trace('posMatrix does not have required argument');
-        }
-
         if (this.currentShader !== shader) {
             this.useProgram(shader.program);
 
@@ -89,20 +85,32 @@ exports.extend = function(context) {
             this.currentShader = shader;
         }
 
-        // Update the matrices if necessary. Note: This relies on object identity!
-        // This means changing the matrix values without the actual matrix object
-        // will FAIL to update the matrix properly.
+        if (posMatrix !== undefined) context.setPosMatrix(posMatrix);
+        if (exMatrix !== undefined) context.setExMatrix(exMatrix);
+    };
+
+    // Update the matrices if necessary. Note: This relies on object identity!
+    // This means changing the matrix values without the actual matrix object
+    // will FAIL to update the matrix properly.
+    context.setPosMatrix = function(posMatrix) {
+        var shader = this.currentShader;
         if (shader.posMatrix !== posMatrix) {
             this.uniformMatrix4fv(shader.u_matrix, false, posMatrix);
             shader.posMatrix = posMatrix;
         }
+    };
+
+    // Update the matrices if necessary. Note: This relies on object identity!
+    // This means changing the matrix values without the actual matrix object
+    // will FAIL to update the matrix properly.
+    context.setExMatrix = function(exMatrix) {
+        var shader = this.currentShader;
         if (exMatrix && shader.exMatrix !== exMatrix && shader.u_exmatrix) {
             this.uniformMatrix4fv(shader.u_exmatrix, false, exMatrix);
             shader.exMatrix = exMatrix;
         }
     };
 
-
     context.vertexAttrib2fv = function(attribute, values) {
         context.vertexAttrib2f(attribute, values[0], values[1]);
     };
diff --git a/js/render/painter.js b/js/render/painter.js
index 1cf6072e07a..cd3e46a2a8b 100644
--- a/js/render/painter.js
+++ b/js/render/painter.js
@@ -4,6 +4,7 @@ var glutil = require('./gl_util');
 var browser = require('../util/browser');
 var mat4 = require('gl-matrix').mat4;
 var FrameHistory = require('./frame_history');
+var TileCoord = require('../source/tile_coord');
 
 /*
  * Initialize a new painter object.
@@ -21,6 +22,11 @@ function Painter(gl, transform) {
     this.frameHistory = new FrameHistory();
 
     this.setup();
+
+    // Within each layer there are 3 distinct z-planes that can be drawn to.
+    // This is implemented using the WebGL depth buffer.
+    this.numSublayers = 3;
+    this.depthEpsilon = 1 / Math.pow(2, 16);
 }
 
 /*
@@ -45,10 +51,16 @@ Painter.prototype.setup = function() {
     // We are blending the new pixels *behind* the existing pixels. That way we can
     // draw front-to-back and use then stencil buffer to cull opaque pixels early.
     gl.enable(gl.BLEND);
-    gl.blendFunc(gl.ONE_MINUS_DST_ALPHA, gl.ONE);
+    gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
 
     gl.enable(gl.STENCIL_TEST);
 
+    gl.enable(gl.DEPTH_TEST);
+    gl.depthFunc(gl.LEQUAL);
+
+    this._depthMask = false;
+    gl.depthMask(false);
+
     // Initialize shaders
     this.debugShader = gl.initializeShader('debug',
         ['a_pos'],
@@ -64,7 +76,7 @@ Painter.prototype.setup = function() {
 
     this.lineShader = gl.initializeShader('line',
         ['a_pos', 'a_data'],
-        ['u_matrix', 'u_linewidth', 'u_color', 'u_ratio', 'u_blur', 'u_extra', 'u_antialiasingmatrix', 'u_offset']);
+        ['u_matrix', 'u_linewidth', 'u_color', 'u_ratio', 'u_blur', 'u_extra', 'u_antialiasingmatrix', 'u_offset', 'u_exmatrix']);
 
     this.linepatternShader = gl.initializeShader('linepattern',
         ['a_pos', 'a_data'],
@@ -93,7 +105,7 @@ Painter.prototype.setup = function() {
 
     this.patternShader = gl.initializeShader('pattern',
         ['a_pos'],
-        ['u_matrix', 'u_pattern_tl_a', 'u_pattern_br_a', 'u_pattern_tl_b', 'u_pattern_br_b', 'u_mix', 'u_patternmatrix_a', 'u_patternmatrix_b', 'u_opacity', 'u_image']
+        ['u_matrix', 'u_pattern_tl_a', 'u_pattern_br_a', 'u_pattern_tl_b', 'u_pattern_br_b', 'u_mix', 'u_patternscale_a', 'u_patternscale_b', 'u_opacity', 'u_image']
     );
 
     this.fillShader = gl.initializeShader('fill',
@@ -186,35 +198,49 @@ Painter.prototype.clearStencil = function() {
     gl.clear(gl.STENCIL_BUFFER_BIT);
 };
 
-Painter.prototype.drawClippingMask = function(tile) {
+Painter.prototype.clearDepth = function() {
+    var gl = this.gl;
+    gl.clearDepth(1);
+    this.depthMask(true);
+    gl.clear(gl.DEPTH_BUFFER_BIT);
+};
+
+Painter.prototype._renderTileClippingMasks = function(coords, sourceMaxZoom) {
     var gl = this.gl;
-    gl.switchShader(this.fillShader, tile.posMatrix);
     gl.colorMask(false, false, false, false);
+    this.depthMask(false);
+    gl.disable(gl.DEPTH_TEST);
 
-    // Clear the entire stencil buffer, except for the 7th bit, which stores
-    // the global clipping mask that allows us to avoid drawing in regions of
-    // tiles we've already painted in.
-    gl.clearStencil(0x0);
-    gl.stencilMask(0xBF);
-    gl.clear(gl.STENCIL_BUFFER_BIT);
+    // Only write clipping IDs to the last 5 bits. The first three are used for drawing fills.
+    gl.stencilMask(0xF8);
+    // Tests will always pass, and ref value will be written to stencil buffer.
+    gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE);
 
-    // The stencil test will fail always, meaning we set all pixels covered
-    // by this geometry to 0x80. We use the highest bit 0x80 to mark the regions
-    // we want to draw in. All pixels that have this bit *not* set will never be
-    // drawn in.
-    gl.stencilFunc(gl.EQUAL, 0xC0, 0x40);
-    gl.stencilMask(0xC0);
-    gl.stencilOp(gl.REPLACE, gl.KEEP, gl.KEEP);
+    var idNext = 1;
+    this._tileClippingMaskIDs = {};
+    for (var i = 0; i < coords.length; i++) {
+        var coord = coords[i];
+        var id = this._tileClippingMaskIDs[coord.id] = (idNext++) << 3;
 
-    // Draw the clipping mask
-    gl.bindBuffer(gl.ARRAY_BUFFER, this.tileExtentBuffer);
-    gl.vertexAttribPointer(this.fillShader.a_pos, this.tileExtentBuffer.itemSize, gl.SHORT, false, 8, 0);
-    gl.drawArrays(gl.TRIANGLE_STRIP, 0, this.tileExtentBuffer.itemCount);
+        gl.stencilFunc(gl.ALWAYS, id, 0xF8);
+
+        gl.switchShader(this.fillShader, this.calculatePosMatrix(coord, sourceMaxZoom));
+
+        // Draw the clipping mask
+        gl.bindBuffer(gl.ARRAY_BUFFER, this.tileExtentBuffer);
+        gl.vertexAttribPointer(this.fillShader.a_pos, this.tileExtentBuffer.itemSize, gl.SHORT, false, 8, 0);
+        gl.drawArrays(gl.TRIANGLE_STRIP, 0, this.tileExtentBuffer.itemCount);
+    }
 
-    gl.stencilFunc(gl.EQUAL, 0x80, 0x80);
-    gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE);
     gl.stencilMask(0x00);
     gl.colorMask(true, true, true, true);
+    this.depthMask(true);
+    gl.enable(gl.DEPTH_TEST);
+};
+
+Painter.prototype.enableTileClippingMask = function(coord) {
+    var gl = this.gl;
+    gl.stencilFunc(gl.EQUAL, this._tileClippingMaskIDs[coord.id], 0xF8);
 };
 
 // Overridden by headless tests.
@@ -231,8 +257,7 @@ var draw = {
     fill: require('./draw_fill'),
     raster: require('./draw_raster'),
     background: require('./draw_background'),
-    debug: require('./draw_debug'),
-    vertices: require('./draw_vertices')
+    debug: require('./draw_debug')
 };
 
 Painter.prototype.render = function(style, options) {
@@ -251,53 +276,70 @@ Painter.prototype.render = function(style, options) {
 
     this.prepareBuffers();
     this.clearColor();
+    this.clearDepth();
+
+    this.depthRange = (style._order.length + 2) * this.numSublayers * this.depthEpsilon;
+
+    this.renderPass({isOpaquePass: true});
+    this.renderPass({isOpaquePass: false});
+};
+
+Painter.prototype.renderPass = function(options) {
+    var groups = this.style._groups;
+    var isOpaquePass = options.isOpaquePass;
+    this.currentLayer = isOpaquePass ? this.style._order.length : -1;
 
-    for (var i = style._groups.length - 1; i >= 0; i--) {
-        var group = style._groups[i];
-        var source = style.sources[group.source];
+    for (var i = 0; i < groups.length; i++) {
+        var group = groups[isOpaquePass ? groups.length - 1 - i : i];
+        var source = this.style.sources[group.source];
 
+        var coords = [];
         if (source) {
+            coords = source.getVisibleCoordinates();
             this.clearStencil();
-            source.render(group, this);
+            if (source.prepare) source.prepare();
+            if (source.isTileClipped) {
+                this._renderTileClippingMasks(coords, source.maxzoom);
+            }
+        }
 
-        } else if (group.source === undefined) {
-            this.drawLayers(group, this.identityMatrix);
+        if (isOpaquePass) {
+            this.gl.disable(this.gl.BLEND);
+            this.isOpaquePass = true;
+        } else {
+            this.gl.enable(this.gl.BLEND);
+            this.isOpaquePass = false;
+            coords.reverse();
         }
-    }
-};
 
-Painter.prototype.drawTile = function(tile, layers) {
-    this.setExtent(tile.tileExtent);
-    this.drawClippingMask(tile);
-    this.drawLayers(layers, tile.posMatrix, tile);
+        for (var j = 0; j < group.length; j++) {
+            var layer = group[isOpaquePass ? group.length - 1 - j : j];
+            this.currentLayer += isOpaquePass ? -1 : 1;
+            this.renderLayer(this, source, layer, coords);
+        }
 
-    if (this.options.debug) {
-        draw.debug(this, tile);
+        draw.debug(this, coords);
     }
 };
 
-Painter.prototype.drawLayers = function(layers, matrix, tile) {
-    for (var i = layers.length - 1; i >= 0; i--) {
-        var layer = layers[i];
-
-        if (layer.hidden)
-            continue;
-
-        draw[layer.type](this, layer, matrix, tile);
-
-        if (this.options.vertices) {
-            draw.vertices(this, layer, matrix, tile);
-        }
+Painter.prototype.depthMask = function(mask) {
+    if (mask !== this._depthMask) {
+        this._depthMask = mask;
+        this.gl.depthMask(mask);
     }
 };
 
+Painter.prototype.renderLayer = function(painter, source, layer, coords) {
+    if (layer.hidden) return;
+    if (layer.type !== 'background' && !coords.length) return;
+    draw[layer.type](painter, source, layer, coords);
+};
+
 // Draws non-opaque areas. This is for debugging purposes.
 Painter.prototype.drawStencilBuffer = function() {
     var gl = this.gl;
     gl.switchShader(this.fillShader, this.identityMatrix);
 
-    // Blend to the front, not the back.
-    gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
     gl.stencilMask(0x00);
     gl.stencilFunc(gl.EQUAL, 0x80, 0x80);
 
@@ -307,12 +349,15 @@ Painter.prototype.drawStencilBuffer = function() {
 
     gl.uniform4fv(this.fillShader.u_color, [0, 0, 0, 0.5]);
     gl.drawArrays(gl.TRIANGLE_STRIP, 0, this.tileExtentBuffer.itemCount);
+};
 
-    // Revert blending mode to blend to the back.
-    gl.blendFunc(gl.ONE_MINUS_DST_ALPHA, gl.ONE);
+Painter.prototype.setDepthSublayer = function(n) {
+    var farDepth = 1 - ((1 + this.currentLayer) * this.numSublayers + n) * this.depthEpsilon;
+    var nearDepth = farDepth - 1 + this.depthRange;
+    this.gl.depthRange(nearDepth, farDepth);
 };
 
-Painter.prototype.translateMatrix = function(matrix, tile, translate, anchor) {
+Painter.prototype.translatePosMatrix = function(matrix, tile, translate, anchor) {
     if (!translate[0] && !translate[1]) return matrix;
 
     if (anchor === 'viewport') {
@@ -336,6 +381,43 @@ Painter.prototype.translateMatrix = function(matrix, tile, translate, anchor) {
     return translatedMatrix;
 };
 
+/**
+ * Calculate the posMatrix that this tile uses to display itself in a map,
+ * given a coordinate as (z, x, y) and a transform
+ * @param {Object} transform
+ * @private
+ */
+Painter.prototype.calculatePosMatrix = function(coord, maxZoom) {
+    var tileExtent = 4096;
+    if (coord instanceof TileCoord) {
+        coord = coord.toCoordinate();
+    }
+    var transform = this.transform;
+
+    if (maxZoom === undefined) maxZoom = Infinity;
+
+    // Initialize model-view matrix that converts from the tile coordinates
+    // to screen coordinates.
+
+    // if z > maxzoom then the tile is actually a overscaled maxzoom tile,
+    // so calculate the matrix the maxzoom tile would use.
+    var z = Math.min(coord.zoom, maxZoom);
+    var x = coord.column;
+    var y = coord.row;
+
+    var scale = transform.worldSize / Math.pow(2, z);
+
+    // The position matrix
+    var posMatrix = new Float64Array(16);
+
+    mat4.identity(posMatrix);
+    mat4.translate(posMatrix, posMatrix, [x * scale, y * scale, 0]);
+    mat4.scale(posMatrix, posMatrix, [ scale / tileExtent, scale / tileExtent, 1 ]);
+    mat4.multiply(posMatrix, transform.projMatrix, posMatrix);
+
+    return new Float32Array(posMatrix);
+};
+
 Painter.prototype.saveTexture = function(texture) {
     var textures = this.reusableTextures[texture.size];
     if (!textures) {
diff --git a/js/source/geojson_source.js b/js/source/geojson_source.js
index be50e1fbb04..7c7613e0697 100644
--- a/js/source/geojson_source.js
+++ b/js/source/geojson_source.js
@@ -64,6 +64,7 @@ GeoJSONSource.prototype = util.inherit(Evented, /** @lends GeoJSONSource.prototy
     minzoom: 0,
     maxzoom: 14,
     _dirty: true,
+    isTileClipped: true,
 
     /**
      * Update source geojson data and rerender map
@@ -107,7 +108,9 @@ GeoJSONSource.prototype = util.inherit(Evented, /** @lends GeoJSONSource.prototy
         }
     },
 
-    render: Source._renderTiles,
+    getVisibleCoordinates: Source._getVisibleCoordinates,
+    getTile: Source._getTile,
+
     featuresAt: Source._vectorFeaturesAt,
     featuresIn: Source._vectorFeaturesIn,
 
diff --git a/js/source/image_source.js b/js/source/image_source.js
index c20d3d484c7..d94e4f9e383 100644
--- a/js/source/image_source.js
+++ b/js/source/image_source.js
@@ -30,8 +30,6 @@ module.exports = ImageSource;
  * map.removeSource('some id');  // remove
  */
 function ImageSource(options) {
-    this.coordinates = options.coordinates;
-
     ajax.getImage(options.url, function(err, image) {
         // @TODO handle errors via event.
         if (err) return;
@@ -45,7 +43,7 @@ function ImageSource(options) {
         this._loaded = true;
 
         if (this.map) {
-            this.createTile();
+            this.createTile(options.coordinates);
             this.fire('change');
         }
     }.bind(this));
@@ -65,20 +63,20 @@ ImageSource.prototype = util.inherit(Evented, {
      * may be outside the tile, because raster tiles aren't clipped when rendering.
      * @private
      */
-    createTile: function() {
+    createTile: function(cornerGeoCoords) {
         var map = this.map;
-        var coords = this.coordinates.map(function(lnglat) {
-            var loc = LngLat.convert(lnglat);
-            return map.transform.locationCoordinate(loc).zoomTo(0);
+        var cornerZ0Coords = cornerGeoCoords.map(function(coord) {
+            return map.transform.locationCoordinate(LngLat.convert(coord)).zoomTo(0);
         });
 
-        var center = util.getCoordinatesCenter(coords);
+        var centerCoord = this.centerCoord = util.getCoordinatesCenter(cornerZ0Coords);
+
         var tileExtent = 4096;
-        var tileCoords = coords.map(function(coord) {
-            var zoomedCoord = coord.zoomTo(center.zoom);
+        var tileCoords = cornerZ0Coords.map(function(coord) {
+            var zoomedCoord = coord.zoomTo(centerCoord.zoom);
             return new Point(
-                Math.round((zoomedCoord.column - center.column) * tileExtent),
-                Math.round((zoomedCoord.row - center.row) * tileExtent));
+                Math.round((zoomedCoord.column - centerCoord.column) * tileExtent),
+                Math.round((zoomedCoord.row - centerCoord.row) * tileExtent));
         });
 
         var gl = map.painter.gl;
@@ -96,8 +94,6 @@ ImageSource.prototype = util.inherit(Evented, {
         this.tile.boundsBuffer = gl.createBuffer();
         gl.bindBuffer(gl.ARRAY_BUFFER, this.tile.boundsBuffer);
         gl.bufferData(gl.ARRAY_BUFFER, array, gl.STATIC_DRAW);
-
-        this.center = center;
     },
 
     loaded: function() {
@@ -112,12 +108,10 @@ ImageSource.prototype = util.inherit(Evented, {
         // noop
     },
 
-    render: function(layers, painter) {
+    prepare: function() {
         if (!this._loaded || !this.loaded()) return;
 
-        var c = this.center;
-        this.tile.calculateMatrices(c.zoom, c.column, c.row, this.map.transform, painter);
-
+        var painter = this.map.painter;
         var gl = painter.gl;
 
         if (!this.tile.texture) {
@@ -132,8 +126,15 @@ ImageSource.prototype = util.inherit(Evented, {
             gl.bindTexture(gl.TEXTURE_2D, this.tile.texture);
             gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, this.image);
         }
+    },
+
+    getVisibleCoordinates: function() {
+        if (this.centerCoord) return [this.centerCoord];
+        else return [];
+    },
 
-        painter.drawLayers(layers, this.tile.posMatrix, this.tile);
+    getTile: function() {
+        return this.tile;
     },
 
     /**
diff --git a/js/source/raster_tile_source.js b/js/source/raster_tile_source.js
index f96bb833e58..cf848e2850d 100644
--- a/js/source/raster_tile_source.js
+++ b/js/source/raster_tile_source.js
@@ -39,7 +39,8 @@ RasterTileSource.prototype = util.inherit(Evented, {
         // noop
     },
 
-    render: Source._renderTiles,
+    getVisibleCoordinates: Source._getVisibleCoordinates,
+    getTile: Source._getTile,
 
     _loadTile: function(tile) {
         var url = normalizeURL(tile.coord.url(this.tiles), this.url);
diff --git a/js/source/source.js b/js/source/source.js
index e248d2996bd..15bfcaef9af 100644
--- a/js/source/source.js
+++ b/js/source/source.js
@@ -4,8 +4,8 @@ var util = require('../util/util');
 var ajax = require('../util/ajax');
 var browser = require('../util/browser');
 var TilePyramid = require('./tile_pyramid');
-var TileCoord = require('./tile_coord');
 var normalizeURL = require('../util/mapbox').normalizeSourceURL;
+var TileCoord = require('./tile_coord');
 
 exports._loadTileJSON = function(options) {
     var loaded = function(err, tileJSON) {
@@ -59,30 +59,13 @@ exports.redoPlacement = function() {
     }
 };
 
-exports._renderTiles = function(layers, painter) {
-    if (!this._pyramid)
-        return;
+exports._getTile = function(coord) {
+    return this._pyramid.getTile(coord.id);
+};
 
-    var ids = this._pyramid.renderedIDs();
-    for (var i = 0; i < ids.length; i++) {
-        var tile = this._pyramid.getTile(ids[i]),
-            // coord is different than tile.coord for wrapped tiles since the actual
-            // tile object is shared between all the visible copies of that tile.
-            coord = TileCoord.fromID(ids[i]),
-            z = coord.z,
-            x = coord.x,
-            y = coord.y,
-            w = coord.w;
-
-        // if z > maxzoom then the tile is actually a overscaled maxzoom tile,
-        // so calculate the matrix the maxzoom tile would use.
-        z = Math.min(z, this.maxzoom);
-
-        x += w * (1 << z);
-        tile.calculateMatrices(z, x, y, painter.transform, painter);
-
-        painter.drawTile(tile, layers);
-    }
+exports._getVisibleCoordinates = function() {
+    if (!this._pyramid) return [];
+    else return this._pyramid.renderedIDs().map(TileCoord.fromID);
 };
 
 exports._vectorFeaturesAt = function(coord, params, callback) {
diff --git a/js/source/tile.js b/js/source/tile.js
index cc66bd2c750..c6519981465 100644
--- a/js/source/tile.js
+++ b/js/source/tile.js
@@ -1,6 +1,5 @@
 'use strict';
 
-var mat4 = require('gl-matrix').mat4;
 var util = require('../util/util');
 var Buffer = require('../data/buffer');
 
@@ -26,39 +25,6 @@ Tile.prototype = {
     // todo unhardcode
     tileExtent: 4096,
 
-    /**
-     * Calculate the internal posMatrix that this tile uses to display
-     * itself in a map, given a coordinate as (z, x, y) and a transform
-     * @param {number} z
-     * @param {number} x
-     * @param {number} y
-     * @param {Object} transform
-     * @private
-     */
-    calculateMatrices: function(z, x, y, transform) {
-
-        // Initialize model-view matrix that converts from the tile coordinates
-        // to screen coordinates.
-        var tileScale = Math.pow(2, z);
-        var scale = transform.worldSize / tileScale;
-
-        // TODO: remove
-        this.scale = scale;
-
-        // The position matrix
-        this.posMatrix = new Float64Array(16);
-
-        mat4.identity(this.posMatrix);
-        mat4.translate(this.posMatrix, this.posMatrix, [x * scale, y * scale, 0]);
-        mat4.scale(this.posMatrix, this.posMatrix, [ scale / this.tileExtent, scale / this.tileExtent, 1 ]);
-        mat4.multiply(this.posMatrix, transform.projMatrix, this.posMatrix);
-
-        this.posMatrix = new Float32Array(this.posMatrix);
-
-        this.exMatrix = transform.exMatrix;
-        this.rotationMatrix = transform.rotationMatrix;
-    },
-
     /**
      * Given a coordinate position, zoom that coordinate to my zoom and
      * scale and return a position in x, y, scale
@@ -169,7 +135,15 @@ Tile.prototype = {
                 this.redoWhenDone = false;
             }
         }
+    },
 
+    /**
+     * Return whether this tile has any data for the given layer.
+     * @param {Object} style layer object
+     * @returns {boolean}
+     */
+    hasLayerData: function(layer) {
+        return Boolean(this.buffers && this.elementGroups[layer.ref || layer.id] && Object.keys(this.elementGroups[layer.ref || layer.id]).length);
     }
 };
 
diff --git a/js/source/tile_coord.js b/js/source/tile_coord.js
index 34e4b39f005..7c3bbbc74de 100644
--- a/js/source/tile_coord.js
+++ b/js/source/tile_coord.js
@@ -1,16 +1,14 @@
 'use strict';
 
 var assert = require('assert');
+var Coordinate = require('../geo/coordinate');
 
 module.exports = TileCoord;
 
 function TileCoord(z, x, y, w) {
-    assert(!isNaN(z));
-    assert(!isNaN(x));
-    assert(!isNaN(y));
-    assert(z >= 0);
-    assert(x >= 0);
-    assert(y >= 0);
+    assert(!isNaN(z) && z >= 0 && z % 1 === 0);
+    assert(!isNaN(x) && x >= 0 && x % 1 === 0);
+    assert(!isNaN(y) && y >= 0 && y % 1 === 0);
 
     if (isNaN(w)) w = 0;
 
@@ -30,6 +28,14 @@ TileCoord.prototype.toString = function() {
     return this.z + "/" + this.x + "/" + this.y;
 };
 
+TileCoord.prototype.toCoordinate = function() {
+    var zoom = this.z;
+    var tileScale = Math.pow(2, zoom);
+    var row = this.y;
+    var column = this.x + tileScale * this.w;
+    return new Coordinate(column, row, zoom);
+};
+
 // Parse a packed integer id into a TileCoord object
 TileCoord.fromID = function(id) {
     var z = id % 32, dim = 1 << z;
diff --git a/js/source/vector_tile_source.js b/js/source/vector_tile_source.js
index d640f5a7d24..82cbb7dea70 100644
--- a/js/source/vector_tile_source.js
+++ b/js/source/vector_tile_source.js
@@ -23,6 +23,7 @@ VectorTileSource.prototype = util.inherit(Evented, {
     tileSize: 512,
     reparseOverscaled: true,
     _loaded: false,
+    isTileClipped: true,
 
     onAdd: function(map) {
         this.map = map;
@@ -44,7 +45,9 @@ VectorTileSource.prototype = util.inherit(Evented, {
         }
     },
 
-    render: Source._renderTiles,
+    getVisibleCoordinates: Source._getVisibleCoordinates,
+    getTile: Source._getTile,
+
     featuresAt: Source._vectorFeaturesAt,
     featuresIn: Source._vectorFeaturesIn,
 
diff --git a/js/source/video_source.js b/js/source/video_source.js
index 6a4ad020f46..b8f27a883b6 100644
--- a/js/source/video_source.js
+++ b/js/source/video_source.js
@@ -32,8 +32,6 @@ module.exports = VideoSource;
  * map.removeSource('some id');  // remove
  */
 function VideoSource(options) {
-    this.coordinates = options.coordinates;
-
     ajax.getVideo(options.urls, function(err, video) {
         // @TODO handle errors via event.
         if (err) return;
@@ -58,7 +56,7 @@ function VideoSource(options) {
 
         if (this.map) {
             this.video.play();
-            this.createTile();
+            this.createTile(options.coordinates);
             this.fire('change');
         }
     }.bind(this));
@@ -84,25 +82,25 @@ VideoSource.prototype = util.inherit(Evented, /** @lends VideoSource.prototype *
         }
     },
 
-    createTile: function() {
+    createTile: function(cornerGeoCoords) {
         /*
          * Calculate which mercator tile is suitable for rendering the video in
          * and create a buffer with the corner coordinates. These coordinates
          * may be outside the tile, because raster tiles aren't clipped when rendering.
          */
         var map = this.map;
-        var coords = this.coordinates.map(function(lnglat) {
-            var loc = LngLat.convert(lnglat);
-            return map.transform.locationCoordinate(loc).zoomTo(0);
+        var cornerZ0Coords = cornerGeoCoords.map(function(coord) {
+            return map.transform.locationCoordinate(LngLat.convert(coord)).zoomTo(0);
         });
 
-        var center = util.getCoordinatesCenter(coords);
+        var centerCoord = this.centerCoord = util.getCoordinatesCenter(cornerZ0Coords);
+
         var tileExtent = 4096;
-        var tileCoords = coords.map(function(coord) {
-            var zoomedCoord = coord.zoomTo(center.zoom);
+        var tileCoords = cornerZ0Coords.map(function(coord) {
+            var zoomedCoord = coord.zoomTo(centerCoord.zoom);
             return new Point(
-                Math.round((zoomedCoord.column - center.column) * tileExtent),
-                Math.round((zoomedCoord.row - center.row) * tileExtent));
+                Math.round((zoomedCoord.column - centerCoord.column) * tileExtent),
+                Math.round((zoomedCoord.row - centerCoord.row) * tileExtent));
         });
 
         var gl = map.painter.gl;
@@ -120,8 +118,6 @@ VideoSource.prototype = util.inherit(Evented, /** @lends VideoSource.prototype *
         this.tile.boundsBuffer = gl.createBuffer();
         gl.bindBuffer(gl.ARRAY_BUFFER, this.tile.boundsBuffer);
         gl.bufferData(gl.ARRAY_BUFFER, array, gl.STATIC_DRAW);
-
-        this.center = center;
     },
 
     loaded: function() {
@@ -136,14 +132,11 @@ VideoSource.prototype = util.inherit(Evented, /** @lends VideoSource.prototype *
         // noop
     },
 
-    render: function(layers, painter) {
+    prepare: function() {
         if (!this._loaded) return;
         if (this.video.readyState < 2) return; // not enough data for current position
 
-        var c = this.center;
-        this.tile.calculateMatrices(c.zoom, c.column, c.row, this.map.transform, painter);
-
-        var gl = painter.gl;
+        var gl = this.map.painter.gl;
         if (!this.tile.texture) {
             this.tile.texture = gl.createTexture();
             gl.bindTexture(gl.TEXTURE_2D, this.tile.texture);
@@ -157,7 +150,16 @@ VideoSource.prototype = util.inherit(Evented, /** @lends VideoSource.prototype *
             gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, this.video);
         }
 
-        painter.drawLayers(layers, this.tile.posMatrix, this.tile);
+        this._currentTime = this.video.currentTime;
+    },
+
+    getVisibleCoordinates: function() {
+        if (this.centerCoord) return [this.centerCoord];
+        else return [];
+    },
+
+    getTile: function() {
+        return this.tile;
     },
 
     featuresAt: function(point, params, callback) {
diff --git a/js/util/browser/canvas.js b/js/util/browser/canvas.js
index 662430db738..61592be9b2e 100644
--- a/js/util/browser/canvas.js
+++ b/js/util/browser/canvas.js
@@ -33,7 +33,7 @@ var requiredContextAttributes = {
     antialias: false,
     alpha: true,
     stencil: true,
-    depth: false
+    depth: true
 };
 
 Canvas.prototype.getWebGLContext = function(attributes) {
diff --git a/js/util/canvas.js b/js/util/canvas.js
index 5de89a52084..14a30d144b3 100644
--- a/js/util/canvas.js
+++ b/js/util/canvas.js
@@ -13,7 +13,7 @@ function Canvas(parent, container) {
         antialias: false,
         alpha: true,
         stencil: true,
-        depth: false,
+        depth: true,
         preserveDrawingBuffer: true
     };
 
diff --git a/package.json b/package.json
index e04032d785b..ce059cb1e89 100644
--- a/package.json
+++ b/package.json
@@ -37,7 +37,7 @@
     "eslint": "^1.5.0",
     "eslint-config-mourner": "^1.0.0",
     "istanbul": "^0.4.1",
-    "mapbox-gl-test-suite": "mapbox/mapbox-gl-test-suite#78bde0077848b4af0efd490d124bde3ea9f56ec9",
+    "mapbox-gl-test-suite": "mapbox/mapbox-gl-test-suite#42d780d6a3ff28417a8a0248b85d7ac0c7ca219f",
     "prova": "^2.1.2",
     "sinon": "^1.15.4",
     "st": "^1.0.0",
diff --git a/shaders/pattern.vertex.glsl b/shaders/pattern.vertex.glsl
index 560dc737227..22d9cf2f8a4 100644
--- a/shaders/pattern.vertex.glsl
+++ b/shaders/pattern.vertex.glsl
@@ -1,8 +1,8 @@
 precision mediump float;
 
 uniform mat4 u_matrix;
-uniform mat3 u_patternmatrix_a;
-uniform mat3 u_patternmatrix_b;
+uniform vec2 u_patternscale_a;
+uniform vec2 u_patternscale_b;
 
 attribute vec2 a_pos;
 
@@ -11,6 +11,6 @@ varying vec2 v_pos_b;
 
 void main() {
     gl_Position = u_matrix * vec4(a_pos, 0, 1);
-    v_pos_a = (u_patternmatrix_a * vec3(a_pos, 1)).xy;
-    v_pos_b = (u_patternmatrix_b * vec3(a_pos, 1)).xy;
+    v_pos_a = u_patternscale_a * a_pos;
+    v_pos_b = u_patternscale_b * a_pos;
 }