Support new EGL extensions

Bug: 21753739

Includes a revert of 13d1b4ab10fbee5e81a2ba1ac59cfae1e51d3ef0
as that only supported EGL_EXT_buffer_age

Change-Id: Ia86a47d19e3355c067934d7764c330b640c6958d
diff --git a/libs/hwui/Android.common.mk b/libs/hwui/Android.common.mk
index 7b2bba1..fe4f1be 100644
--- a/libs/hwui/Android.common.mk
+++ b/libs/hwui/Android.common.mk
@@ -19,7 +19,6 @@
     renderthread/RenderTask.cpp \
     renderthread/RenderThread.cpp \
     renderthread/TimeLord.cpp \
-    renderthread/DirtyHistory.cpp \
     thread/TaskManager.cpp \
     utils/Blur.cpp \
     utils/GLUtils.cpp \
diff --git a/libs/hwui/Caches.cpp b/libs/hwui/Caches.cpp
index 693b28e..f663f07 100644
--- a/libs/hwui/Caches.cpp
+++ b/libs/hwui/Caches.cpp
@@ -68,8 +68,6 @@
     mRegionMesh = nullptr;
     mProgram = nullptr;
 
-    mFunctorsCount = 0;
-
     patchCache.init();
 
     mInitialized = true;
@@ -276,38 +274,6 @@
 }
 
 ///////////////////////////////////////////////////////////////////////////////
-// Tiling
-///////////////////////////////////////////////////////////////////////////////
-
-void Caches::startTiling(GLuint x, GLuint y, GLuint width, GLuint height, bool discard) {
-    if (mExtensions.hasTiledRendering() && !Properties::debugOverdraw) {
-        glStartTilingQCOM(x, y, width, height, (discard ? GL_NONE : GL_COLOR_BUFFER_BIT0_QCOM));
-    }
-}
-
-void Caches::endTiling() {
-    if (mExtensions.hasTiledRendering() && !Properties::debugOverdraw) {
-        glEndTilingQCOM(GL_COLOR_BUFFER_BIT0_QCOM);
-    }
-}
-
-bool Caches::hasRegisteredFunctors() {
-    return mFunctorsCount > 0;
-}
-
-void Caches::registerFunctors(uint32_t functorCount) {
-    mFunctorsCount += functorCount;
-}
-
-void Caches::unregisterFunctors(uint32_t functorCount) {
-    if (functorCount > mFunctorsCount) {
-        mFunctorsCount = 0;
-    } else {
-        mFunctorsCount -= functorCount;
-    }
-}
-
-///////////////////////////////////////////////////////////////////////////////
 // Regions
 ///////////////////////////////////////////////////////////////////////////////
 
diff --git a/libs/hwui/Caches.h b/libs/hwui/Caches.h
index 049d58b..a02e15d 100644
--- a/libs/hwui/Caches.h
+++ b/libs/hwui/Caches.h
@@ -124,10 +124,6 @@
      */
     void deleteLayerDeferred(Layer* layer);
 
-
-    void startTiling(GLuint x, GLuint y, GLuint width, GLuint height, bool discard);
-    void endTiling();
-
     /**
      * Returns the mesh used to draw regions. Calling this method will
      * bind a VBO of type GL_ELEMENT_ARRAY_BUFFER that contains the
@@ -141,10 +137,6 @@
     void dumpMemoryUsage();
     void dumpMemoryUsage(String8& log);
 
-    bool hasRegisteredFunctors();
-    void registerFunctors(uint32_t functorCount);
-    void unregisterFunctors(uint32_t functorCount);
-
     // Misc
     GLint maxTextureSize;
 
@@ -206,8 +198,6 @@
 
     bool mInitialized;
 
-    uint32_t mFunctorsCount;
-
     // TODO: move below to RenderState
     PixelBufferState* mPixelBufferState = nullptr;
     TextureState* mTextureState = nullptr;
diff --git a/libs/hwui/Extensions.cpp b/libs/hwui/Extensions.cpp
index 814bada..3d350c9 100644
--- a/libs/hwui/Extensions.cpp
+++ b/libs/hwui/Extensions.cpp
@@ -53,21 +53,10 @@
     mHasFramebufferFetch = hasGlExtension("GL_NV_shader_framebuffer_fetch");
     mHasDiscardFramebuffer = hasGlExtension("GL_EXT_discard_framebuffer");
     mHasDebugMarker = hasGlExtension("GL_EXT_debug_marker");
-    mHasTiledRendering = hasGlExtension("GL_QCOM_tiled_rendering");
     mHas1BitStencil = hasGlExtension("GL_OES_stencil1");
     mHas4BitStencil = hasGlExtension("GL_OES_stencil4");
     mHasUnpackSubImage = hasGlExtension("GL_EXT_unpack_subimage");
 
-    // Query EGL extensions
-    findExtensions(eglQueryString(eglGetCurrentDisplay(), EGL_EXTENSIONS), mEglExtensionList);
-
-    char property[PROPERTY_VALUE_MAX];
-    if (property_get(PROPERTY_DEBUG_NV_PROFILING, property, nullptr) > 0) {
-        mHasNvSystemTime = !strcmp(property, "true") && hasEglExtension("EGL_NV_system_time");
-    } else {
-        mHasNvSystemTime = false;
-    }
-
     const char* version = (const char*) glGetString(GL_VERSION);
 
     // Section 6.1.5 of the OpenGL ES specification indicates the GL version
diff --git a/libs/hwui/Extensions.h b/libs/hwui/Extensions.h
index a4eef0f..636b631 100644
--- a/libs/hwui/Extensions.h
+++ b/libs/hwui/Extensions.h
@@ -40,10 +40,8 @@
     inline bool hasFramebufferFetch() const { return mHasFramebufferFetch; }
     inline bool hasDiscardFramebuffer() const { return mHasDiscardFramebuffer; }
     inline bool hasDebugMarker() const { return mHasDebugMarker; }
-    inline bool hasTiledRendering() const { return mHasTiledRendering; }
     inline bool has1BitStencil() const { return mHas1BitStencil; }
     inline bool has4BitStencil() const { return mHas4BitStencil; }
-    inline bool hasNvSystemTime() const { return mHasNvSystemTime; }
     inline bool hasUnpackRowLength() const { return mVersionMajor >= 3 || mHasUnpackSubImage; }
     inline bool hasPixelBufferObjects() const { return mVersionMajor >= 3; }
     inline bool hasOcclusionQueries() const { return mVersionMajor >= 3; }
@@ -67,10 +65,8 @@
     bool mHasFramebufferFetch;
     bool mHasDiscardFramebuffer;
     bool mHasDebugMarker;
-    bool mHasTiledRendering;
     bool mHas1BitStencil;
     bool mHas4BitStencil;
-    bool mHasNvSystemTime;
     bool mHasUnpackSubImage;
 
     int mVersionMajor;
diff --git a/libs/hwui/OpenGLRenderer.cpp b/libs/hwui/OpenGLRenderer.cpp
index 8e04bdf..7a56d42 100644
--- a/libs/hwui/OpenGLRenderer.cpp
+++ b/libs/hwui/OpenGLRenderer.cpp
@@ -70,8 +70,6 @@
         , mRenderState(renderState)
         , mFrameStarted(false)
         , mScissorOptimizationDisabled(false)
-        , mSuppressTiling(false)
-        , mFirstFrameAfterResize(true)
         , mDirty(false)
         , mLightCenter((Vector3){FLT_MIN, FLT_MIN, FLT_MIN})
         , mLightRadius(FLT_MIN)
@@ -113,7 +111,6 @@
 void OpenGLRenderer::onViewportInitialized() {
     glDisable(GL_DITHER);
     glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
-    mFirstFrameAfterResize = true;
 }
 
 void OpenGLRenderer::setupFrameState(float left, float top,
@@ -134,15 +131,6 @@
 
     mRenderState.setViewport(mState.getWidth(), mState.getHeight());
 
-    // Functors break the tiling extension in pretty spectacular ways
-    // This ensures we don't use tiling when a functor is going to be
-    // invoked during the frame
-    mSuppressTiling = mCaches.hasRegisteredFunctors()
-            || mFirstFrameAfterResize;
-    mFirstFrameAfterResize = false;
-
-    startTilingCurrentClip(true);
-
     debugOverdraw(true, true);
 
     clear(mTilingClip.left, mTilingClip.top,
@@ -192,46 +180,8 @@
     mRenderState.scissor().reset();
 }
 
-void OpenGLRenderer::startTilingCurrentClip(bool opaque, bool expand) {
-    if (!mSuppressTiling) {
-        const Snapshot* snapshot = currentSnapshot();
-
-        const Rect* clip = &mTilingClip;
-        if (snapshot->flags & Snapshot::kFlagFboTarget) {
-            clip = &(snapshot->layer->clipRect);
-        }
-
-        startTiling(*clip, getViewportHeight(), opaque, expand);
-    }
-}
-
-void OpenGLRenderer::startTiling(const Rect& clip, int windowHeight, bool opaque, bool expand) {
-    if (!mSuppressTiling) {
-        if(expand) {
-            // Expand the startTiling region by 1
-            int leftNotZero = (clip.left > 0) ? 1 : 0;
-            int topNotZero = (windowHeight - clip.bottom > 0) ? 1 : 0;
-
-            mCaches.startTiling(
-                clip.left - leftNotZero,
-                windowHeight - clip.bottom - topNotZero,
-                clip.right - clip.left + leftNotZero + 1,
-                clip.bottom - clip.top + topNotZero + 1,
-                opaque);
-        } else {
-            mCaches.startTiling(clip.left, windowHeight - clip.bottom,
-                clip.right - clip.left, clip.bottom - clip.top, opaque);
-        }
-    }
-}
-
-void OpenGLRenderer::endTiling() {
-    if (!mSuppressTiling) mCaches.endTiling();
-}
-
 bool OpenGLRenderer::finish() {
     renderOverdraw();
-    endTiling();
     mTempPaths.clear();
 
     // When finish() is invoked on FBO 0 we've reached the end
@@ -381,7 +331,6 @@
             && layer->renderNode.get() && layer->renderNode->isRenderable()) {
 
         if (inFrame) {
-            endTiling();
             debugOverdraw(false, false);
         }
 
@@ -393,7 +342,6 @@
 
         if (inFrame) {
             resumeAfterLayer();
-            startTilingCurrentClip();
         }
 
         layer->debugDrawUpdate = Properties::debugLayersUpdates;
@@ -736,7 +684,6 @@
     writableSnapshot()->initializeViewport(bounds.getWidth(), bounds.getHeight());
     writableSnapshot()->roundRectClipState = nullptr;
 
-    endTiling();
     debugOverdraw(false, false);
     // Bind texture to FBO
     mRenderState.bindFramebuffer(layer->getFbo());
@@ -751,9 +698,6 @@
     glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
             layer->getTextureId(), 0);
 
-    // Expand the startTiling region by 1
-    startTilingCurrentClip(true, true);
-
     // Clear the FBO, expand the clear region by 1 to get nice bilinear filtering
     mRenderState.scissor().setEnabled(true);
     mRenderState.scissor().set(clip.left - 1.0f, bounds.getHeight() - clip.bottom - 1.0f,
@@ -786,8 +730,6 @@
     mRenderState.scissor().setEnabled(mScissorOptimizationDisabled || clipRequired);
 
     if (fboLayer) {
-        endTiling();
-
         // Detach the texture from the FBO
         glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
 
@@ -796,8 +738,6 @@
         // Unbind current FBO and restore previous one
         mRenderState.bindFramebuffer(restored.fbo);
         debugOverdraw(true, false);
-
-        startTilingCurrentClip();
     }
 
     if (!fboLayer && layer->getAlpha() < 255) {
@@ -1267,17 +1207,10 @@
 void OpenGLRenderer::attachStencilBufferToLayer(Layer* layer) {
     // The layer's FBO is already bound when we reach this stage
     if (!layer->getStencilRenderBuffer()) {
-        // GL_QCOM_tiled_rendering doesn't like it if a renderbuffer
-        // is attached after we initiated tiling. We must turn it off,
-        // attach the new render buffer then turn tiling back on
-        endTiling();
-
         RenderBuffer* buffer = mCaches.renderBufferCache.get(
                 Stencil::getLayerStencilFormat(),
                 layer->getWidth(), layer->getHeight());
         layer->setStencilRenderBuffer(buffer);
-
-        startTiling(layer->clipRect, layer->layer.getHeight());
     }
 }
 
diff --git a/libs/hwui/OpenGLRenderer.h b/libs/hwui/OpenGLRenderer.h
index 390f620..4f75482 100755
--- a/libs/hwui/OpenGLRenderer.h
+++ b/libs/hwui/OpenGLRenderer.h
@@ -555,27 +555,6 @@
     void discardFramebuffer(float left, float top, float right, float bottom);
 
     /**
-     * Tells the GPU what part of the screen is about to be redrawn.
-     * This method will use the current layer space clip rect.
-     * This method needs to be invoked every time getTargetFbo() is
-     * bound again.
-     */
-    void startTilingCurrentClip(bool opaque = false, bool expand = false);
-
-    /**
-     * Tells the GPU what part of the screen is about to be redrawn.
-     * This method needs to be invoked every time getTargetFbo() is
-     * bound again.
-     */
-    void startTiling(const Rect& clip, int windowHeight, bool opaque = false, bool expand = false);
-
-    /**
-     * Tells the GPU that we are done drawing the frame or that we
-     * are switching to another render target.
-     */
-    void endTiling();
-
-    /**
      * Sets the clipping rectangle using glScissor. The clip is defined by
      * the current snapshot's clipRect member.
      */
@@ -862,10 +841,6 @@
     // Properties.h
     bool mScissorOptimizationDisabled;
 
-    // No-ops start/endTiling when set
-    bool mSuppressTiling;
-    bool mFirstFrameAfterResize;
-
     bool mSkipOutlineClip;
 
     // True if anything has been drawn since the last call to
diff --git a/libs/hwui/Properties.cpp b/libs/hwui/Properties.cpp
index 2e63793..b8f8585 100644
--- a/libs/hwui/Properties.cpp
+++ b/libs/hwui/Properties.cpp
@@ -30,6 +30,8 @@
 bool Properties::showDirtyRegions = false;
 bool Properties::skipEmptyFrames = true;
 bool Properties::swapBuffersWithDamage = true;
+bool Properties::useBufferAge = true;
+bool Properties::enablePartialUpdates = true;
 
 DebugLevel Properties::debugLevel = kDebugDisabled;
 OverdrawColorSet Properties::overdrawColorSet = OverdrawColorSet::Default;
@@ -105,6 +107,8 @@
 
     skipEmptyFrames = property_get_bool(PROPERTY_SKIP_EMPTY_DAMAGE, true);
     swapBuffersWithDamage = property_get_bool(PROPERTY_SWAP_WITH_DAMAGE, true);
+    useBufferAge = property_get_bool(PROPERTY_USE_BUFFER_AGE, true);
+    enablePartialUpdates = property_get_bool(PROPERTY_ENABLE_PARTIAL_UPDATES, true);
 
     return (prevDebugLayersUpdates != debugLayersUpdates)
             || (prevDebugOverdraw != debugOverdraw)
diff --git a/libs/hwui/Properties.h b/libs/hwui/Properties.h
index 1a70d7c..7602848 100644
--- a/libs/hwui/Properties.h
+++ b/libs/hwui/Properties.h
@@ -84,12 +84,6 @@
 #define PROPERTY_DEBUG_OVERDRAW "debug.hwui.overdraw"
 
 /**
- * Used to enable/disable PerfHUD ES profiling. The accepted values
- * are "true" and "false". The default value is "false".
- */
-#define PROPERTY_DEBUG_NV_PROFILING "debug.hwui.nv_profiling"
-
-/**
  *  System property used to enable or disable hardware rendering profiling.
  * The default value of this property is assumed to be false.
  *
@@ -150,9 +144,25 @@
 /**
  * Setting this property will enable or disable usage of EGL_KHR_swap_buffers_with_damage
  * See: https://www.khronos.org/registry/egl/extensions/KHR/EGL_KHR_swap_buffers_with_damage.txt
+ * Default is "true"
  */
 #define PROPERTY_SWAP_WITH_DAMAGE "debug.hwui.swap_with_damage"
 
+/**
+ * Controls whether or not HWUI will use the EGL_EXT_buffer_age extension
+ * to do partial invalidates. Setting this to "false" will fall back to
+ * using BUFFER_PRESERVED instead
+ * Default is "true"
+ */
+#define PROPERTY_USE_BUFFER_AGE "debug.hwui.use_buffer_age"
+
+/**
+ * Setting this to "false" will force HWUI to always do full-redraws of the surface.
+ * This will disable the use of EGL_EXT_buffer_age and BUFFER_PRESERVED.
+ * Default is "true"
+ */
+#define PROPERTY_ENABLE_PARTIAL_UPDATES "debug.hwui.enable_partial_updates"
+
 ///////////////////////////////////////////////////////////////////////////////
 // Runtime configuration properties
 ///////////////////////////////////////////////////////////////////////////////
@@ -287,6 +297,8 @@
     static bool skipEmptyFrames;
     // TODO: Remove after stabilization period
     static bool swapBuffersWithDamage;
+    static bool useBufferAge;
+    static bool enablePartialUpdates;
 
     static DebugLevel debugLevel;
     static OverdrawColorSet overdrawColorSet;
diff --git a/libs/hwui/RenderNode.cpp b/libs/hwui/RenderNode.cpp
index af8aaa20..b75b764 100644
--- a/libs/hwui/RenderNode.cpp
+++ b/libs/hwui/RenderNode.cpp
@@ -303,10 +303,6 @@
         // changes in isRenderable or, in the future, bounds
         damageSelf(info);
         deleteDisplayListData();
-        // TODO: Remove this caches stuff
-        if (mStagingDisplayListData && mStagingDisplayListData->functors.size()) {
-            Caches::getInstance().registerFunctors(mStagingDisplayListData->functors.size());
-        }
         mDisplayListData = mStagingDisplayListData;
         mStagingDisplayListData = nullptr;
         if (mDisplayListData) {
@@ -323,9 +319,6 @@
         for (size_t i = 0; i < mDisplayListData->children().size(); i++) {
             mDisplayListData->children()[i]->mRenderNode->decParentRefCount();
         }
-        if (mDisplayListData->functors.size()) {
-            Caches::getInstance().unregisterFunctors(mDisplayListData->functors.size());
-        }
     }
     delete mDisplayListData;
     mDisplayListData = nullptr;
diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp
index ea73387..67c42f3 100644
--- a/libs/hwui/renderthread/CanvasContext.cpp
+++ b/libs/hwui/renderthread/CanvasContext.cpp
@@ -35,6 +35,14 @@
 #define TRIM_MEMORY_COMPLETE 80
 #define TRIM_MEMORY_UI_HIDDEN 20
 
+#define LOG_FRAMETIME_MMA 0
+
+#if LOG_FRAMETIME_MMA
+static float sBenchMma = 0;
+static int sFrameCount = 0;
+static const float NANOS_PER_MILLIS_F = 1000000.0f;
+#endif
+
 namespace android {
 namespace uirenderer {
 namespace renderthread {
@@ -93,16 +101,6 @@
     }
 }
 
-void CanvasContext::swapBuffers(const SkRect& dirty, EGLint width, EGLint height) {
-    if (CC_UNLIKELY(!mEglManager.swapBuffers(mEglSurface, dirty, width, height))) {
-        setSurface(nullptr);
-    }
-    mHaveNewSurface = false;
-    if (mEglManager.useBufferAgeExt()) {
-        mDirtyHistory.prepend(Rect(dirty));
-    }
-}
-
 void CanvasContext::requireSurface() {
     LOG_ALWAYS_FATAL_IF(mEglSurface == EGL_NO_SURFACE,
             "requireSurface() called but no surface set!");
@@ -230,8 +228,6 @@
             "drawRenderNode called on a context with no canvas or surface!");
 
     SkRect dirty;
-    bool useBufferAgeExt = mEglManager.useBufferAgeExt();
-    Rect patchedDirty;
     mDamageAccumulator.finish(&dirty);
 
     // TODO: Re-enable after figuring out cause of b/22592975
@@ -242,40 +238,59 @@
 
     mCurrentFrameInfo->markIssueDrawCommandsStart();
 
-    EGLint width, height, framebufferAge;
-    mEglManager.beginFrame(mEglSurface, &width, &height, &framebufferAge);
-
-    if (useBufferAgeExt && mHaveNewSurface) {
-        mDirtyHistory.clear();
-    }
-
-    if (width != mCanvas->getViewportWidth() || height != mCanvas->getViewportHeight()) {
-        mCanvas->setViewport(width, height);
+    Frame frame = mEglManager.beginFrame(mEglSurface);
+    if (frame.width() != mCanvas->getViewportWidth()
+            || frame.height() != mCanvas->getViewportHeight()) {
+        mCanvas->setViewport(frame.width(), frame.height());
         dirty.setEmpty();
-    } else if (!mBufferPreserved || mHaveNewSurface) {
-        mDirtyHistory.clear();
+    } else if (mHaveNewSurface || frame.bufferAge() == 0) {
+        // New surface needs a full draw
         dirty.setEmpty();
     } else {
-        if (!dirty.isEmpty() && !dirty.intersect(0, 0, width, height)) {
+        if (!dirty.isEmpty() && !dirty.intersect(0, 0, frame.width(), frame.height())) {
             ALOGW("Dirty " RECT_STRING " doesn't intersect with 0 0 %d %d ?",
-                    SK_RECT_ARGS(dirty), width, height);
+                    SK_RECT_ARGS(dirty), frame.width(), frame.height());
             dirty.setEmpty();
         }
         profiler().unionDirty(&dirty);
     }
 
-    patchedDirty = dirty;
-    if (useBufferAgeExt && !dirty.isEmpty()) {
-        patchedDirty = mDirtyHistory.unionWith(Rect(dirty), framebufferAge-1);
+    if (dirty.isEmpty()) {
+        dirty.set(0, 0, frame.width(), frame.height());
     }
 
-    if (!patchedDirty.isEmpty()) {
-        mCanvas->prepareDirty(patchedDirty.left, patchedDirty.top,
-                patchedDirty.right, patchedDirty.bottom, mOpaque);
-    } else {
-        mCanvas->prepare(mOpaque);
+    // At this point dirty is the area of the screen to update. However,
+    // the area of the frame we need to repaint is potentially different, so
+    // stash the screen area for later
+    SkRect screenDirty(dirty);
+
+    // If the buffer age is 0 we do a full-screen repaint (handled above)
+    // If the buffer age is 1 the buffer contents are the same as they were
+    // last frame so there's nothing to union() against
+    // Therefore we only care about the > 1 case.
+    if (frame.bufferAge() > 1) {
+        if (frame.bufferAge() > (int) mDamageHistory.size()) {
+            // We don't have enough history to handle this old of a buffer
+            // Just do a full-draw
+            dirty.set(0, 0, frame.width(), frame.height());
+        } else {
+            // At this point we haven't yet added the latest frame
+            // to the damage history (happens below)
+            // So we need to damage
+            for (int i = mDamageHistory.size() - 1;
+                    i > ((int) mDamageHistory.size()) - frame.bufferAge(); i--) {
+                dirty.join(mDamageHistory[i]);
+            }
+        }
     }
 
+    // Add the screen damage to the ring buffer.
+    mDamageHistory.next() = screenDirty;
+
+    mEglManager.damageFrame(frame, dirty);
+    mCanvas->prepareDirty(dirty.fLeft, dirty.fTop,
+            dirty.fRight, dirty.fBottom, mOpaque);
+
     Rect outBounds;
     mCanvas->drawRenderNode(mRootRenderNode.get(), outBounds);
 
@@ -288,11 +303,30 @@
     mCurrentFrameInfo->markSwapBuffers();
 
     if (drew) {
-        swapBuffers(dirty, width, height);
+        if (CC_UNLIKELY(!mEglManager.swapBuffers(frame, screenDirty))) {
+            setSurface(nullptr);
+        }
+        mHaveNewSurface = false;
     }
 
     // TODO: Use a fence for real completion?
     mCurrentFrameInfo->markFrameCompleted();
+
+#if LOG_FRAMETIME_MMA
+    float thisFrame = mCurrentFrameInfo->duration(
+            FrameInfoIndex::IssueDrawCommandsStart,
+            FrameInfoIndex::FrameCompleted) / NANOS_PER_MILLIS_F;
+    if (sFrameCount) {
+        sBenchMma = ((9 * sBenchMma) + thisFrame) / 10;
+    } else {
+        sBenchMma = thisFrame;
+    }
+    if (++sFrameCount == 10) {
+        sFrameCount = 1;
+        ALOGD("Average frame time: %.4f", sBenchMma);
+    }
+#endif
+
     mJankTracker.addFrame(*mCurrentFrameInfo);
     mRenderThread.jankTracker().addFrame(*mCurrentFrameInfo);
 }
diff --git a/libs/hwui/renderthread/CanvasContext.h b/libs/hwui/renderthread/CanvasContext.h
index 59f9c3a..0ceb9f1 100644
--- a/libs/hwui/renderthread/CanvasContext.h
+++ b/libs/hwui/renderthread/CanvasContext.h
@@ -25,7 +25,6 @@
 #include "utils/RingBuffer.h"
 #include "renderthread/RenderTask.h"
 #include "renderthread/RenderThread.h"
-#include "renderthread/DirtyHistory.h"
 
 #include <cutils/compiler.h>
 #include <EGL/egl.h>
@@ -119,7 +118,6 @@
     friend class android::uirenderer::RenderState;
 
     void setSurface(ANativeWindow* window);
-    void swapBuffers(const SkRect& dirty, EGLint width, EGLint height);
     void requireSurface();
 
     void freePrefetechedLayers();
@@ -130,6 +128,7 @@
     EGLSurface mEglSurface = EGL_NO_SURFACE;
     bool mBufferPreserved = false;
     SwapBehavior mSwapBehavior = kSwap_default;
+    RingBuffer<SkRect, 3> mDamageHistory;
 
     bool mOpaque;
     OpenGLRenderer* mCanvas = nullptr;
@@ -147,8 +146,6 @@
     FrameInfoVisualizer mProfiler;
 
     std::set<RenderNode*> mPrefetechedLayers;
-
-    DirtyHistory mDirtyHistory;
 };
 
 } /* namespace renderthread */
diff --git a/libs/hwui/renderthread/DirtyHistory.cpp b/libs/hwui/renderthread/DirtyHistory.cpp
deleted file mode 100644
index 1419e84..0000000
--- a/libs/hwui/renderthread/DirtyHistory.cpp
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright (C) 2015 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.
- */
-#include "DirtyHistory.h"
-
-namespace android {
-namespace uirenderer {
-namespace renderthread {
-
-DirtyHistory::DirtyHistory()
-        : mBack(DIRTY_HISTORY_SIZE - 1) {
-    clear();
-}
-
-void DirtyHistory::clear()
-{
-    for (int i = 0; i < DIRTY_HISTORY_SIZE; i++) {
-        mHistory[i].clear();
-    }
-}
-
-Rect DirtyHistory::get(int index) {
-    if (index >= DIRTY_HISTORY_SIZE || index < 0)
-        return Rect();
-    return mHistory[(1 + mBack + index) % DIRTY_HISTORY_SIZE];
-}
-
-Rect DirtyHistory::unionWith(Rect rect, int count) {
-    if (rect.isEmpty() || count > DIRTY_HISTORY_SIZE || count < 0)
-        return Rect();
-
-    for (int i = 0; i < count; i++) {
-        Rect ith = get(i);
-        if (ith.isEmpty())
-            return Rect();
-
-        // rect union
-        rect.left = fminf(rect.left, ith.left);
-        rect.top = fminf(rect.top, ith.top);
-        rect.right = fmaxf(rect.right, ith.right);
-        rect.bottom = fmaxf(rect.bottom, ith.bottom);
-    }
-    return rect;
-}
-
-void DirtyHistory::prepend(Rect rect) {
-    if (rect.isEmpty()) {
-        mHistory[mBack].clear();
-    } else {
-        mHistory[mBack].set(rect);
-    }
-    mBack = (mBack + DIRTY_HISTORY_SIZE - 1) % DIRTY_HISTORY_SIZE;
-}
-
-} /* namespace renderthread */
-} /* namespace uirenderer */
-} /* namespace android */
diff --git a/libs/hwui/renderthread/DirtyHistory.h b/libs/hwui/renderthread/DirtyHistory.h
deleted file mode 100644
index d5ea597..0000000
--- a/libs/hwui/renderthread/DirtyHistory.h
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Copyright (C) 2015 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 DIRTYHISTORY_H
-#define DIRTYHISTORY_H
-
-#include <Rect.h>
-
-namespace android {
-namespace uirenderer {
-namespace renderthread {
-
-#define DIRTY_HISTORY_SIZE 4
-
-class DirtyHistory {
-public:
-    DirtyHistory();
-    ~DirtyHistory() {}
-
-    Rect get(int index);
-    Rect unionWith(Rect rect, int count);
-    void prepend(Rect rect);
-    void clear();
-private:
-    Rect mHistory[DIRTY_HISTORY_SIZE];
-    int mBack;
-};
-
-} /* namespace renderthread */
-} /* namespace uirenderer */
-} /* namespace android */
-
-#endif /* DIRTYHISTORY_H */
diff --git a/libs/hwui/renderthread/EglManager.cpp b/libs/hwui/renderthread/EglManager.cpp
index ac36f53..d2ce49f 100644
--- a/libs/hwui/renderthread/EglManager.cpp
+++ b/libs/hwui/renderthread/EglManager.cpp
@@ -25,7 +25,8 @@
 #include <cutils/properties.h>
 #include <EGL/eglext.h>
 
-#define PROPERTY_RENDER_DIRTY_REGIONS "debug.hwui.render_dirty_regions"
+#include <string>
+
 #define GLES_VERSION 2
 
 #define WAIT_FOR_GPU_COMPLETION 0
@@ -63,10 +64,27 @@
     return egl_error_str(eglGetError());
 }
 
-static bool load_dirty_regions_property() {
-    char buf[PROPERTY_VALUE_MAX];
-    int len = property_get(PROPERTY_RENDER_DIRTY_REGIONS, buf, "true");
-    return !strncasecmp("true", buf, len);
+static struct {
+    bool bufferAge = false;
+    bool setDamage = false;
+} EglExtensions;
+
+void Frame::map(const SkRect& in, EGLint* out) const {
+    /* The rectangles are specified relative to the bottom-left of the surface
+     * and the x and y components of each rectangle specify the bottom-left
+     * position of that rectangle.
+     *
+     * HWUI does everything with 0,0 being top-left, so need to map
+     * the rect
+     */
+    SkIRect idirty;
+    in.roundOut(&idirty);
+    EGLint y = mHeight - (idirty.y() + idirty.height());
+    // layout: {x, y, width, height}
+    out[0] = idirty.x();
+    out[1] = y;
+    out[2] = idirty.width();
+    out[3] = idirty.height();
 }
 
 EglManager::EglManager(RenderThread& thread)
@@ -75,13 +93,9 @@
         , mEglConfig(nullptr)
         , mEglContext(EGL_NO_CONTEXT)
         , mPBufferSurface(EGL_NO_SURFACE)
-        , mAllowPreserveBuffer(load_dirty_regions_property())
-        , mHasBufferAgeExt(false)
         , mCurrentSurface(EGL_NO_SURFACE)
         , mAtlasMap(nullptr)
         , mAtlasMapSize(0) {
-    mCanSetPreserveBuffer = mAllowPreserveBuffer;
-    ALOGD("Use EGL_SWAP_BEHAVIOR_PRESERVED: %s", mAllowPreserveBuffer ? "true" : "false");
 }
 
 void EglManager::initialize() {
@@ -99,10 +113,18 @@
 
     ALOGI("Initialized EGL, version %d.%d", (int)major, (int)minor);
 
-    findExtensions(eglQueryString(mEglDisplay, EGL_EXTENSIONS), mEglExtensionList);
-    mHasBufferAgeExt = hasEglExtension("EGL_EXT_buffer_age");
+    initExtensions();
 
-    loadConfig(mHasBufferAgeExt);
+    // Now that extensions are loaded, pick a swap behavior
+    if (Properties::enablePartialUpdates) {
+        if (Properties::useBufferAge && EglExtensions.bufferAge) {
+            mSwapBehavior = SwapBehavior::BufferAge;
+        } else {
+            mSwapBehavior = SwapBehavior::Preserved;
+        }
+    }
+
+    loadConfig();
     createContext();
     createPBufferSurface();
     makeCurrent(mPBufferSurface);
@@ -110,17 +132,23 @@
     initAtlas();
 }
 
+void EglManager::initExtensions() {
+    std::string extensions(eglQueryString(mEglDisplay, EGL_EXTENSIONS));
+    auto has = [&](const char* ext) {
+        return extensions.find(ext) != std::string::npos;
+    };
+    EglExtensions.bufferAge = has("EGL_EXT_buffer_age");
+    EglExtensions.setDamage = has("EGL_KHR_partial_update");
+}
+
 bool EglManager::hasEglContext() {
     return mEglDisplay != EGL_NO_DISPLAY;
 }
 
-bool EglManager::hasEglExtension(const char* extension) const {
-   const std::string s(extension);
-   return mEglExtensionList.find(s) != mEglExtensionList.end();
-}
-
-void EglManager::loadConfig(bool useBufferAgeExt) {
-    EGLint swapBehavior = (!useBufferAgeExt && mCanSetPreserveBuffer) ? EGL_SWAP_BEHAVIOR_PRESERVED_BIT : 0;
+void EglManager::loadConfig() {
+    ALOGD("Swap behavior %d", static_cast<int>(mSwapBehavior));
+    EGLint swapBehavior = (mSwapBehavior == SwapBehavior::Preserved)
+            ? EGL_SWAP_BEHAVIOR_PRESERVED_BIT : 0;
     EGLint attribs[] = {
             EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
             EGL_RED_SIZE, 8,
@@ -137,13 +165,13 @@
     EGLint num_configs = 1;
     if (!eglChooseConfig(mEglDisplay, attribs, &mEglConfig, num_configs, &num_configs)
             || num_configs != 1) {
-        // Failed to get a valid config
-        if (mCanSetPreserveBuffer) {
-            ALOGW("Failed to choose config with EGL_SWAP_BEHAVIOR_PRESERVED, retrying without...");
+        if (mSwapBehavior == SwapBehavior::Preserved) {
             // Try again without dirty regions enabled
-            mCanSetPreserveBuffer = false;
-            loadConfig(useBufferAgeExt);
+            ALOGW("Failed to choose config with EGL_SWAP_BEHAVIOR_PRESERVED, retrying without...");
+            mSwapBehavior = SwapBehavior::Discard;
+            loadConfig();
         } else {
+            // Failed to get a valid config
             LOG_ALWAYS_FATAL("Failed to choose config, error = %s", egl_error_str());
         }
     }
@@ -247,24 +275,47 @@
     return true;
 }
 
-void EglManager::beginFrame(EGLSurface surface, EGLint* width, EGLint* height, EGLint* framebufferAge) {
+EGLint EglManager::queryBufferAge(EGLSurface surface) {
+    switch (mSwapBehavior) {
+    case SwapBehavior::Discard:
+        return 0;
+    case SwapBehavior::Preserved:
+        return 1;
+    case SwapBehavior::BufferAge:
+        EGLint bufferAge;
+        eglQuerySurface(mEglDisplay, surface, EGL_BUFFER_AGE_EXT, &bufferAge);
+        return bufferAge;
+    }
+    return 0;
+}
+
+Frame EglManager::beginFrame(EGLSurface surface) {
     LOG_ALWAYS_FATAL_IF(surface == EGL_NO_SURFACE,
             "Tried to beginFrame on EGL_NO_SURFACE!");
     makeCurrent(surface);
-    if (width) {
-        eglQuerySurface(mEglDisplay, surface, EGL_WIDTH, width);
-    }
-    if (height) {
-        eglQuerySurface(mEglDisplay, surface, EGL_HEIGHT, height);
-    }
-    if (useBufferAgeExt()) {
-        eglQuerySurface(mEglDisplay, surface, EGL_BUFFER_AGE_EXT, framebufferAge);
-    }
+    Frame frame;
+    frame.mSurface = surface;
+    eglQuerySurface(mEglDisplay, surface, EGL_WIDTH, &frame.mWidth);
+    eglQuerySurface(mEglDisplay, surface, EGL_HEIGHT, &frame.mHeight);
+    frame.mBufferAge = queryBufferAge(surface);
     eglBeginFrame(mEglDisplay, surface);
+    return frame;
 }
 
-bool EglManager::swapBuffers(EGLSurface surface, const SkRect& dirty,
-        EGLint width, EGLint height) {
+void EglManager::damageFrame(const Frame& frame, const SkRect& dirty) {
+#ifdef EGL_KHR_partial_update
+    if (EglExtensions.setDamage && mSwapBehavior == SwapBehavior::BufferAge) {
+        EGLint rects[4];
+        frame.map(dirty, rects);
+        if (!eglSetDamageRegionKHR(mEglDisplay, frame.mSurface, rects, 1)) {
+            LOG_ALWAYS_FATAL("Failed to set damage region on surface %p, error=%s",
+                    (void*)frame.mSurface, egl_error_str());
+        }
+    }
+#endif
+}
+
+bool EglManager::swapBuffers(const Frame& frame, const SkRect& screenDirty) {
 
 #if WAIT_FOR_GPU_COMPLETION
     {
@@ -275,28 +326,15 @@
 
 #ifdef EGL_KHR_swap_buffers_with_damage
     if (CC_LIKELY(Properties::swapBuffersWithDamage)) {
-        SkIRect idirty;
-        dirty.roundOut(&idirty);
-        /*
-         * EGL_KHR_swap_buffers_with_damage spec states:
-         *
-         * The rectangles are specified relative to the bottom-left of the surface
-         * and the x and y components of each rectangle specify the bottom-left
-         * position of that rectangle.
-         *
-         * HWUI does everything with 0,0 being top-left, so need to map
-         * the rect
-         */
-        EGLint y = height - (idirty.y() + idirty.height());
-        // layout: {x, y, width, height}
-        EGLint rects[4] = { idirty.x(), y, idirty.width(), idirty.height() };
-        EGLint numrects = dirty.isEmpty() ? 0 : 1;
-        eglSwapBuffersWithDamageKHR(mEglDisplay, surface, rects, numrects);
+        EGLint rects[4];
+        frame.map(screenDirty, rects);
+        eglSwapBuffersWithDamageKHR(mEglDisplay, frame.mSurface, rects,
+                screenDirty.isEmpty() ? 0 : 1);
     } else {
-        eglSwapBuffers(mEglDisplay, surface);
+        eglSwapBuffers(mEglDisplay, frame.mSurface);
     }
 #else
-    eglSwapBuffers(mEglDisplay, surface);
+    eglSwapBuffers(mEglDisplay, frame.mSurface);
 #endif
 
     EGLint err = eglGetError();
@@ -307,7 +345,8 @@
         // For some reason our surface was destroyed out from under us
         // This really shouldn't happen, but if it does we can recover easily
         // by just not trying to use the surface anymore
-        ALOGW("swapBuffers encountered EGL_BAD_SURFACE on %p, halting rendering...", surface);
+        ALOGW("swapBuffers encountered EGL_BAD_SURFACE on %p, halting rendering...",
+                frame.mSurface);
         return false;
     }
     LOG_ALWAYS_FATAL("Encountered EGL error %d %s during rendering",
@@ -316,10 +355,6 @@
     return false;
 }
 
-bool EglManager::useBufferAgeExt() {
-    return mAllowPreserveBuffer && mHasBufferAgeExt;
-}
-
 void EglManager::fence() {
     EGLSyncKHR fence = eglCreateSyncKHR(mEglDisplay, EGL_SYNC_FENCE_KHR, NULL);
     eglClientWaitSyncKHR(mEglDisplay, fence,
@@ -328,21 +363,13 @@
 }
 
 bool EglManager::setPreserveBuffer(EGLSurface surface, bool preserve) {
-    if (CC_UNLIKELY(!mAllowPreserveBuffer)) return false;
+    if (mSwapBehavior != SwapBehavior::Preserved) return false;
 
-    // Use EGL_EXT_buffer_age instead if supported
-    if (mHasBufferAgeExt) return true;
-
-    bool preserved = false;
-    if (mCanSetPreserveBuffer) {
-        preserved = eglSurfaceAttrib(mEglDisplay, surface, EGL_SWAP_BEHAVIOR,
-                preserve ? EGL_BUFFER_PRESERVED : EGL_BUFFER_DESTROYED);
-        if (CC_UNLIKELY(!preserved)) {
-            ALOGW("Failed to set EGL_SWAP_BEHAVIOR on surface %p, error=%s",
-                    (void*) surface, egl_error_str());
-        }
-    }
-    if (CC_UNLIKELY(!preserved)) {
+    bool preserved = eglSurfaceAttrib(mEglDisplay, surface, EGL_SWAP_BEHAVIOR,
+            preserve ? EGL_BUFFER_PRESERVED : EGL_BUFFER_DESTROYED);
+    if (!preserved) {
+        ALOGW("Failed to set EGL_SWAP_BEHAVIOR on surface %p, error=%s",
+                (void*) surface, egl_error_str());
         // Maybe it's already set?
         EGLint swapBehavior;
         if (eglQuerySurface(mEglDisplay, surface, EGL_SWAP_BEHAVIOR, &swapBehavior)) {
@@ -356,19 +383,6 @@
     return preserved;
 }
 
-void EglManager::findExtensions(const char* extensions, std::set<std::string>& list) const {
-    const char* current = extensions;
-    const char* head = current;
-    do {
-        head = strchr(current, ' ');
-        std::string s(current, head ? head - current : strlen(current));
-        if (s.length()) {
-            list.insert(s);
-        }
-        current = head + 1;
-    } while (head);
-}
-
 } /* namespace renderthread */
 } /* namespace uirenderer */
 } /* namespace android */
diff --git a/libs/hwui/renderthread/EglManager.h b/libs/hwui/renderthread/EglManager.h
index bb5d24b..62b5b99 100644
--- a/libs/hwui/renderthread/EglManager.h
+++ b/libs/hwui/renderthread/EglManager.h
@@ -21,13 +21,35 @@
 #include <SkRect.h>
 #include <ui/GraphicBuffer.h>
 #include <utils/StrongPointer.h>
-#include <set>
 
 namespace android {
 namespace uirenderer {
 namespace renderthread {
 
 class RenderThread;
+class EglManager;
+
+class Frame {
+public:
+    EGLint width() const { return mWidth; }
+    EGLint height() const { return mHeight; }
+
+    // See: https://www.khronos.org/registry/egl/extensions/EXT/EGL_EXT_buffer_age.txt
+    // for what this means
+    EGLint bufferAge() const { return mBufferAge; }
+
+private:
+    friend class EglManager;
+
+    EGLSurface mSurface;
+    EGLint mWidth;
+    EGLint mHeight;
+    EGLint mBufferAge;
+
+    // Maps from 0,0 in top-left to 0,0 in bottom-left
+    // If out is not an EGLint[4] you're going to have a bad time
+    void map(const SkRect& in, EGLint* out) const;
+};
 
 // This class contains the shared global EGL objects, such as EGLDisplay
 // and EGLConfig, which are re-used by CanvasContext
@@ -38,8 +60,6 @@
 
     bool hasEglContext();
 
-    bool hasEglExtension(const char* extension) const;
-
     EGLSurface createSurface(EGLNativeWindowType window);
     void destroySurface(EGLSurface surface);
 
@@ -48,14 +68,13 @@
     bool isCurrent(EGLSurface surface) { return mCurrentSurface == surface; }
     // Returns true if the current surface changed, false if it was already current
     bool makeCurrent(EGLSurface surface, EGLint* errOut = nullptr);
-    void beginFrame(EGLSurface surface, EGLint* width, EGLint* height, EGLint* framebufferAge);
-    bool swapBuffers(EGLSurface surface, const SkRect& dirty, EGLint width, EGLint height);
+    Frame beginFrame(EGLSurface surface);
+    void damageFrame(const Frame& frame, const SkRect& dirty);
+    bool swapBuffers(const Frame& frame, const SkRect& screenDirty);
 
     // Returns true iff the surface is now preserving buffers.
     bool setPreserveBuffer(EGLSurface surface, bool preserve);
 
-    bool useBufferAgeExt();
-
     void setTextureAtlas(const sp<GraphicBuffer>& buffer, int64_t* map, size_t mapSize);
 
     void fence();
@@ -67,12 +86,12 @@
     // EglContext is never destroyed, method is purposely not implemented
     ~EglManager();
 
+    void initExtensions();
     void createPBufferSurface();
-    void loadConfig(bool useBufferAgeExt);
+    void loadConfig();
     void createContext();
     void initAtlas();
-
-    void findExtensions(const char* extensions, std::set<std::string>& list) const;
+    EGLint queryBufferAge(EGLSurface surface);
 
     RenderThread& mRenderThread;
 
@@ -81,18 +100,18 @@
     EGLContext mEglContext;
     EGLSurface mPBufferSurface;
 
-    const bool mAllowPreserveBuffer;
-    bool mCanSetPreserveBuffer;
-
-    bool mHasBufferAgeExt;
-
     EGLSurface mCurrentSurface;
 
     sp<GraphicBuffer> mAtlasBuffer;
     int64_t* mAtlasMap;
     size_t mAtlasMapSize;
 
-    std::set<std::string> mEglExtensionList;
+    enum class SwapBehavior {
+        Discard,
+        Preserved,
+        BufferAge,
+    };
+    SwapBehavior mSwapBehavior = SwapBehavior::Discard;
 };
 
 } /* namespace renderthread */
diff --git a/libs/hwui/tests/main.cpp b/libs/hwui/tests/main.cpp
index 69e8225..483fb35 100644
--- a/libs/hwui/tests/main.cpp
+++ b/libs/hwui/tests/main.cpp
@@ -283,6 +283,69 @@
     }
 };
 
+class PartialInvalTest : public TreeContentAnimation {
+public:
+    std::vector< sp<RenderNode> > cards;
+    void createContent(int width, int height, DisplayListCanvas* renderer) override {
+        static SkColor COLORS[] = {
+                0xFFF44336,
+                0xFF9C27B0,
+                0xFF2196F3,
+                0xFF4CAF50,
+        };
+
+        renderer->drawColor(0xFFFFFFFF, SkXfermode::kSrcOver_Mode);
+
+        for (int x = dp(16); x < (width - dp(116)); x += dp(116)) {
+            for (int y = dp(16); y < (height - dp(116)); y += dp(116)) {
+                sp<RenderNode> card = createCard(x, y, dp(100), dp(100),
+                        COLORS[static_cast<int>((y / dp(116))) % 4]);
+                renderer->drawRenderNode(card.get());
+                cards.push_back(card);
+            }
+        }
+    }
+    void doFrame(int frameNr) override {
+        int curFrame = frameNr % 150;
+        cards[0]->mutateStagingProperties().setTranslationX(curFrame);
+        cards[0]->mutateStagingProperties().setTranslationY(curFrame);
+        cards[0]->setPropertyFieldsDirty(RenderNode::X | RenderNode::Y);
+
+        DisplayListCanvas* renderer = startRecording(cards[0].get());
+        renderer->drawColor(interpolateColor(curFrame / 150.0f, 0xFFF44336, 0xFFF8BBD0),
+                SkXfermode::kSrcOver_Mode);
+        endRecording(renderer, cards[0].get());
+    }
+
+    static SkColor interpolateColor(float fraction, SkColor start, SkColor end) {
+        int startA = (start >> 24) & 0xff;
+        int startR = (start >> 16) & 0xff;
+        int startG = (start >> 8) & 0xff;
+        int startB = start & 0xff;
+
+        int endA = (end >> 24) & 0xff;
+        int endR = (end >> 16) & 0xff;
+        int endG = (end >> 8) & 0xff;
+        int endB = end & 0xff;
+
+        return (int)((startA + (int)(fraction * (endA - startA))) << 24) |
+                (int)((startR + (int)(fraction * (endR - startR))) << 16) |
+                (int)((startG + (int)(fraction * (endG - startG))) << 8) |
+                (int)((startB + (int)(fraction * (endB - startB))));
+    }
+private:
+    sp<RenderNode> createCard(int x, int y, int width, int height, SkColor color) {
+        sp<RenderNode> node = new RenderNode();
+        node->mutateStagingProperties().setLeftTopRightBottom(x, y, x + width, y + height);
+        node->setPropertyFieldsDirty(RenderNode::X | RenderNode::Y);
+
+        DisplayListCanvas* renderer = startRecording(node.get());
+        renderer->drawColor(color, SkXfermode::kSrcOver_Mode);
+        endRecording(renderer, node.get());
+        return node;
+    }
+};
+
 struct cstr_cmp {
     bool operator()(const char *a, const char *b) const {
         return std::strcmp(a, b) < 0;
@@ -296,6 +359,7 @@
     {"shadowgrid2", TreeContentAnimation::run<ShadowGrid2Animation>},
     {"rectgrid", TreeContentAnimation::run<RectGridAnimation> },
     {"oval", TreeContentAnimation::run<OvalAnimation> },
+    {"partialinval", TreeContentAnimation::run<PartialInvalTest> },
 };
 
 int main(int argc, char* argv[]) {
diff --git a/libs/hwui/tests/nullgles.cpp b/libs/hwui/tests/nullgles.cpp
index 8ca7598..f8e8c98 100644
--- a/libs/hwui/tests/nullgles.cpp
+++ b/libs/hwui/tests/nullgles.cpp
@@ -261,8 +261,6 @@
 void glPushGroupMarkerEXT(GLsizei length, const GLchar *marker) {}
 void glPopGroupMarkerEXT(void) {}
 void glDiscardFramebufferEXT(GLenum target, GLsizei numAttachments, const GLenum *attachments) {}
-void glStartTilingQCOM(GLuint x, GLuint y, GLuint width, GLuint height, GLbitfield preserveMask) {}
-void glEndTilingQCOM(GLbitfield preserveMask) {}
 void glEGLImageTargetTexture2DOES(GLenum target, GLeglImageOES image) {}
 
 // GLES3