Pack preloaded framework assets in a texture atlas

When the Android runtime starts, the system preloads a series of assets
in the Zygote process. These assets are shared across all processes.
Unfortunately, each one of these assets is later uploaded in its own
OpenGL texture, once per process. This wastes memory and generates
unnecessary OpenGL state changes.

This CL introduces an asset server that provides an atlas to all processes.

Note: bitmaps used by skia shaders are *not* sampled from the atlas.
It's an uncommon use case and would require extra texture transforms
in the GL shaders.

WHAT IS THE ASSETS ATLAS

The "assets atlas" is a single, shareable graphic buffer that contains
all the system's preloaded bitmap drawables (this includes 9-patches.)
The atlas is made of two distinct objects: the graphic buffer that
contains the actual pixels and the map which indicates where each
preloaded bitmap can be found in the atlas (essentially a pair of
x and y coordinates.)

HOW IS THE ASSETS ATLAS GENERATED

Because we need to support a wide variety of devices and because it
is easy to change the list of preloaded drawables, the atlas is
generated at runtime, during the startup phase of the system process.

There are several steps that lead to the atlas generation:

1. If the device is booting for the first time, or if the device was
updated, we need to find the best atlas configuration. To do so,
the atlas service tries a number of width, height and algorithm
variations that allows us to pack as many assets as possible while
using as little memory as possible. Once a best configuration is found,
it gets written to disk in /data/system/framework_atlas

2. Given a best configuration (algorithm variant, dimensions and
number of bitmaps that can be packed in the atlas), the atlas service
packs all the preloaded bitmaps into a single graphic buffer object.

3. The packing is done using Skia in a temporary native bitmap. The
Skia bitmap is then copied into the graphic buffer using OpenGL ES
to benefit from texture swizzling.

HOW PROCESSES USE THE ATLAS

Whenever a process' hardware renderer initializes its EGL context,
it queries the atlas service for the graphic buffer and the map.

It is important to remember that both the context and the map will
be valid for the lifetime of the hardware renderer (if the system
process goes down, all apps get killed as well.)

Every time the hardware renderer needs to render a bitmap, it first
checks whether the bitmap can be found in the assets atlas. When
the bitmap is part of the atlas, texture coordinates are remapped
appropriately before rendering.

Change-Id: I8eaecf53e7f6a33d90da3d0047c5ceec89ea3af0
diff --git a/libs/hwui/Android.mk b/libs/hwui/Android.mk
index 281f9a5..3433d0e 100644
--- a/libs/hwui/Android.mk
+++ b/libs/hwui/Android.mk
@@ -10,6 +10,7 @@
 		thread/TaskManager.cpp \
 		font/CacheTexture.cpp \
 		font/Font.cpp \
+		AssetAtlas.cpp \
 		FontRenderer.cpp \
 		GammaFontRenderer.cpp \
 		Caches.cpp \
@@ -54,9 +55,9 @@
 		external/skia/src/ports \
 		external/skia/include/utils
 
-	LOCAL_CFLAGS += -DUSE_OPENGL_RENDERER -DGL_GLEXT_PROTOTYPES
+	LOCAL_CFLAGS += -DUSE_OPENGL_RENDERER -DEGL_EGLEXT_PROTOTYPES -DGL_GLEXT_PROTOTYPES
 	LOCAL_MODULE_CLASS := SHARED_LIBRARIES
-	LOCAL_SHARED_LIBRARIES := liblog libcutils libutils libGLESv2 libskia libui
+	LOCAL_SHARED_LIBRARIES := liblog libcutils libutils libEGL libGLESv2 libskia libui
 	LOCAL_MODULE := libhwui
 	LOCAL_MODULE_TAGS := optional
 
diff --git a/libs/hwui/AssetAtlas.cpp b/libs/hwui/AssetAtlas.cpp
new file mode 100644
index 0000000..cf8cc97
--- /dev/null
+++ b/libs/hwui/AssetAtlas.cpp
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define LOG_TAG "OpenGLRenderer"
+
+#include "AssetAtlas.h"
+
+#include <GLES2/gl2ext.h>
+
+#include <utils/Log.h>
+
+namespace android {
+namespace uirenderer {
+
+///////////////////////////////////////////////////////////////////////////////
+// Lifecycle
+///////////////////////////////////////////////////////////////////////////////
+
+void AssetAtlas::init(sp<GraphicBuffer> buffer, int* map, int count) {
+    if (mImage != EGL_NO_IMAGE_KHR) {
+        return;
+    }
+
+    // Create the EGLImage object that maps the GraphicBuffer
+    EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
+    EGLClientBuffer clientBuffer = (EGLClientBuffer) buffer->getNativeBuffer();
+    EGLint attrs[] = { EGL_IMAGE_PRESERVED_KHR, EGL_TRUE, EGL_NONE };
+
+    mImage = eglCreateImageKHR(display, EGL_NO_CONTEXT,
+            EGL_NATIVE_BUFFER_ANDROID, clientBuffer, attrs);
+
+    if (mImage == EGL_NO_IMAGE_KHR) {
+        ALOGW("Error creating atlas image (%#x)", eglGetError());
+        return;
+    }
+
+    // Create a 2D texture to sample from the EGLImage
+    glGenTextures(1, &mTexture);
+    glBindTexture(GL_TEXTURE_2D, mTexture);
+    glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, mImage);
+
+    mWidth = buffer->getWidth();
+    mHeight = buffer->getHeight();
+
+    createEntries(map, count);
+}
+
+void AssetAtlas::terminate() {
+    if (mImage != EGL_NO_IMAGE_KHR) {
+        eglDestroyImageKHR(eglGetDisplay(EGL_DEFAULT_DISPLAY), mImage);
+        mImage = EGL_NO_IMAGE_KHR;
+
+        glDeleteTextures(1, &mTexture);
+        mTexture = 0;
+
+        for (size_t i = 0; i < mEntries.size(); i++) {
+            delete mEntries.valueAt(i);
+        }
+        mEntries.clear();
+
+        mWidth = mHeight = 0;
+    }
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Entries
+///////////////////////////////////////////////////////////////////////////////
+
+AssetAtlas::Entry* AssetAtlas::getEntry(SkBitmap* const bitmap) const {
+    ssize_t index = mEntries.indexOfKey(bitmap);
+    return index >= 0 ? mEntries.valueAt(index) : NULL;
+}
+
+Texture* AssetAtlas::getEntryTexture(SkBitmap* const bitmap) const {
+    ssize_t index = mEntries.indexOfKey(bitmap);
+    return index >= 0 ? &mEntries.valueAt(index)->texture : NULL;
+}
+
+/**
+ * TODO: This method does not take the rotation flag into account
+ */
+void AssetAtlas::createEntries(int* map, int count) {
+    for (int i = 0; i < count; ) {
+        SkBitmap* bitmap = (SkBitmap*) map[i++];
+        int x = map[i++];
+        int y = map[i++];
+        bool rotated = map[i++] > 0;
+
+        // Bitmaps should never be null, we're just extra paranoid
+        if (!bitmap) continue;
+
+        const UvMapper mapper(
+                x / (float) mWidth, (x + bitmap->width()) / (float) mWidth,
+                y / (float) mHeight, (y + bitmap->height()) / (float) mHeight);
+
+        Entry* entry = new Entry(bitmap, x, y, rotated, mapper, *this);
+        entry->texture.id = mTexture;
+        entry->texture.blend = !bitmap->isOpaque();
+        entry->texture.width = bitmap->width();
+        entry->texture.height = bitmap->height();
+        entry->texture.uvMapper = &entry->uvMapper;
+
+        mEntries.add(entry->bitmap, entry);
+    }
+}
+
+}; // namespace uirenderer
+}; // namespace android
diff --git a/libs/hwui/AssetAtlas.h b/libs/hwui/AssetAtlas.h
new file mode 100644
index 0000000..4ede716
--- /dev/null
+++ b/libs/hwui/AssetAtlas.h
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef ANDROID_HWUI_ASSET_ATLAS_H
+#define ANDROID_HWUI_ASSET_ATLAS_H
+
+#include <GLES2/gl2.h>
+
+#include <EGL/egl.h>
+#include <EGL/eglext.h>
+
+#include <ui/GraphicBuffer.h>
+
+#include <utils/KeyedVector.h>
+
+#include <cutils/compiler.h>
+
+#include <SkBitmap.h>
+
+#include "Texture.h"
+#include "UvMapper.h"
+
+namespace android {
+namespace uirenderer {
+
+/**
+ * An asset atlas holds a collection of framework bitmaps in a single OpenGL
+ * texture. Each bitmap is associated with a location, defined in pixels,
+ * inside the atlas. The atlas is generated by the framework and bound as
+ * an external texture using the EGLImageKHR extension.
+ */
+class AssetAtlas {
+public:
+    /**
+     * Entry representing the position and rotation of a
+     * bitmap inside the atlas.
+     */
+    struct Entry {
+        /**
+         * The bitmap that generated this atlas entry.
+         */
+        SkBitmap* bitmap;
+
+        /**
+         * Location of the bitmap inside the atlas, in pixels.
+         */
+        int x;
+        int y;
+
+        /**
+         * If set, the bitmap is rotated 90 degrees (clockwise)
+         * inside the atlas.
+         */
+        bool rotated;
+
+        /**
+         * Maps texture coordinates in the [0..1] range into the
+         * correct range to sample this entry from the atlas.
+         */
+        const UvMapper uvMapper;
+
+        /**
+         * Atlas this entry belongs to.
+         */
+        const AssetAtlas& atlas;
+
+        /*
+         * A "virtual texture" object that represents the texture
+         * this entry belongs to. This texture should never be
+         * modified.
+         */
+        Texture texture;
+
+    private:
+        Entry(SkBitmap* bitmap, int x, int y, bool rotated,
+                const UvMapper& mapper, const AssetAtlas& atlas):
+                bitmap(bitmap), x(x), y(y), rotated(rotated), uvMapper(mapper), atlas(atlas) { }
+
+        friend class AssetAtlas;
+    };
+
+    AssetAtlas(): mWidth(0), mHeight(0), mTexture(0), mImage(EGL_NO_IMAGE_KHR) { }
+    ~AssetAtlas() { terminate(); }
+
+    /**
+     * Initializes the atlas with the specified buffer and
+     * map. The buffer is a gralloc'd texture that will be
+     * used as an EGLImage. The map is a list of SkBitmap*
+     * and their (x, y) positions as well as their rotation
+     * flags.
+     *
+     * This method returns immediately if the atlas is already
+     * initialized. To re-initialize the atlas, you must
+     * first call terminate().
+     */
+    ANDROID_API void init(sp<GraphicBuffer> buffer, int* map, int count);
+
+    /**
+     * Destroys the atlas texture. This object can be
+     * re-initialized after calling this method.
+     *
+     * After calling this method, the width, height
+     * and texture are set to 0.
+     */
+    ANDROID_API void terminate();
+
+    /**
+     * Returns the width of this atlas in pixels.
+     * Can return 0 if the atlas is not initialized.
+     */
+    uint32_t getWidth() const {
+        return mWidth;
+    }
+
+    /**
+     * Returns the height of this atlas in pixels.
+     * Can return 0 if the atlas is not initialized.
+     */
+    uint32_t getHeight() const {
+        return mHeight;
+    }
+
+    /**
+     * Returns the OpenGL name of the texture backing this atlas.
+     * Can return 0 if the atlas is not initialized.
+     */
+    GLuint getTexture() const {
+        return mTexture;
+    }
+
+    /**
+     * Returns the entry in the atlas associated with the specified
+     * bitmap. If the bitmap is not in the atlas, return NULL.
+     */
+    Entry* getEntry(SkBitmap* const bitmap) const;
+
+    /**
+     * Returns the texture for the atlas entry associated with the
+     * specified bitmap. If the bitmap is not in the atlas, return NULL.
+     */
+    Texture* getEntryTexture(SkBitmap* const bitmap) const;
+
+private:
+    void createEntries(int* map, int count);
+
+    uint32_t mWidth;
+    uint32_t mHeight;
+
+    GLuint mTexture;
+    EGLImageKHR mImage;
+
+    KeyedVector<SkBitmap*, Entry*> mEntries;
+}; // class AssetAtlas
+
+}; // namespace uirenderer
+}; // namespace android
+
+#endif // ANDROID_HWUI_ASSET_ATLAS_H
diff --git a/libs/hwui/Caches.cpp b/libs/hwui/Caches.cpp
index a381a68..c60848c 100644
--- a/libs/hwui/Caches.cpp
+++ b/libs/hwui/Caches.cpp
@@ -58,8 +58,8 @@
     ALOGD("Enabling debug mode %d", mDebugLevel);
 }
 
-void Caches::init() {
-    if (mInitialized) return;
+bool Caches::init() {
+    if (mInitialized) return false;
 
     glGenBuffers(1, &meshBuffer);
     glBindBuffer(GL_ARRAY_BUFFER, meshBuffer);
@@ -82,6 +82,7 @@
     mTextureUnit = 0;
 
     mRegionMesh = NULL;
+    mMeshIndices = 0;
 
     blend = false;
     lastSrcMode = GL_ZERO;
@@ -94,7 +95,11 @@
     debugOverdraw = false;
     debugStencilClip = kStencilHide;
 
+    patchCache.init(*this);
+
     mInitialized = true;
+
+    return true;
 }
 
 void Caches::initFont() {
@@ -191,8 +196,9 @@
     glDeleteBuffers(1, &meshBuffer);
     mCurrentBuffer = 0;
 
-    glDeleteBuffers(1, &mRegionMeshIndices);
+    glDeleteBuffers(1, &mMeshIndices);
     delete[] mRegionMesh;
+    mMeshIndices = 0;
     mRegionMesh = NULL;
 
     fboCache.clear();
@@ -200,6 +206,10 @@
     programCache.clear();
     currentProgram = NULL;
 
+    assetAtlas.terminate();
+
+    patchCache.clear();
+
     mInitialized = false;
 }
 
@@ -227,6 +237,8 @@
             pathCache.getSize(), pathCache.getMaxSize());
     log.appendFormat("  TextDropShadowCache  %8d / %8d\n", dropShadowCache.getSize(),
             dropShadowCache.getMaxSize());
+    log.appendFormat("  PatchCache           %8d / %8d\n",
+            patchCache.getSize(), patchCache.getMaxSize());
     for (uint32_t i = 0; i < fontRenderer->getFontRendererCount(); i++) {
         const uint32_t size = fontRenderer->getFontRendererSize(i);
         log.appendFormat("  FontRenderer %d       %8d / %8d\n", i, size, size);
@@ -234,8 +246,6 @@
     log.appendFormat("Other:\n");
     log.appendFormat("  FboCache             %8d / %8d\n",
             fboCache.getSize(), fboCache.getMaxSize());
-    log.appendFormat("  PatchCache           %8d / %8d\n",
-            patchCache.getSize(), patchCache.getMaxSize());
 
     uint32_t total = 0;
     total += textureCache.getSize();
@@ -244,6 +254,7 @@
     total += gradientCache.getSize();
     total += pathCache.getSize();
     total += dropShadowCache.getSize();
+    total += patchCache.getSize();
     for (uint32_t i = 0; i < fontRenderer->getFontRendererCount(); i++) {
         total += fontRenderer->getFontRendererSize(i);
     }
@@ -357,6 +368,32 @@
     return false;
 }
 
+bool Caches::bindIndicesBuffer() {
+    if (!mMeshIndices) {
+        uint16_t* regionIndices = new uint16_t[REGION_MESH_QUAD_COUNT * 6];
+        for (int i = 0; i < REGION_MESH_QUAD_COUNT; i++) {
+            uint16_t quad = i * 4;
+            int index = i * 6;
+            regionIndices[index    ] = quad;       // top-left
+            regionIndices[index + 1] = quad + 1;   // top-right
+            regionIndices[index + 2] = quad + 2;   // bottom-left
+            regionIndices[index + 3] = quad + 2;   // bottom-left
+            regionIndices[index + 4] = quad + 1;   // top-right
+            regionIndices[index + 5] = quad + 3;   // bottom-right
+        }
+
+        glGenBuffers(1, &mMeshIndices);
+        bool force = bindIndicesBuffer(mMeshIndices);
+        glBufferData(GL_ELEMENT_ARRAY_BUFFER, REGION_MESH_QUAD_COUNT * 6 * sizeof(uint16_t),
+                regionIndices, GL_STATIC_DRAW);
+
+        delete[] regionIndices;
+        return force;
+    }
+
+    return bindIndicesBuffer(mMeshIndices);
+}
+
 bool Caches::unbindIndicesBuffer() {
     if (mCurrentIndicesBuffer) {
         glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
@@ -546,27 +583,6 @@
     // Create the mesh, 2 triangles and 4 vertices per rectangle in the region
     if (!mRegionMesh) {
         mRegionMesh = new TextureVertex[REGION_MESH_QUAD_COUNT * 4];
-
-        uint16_t* regionIndices = new uint16_t[REGION_MESH_QUAD_COUNT * 6];
-        for (int i = 0; i < REGION_MESH_QUAD_COUNT; i++) {
-            uint16_t quad = i * 4;
-            int index = i * 6;
-            regionIndices[index    ] = quad;       // top-left
-            regionIndices[index + 1] = quad + 1;   // top-right
-            regionIndices[index + 2] = quad + 2;   // bottom-left
-            regionIndices[index + 3] = quad + 2;   // bottom-left
-            regionIndices[index + 4] = quad + 1;   // top-right
-            regionIndices[index + 5] = quad + 3;   // bottom-right
-        }
-
-        glGenBuffers(1, &mRegionMeshIndices);
-        bindIndicesBuffer(mRegionMeshIndices);
-        glBufferData(GL_ELEMENT_ARRAY_BUFFER, REGION_MESH_QUAD_COUNT * 6 * sizeof(uint16_t),
-                regionIndices, GL_STATIC_DRAW);
-
-        delete[] regionIndices;
-    } else {
-        bindIndicesBuffer(mRegionMeshIndices);
     }
 
     return mRegionMesh;
diff --git a/libs/hwui/Caches.h b/libs/hwui/Caches.h
index 91b938b..18aeeab 100644
--- a/libs/hwui/Caches.h
+++ b/libs/hwui/Caches.h
@@ -21,13 +21,18 @@
     #define LOG_TAG "OpenGLRenderer"
 #endif
 
+#include <GLES3/gl3.h>
+
+#include <utils/KeyedVector.h>
 #include <utils/Singleton.h>
+#include <utils/Vector.h>
 
 #include <cutils/compiler.h>
 
 #include "thread/TaskProcessor.h"
 #include "thread/TaskManager.h"
 
+#include "AssetAtlas.h"
 #include "FontRenderer.h"
 #include "GammaFontRenderer.h"
 #include "TextureCache.h"
@@ -113,7 +118,7 @@
     /**
      * Initialize caches.
      */
-    void init();
+    bool init();
 
     /**
      * Initialize global system properties.
@@ -172,6 +177,11 @@
      */
     bool unbindMeshBuffer();
 
+    /**
+     * Binds a global indices buffer that can draw up to
+     * REGION_MESH_QUAD_COUNT quads.
+     */
+    bool bindIndicesBuffer();
     bool bindIndicesBuffer(const GLuint buffer);
     bool unbindIndicesBuffer();
 
@@ -290,6 +300,8 @@
     Dither dither;
     Stencil stencil;
 
+    AssetAtlas assetAtlas;
+
     // Debug methods
     PFNGLINSERTEVENTMARKEREXTPROC eventMark;
     PFNGLPUSHGROUPMARKEREXTPROC startMark;
@@ -336,7 +348,9 @@
 
     // Used to render layers
     TextureVertex* mRegionMesh;
-    GLuint mRegionMeshIndices;
+
+    // Global index buffer
+    GLuint mMeshIndices;
 
     mutable Mutex mGarbageLock;
     Vector<Layer*> mLayerGarbage;
diff --git a/libs/hwui/Debug.h b/libs/hwui/Debug.h
index 790c4f4..786f12a 100644
--- a/libs/hwui/Debug.h
+++ b/libs/hwui/Debug.h
@@ -53,8 +53,6 @@
 
 // Turn on to display debug info about 9patch objects
 #define DEBUG_PATCHES 0
-// Turn on to "explode" 9patch objects
-#define DEBUG_EXPLODE_PATCHES 0
 // Turn on to display vertex and tex coords data about 9patch objects
 // This flag requires DEBUG_PATCHES to be turned on
 #define DEBUG_PATCHES_VERTICES 0
diff --git a/libs/hwui/DisplayListOp.h b/libs/hwui/DisplayListOp.h
index a0290e3..990372e 100644
--- a/libs/hwui/DisplayListOp.h
+++ b/libs/hwui/DisplayListOp.h
@@ -26,8 +26,10 @@
 #include <private/hwui/DrawGlInfo.h>
 
 #include "OpenGLRenderer.h"
+#include "AssetAtlas.h"
 #include "DeferredDisplayList.h"
 #include "DisplayListRenderer.h"
+#include "UvMapper.h"
 #include "utils/LinearAllocator.h"
 
 #define CRASH() do { \
@@ -721,7 +723,6 @@
     int mSetBits;
 };
 
-
 ///////////////////////////////////////////////////////////////////////////////
 // DRAW OPERATIONS - these are operations that can draw to the canvas's device
 ///////////////////////////////////////////////////////////////////////////////
@@ -729,9 +730,16 @@
 class DrawBitmapOp : public DrawBoundedOp {
 public:
     DrawBitmapOp(SkBitmap* bitmap, float left, float top, SkPaint* paint)
-            : DrawBoundedOp(left, top, left + bitmap->width(), top + bitmap->height(),
-                    paint),
-            mBitmap(bitmap) {}
+            : DrawBoundedOp(left, top, left + bitmap->width(), top + bitmap->height(), paint),
+            mBitmap(bitmap), mAtlasEntry(NULL) {
+    }
+
+    DrawBitmapOp(SkBitmap* bitmap, float left, float top, SkPaint* paint,
+            const AssetAtlas::Entry* entry)
+            : DrawBoundedOp(left, top, left + bitmap->width(), top + bitmap->height(), paint),
+            mBitmap(bitmap), mAtlasEntry(entry) {
+        if (entry) mUvMapper = entry->uvMapper;
+    }
 
     virtual status_t applyDraw(OpenGLRenderer& renderer, Rect& dirty) {
         return renderer.drawBitmap(mBitmap, mLocalBounds.left, mLocalBounds.top,
@@ -749,14 +757,14 @@
         TextureVertex vertices[6 * ops.size()];
         TextureVertex* vertex = &vertices[0];
 
-        // TODO: manually handle rect clip for bitmaps by adjusting texCoords per op, and allowing
-        // them to be merged in getBatchId()
-        const Rect texCoords(0, 0, 1, 1);
-
-        const float width = mBitmap->width();
-        const float height = mBitmap->height();
+        // TODO: manually handle rect clip for bitmaps by adjusting texCoords per op,
+        // and allowing them to be merged in getBatchId()
         for (unsigned int i = 0; i < ops.size(); i++) {
             const Rect& opBounds = ops[i]->state.mBounds;
+
+            Rect texCoords(0, 0, 1, 1);
+            ((DrawBitmapOp*) ops[i])->mUvMapper.map(texCoords);
+
             SET_TEXTURE(vertex, opBounds, bounds, texCoords, left, top);
             SET_TEXTURE(vertex, opBounds, bounds, texCoords, right, top);
             SET_TEXTURE(vertex, opBounds, bounds, texCoords, left, bottom);
@@ -777,7 +785,7 @@
 
     virtual bool onDefer(OpenGLRenderer& renderer, int* batchId, mergeid_t* mergeId) {
         *batchId = DeferredDisplayList::kOpBatch_Bitmap;
-        *mergeId = (mergeid_t)mBitmap;
+        *mergeId = mAtlasEntry ? (mergeid_t) &mAtlasEntry->atlas : (mergeid_t) mBitmap;
 
         // don't merge A8 bitmaps - the paint's color isn't compared by mergeId, or in
         // MergingDrawBatch::canMergeWith
@@ -787,6 +795,8 @@
     const SkBitmap* bitmap() { return mBitmap; }
 protected:
     SkBitmap* mBitmap;
+    const AssetAtlas::Entry* mAtlasEntry;
+    UvMapper mUvMapper;
 };
 
 class DrawBitmapMatrixOp : public DrawBoundedOp {
@@ -904,20 +914,16 @@
 
 class DrawPatchOp : public DrawBoundedOp {
 public:
-    DrawPatchOp(SkBitmap* bitmap, const int32_t* xDivs,
-            const int32_t* yDivs, const uint32_t* colors, uint32_t width, uint32_t height,
-            int8_t numColors, float left, float top, float right, float bottom,
-            int alpha, SkXfermode::Mode mode)
+    DrawPatchOp(SkBitmap* bitmap, Res_png_9patch* patch,
+            float left, float top, float right, float bottom, int alpha, SkXfermode::Mode mode)
             : DrawBoundedOp(left, top, right, bottom, 0),
-            mBitmap(bitmap), mxDivs(xDivs), myDivs(yDivs),
-            mColors(colors), mxDivsCount(width), myDivsCount(height),
-            mNumColors(numColors), mAlpha(alpha), mMode(mode) {};
+            mBitmap(bitmap), mPatch(patch), mAlpha(alpha), mMode(mode) {
+        mEntry = Caches::getInstance().assetAtlas.getEntry(bitmap);
+    };
 
     virtual status_t applyDraw(OpenGLRenderer& renderer, Rect& dirty) {
         // NOTE: not calling the virtual method, which takes a paint
-        return renderer.drawPatch(mBitmap, mxDivs, myDivs, mColors,
-                mxDivsCount, myDivsCount, mNumColors,
-                mLocalBounds.left, mLocalBounds.top,
+        return renderer.drawPatch(mBitmap, mPatch, mEntry, mLocalBounds.left, mLocalBounds.top,
                 mLocalBounds.right, mLocalBounds.bottom, mAlpha, mMode);
     }
 
@@ -929,20 +935,16 @@
 
     virtual bool onDefer(OpenGLRenderer& renderer, int* batchId, mergeid_t* mergeId) {
         *batchId = DeferredDisplayList::kOpBatch_Patch;
-        *mergeId = (mergeid_t)mBitmap;
+        *mergeId = (mergeid_t) mBitmap;
         return true;
     }
 
 private:
     SkBitmap* mBitmap;
-    const int32_t* mxDivs;
-    const int32_t* myDivs;
-    const uint32_t* mColors;
-    uint32_t mxDivsCount;
-    uint32_t myDivsCount;
-    int8_t mNumColors;
+    Res_png_9patch* mPatch;
     int mAlpha;
     SkXfermode::Mode mMode;
+    AssetAtlas::Entry* mEntry;
 };
 
 class DrawColorOp : public DrawOp {
diff --git a/libs/hwui/DisplayListRenderer.cpp b/libs/hwui/DisplayListRenderer.cpp
index 876c38a..bfd4086 100644
--- a/libs/hwui/DisplayListRenderer.cpp
+++ b/libs/hwui/DisplayListRenderer.cpp
@@ -257,7 +257,8 @@
     bitmap = refBitmap(bitmap);
     paint = refPaint(paint);
 
-    addDrawOp(new (alloc()) DrawBitmapOp(bitmap, left, top, paint));
+    const AssetAtlas::Entry* entry = mCaches.assetAtlas.getEntry(bitmap);
+    addDrawOp(new (alloc()) DrawBitmapOp(bitmap, left, top, paint, entry));
     return DrawGlInfo::kStatusDone;
 }
 
@@ -281,7 +282,8 @@
             (srcBottom - srcTop == dstBottom - dstTop) &&
             (srcRight - srcLeft == dstRight - dstLeft)) {
         // transform simple rect to rect drawing case into position bitmap ops, since they merge
-        addDrawOp(new (alloc()) DrawBitmapOp(bitmap, dstLeft, dstTop, paint));
+        const AssetAtlas::Entry* entry = mCaches.assetAtlas.getEntry(bitmap);
+        addDrawOp(new (alloc()) DrawBitmapOp(bitmap, dstLeft, dstTop, paint, entry));
         return DrawGlInfo::kStatusDone;
     }
 
@@ -313,20 +315,15 @@
     return DrawGlInfo::kStatusDone;
 }
 
-status_t DisplayListRenderer::drawPatch(SkBitmap* bitmap, const int32_t* xDivs,
-        const int32_t* yDivs, const uint32_t* colors, uint32_t width, uint32_t height,
-        int8_t numColors, float left, float top, float right, float bottom, SkPaint* paint) {
+status_t DisplayListRenderer::drawPatch(SkBitmap* bitmap, Res_png_9patch* patch,
+        float left, float top, float right, float bottom, SkPaint* paint) {
     int alpha;
     SkXfermode::Mode mode;
     OpenGLRenderer::getAlphaAndModeDirect(paint, &alpha, &mode);
 
     bitmap = refBitmap(bitmap);
-    xDivs = refBuffer<int>(xDivs, width);
-    yDivs = refBuffer<int>(yDivs, height);
-    colors = refBuffer<uint32_t>(colors, numColors);
 
-    addDrawOp(new (alloc()) DrawPatchOp(bitmap, xDivs, yDivs, colors, width, height, numColors,
-                    left, top, right, bottom, alpha, mode));
+    addDrawOp(new (alloc()) DrawPatchOp(bitmap, patch, left, top, right, bottom, alpha, mode));
     return DrawGlInfo::kStatusDone;
 }
 
diff --git a/libs/hwui/DisplayListRenderer.h b/libs/hwui/DisplayListRenderer.h
index 75abad6..db08921 100644
--- a/libs/hwui/DisplayListRenderer.h
+++ b/libs/hwui/DisplayListRenderer.h
@@ -103,8 +103,7 @@
     virtual status_t drawBitmapData(SkBitmap* bitmap, float left, float top, SkPaint* paint);
     virtual status_t drawBitmapMesh(SkBitmap* bitmap, int meshWidth, int meshHeight,
             float* vertices, int* colors, SkPaint* paint);
-    virtual status_t drawPatch(SkBitmap* bitmap, const int32_t* xDivs, const int32_t* yDivs,
-            const uint32_t* colors, uint32_t width, uint32_t height, int8_t numColors,
+    virtual status_t drawPatch(SkBitmap* bitmap, Res_png_9patch* patch,
             float left, float top, float right, float bottom, SkPaint* paint);
     virtual status_t drawColor(int color, SkXfermode::Mode mode);
     virtual status_t drawRect(float left, float top, float right, float bottom, SkPaint* paint);
diff --git a/libs/hwui/Extensions.h b/libs/hwui/Extensions.h
index 54a3987..a3f7c44 100644
--- a/libs/hwui/Extensions.h
+++ b/libs/hwui/Extensions.h
@@ -33,9 +33,6 @@
 
 class Extensions: public Singleton<Extensions> {
 public:
-    Extensions();
-    ~Extensions();
-
     inline bool hasNPot() const { return mHasNPot; }
     inline bool hasFramebufferFetch() const { return mHasFramebufferFetch; }
     inline bool hasDiscardFramebuffer() const { return mHasDiscardFramebuffer; }
@@ -53,6 +50,9 @@
     void dump() const;
 
 private:
+    Extensions();
+    ~Extensions();
+
     friend class Singleton<Extensions>;
 
     SortedVector<String8> mExtensionList;
diff --git a/libs/hwui/OpenGLRenderer.cpp b/libs/hwui/OpenGLRenderer.cpp
index a4f9860..025e9f8 100644
--- a/libs/hwui/OpenGLRenderer.cpp
+++ b/libs/hwui/OpenGLRenderer.cpp
@@ -1873,7 +1873,7 @@
 
 void OpenGLRenderer::setupDrawMesh(GLvoid* vertices, GLvoid* texCoords, GLuint vbo) {
     bool force = false;
-    if (!vertices) {
+    if (!vertices || vbo) {
         force = mCaches.bindMeshBuffer(vbo == 0 ? mCaches.meshBuffer : vbo);
     } else {
         force = mCaches.unbindMeshBuffer();
@@ -1904,8 +1904,18 @@
     mCaches.unbindIndicesBuffer();
 }
 
-void OpenGLRenderer::setupDrawMeshIndices(GLvoid* vertices, GLvoid* texCoords) {
-    bool force = mCaches.unbindMeshBuffer();
+void OpenGLRenderer::setupDrawMeshIndices(GLvoid* vertices, GLvoid* texCoords, GLuint vbo) {
+    bool force = false;
+    // If vbo is != 0 we want to treat the vertices parameter as an offset inside
+    // a VBO. However, if vertices is set to NULL and vbo == 0 then we want to
+    // use the default VBO found in Caches
+    if (!vertices || vbo) {
+        force = mCaches.bindMeshBuffer(vbo == 0 ? mCaches.meshBuffer : vbo);
+    } else {
+        force = mCaches.unbindMeshBuffer();
+    }
+    mCaches.bindIndicesBuffer();
+
     mCaches.bindPositionVertexPointer(force, vertices);
     if (mCaches.currentProgram->texCoords >= 0) {
         mCaches.bindTexCoordsVertexPointer(force, texCoords);
@@ -1980,9 +1990,11 @@
         texture->setFilter(FILTER(paint), true);
     }
 
+    // No need to check for a UV mapper on the texture object, only ARGB_8888
+    // bitmaps get packed in the atlas
     drawAlpha8TextureMesh(x, y, x + texture->width, y + texture->height, texture->id,
-            paint != NULL, color, alpha, mode, (GLvoid*) NULL,
-            (GLvoid*) gMeshTextureOffset, GL_TRIANGLE_STRIP, gMeshCount, ignoreTransform);
+            paint != NULL, color, alpha, mode, (GLvoid*) NULL, (GLvoid*) gMeshTextureOffset,
+            GL_TRIANGLE_STRIP, gMeshCount, ignoreTransform);
 }
 
 status_t OpenGLRenderer::drawBitmaps(SkBitmap* bitmap, int bitmapCount, TextureVertex* vertices,
@@ -1992,8 +2004,9 @@
     mCaches.setScissorEnabled(mScissorOptimizationDisabled);
 
     mCaches.activeTexture(0);
-    Texture* texture = mCaches.textureCache.get(bitmap);
+    Texture* texture = getTexture(bitmap);
     if (!texture) return DrawGlInfo::kStatusDone;
+
     const AutoTexture autoCleanup(texture);
 
     int alpha;
@@ -2030,7 +2043,7 @@
     }
 
     mCaches.activeTexture(0);
-    Texture* texture = mCaches.textureCache.get(bitmap);
+    Texture* texture = getTexture(bitmap);
     if (!texture) return DrawGlInfo::kStatusDone;
     const AutoTexture autoCleanup(texture);
 
@@ -2053,7 +2066,7 @@
     }
 
     mCaches.activeTexture(0);
-    Texture* texture = mCaches.textureCache.get(bitmap);
+    Texture* texture = getTexture(bitmap);
     if (!texture) return DrawGlInfo::kStatusDone;
     const AutoTexture autoCleanup(texture);
 
@@ -2116,6 +2129,10 @@
         cleanupColors = true;
     }
 
+    mCaches.activeTexture(0);
+    Texture* texture = mCaches.assetAtlas.getEntryTexture(bitmap);
+    const UvMapper& mapper(getMapper(texture));
+
     for (int32_t y = 0; y < meshHeight; y++) {
         for (int32_t x = 0; x < meshWidth; x++) {
             uint32_t i = (y * (meshWidth + 1) + x) * 2;
@@ -2125,6 +2142,8 @@
             float v1 = float(y) / meshHeight;
             float v2 = float(y + 1) / meshHeight;
 
+            mapper.map(u1, v1, u2, v2);
+
             int ax = i + (meshWidth + 1) * 2;
             int ay = ax + 1;
             int bx = i;
@@ -2154,11 +2173,12 @@
         return DrawGlInfo::kStatusDone;
     }
 
-    mCaches.activeTexture(0);
-    Texture* texture = mCaches.textureCache.get(bitmap);
     if (!texture) {
-        if (cleanupColors) delete[] colors;
-        return DrawGlInfo::kStatusDone;
+        texture = mCaches.textureCache.get(bitmap);
+        if (!texture) {
+            if (cleanupColors) delete[] colors;
+            return DrawGlInfo::kStatusDone;
+        }
     }
     const AutoTexture autoCleanup(texture);
 
@@ -2211,17 +2231,19 @@
     }
 
     mCaches.activeTexture(0);
-    Texture* texture = mCaches.textureCache.get(bitmap);
+    Texture* texture = getTexture(bitmap);
     if (!texture) return DrawGlInfo::kStatusDone;
     const AutoTexture autoCleanup(texture);
 
     const float width = texture->width;
     const float height = texture->height;
 
-    const float u1 = fmax(0.0f, srcLeft / width);
-    const float v1 = fmax(0.0f, srcTop / height);
-    const float u2 = fmin(1.0f, srcRight / width);
-    const float v2 = fmin(1.0f, srcBottom / height);
+    float u1 = fmax(0.0f, srcLeft / width);
+    float v1 = fmax(0.0f, srcTop / height);
+    float u2 = fmin(1.0f, srcRight / width);
+    float v2 = fmin(1.0f, srcBottom / height);
+
+    getMapper(texture).map(u1, v1, u2, v2);
 
     mCaches.unbindMeshBuffer();
     resetDrawTextureTexCoords(u1, v1, u2, v2);
@@ -2292,34 +2314,32 @@
     return DrawGlInfo::kStatusDrew;
 }
 
-status_t OpenGLRenderer::drawPatch(SkBitmap* bitmap, const int32_t* xDivs, const int32_t* yDivs,
-        const uint32_t* colors, uint32_t width, uint32_t height, int8_t numColors,
+status_t OpenGLRenderer::drawPatch(SkBitmap* bitmap, Res_png_9patch* patch,
         float left, float top, float right, float bottom, SkPaint* paint) {
     int alpha;
     SkXfermode::Mode mode;
     getAlphaAndMode(paint, &alpha, &mode);
 
-    return drawPatch(bitmap, xDivs, yDivs, colors, width, height, numColors,
+    return drawPatch(bitmap, patch, mCaches.assetAtlas.getEntry(bitmap),
             left, top, right, bottom, alpha, mode);
 }
 
-status_t OpenGLRenderer::drawPatch(SkBitmap* bitmap, const int32_t* xDivs, const int32_t* yDivs,
-        const uint32_t* colors, uint32_t width, uint32_t height, int8_t numColors,
-        float left, float top, float right, float bottom, int alpha, SkXfermode::Mode mode) {
+status_t OpenGLRenderer::drawPatch(SkBitmap* bitmap, Res_png_9patch* patch,
+        AssetAtlas::Entry* entry, float left, float top, float right, float bottom,
+        int alpha, SkXfermode::Mode mode) {
     if (quickReject(left, top, right, bottom)) {
         return DrawGlInfo::kStatusDone;
     }
 
-    alpha *= mSnapshot->alpha;
-
-    const Patch* mesh = mCaches.patchCache.get(bitmap->width(), bitmap->height(),
-            right - left, bottom - top, xDivs, yDivs, colors, width, height, numColors);
+    const Patch* mesh = mCaches.patchCache.get(entry, bitmap->width(), bitmap->height(),
+            right - left, bottom - top, patch);
 
     if (CC_LIKELY(mesh && mesh->verticesCount > 0)) {
         mCaches.activeTexture(0);
-        Texture* texture = mCaches.textureCache.get(bitmap);
+        Texture* texture = entry ? &entry->texture : mCaches.textureCache.get(bitmap);
         if (!texture) return DrawGlInfo::kStatusDone;
         const AutoTexture autoCleanup(texture);
+
         texture->setWrap(GL_CLAMP_TO_EDGE, true);
         texture->setFilter(GL_LINEAR, true);
 
@@ -2342,19 +2362,23 @@
             }
         }
 
+        alpha *= mSnapshot->alpha;
+
         if (CC_LIKELY(pureTranslate)) {
             const float x = (int) floorf(left + currentTransform().getTranslateX() + 0.5f);
             const float y = (int) floorf(top + currentTransform().getTranslateY() + 0.5f);
 
-            drawTextureMesh(x, y, x + right - left, y + bottom - top, texture->id, alpha / 255.0f,
-                    mode, texture->blend, (GLvoid*) 0, (GLvoid*) gMeshTextureOffset,
-                    GL_TRIANGLES, mesh->verticesCount, false, true, mesh->meshBuffer,
-                    true, !mesh->hasEmptyQuads);
+            right = x + right - left;
+            bottom = y + bottom - top;
+            drawIndexedTextureMesh(x, y, right, bottom, texture->id, alpha / 255.0f,
+                    mode, texture->blend, (GLvoid*) mesh->offset, (GLvoid*) mesh->textureOffset,
+                    GL_TRIANGLES, mesh->indexCount, false, true,
+                    mCaches.patchCache.getMeshBuffer(), true, !mesh->hasEmptyQuads);
         } else {
-            drawTextureMesh(left, top, right, bottom, texture->id, alpha / 255.0f,
-                    mode, texture->blend, (GLvoid*) 0, (GLvoid*) gMeshTextureOffset,
-                    GL_TRIANGLES, mesh->verticesCount, false, false, mesh->meshBuffer,
-                    true, !mesh->hasEmptyQuads);
+            drawIndexedTextureMesh(left, top, right, bottom, texture->id, alpha / 255.0f,
+                    mode, texture->blend, (GLvoid*) mesh->offset, (GLvoid*) mesh->textureOffset,
+                    GL_TRIANGLES, mesh->indexCount, false, false,
+                    mCaches.patchCache.getMeshBuffer(), true, !mesh->hasEmptyQuads);
         }
     }
 
@@ -3196,6 +3220,14 @@
 // Drawing implementation
 ///////////////////////////////////////////////////////////////////////////////
 
+Texture* OpenGLRenderer::getTexture(SkBitmap* bitmap) {
+    Texture* texture = mCaches.assetAtlas.getEntryTexture(bitmap);
+    if (!texture) {
+        return mCaches.textureCache.get(bitmap);
+    }
+    return texture;
+}
+
 void OpenGLRenderer::drawPathTexture(const PathTexture* texture,
         float x, float y, SkPaint* paint) {
     if (quickReject(x, y, x + texture->width, y + texture->height)) {
@@ -3389,19 +3421,35 @@
 
     texture->setWrap(GL_CLAMP_TO_EDGE, true);
 
+    GLvoid* vertices = (GLvoid*) NULL;
+    GLvoid* texCoords = (GLvoid*) gMeshTextureOffset;
+
+    if (texture->uvMapper) {
+        vertices = &mMeshVertices[0].position[0];
+        texCoords = &mMeshVertices[0].texture[0];
+
+        Rect uvs(0.0f, 0.0f, 1.0f, 1.0f);
+        texture->uvMapper->map(uvs);
+
+        resetDrawTextureTexCoords(uvs.left, uvs.top, uvs.right, uvs.bottom);
+    }
+
     if (CC_LIKELY(currentTransform().isPureTranslate())) {
         const float x = (int) floorf(left + currentTransform().getTranslateX() + 0.5f);
         const float y = (int) floorf(top + currentTransform().getTranslateY() + 0.5f);
 
         texture->setFilter(GL_NEAREST, true);
         drawTextureMesh(x, y, x + texture->width, y + texture->height, texture->id,
-                alpha / 255.0f, mode, texture->blend, (GLvoid*) NULL,
-                (GLvoid*) gMeshTextureOffset, GL_TRIANGLE_STRIP, gMeshCount, false, true);
+                alpha / 255.0f, mode, texture->blend, vertices, texCoords,
+                GL_TRIANGLE_STRIP, gMeshCount, false, true);
     } else {
         texture->setFilter(FILTER(paint), true);
         drawTextureMesh(left, top, right, bottom, texture->id, alpha / 255.0f, mode,
-                texture->blend, (GLvoid*) NULL, (GLvoid*) gMeshTextureOffset,
-                GL_TRIANGLE_STRIP, gMeshCount);
+                texture->blend, vertices, texCoords, GL_TRIANGLE_STRIP, gMeshCount);
+    }
+
+    if (texture->uvMapper) {
+        resetDrawTextureTexCoords(0.0f, 0.0f, 1.0f, 1.0f);
     }
 }
 
@@ -3438,6 +3486,33 @@
     finishDrawTexture();
 }
 
+void OpenGLRenderer::drawIndexedTextureMesh(float left, float top, float right, float bottom,
+        GLuint texture, float alpha, SkXfermode::Mode mode, bool blend,
+        GLvoid* vertices, GLvoid* texCoords, GLenum drawMode, GLsizei elementsCount,
+        bool swapSrcDst, bool ignoreTransform, GLuint vbo, bool ignoreScale, bool dirty) {
+
+    setupDraw();
+    setupDrawWithTexture();
+    setupDrawColor(alpha, alpha, alpha, alpha);
+    setupDrawColorFilter();
+    setupDrawBlending(blend, mode, swapSrcDst);
+    setupDrawProgram();
+    if (!dirty) setupDrawDirtyRegionsDisabled();
+    if (!ignoreScale) {
+        setupDrawModelView(left, top, right, bottom, ignoreTransform);
+    } else {
+        setupDrawModelViewTranslate(left, top, right, bottom, ignoreTransform);
+    }
+    setupDrawTexture(texture);
+    setupDrawPureColorUniforms();
+    setupDrawColorFilterUniforms();
+    setupDrawMeshIndices(vertices, texCoords, vbo);
+
+    glDrawElements(drawMode, elementsCount, GL_UNSIGNED_SHORT, NULL);
+
+    finishDrawTexture();
+}
+
 void OpenGLRenderer::drawAlpha8TextureMesh(float left, float top, float right, float bottom,
         GLuint texture, bool hasColor, int color, int alpha, SkXfermode::Mode mode,
         GLvoid* vertices, GLvoid* texCoords, GLenum drawMode, GLsizei elementsCount,
diff --git a/libs/hwui/OpenGLRenderer.h b/libs/hwui/OpenGLRenderer.h
index a0ad888..640c7db 100644
--- a/libs/hwui/OpenGLRenderer.h
+++ b/libs/hwui/OpenGLRenderer.h
@@ -34,6 +34,8 @@
 
 #include <cutils/compiler.h>
 
+#include <androidfw/ResourceTypes.h>
+
 #include "Debug.h"
 #include "Extensions.h"
 #include "Matrix.h"
@@ -43,6 +45,7 @@
 #include "Vertex.h"
 #include "SkiaShader.h"
 #include "SkiaColorFilter.h"
+#include "UvMapper.h"
 #include "Caches.h"
 
 namespace android {
@@ -78,7 +81,8 @@
 };
 
 struct DeferredDisplayState {
-    Rect mBounds; // global op bounds, mapped by mMatrix to be in screen space coordinates, clipped.
+    // global op bounds, mapped by mMatrix to be in screen space coordinates, clipped
+    Rect mBounds;
 
     // the below are set and used by the OpenGLRenderer at record and deferred playback
     bool mClipValid;
@@ -248,11 +252,9 @@
     virtual status_t drawBitmapData(SkBitmap* bitmap, float left, float top, SkPaint* paint);
     virtual status_t drawBitmapMesh(SkBitmap* bitmap, int meshWidth, int meshHeight,
             float* vertices, int* colors, SkPaint* paint);
-    virtual status_t drawPatch(SkBitmap* bitmap, const int32_t* xDivs, const int32_t* yDivs,
-            const uint32_t* colors, uint32_t width, uint32_t height, int8_t numColors,
+    virtual status_t drawPatch(SkBitmap* bitmap, Res_png_9patch* patch,
             float left, float top, float right, float bottom, SkPaint* paint);
-    status_t drawPatch(SkBitmap* bitmap, const int32_t* xDivs, const int32_t* yDivs,
-            const uint32_t* colors, uint32_t width, uint32_t height, int8_t numColors,
+    status_t drawPatch(SkBitmap* bitmap, Res_png_9patch* patch, AssetAtlas::Entry* entry,
             float left, float top, float right, float bottom, int alpha, SkXfermode::Mode mode);
     virtual status_t drawColor(int color, SkXfermode::Mode mode);
     virtual status_t drawRect(float left, float top, float right, float bottom, SkPaint* paint);
@@ -798,6 +800,12 @@
             bool swapSrcDst = false, bool ignoreTransform = false, GLuint vbo = 0,
             bool ignoreScale = false, bool dirty = true);
 
+    void drawIndexedTextureMesh(float left, float top, float right, float bottom, GLuint texture,
+            float alpha, SkXfermode::Mode mode, bool blend,
+            GLvoid* vertices, GLvoid* texCoords, GLenum drawMode, GLsizei elementsCount,
+            bool swapSrcDst = false, bool ignoreTransform = false, GLuint vbo = 0,
+            bool ignoreScale = false, bool dirty = true);
+
     void drawAlpha8TextureMesh(float left, float top, float right, float bottom,
             GLuint texture, bool hasColor, int color, int alpha, SkXfermode::Mode mode,
             GLvoid* vertices, GLvoid* texCoords, GLenum drawMode, GLsizei elementsCount,
@@ -943,7 +951,7 @@
     void setupDrawTextGammaUniforms();
     void setupDrawMesh(GLvoid* vertices, GLvoid* texCoords = NULL, GLuint vbo = 0);
     void setupDrawMesh(GLvoid* vertices, GLvoid* texCoords, GLvoid* colors);
-    void setupDrawMeshIndices(GLvoid* vertices, GLvoid* texCoords);
+    void setupDrawMeshIndices(GLvoid* vertices, GLvoid* texCoords, GLuint vbo = 0);
     void setupDrawVertices(GLvoid* vertices);
     void finishDrawTexture();
     void accountForClear(SkXfermode::Mode mode);
@@ -985,6 +993,17 @@
         return *mSnapshot->transform;
     }
 
+    inline const UvMapper& getMapper(const Texture* texture) {
+        return texture && texture->uvMapper ? *texture->uvMapper : mUvMapper;
+    }
+
+    /**
+     * Returns a texture object for the specified bitmap. The texture can
+     * come from the texture cache or an atlas. If this method returns
+     * NULL, the texture could not be found and/or allocated.
+     */
+    Texture* getTexture(SkBitmap* bitmap);
+
     // Dimensions of the drawing surface
     int mWidth, mHeight;
 
@@ -1010,6 +1029,9 @@
     // Used to draw textured quads
     TextureVertex mMeshVertices[4];
 
+    // Default UV mapper
+    const UvMapper mUvMapper;
+
     // shader, filters, and shadow
     DrawModifiers mDrawModifiers;
     SkPaint mFilteredPaint;
diff --git a/libs/hwui/Patch.cpp b/libs/hwui/Patch.cpp
index 45c619e..6b0734a 100644
--- a/libs/hwui/Patch.cpp
+++ b/libs/hwui/Patch.cpp
@@ -20,9 +20,10 @@
 
 #include <utils/Log.h>
 
-#include "Patch.h"
 #include "Caches.h"
+#include "Patch.h"
 #include "Properties.h"
+#include "UvMapper.h"
 
 namespace android {
 namespace uirenderer {
@@ -31,90 +32,61 @@
 // Constructors/destructor
 ///////////////////////////////////////////////////////////////////////////////
 
-Patch::Patch(const uint32_t xCount, const uint32_t yCount, const int8_t emptyQuads):
-        mXCount(xCount), mYCount(yCount), mEmptyQuads(emptyQuads) {
-    // Initialized with the maximum number of vertices we will need
-    // 2 triangles per patch, 3 vertices per triangle
-    uint32_t maxVertices = ((xCount + 1) * (yCount + 1) - emptyQuads) * 2 * 3;
-    mVertices = new TextureVertex[maxVertices];
-    mAllocatedVerticesCount = 0;
-
-    verticesCount = 0;
-    hasEmptyQuads = emptyQuads > 0;
-
-    mColorKey = 0;
-    mXDivs = new int32_t[mXCount];
-    mYDivs = new int32_t[mYCount];
-
-    PATCH_LOGD("    patch: xCount = %d, yCount = %d, emptyQuads = %d, max vertices = %d",
-            xCount, yCount, emptyQuads, maxVertices);
-
-    glGenBuffers(1, &meshBuffer);
+Patch::Patch(): verticesCount(0), indexCount(0), hasEmptyQuads(false) {
 }
 
 Patch::~Patch() {
-    delete[] mVertices;
-    delete[] mXDivs;
-    delete[] mYDivs;
-    glDeleteBuffers(1, &meshBuffer);
-}
-
-///////////////////////////////////////////////////////////////////////////////
-// Patch management
-///////////////////////////////////////////////////////////////////////////////
-
-void Patch::copy(const int32_t* xDivs, const int32_t* yDivs) {
-    memcpy(mXDivs, xDivs, mXCount * sizeof(int32_t));
-    memcpy(mYDivs, yDivs, mYCount * sizeof(int32_t));
-}
-
-void Patch::updateColorKey(const uint32_t colorKey) {
-    mColorKey = colorKey;
-}
-
-bool Patch::matches(const int32_t* xDivs, const int32_t* yDivs,
-        const uint32_t colorKey, const int8_t emptyQuads) {
-
-    bool matches = true;
-
-    if (mEmptyQuads != emptyQuads) {
-        mEmptyQuads = emptyQuads;
-        hasEmptyQuads = emptyQuads > 0;
-        matches = false;
-    }
-
-    if (mColorKey != colorKey) {
-        updateColorKey(colorKey);
-        matches = false;
-    }
-
-    if (memcmp(mXDivs, xDivs, mXCount * sizeof(int32_t))) {
-        memcpy(mXDivs, xDivs, mXCount * sizeof(int32_t));
-        matches = false;
-    }
-
-    if (memcmp(mYDivs, yDivs, mYCount * sizeof(int32_t))) {
-        memcpy(mYDivs, yDivs, mYCount * sizeof(int32_t));
-        matches = false;
-    }
-
-    return matches;
 }
 
 ///////////////////////////////////////////////////////////////////////////////
 // Vertices management
 ///////////////////////////////////////////////////////////////////////////////
 
-void Patch::updateVertices(const float bitmapWidth, const float bitmapHeight,
-        float left, float top, float right, float bottom) {
-    if (hasEmptyQuads) quads.clear();
+uint32_t Patch::getSize() const {
+    return verticesCount * sizeof(TextureVertex);
+}
 
-    // Reset the vertices count here, we will count exactly how many
-    // vertices we actually need when generating the quads
-    verticesCount = 0;
+TextureVertex* Patch::createMesh(const float bitmapWidth, const float bitmapHeight,
+        float left, float top, float right, float bottom, const Res_png_9patch* patch) {
+    UvMapper mapper;
+    return createMesh(bitmapWidth, bitmapHeight, left, top, right, bottom, mapper, patch);
+}
 
-    const uint32_t xStretchCount = (mXCount + 1) >> 1;
-    const uint32_t yStretchCount = (mYCount + 1) >> 1;
+TextureVertex* Patch::createMesh(const float bitmapWidth, const float bitmapHeight,
+        float left, float top, float right, float bottom,
+        const UvMapper& mapper, const Res_png_9patch* patch) {
+
+    const uint32_t* colors = &patch->colors[0];
+    const int8_t numColors = patch->numColors;
+
+    mColorKey = 0;
+    int8_t emptyQuads = 0;
+
+    if (uint8_t(numColors) < sizeof(uint32_t) * 4) {
+        for (int8_t i = 0; i < numColors; i++) {
+            if (colors[i] == 0x0) {
+                emptyQuads++;
+                mColorKey |= 0x1 << i;
+            }
+        }
+    }
+
+    hasEmptyQuads = emptyQuads > 0;
+
+    uint32_t xCount = patch->numXDivs;
+    uint32_t yCount = patch->numYDivs;
+
+    uint32_t maxVertices = ((xCount + 1) * (yCount + 1) - emptyQuads) * 4;
+    if (maxVertices == 0) return NULL;
+
+    TextureVertex* vertices = new TextureVertex[maxVertices];
+    TextureVertex* vertex = vertices;
+
+    const int32_t* xDivs = patch->xDivs;
+    const int32_t* yDivs = patch->yDivs;
+
+    const uint32_t xStretchCount = (xCount + 1) >> 1;
+    const uint32_t yStretchCount = (yCount + 1) >> 1;
 
     float stretchX = 0.0f;
     float stretchY = 0.0f;
@@ -124,8 +96,8 @@
 
     if (xStretchCount > 0) {
         uint32_t stretchSize = 0;
-        for (uint32_t i = 1; i < mXCount; i += 2) {
-            stretchSize += mXDivs[i] - mXDivs[i - 1];
+        for (uint32_t i = 1; i < xCount; i += 2) {
+            stretchSize += xDivs[i] - xDivs[i - 1];
         }
         const float xStretchTex = stretchSize;
         const float fixed = bitmapWidth - stretchSize;
@@ -136,8 +108,8 @@
 
     if (yStretchCount > 0) {
         uint32_t stretchSize = 0;
-        for (uint32_t i = 1; i < mYCount; i += 2) {
-            stretchSize += mYDivs[i] - mYDivs[i - 1];
+        for (uint32_t i = 1; i < yCount; i += 2) {
+            stretchSize += yDivs[i] - yDivs[i - 1];
         }
         const float yStretchTex = stretchSize;
         const float fixed = bitmapHeight - stretchSize;
@@ -146,7 +118,6 @@
         rescaleY = fixed == 0.0f ? 0.0f : fminf(fmaxf(bottom - top, 0.0f) / fixed, 1.0f);
     }
 
-    TextureVertex* vertex = mVertices;
     uint32_t quadCount = 0;
 
     float previousStepY = 0.0f;
@@ -155,8 +126,10 @@
     float y2 = 0.0f;
     float v1 = 0.0f;
 
-    for (uint32_t i = 0; i < mYCount; i++) {
-        float stepY = mYDivs[i];
+    mUvMapper = mapper;
+
+    for (uint32_t i = 0; i < yCount; i++) {
+        float stepY = yDivs[i];
         const float segment = stepY - previousStepY;
 
         if (i & 1) {
@@ -170,15 +143,8 @@
         v1 += vOffset / bitmapHeight;
 
         if (stepY > 0.0f) {
-#if DEBUG_EXPLODE_PATCHES
-            y1 += i * EXPLODE_GAP;
-            y2 += i * EXPLODE_GAP;
-#endif
-            generateRow(vertex, y1, y2, v1, v2, stretchX, rescaleX, right - left,
-                    bitmapWidth, quadCount);
-#if DEBUG_EXPLODE_PATCHES
-            y2 -= i * EXPLODE_GAP;
-#endif
+            generateRow(xDivs, xCount, vertex, y1, y2, v1, v2, stretchX, rescaleX,
+                    right - left, bitmapWidth, quadCount);
         }
 
         y1 = y2;
@@ -189,33 +155,16 @@
 
     if (previousStepY != bitmapHeight) {
         y2 = bottom - top;
-#if DEBUG_EXPLODE_PATCHES
-        y1 += mYCount * EXPLODE_GAP;
-        y2 += mYCount * EXPLODE_GAP;
-#endif
-        generateRow(vertex, y1, y2, v1, 1.0f, stretchX, rescaleX, right - left,
-                bitmapWidth, quadCount);
+        generateRow(xDivs, xCount, vertex, y1, y2, v1, 1.0f, stretchX, rescaleX,
+                right - left, bitmapWidth, quadCount);
     }
 
-    if (verticesCount > 0) {
-        Caches& caches = Caches::getInstance();
-        caches.bindMeshBuffer(meshBuffer);
-        if (mAllocatedVerticesCount < verticesCount) {
-            glBufferData(GL_ARRAY_BUFFER, sizeof(TextureVertex) * verticesCount,
-                    mVertices, GL_DYNAMIC_DRAW);
-            mAllocatedVerticesCount = verticesCount;
-        } else {
-            glBufferSubData(GL_ARRAY_BUFFER, 0,
-                    sizeof(TextureVertex) * verticesCount, mVertices);
-        }
-        caches.resetVertexPointers();
-    }
-
-    PATCH_LOGD("    patch: new vertices count = %d", verticesCount);
+    return vertices;
 }
 
-void Patch::generateRow(TextureVertex*& vertex, float y1, float y2, float v1, float v2,
-        float stretchX, float rescaleX, float width, float bitmapWidth, uint32_t& quadCount) {
+void Patch::generateRow(const int32_t* xDivs, uint32_t xCount, TextureVertex*& vertex,
+        float y1, float y2, float v1, float v2, float stretchX, float rescaleX,
+        float width, float bitmapWidth, uint32_t& quadCount) {
     float previousStepX = 0.0f;
 
     float x1 = 0.0f;
@@ -223,8 +172,8 @@
     float u1 = 0.0f;
 
     // Generate the row quad by quad
-    for (uint32_t i = 0; i < mXCount; i++) {
-        float stepX = mXDivs[i];
+    for (uint32_t i = 0; i < xCount; i++) {
+        float stepX = xDivs[i];
         const float segment = stepX - previousStepX;
 
         if (i & 1) {
@@ -238,14 +187,7 @@
         u1 += uOffset / bitmapWidth;
 
         if (stepX > 0.0f) {
-#if DEBUG_EXPLODE_PATCHES
-            x1 += i * EXPLODE_GAP;
-            x2 += i * EXPLODE_GAP;
-#endif
             generateQuad(vertex, x1, y1, x2, y2, u1, v1, u2, v2, quadCount);
-#if DEBUG_EXPLODE_PATCHES
-            x2 -= i * EXPLODE_GAP;
-#endif
         }
 
         x1 = x2;
@@ -256,10 +198,6 @@
 
     if (previousStepX != bitmapWidth) {
         x2 = width;
-#if DEBUG_EXPLODE_PATCHES
-        x1 += mXCount * EXPLODE_GAP;
-        x2 += mXCount * EXPLODE_GAP;
-#endif
         generateQuad(vertex, x1, y1, x2, y2, u1, v1, 1.0f, v2, quadCount);
     }
 }
@@ -290,18 +228,15 @@
         quads.add(bounds);
     }
 
-    // Left triangle
+    mUvMapper.map(u1, v1, u2, v2);
+
     TextureVertex::set(vertex++, x1, y1, u1, v1);
     TextureVertex::set(vertex++, x2, y1, u2, v1);
     TextureVertex::set(vertex++, x1, y2, u1, v2);
-
-    // Right triangle
-    TextureVertex::set(vertex++, x1, y2, u1, v2);
-    TextureVertex::set(vertex++, x2, y1, u2, v1);
     TextureVertex::set(vertex++, x2, y2, u2, v2);
 
-    // A quad is made of 2 triangles, 6 vertices
-    verticesCount += 6;
+    verticesCount += 4;
+    indexCount += 6;
 
 #if DEBUG_PATCHES_VERTICES
     PATCH_LOGD("    quad %d", oldQuadCount);
diff --git a/libs/hwui/Patch.h b/libs/hwui/Patch.h
index ee7bf70..448cf60 100644
--- a/libs/hwui/Patch.h
+++ b/libs/hwui/Patch.h
@@ -23,62 +23,52 @@
 
 #include <utils/Vector.h>
 
+#include <androidfw/ResourceTypes.h>
+
 #include "Rect.h"
+#include "UvMapper.h"
 #include "Vertex.h"
 
 namespace android {
 namespace uirenderer {
 
 ///////////////////////////////////////////////////////////////////////////////
-// Defines
-///////////////////////////////////////////////////////////////////////////////
-
-#define EXPLODE_GAP 4
-
-///////////////////////////////////////////////////////////////////////////////
 // 9-patch structures
 ///////////////////////////////////////////////////////////////////////////////
 
-/**
- * An OpenGL patch. This contains an array of vertices and an array of
- * indices to render the vertices.
- */
 struct Patch {
-    Patch(const uint32_t xCount, const uint32_t yCount, const int8_t emptyQuads);
+    Patch();
     ~Patch();
 
-    void updateVertices(const float bitmapWidth, const float bitmapHeight,
-            float left, float top, float right, float bottom);
+    /**
+     * Returns the size of this patch's mesh in bytes.
+     */
+    uint32_t getSize() const;
 
-    void updateColorKey(const uint32_t colorKey);
-    void copy(const int32_t* xDivs, const int32_t* yDivs);
-    bool matches(const int32_t* xDivs, const int32_t* yDivs,
-            const uint32_t colorKey, const int8_t emptyQuads);
-
-    GLuint meshBuffer;
     uint32_t verticesCount;
+    uint32_t indexCount;
     bool hasEmptyQuads;
     Vector<Rect> quads;
 
+    GLintptr offset;
+    GLintptr textureOffset;
+
+    TextureVertex* createMesh(const float bitmapWidth, const float bitmapHeight,
+            float left, float top, float right, float bottom,
+            const Res_png_9patch* patch);
+    TextureVertex* createMesh(const float bitmapWidth, const float bitmapHeight,
+            float left, float top, float right, float bottom,
+            const UvMapper& mapper, const Res_png_9patch* patch);
+
 private:
-    TextureVertex* mVertices;
-    uint32_t mAllocatedVerticesCount;
-
-    int32_t* mXDivs;
-    int32_t* mYDivs;
-    uint32_t mColorKey;
-
-    uint32_t mXCount;
-    uint32_t mYCount;
-    int8_t mEmptyQuads;
-
-    void generateRow(TextureVertex*& vertex, float y1, float y2,
-            float v1, float v2, float stretchX, float rescaleX,
+    void generateRow(const int32_t* xDivs, uint32_t xCount, TextureVertex*& vertex,
+            float y1, float y2, float v1, float v2, float stretchX, float rescaleX,
             float width, float bitmapWidth, uint32_t& quadCount);
-    void generateQuad(TextureVertex*& vertex,
-            float x1, float y1, float x2, float y2,
-            float u1, float v1, float u2, float v2,
-            uint32_t& quadCount);
+    void generateQuad(TextureVertex*& vertex, float x1, float y1, float x2, float y2,
+            float u1, float v1, float u2, float v2, uint32_t& quadCount);
+
+    uint32_t mColorKey;
+    UvMapper mUvMapper;
 }; // struct Patch
 
 }; // namespace uirenderer
diff --git a/libs/hwui/PatchCache.cpp b/libs/hwui/PatchCache.cpp
index 62e38d3..5fa75e9 100644
--- a/libs/hwui/PatchCache.cpp
+++ b/libs/hwui/PatchCache.cpp
@@ -16,8 +16,10 @@
 
 #define LOG_TAG "OpenGLRenderer"
 
+#include <utils/JenkinsHash.h>
 #include <utils/Log.h>
 
+#include "Caches.h"
 #include "PatchCache.h"
 #include "Properties.h"
 
@@ -28,107 +30,107 @@
 // Constructors/destructor
 ///////////////////////////////////////////////////////////////////////////////
 
-PatchCache::PatchCache(): mMaxEntries(DEFAULT_PATCH_CACHE_SIZE) {
-}
-
-PatchCache::PatchCache(uint32_t maxEntries): mMaxEntries(maxEntries) {
+PatchCache::PatchCache(): mCache(LruCache<PatchDescription, Patch*>::kUnlimitedCapacity) {
+    char property[PROPERTY_VALUE_MAX];
+    if (property_get(PROPERTY_PATCH_CACHE_SIZE, property, NULL) > 0) {
+        INIT_LOGD("  Setting patch cache size to %skB", property);
+        mMaxSize = KB(atoi(property));
+    } else {
+        INIT_LOGD("  Using default patch cache size of %.2fkB", DEFAULT_PATCH_CACHE_SIZE);
+        mMaxSize = KB(DEFAULT_PATCH_CACHE_SIZE);
+    }
+    mSize = 0;
 }
 
 PatchCache::~PatchCache() {
     clear();
 }
 
+void PatchCache::init(Caches& caches) {
+    glGenBuffers(1, &mMeshBuffer);
+    caches.bindMeshBuffer(mMeshBuffer);
+    caches.resetVertexPointers();
+
+    glBufferData(GL_ARRAY_BUFFER, mMaxSize, NULL, GL_DYNAMIC_DRAW);
+}
+
 ///////////////////////////////////////////////////////////////////////////////
 // Caching
 ///////////////////////////////////////////////////////////////////////////////
 
-int PatchCache::PatchDescription::compare(
-        const PatchCache::PatchDescription& lhs, const PatchCache::PatchDescription& rhs) {
-    int deltaInt = lhs.bitmapWidth - rhs.bitmapWidth;
-    if (deltaInt != 0) return deltaInt;
+hash_t PatchCache::PatchDescription::hash() const {
+    uint32_t hash = JenkinsHashMix(0, android::hash_type(mPatch));
+    hash = JenkinsHashMix(hash, mBitmapWidth);
+    hash = JenkinsHashMix(hash, mBitmapHeight);
+    hash = JenkinsHashMix(hash, mPixelWidth);
+    hash = JenkinsHashMix(hash, mPixelHeight);
+    return JenkinsHashWhiten(hash);
+}
 
-    deltaInt = lhs.bitmapHeight - rhs.bitmapHeight;
-    if (deltaInt != 0) return deltaInt;
-
-    if (lhs.pixelWidth < rhs.pixelWidth) return -1;
-    if (lhs.pixelWidth > rhs.pixelWidth) return +1;
-
-    if (lhs.pixelHeight < rhs.pixelHeight) return -1;
-    if (lhs.pixelHeight > rhs.pixelHeight) return +1;
-
-    deltaInt = lhs.xCount - rhs.xCount;
-    if (deltaInt != 0) return deltaInt;
-
-    deltaInt = lhs.yCount - rhs.yCount;
-    if (deltaInt != 0) return deltaInt;
-
-    deltaInt = lhs.emptyCount - rhs.emptyCount;
-    if (deltaInt != 0) return deltaInt;
-
-    deltaInt = lhs.colorKey - rhs.colorKey;
-    if (deltaInt != 0) return deltaInt;
-
-    return 0;
+int PatchCache::PatchDescription::compare(const PatchCache::PatchDescription& lhs,
+            const PatchCache::PatchDescription& rhs) {
+    return memcmp(&lhs, &rhs, sizeof(PatchDescription));
 }
 
 void PatchCache::clear() {
-    size_t count = mCache.size();
-    for (size_t i = 0; i < count; i++) {
-        delete mCache.valueAt(i);
+    glDeleteBuffers(1, &mMeshBuffer);
+    clearCache();
+    mSize = 0;
+}
+
+void PatchCache::clearCache() {
+    LruCache<PatchDescription, Patch*>::Iterator i(mCache);
+    while (i.next()) {
+        ALOGD("Delete %p", i.value());
+        delete i.value();
     }
     mCache.clear();
 }
 
-Patch* PatchCache::get(const uint32_t bitmapWidth, const uint32_t bitmapHeight,
-        const float pixelWidth, const float pixelHeight,
-        const int32_t* xDivs, const int32_t* yDivs, const uint32_t* colors,
-        const uint32_t width, const uint32_t height, const int8_t numColors) {
+const Patch* PatchCache::get(const AssetAtlas::Entry* entry,
+        const uint32_t bitmapWidth, const uint32_t bitmapHeight,
+        const float pixelWidth, const float pixelHeight, const Res_png_9patch* patch) {
 
-    int8_t transparentQuads = 0;
-    uint32_t colorKey = 0;
-
-    if (uint8_t(numColors) < sizeof(uint32_t) * 4) {
-        for (int8_t i = 0; i < numColors; i++) {
-            if (colors[i] == 0x0) {
-                transparentQuads++;
-                colorKey |= 0x1 << i;
-            }
-        }
-    }
-
-    // If the 9patch is made of only transparent quads
-    if (transparentQuads == int8_t((width + 1) * (height + 1))) {
-        return NULL;
-    }
-
-    const PatchDescription description(bitmapWidth, bitmapHeight,
-            pixelWidth, pixelHeight, width, height, transparentQuads, colorKey);
-
-    ssize_t index = mCache.indexOfKey(description);
-    Patch* mesh = NULL;
-    if (index >= 0) {
-        mesh = mCache.valueAt(index);
-    }
+    const PatchDescription description(bitmapWidth, bitmapHeight, pixelWidth, pixelHeight, patch);
+    const Patch* mesh = mCache.get(description);
 
     if (!mesh) {
-        PATCH_LOGD("New patch mesh "
-                "xCount=%d yCount=%d, w=%.2f h=%.2f, bw=%.2f bh=%.2f",
-                width, height, pixelWidth, pixelHeight, bitmapWidth, bitmapHeight);
+        Patch* newMesh = new Patch();
+        TextureVertex* vertices;
 
-        mesh = new Patch(width, height, transparentQuads);
-        mesh->updateColorKey(colorKey);
-        mesh->copy(xDivs, yDivs);
-        mesh->updateVertices(bitmapWidth, bitmapHeight, 0.0f, 0.0f, pixelWidth, pixelHeight);
-
-        if (mCache.size() >= mMaxEntries) {
-            delete mCache.valueAt(mCache.size() - 1);
-            mCache.removeItemsAt(mCache.size() - 1, 1);
+        if (entry) {
+            vertices = newMesh->createMesh(bitmapWidth, bitmapHeight,
+                    0.0f, 0.0f, pixelWidth, pixelHeight, entry->uvMapper, patch);
+        } else {
+            vertices = newMesh->createMesh(bitmapWidth, bitmapHeight,
+                    0.0f, 0.0f, pixelWidth, pixelHeight, patch);
         }
 
-        mCache.add(description, mesh);
-    } else if (!mesh->matches(xDivs, yDivs, colorKey, transparentQuads)) {
-        PATCH_LOGD("Patch mesh does not match, refreshing vertices");
-        mesh->updateVertices(bitmapWidth, bitmapHeight, 0.0f, 0.0f, pixelWidth, pixelHeight);
+        if (vertices) {
+            Caches& caches = Caches::getInstance();
+            caches.bindMeshBuffer(mMeshBuffer);
+            caches.resetVertexPointers();
+
+            // TODO: Simply remove the oldest items until we have enough room
+            // This will require to keep a list of free blocks in the VBO
+            uint32_t size = newMesh->getSize();
+            if (mSize + size > mMaxSize) {
+                clearCache();
+                glBufferData(GL_ARRAY_BUFFER, mMaxSize, NULL, GL_DYNAMIC_DRAW);
+                mSize = 0;
+            }
+
+            newMesh->offset = (GLintptr) mSize;
+            newMesh->textureOffset = newMesh->offset + gMeshTextureOffset;
+            mSize += size;
+
+            glBufferSubData(GL_ARRAY_BUFFER, newMesh->offset, size, vertices);
+
+            delete[] vertices;
+        }
+
+        mCache.put(description, newMesh);
+        return newMesh;
     }
 
     return mesh;
diff --git a/libs/hwui/PatchCache.h b/libs/hwui/PatchCache.h
index 0822cba..129a0dc 100644
--- a/libs/hwui/PatchCache.h
+++ b/libs/hwui/PatchCache.h
@@ -17,8 +17,13 @@
 #ifndef ANDROID_HWUI_PATCH_CACHE_H
 #define ANDROID_HWUI_PATCH_CACHE_H
 
-#include <utils/KeyedVector.h>
+#include <GLES2/gl2.h>
 
+#include <utils/LruCache.h>
+
+#include <androidfw/ResourceTypes.h>
+
+#include "AssetAtlas.h"
 #include "Debug.h"
 #include "Patch.h"
 
@@ -40,45 +45,47 @@
 // Cache
 ///////////////////////////////////////////////////////////////////////////////
 
+class Caches;
+
 class PatchCache {
 public:
     PatchCache();
-    PatchCache(uint32_t maxCapacity);
     ~PatchCache();
+    void init(Caches& caches);
 
-    Patch* get(const uint32_t bitmapWidth, const uint32_t bitmapHeight,
-            const float pixelWidth, const float pixelHeight,
-            const int32_t* xDivs, const int32_t* yDivs, const uint32_t* colors,
-            const uint32_t width, const uint32_t height, const int8_t numColors);
+    const Patch* get(const AssetAtlas::Entry* entry,
+            const uint32_t bitmapWidth, const uint32_t bitmapHeight,
+            const float pixelWidth, const float pixelHeight, const Res_png_9patch* patch);
     void clear();
 
     uint32_t getSize() const {
-        return mCache.size();
+        return mSize;
     }
 
     uint32_t getMaxSize() const {
-        return mMaxEntries;
+        return mMaxSize;
+    }
+
+    GLuint getMeshBuffer() const {
+        return mMeshBuffer;
     }
 
 private:
-    /**
-     * Description of a patch.
-     */
+    void clearCache();
+
     struct PatchDescription {
-        PatchDescription(): bitmapWidth(0), bitmapHeight(0), pixelWidth(0), pixelHeight(0),
-                xCount(0), yCount(0), emptyCount(0), colorKey(0) {
+        PatchDescription(): mPatch(NULL), mBitmapWidth(0), mBitmapHeight(0),
+                mPixelWidth(0), mPixelHeight(0) {
         }
 
         PatchDescription(const uint32_t bitmapWidth, const uint32_t bitmapHeight,
-                const float pixelWidth, const float pixelHeight,
-                const uint32_t xCount, const uint32_t yCount,
-                const int8_t emptyCount, const uint32_t colorKey):
-                bitmapWidth(bitmapWidth), bitmapHeight(bitmapHeight),
-                pixelWidth(pixelWidth), pixelHeight(pixelHeight),
-                xCount(xCount), yCount(yCount),
-                emptyCount(emptyCount), colorKey(colorKey) {
+                const float pixelWidth, const float pixelHeight, const Res_png_9patch* patch):
+                mPatch(patch), mBitmapWidth(bitmapWidth), mBitmapHeight(bitmapHeight),
+                mPixelWidth(pixelWidth), mPixelHeight(pixelHeight) {
         }
 
+        hash_t hash() const;
+
         static int compare(const PatchDescription& lhs, const PatchDescription& rhs);
 
         bool operator==(const PatchDescription& other) const {
@@ -99,21 +106,24 @@
             return PatchDescription::compare(lhs, rhs);
         }
 
+        friend inline hash_t hash_type(const PatchDescription& entry) {
+            return entry.hash();
+        }
+
     private:
-        uint32_t bitmapWidth;
-        uint32_t bitmapHeight;
-        float pixelWidth;
-        float pixelHeight;
-        uint32_t xCount;
-        uint32_t yCount;
-        int8_t emptyCount;
-        uint32_t colorKey;
+        const Res_png_9patch* mPatch;
+        uint32_t mBitmapWidth;
+        uint32_t mBitmapHeight;
+        float mPixelWidth;
+        float mPixelHeight;
 
     }; // struct PatchDescription
 
-    uint32_t mMaxEntries;
-    KeyedVector<PatchDescription, Patch*> mCache;
+    uint32_t mMaxSize;
+    uint32_t mSize;
+    LruCache<PatchDescription, Patch*> mCache;
 
+    GLuint mMeshBuffer;
 }; // class PatchCache
 
 }; // namespace uirenderer
diff --git a/libs/hwui/Program.cpp b/libs/hwui/Program.cpp
index 14a2376..c127d68 100644
--- a/libs/hwui/Program.cpp
+++ b/libs/hwui/Program.cpp
@@ -15,6 +15,9 @@
  */
 
 #define LOG_TAG "OpenGLRenderer"
+#define ATRACE_TAG ATRACE_TAG_VIEW
+
+#include <utils/Trace.h>
 
 #include "Program.h"
 
@@ -25,7 +28,6 @@
 // Base program
 ///////////////////////////////////////////////////////////////////////////////
 
-// TODO: Program instance should be created from a factory method
 Program::Program(const ProgramDescription& description, const char* vertex, const char* fragment) {
     mInitialized = false;
     mHasColorUniform = false;
@@ -50,7 +52,9 @@
                 texCoords = -1;
             }
 
+            ATRACE_BEGIN("linkProgram");
             glLinkProgram(mProgramId);
+            ATRACE_END();
 
             GLint status;
             glGetProgramiv(mProgramId, GL_LINK_STATUS, &status);
@@ -87,6 +91,9 @@
 
 Program::~Program() {
     if (mInitialized) {
+        // This would ideally happen after linking the program
+        // but Tegra drivers, especially when perfhud is enabled,
+        // sometimes crash if we do so
         glDetachShader(mProgramId, mVertexShader);
         glDetachShader(mProgramId, mFragmentShader);
 
@@ -132,6 +139,8 @@
 }
 
 GLuint Program::buildShader(const char* source, GLenum type) {
+    ATRACE_CALL();
+
     GLuint shader = glCreateShader(type);
     glShaderSource(shader, 1, &source, 0);
     glCompileShader(shader);
@@ -153,20 +162,24 @@
 
 void Program::set(const mat4& projectionMatrix, const mat4& modelViewMatrix,
         const mat4& transformMatrix, bool offset) {
-    mat4 p(projectionMatrix);
-    if (offset) {
-        // offset screenspace xy by an amount that compensates for typical precision
-        // issues in GPU hardware that tends to paint hor/vert lines in pixels shifted
-        // up and to the left.
-        // This offset value is based on an assumption that some hardware may use as
-        // little as 12.4 precision, so we offset by slightly more than 1/16.
-        p.translate(.065, .065, 0);
+    if (projectionMatrix != mProjection) {
+        if (CC_LIKELY(!offset)) {
+            glUniformMatrix4fv(projection, 1, GL_FALSE, &projectionMatrix.data[0]);
+        } else {
+            mat4 p(projectionMatrix);
+            // offset screenspace xy by an amount that compensates for typical precision
+            // issues in GPU hardware that tends to paint hor/vert lines in pixels shifted
+            // up and to the left.
+            // This offset value is based on an assumption that some hardware may use as
+            // little as 12.4 precision, so we offset by slightly more than 1/16.
+            p.translate(.065, .065, 0);
+            glUniformMatrix4fv(projection, 1, GL_FALSE, &p.data[0]);
+        }
+        mProjection = projectionMatrix;
     }
 
     mat4 t(transformMatrix);
     t.multiply(modelViewMatrix);
-
-    glUniformMatrix4fv(projection, 1, GL_FALSE, &p.data[0]);
     glUniformMatrix4fv(transform, 1, GL_FALSE, &t.data[0]);
 }
 
diff --git a/libs/hwui/Program.h b/libs/hwui/Program.h
index e8b6d47..a252209 100644
--- a/libs/hwui/Program.h
+++ b/libs/hwui/Program.h
@@ -430,10 +430,13 @@
     bool mUse;
     bool mInitialized;
 
+    // Uniforms caching
     bool mHasColorUniform;
     int mColorUniform;
 
     bool mHasSampler;
+
+    mat4 mProjection;
 }; // class Program
 
 }; // namespace uirenderer
diff --git a/libs/hwui/Properties.h b/libs/hwui/Properties.h
index 6eea00c..87ac845 100644
--- a/libs/hwui/Properties.h
+++ b/libs/hwui/Properties.h
@@ -133,6 +133,7 @@
 #define PROPERTY_RENDER_BUFFER_CACHE_SIZE "ro.hwui.r_buffer_cache_size"
 #define PROPERTY_GRADIENT_CACHE_SIZE "ro.hwui.gradient_cache_size"
 #define PROPERTY_PATH_CACHE_SIZE "ro.hwui.path_cache_size"
+#define PROPERTY_PATCH_CACHE_SIZE "ro.hwui.patch_cache_size"
 #define PROPERTY_DROP_SHADOW_CACHE_SIZE "ro.hwui.drop_shadow_cache_size"
 #define PROPERTY_FBO_CACHE_SIZE "ro.hwui.fbo_cache_size"
 
@@ -178,7 +179,7 @@
 #define DEFAULT_LAYER_CACHE_SIZE 16.0f
 #define DEFAULT_RENDER_BUFFER_CACHE_SIZE 2.0f
 #define DEFAULT_PATH_CACHE_SIZE 10.0f
-#define DEFAULT_PATCH_CACHE_SIZE 512
+#define DEFAULT_PATCH_CACHE_SIZE 128 // in kB
 #define DEFAULT_GRADIENT_CACHE_SIZE 0.5f
 #define DEFAULT_DROP_SHADOW_CACHE_SIZE 2.0f
 #define DEFAULT_FBO_CACHE_SIZE 16
@@ -195,6 +196,8 @@
 
 // Converts a number of mega-bytes into bytes
 #define MB(s) s * 1024 * 1024
+// Converts a number of kilo-bytes into bytes
+#define KB(s) s * 1024
 
 static DebugLevel readDebugLevel() {
     char property[PROPERTY_VALUE_MAX];
diff --git a/libs/hwui/Texture.h b/libs/hwui/Texture.h
index 8d88bdc..dd39cae 100644
--- a/libs/hwui/Texture.h
+++ b/libs/hwui/Texture.h
@@ -22,6 +22,8 @@
 namespace android {
 namespace uirenderer {
 
+class UvMapper;
+
 /**
  * Represents an OpenGL texture.
  */
@@ -42,6 +44,8 @@
         firstWrap = true;
 
         id = 0;
+
+        uvMapper = NULL;
     }
 
     void setWrap(GLenum wrap, bool bindTexture = false, bool force = false,
@@ -125,6 +129,11 @@
      */
     bool mipMap;
 
+    /**
+     * Optional, pointer to a texture coordinates mapper.
+     */
+    const UvMapper* uvMapper;
+
 private:
     /**
      * Last wrap modes set on this texture. Defaults to GL_CLAMP_TO_EDGE.
diff --git a/libs/hwui/UvMapper.h b/libs/hwui/UvMapper.h
new file mode 100644
index 0000000..70428d2
--- /dev/null
+++ b/libs/hwui/UvMapper.h
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef ANDROID_HWUI_UV_MAPPER_H
+#define ANDROID_HWUI_UV_MAPPER_H
+
+#include "Rect.h"
+
+namespace android {
+namespace uirenderer {
+
+/**
+ * This class can be used to map UV coordinates from the [0..1]
+ * range to other arbitrary ranges. All the methods below assume
+ * that the input values lie in the [0..1] range already.
+ */
+class UvMapper {
+public:
+    /**
+     * Using this constructor is equivalent to not using any mapping at all.
+     * UV coordinates in the [0..1] range remain in the [0..1] range.
+     */
+    UvMapper(): mIdentity(true), mMinU(0.0f), mMaxU(1.0f), mMinV(0.0f), mMaxV(1.0f) {
+    }
+
+    /**
+     * Creates a new mapper with the specified ranges for U and V coordinates.
+     * The parameter minU must be < maxU and minV must be < maxV.
+     */
+    UvMapper(float minU, float maxU, float minV, float maxV):
+        mMinU(minU), mMaxU(maxU), mMinV(minV), mMaxV(maxV) {
+        checkIdentity();
+    }
+
+    /**
+     * Returns true if calling the map*() methods has no effect (that is,
+     * texture coordinates remain in the 0..1 range.)
+     */
+    bool isIdentity() const {
+        return mIdentity;
+    }
+
+    /**
+     * Changes the U and V mapping ranges.
+     * The parameter minU must be < maxU and minV must be < maxV.
+     */
+    void setMapping(float minU, float maxU, float minV, float maxV) {
+        mMinU = minU;
+        mMaxU = maxU;
+        mMinV = minV;
+        mMaxV = maxV;
+        checkIdentity();
+    }
+
+    /**
+     * Maps a single value in the U range.
+     */
+    void mapU(float& u) const {
+        if (!mIdentity) u = lerp(mMinU, mMaxU, u);
+    }
+
+    /**
+     * Maps a single value in the V range.
+     */
+    void mapV(float& v) const {
+        if (!mIdentity) v = lerp(mMinV, mMaxV, v);
+    }
+
+    /**
+     * Maps the specified rectangle in place. This method assumes:
+     * - left = min. U
+     * - top = min. V
+     * - right = max. U
+     * - bottom = max. V
+     */
+    void map(Rect& texCoords) const {
+        if (!mIdentity) {
+            texCoords.left = lerp(mMinU, mMaxU, texCoords.left);
+            texCoords.right = lerp(mMinU, mMaxU, texCoords.right);
+            texCoords.top = lerp(mMinV, mMaxV, texCoords.top);
+            texCoords.bottom = lerp(mMinV, mMaxV, texCoords.bottom);
+        }
+    }
+
+    /**
+     * Maps the specified UV coordinates in place.
+     */
+    void map(float& u1, float& v1, float& u2, float& v2) const {
+        if (!mIdentity) {
+            u1 = lerp(mMinU, mMaxU, u1);
+            u2 = lerp(mMinU, mMaxU, u2);
+            v1 = lerp(mMinV, mMaxV, v1);
+            v2 = lerp(mMinV, mMaxV, v2);
+        }
+    }
+
+    void dump() const {
+        ALOGD("mapper[minU=%.2f maxU=%.2f minV=%.2f maxV=%.2f]", mMinU, mMaxU, mMinV, mMaxV);
+    }
+
+private:
+    static float lerp(float start, float stop, float amount) {
+        return start + (stop - start) * amount;
+    }
+
+    void checkIdentity() {
+        mIdentity = mMinU == 0.0f && mMaxU == 1.0f && mMinV == 0.0f && mMaxV == 1.0f;
+    }
+
+    bool mIdentity;
+    float mMinU;
+    float mMaxU;
+    float mMinV;
+    float mMaxV;
+};
+
+}; // namespace uirenderer
+}; // namespace android
+
+#endif // ANDROID_HWUI_UV_MAPPER_H