Add blur support in caching

Fixes some flickering issues:
* Add blur metadata into LayerState
* Only cache a layer that requires blurring if all layers below it are
part of the same CachedSet to avoid caching part of, but not all of, the
blur

This doesn't preclude caching intermediate blurs: e.g., the blur radius
of a layer does not change, everything else below the layer does not
change, but some other property of that layer changes - ideally this
layer should be cached, but is not currently supported in this patch.

Bug: 186692925
Test: Notification shade blur overlayed on Maps
Test: Notification shade blur overlayed on Wallpaper

Change-Id: I2f112927accc35cab406292486890fe41423d022
diff --git a/libs/renderengine/include/renderengine/LayerSettings.h b/libs/renderengine/include/renderengine/LayerSettings.h
index 6e98352..28fa782 100644
--- a/libs/renderengine/include/renderengine/LayerSettings.h
+++ b/libs/renderengine/include/renderengine/LayerSettings.h
@@ -195,18 +195,6 @@
             lhs.length == rhs.length && lhs.casterIsTranslucent == rhs.casterIsTranslucent;
 }
 
-static inline bool operator==(const BlurRegion& lhs, const BlurRegion& rhs) {
-    return lhs.alpha == rhs.alpha && lhs.cornerRadiusTL == rhs.cornerRadiusTL &&
-            lhs.cornerRadiusTR == rhs.cornerRadiusTR && lhs.cornerRadiusBL == rhs.cornerRadiusBL &&
-            lhs.cornerRadiusBR == rhs.cornerRadiusBR && lhs.blurRadius == rhs.blurRadius &&
-            lhs.left == rhs.left && lhs.top == rhs.top && lhs.right == rhs.right &&
-            lhs.bottom == rhs.bottom;
-}
-
-static inline bool operator!=(const BlurRegion& lhs, const BlurRegion& rhs) {
-    return !(lhs == rhs);
-}
-
 static inline bool operator==(const LayerSettings& lhs, const LayerSettings& rhs) {
     if (lhs.blurRegions.size() != rhs.blurRegions.size()) {
         return false;
diff --git a/libs/ui/include/ui/BlurRegion.h b/libs/ui/include/ui/BlurRegion.h
index 69a586e..a9ca369 100644
--- a/libs/ui/include/ui/BlurRegion.h
+++ b/libs/ui/include/ui/BlurRegion.h
@@ -20,6 +20,8 @@
 #include <iosfwd>
 #include <iostream>
 
+#include <math/HashCombine.h>
+
 namespace android {
 
 struct BlurRegion {
@@ -33,6 +35,16 @@
     int top;
     int right;
     int bottom;
+
+    inline bool operator==(const BlurRegion& other) const {
+        return blurRadius == other.blurRadius && cornerRadiusTL == other.cornerRadiusTL &&
+                cornerRadiusTR == other.cornerRadiusTR && cornerRadiusBL == other.cornerRadiusBL &&
+                cornerRadiusBR == other.cornerRadiusBR && alpha == other.alpha &&
+                left == other.left && top == other.top && right == other.right &&
+                bottom == other.bottom;
+    }
+
+    inline bool operator!=(const BlurRegion& other) const { return !(*this == other); }
 };
 
 static inline void PrintTo(const BlurRegion& blurRegion, ::std::ostream* os) {
@@ -50,4 +62,15 @@
     *os << "\n}";
 }
 
-} // namespace android
\ No newline at end of file
+} // namespace android
+
+namespace std {
+template <>
+struct hash<android::BlurRegion> {
+    size_t operator()(const android::BlurRegion& region) const {
+        return android::hashCombine(region.blurRadius, region.cornerRadiusTL, region.cornerRadiusTR,
+                                    region.cornerRadiusBL, region.cornerRadiusBR, region.alpha,
+                                    region.left, region.top, region.right, region.bottom);
+    }
+};
+} // namespace std
\ No newline at end of file
diff --git a/services/surfaceflinger/CompositionEngine/include/compositionengine/impl/planner/CachedSet.h b/services/surfaceflinger/CompositionEngine/include/compositionengine/impl/planner/CachedSet.h
index 06f26eb..dc6eab4 100644
--- a/services/surfaceflinger/CompositionEngine/include/compositionengine/impl/planner/CachedSet.h
+++ b/services/surfaceflinger/CompositionEngine/include/compositionengine/impl/planner/CachedSet.h
@@ -110,6 +110,9 @@
     // CachedSet and punching a hole.
     bool requiresHolePunch() const;
 
+    // True if any constituent layer is configured to blur any layers behind.
+    bool hasBlurBehind() const;
+
     // Add a layer that will be drawn behind this one. ::render() will render a
     // hole in this CachedSet's buffer, allowing the supplied layer to peek
     // through. Must be called before ::render().
diff --git a/services/surfaceflinger/CompositionEngine/include/compositionengine/impl/planner/Flattener.h b/services/surfaceflinger/CompositionEngine/include/compositionengine/impl/planner/Flattener.h
index 864251f..213c55e 100644
--- a/services/surfaceflinger/CompositionEngine/include/compositionengine/impl/planner/Flattener.h
+++ b/services/surfaceflinger/CompositionEngine/include/compositionengine/impl/planner/Flattener.h
@@ -20,6 +20,7 @@
 #include <compositionengine/impl/planner/CachedSet.h>
 #include <compositionengine/impl/planner/LayerState.h>
 
+#include <numeric>
 #include <vector>
 
 namespace android {
@@ -60,6 +61,73 @@
     bool mergeWithCachedSets(const std::vector<const LayerState*>& layers,
                              std::chrono::steady_clock::time_point now);
 
+    // A Run is a sequence of CachedSets, which is a candidate for flattening into a single
+    // CachedSet. Because it is wasteful to flatten 1 CachedSet, a Run must contain more than 1
+    // CachedSet
+    class Run {
+    public:
+        // A builder for a Run, to aid in construction
+        class Builder {
+        private:
+            std::vector<CachedSet>::const_iterator mStart;
+            std::vector<size_t> mLengths;
+            const CachedSet* mHolePunchCandidate = nullptr;
+
+        public:
+            // Initializes a Builder a CachedSet to start from.
+            // This start iterator must be an iterator for mLayers
+            void init(const std::vector<CachedSet>::const_iterator& start) {
+                mStart = start;
+                mLengths.push_back(start->getLayerCount());
+            }
+
+            // Appends a new CachedSet to the end of the run
+            // The provided length must be the size of the next sequential CachedSet in layers
+            void append(size_t length) { mLengths.push_back(length); }
+
+            // Sets the hole punch candidate for the Run.
+            void setHolePunchCandidate(const CachedSet* holePunchCandidate) {
+                mHolePunchCandidate = holePunchCandidate;
+            }
+
+            // Builds a Run instance, if a valid Run may be built.
+            std::optional<Run> validateAndBuild() {
+                if (mLengths.size() <= 1) {
+                    return std::nullopt;
+                }
+
+                return Run(mStart,
+                           std::reduce(mLengths.cbegin(), mLengths.cend(), 0u,
+                                       [](size_t left, size_t right) { return left + right; }),
+                           mHolePunchCandidate);
+            }
+
+            void reset() { *this = {}; }
+        };
+
+        // Gets the starting CachedSet of this run.
+        // This is an iterator into mLayers
+        const std::vector<CachedSet>::const_iterator& getStart() const { return mStart; }
+        // Gets the total number of layers encompassing this Run.
+        size_t getLayerLength() const { return mLength; }
+        // Gets the hole punch candidate for this Run.
+        const CachedSet* getHolePunchCandidate() const { return mHolePunchCandidate; }
+
+    private:
+        Run(std::vector<CachedSet>::const_iterator start, size_t length,
+            const CachedSet* holePunchCandidate)
+              : mStart(start), mLength(length), mHolePunchCandidate(holePunchCandidate) {}
+        const std::vector<CachedSet>::const_iterator mStart;
+        const size_t mLength;
+        const CachedSet* const mHolePunchCandidate;
+
+        friend class Builder;
+    };
+
+    std::vector<Run> findCandidateRuns(std::chrono::steady_clock::time_point now) const;
+
+    std::optional<Run> findBestRun(std::vector<Run>& runs) const;
+
     void buildCachedSets(std::chrono::steady_clock::time_point now);
 
     const bool mEnableHolePunch;
diff --git a/services/surfaceflinger/CompositionEngine/include/compositionengine/impl/planner/LayerState.h b/services/surfaceflinger/CompositionEngine/include/compositionengine/impl/planner/LayerState.h
index 3391273..fef0dfb 100644
--- a/services/surfaceflinger/CompositionEngine/include/compositionengine/impl/planner/LayerState.h
+++ b/services/surfaceflinger/CompositionEngine/include/compositionengine/impl/planner/LayerState.h
@@ -26,6 +26,7 @@
 #include <string>
 
 #include "DisplayHardware/Hal.h"
+#include "math/HashCombine.h"
 
 namespace std {
 template <typename T>
@@ -48,24 +49,26 @@
 
 // clang-format off
 enum class LayerStateField : uint32_t {
-    None            = 0u,
-    Id              = 1u << 0,
-    Name            = 1u << 1,
-    DisplayFrame    = 1u << 2,
-    SourceCrop      = 1u << 3,
-    BufferTransform = 1u << 4,
-    BlendMode       = 1u << 5,
-    Alpha           = 1u << 6,
-    LayerMetadata   = 1u << 7,
-    VisibleRegion   = 1u << 8,
-    Dataspace       = 1u << 9,
-    PixelFormat     = 1u << 10,
-    ColorTransform  = 1u << 11,
-    SurfaceDamage   = 1u << 12,
-    CompositionType = 1u << 13,
-    SidebandStream  = 1u << 14,
-    Buffer          = 1u << 15,
-    SolidColor      = 1u << 16,
+    None                  = 0u,
+    Id                    = 1u << 0,
+    Name                  = 1u << 1,
+    DisplayFrame          = 1u << 2,
+    SourceCrop            = 1u << 3,
+    BufferTransform       = 1u << 4,
+    BlendMode             = 1u << 5,
+    Alpha                 = 1u << 6,
+    LayerMetadata         = 1u << 7,
+    VisibleRegion         = 1u << 8,
+    Dataspace             = 1u << 9,
+    PixelFormat           = 1u << 10,
+    ColorTransform        = 1u << 11,
+    SurfaceDamage         = 1u << 12,
+    CompositionType       = 1u << 13,
+    SidebandStream        = 1u << 14,
+    Buffer                = 1u << 15,
+    SolidColor            = 1u << 16,
+    BackgroundBlurRadius  = 1u << 17,
+    BlurRegions           = 1u << 18,
 };
 // clang-format on
 
@@ -225,6 +228,9 @@
     const std::string& getName() const { return mName.get(); }
     Rect getDisplayFrame() const { return mDisplayFrame.get(); }
     const Region& getVisibleRegion() const { return mVisibleRegion.get(); }
+    bool hasBlurBehind() const {
+        return mBackgroundBlurRadius.get() > 0 || !mBlurRegions.get().empty();
+    }
     hardware::graphics::composer::hal::Composition getCompositionType() const {
         return mCompositionType.get();
     }
@@ -398,7 +404,45 @@
                             return std::vector<std::string>{stream.str()};
                         }};
 
-    static const constexpr size_t kNumNonUniqueFields = 14;
+    OutputLayerState<int32_t, LayerStateField::BackgroundBlurRadius> mBackgroundBlurRadius{
+            [](auto layer) {
+                return layer->getLayerFE().getCompositionState()->backgroundBlurRadius;
+            }};
+
+    using BlurRegionsState =
+            OutputLayerState<std::vector<BlurRegion>, LayerStateField::BlurRegions>;
+    BlurRegionsState mBlurRegions{[](auto layer) {
+                                      return layer->getLayerFE().getCompositionState()->blurRegions;
+                                  },
+                                  [](const std::vector<BlurRegion>& regions) {
+                                      std::vector<std::string> result;
+                                      for (const auto region : regions) {
+                                          std::string str;
+                                          base::StringAppendF(&str,
+                                                              "{radius=%du, cornerRadii=[%f, %f, "
+                                                              "%f, %f], alpha=%f, rect=[%d, "
+                                                              "%d, %d, %d]",
+                                                              region.blurRadius,
+                                                              region.cornerRadiusTL,
+                                                              region.cornerRadiusTR,
+                                                              region.cornerRadiusBL,
+                                                              region.cornerRadiusBR, region.alpha,
+                                                              region.left, region.top, region.right,
+                                                              region.bottom);
+                                          result.push_back(str);
+                                      }
+                                      return result;
+                                  },
+                                  BlurRegionsState::getDefaultEquals(),
+                                  [](const std::vector<BlurRegion>& regions) {
+                                      size_t hash = 0;
+                                      for (const auto& region : regions) {
+                                          android::hashCombineSingle(hash, region);
+                                      }
+                                      return hash;
+                                  }};
+
+    static const constexpr size_t kNumNonUniqueFields = 16;
 
     std::array<StateInterface*, kNumNonUniqueFields> getNonUniqueFields() {
         std::array<const StateInterface*, kNumNonUniqueFields> constFields =
@@ -413,10 +457,10 @@
 
     std::array<const StateInterface*, kNumNonUniqueFields> getNonUniqueFields() const {
         return {
-                &mDisplayFrame, &mSourceCrop,     &mBufferTransform, &mBlendMode,
-                &mAlpha,        &mLayerMetadata,  &mVisibleRegion,   &mOutputDataspace,
-                &mPixelFormat,  &mColorTransform, &mCompositionType, &mSidebandStream,
-                &mBuffer,       &mSolidColor,
+                &mDisplayFrame, &mSourceCrop,     &mBufferTransform,      &mBlendMode,
+                &mAlpha,        &mLayerMetadata,  &mVisibleRegion,        &mOutputDataspace,
+                &mPixelFormat,  &mColorTransform, &mCompositionType,      &mSidebandStream,
+                &mBuffer,       &mSolidColor,     &mBackgroundBlurRadius, &mBlurRegions,
         };
     }
 };
diff --git a/services/surfaceflinger/CompositionEngine/src/planner/CachedSet.cpp b/services/surfaceflinger/CompositionEngine/src/planner/CachedSet.cpp
index 67854cf..63085cc 100644
--- a/services/surfaceflinger/CompositionEngine/src/planner/CachedSet.cpp
+++ b/services/surfaceflinger/CompositionEngine/src/planner/CachedSet.cpp
@@ -280,6 +280,11 @@
     return layerFE.hasRoundedCorners();
 }
 
+bool CachedSet::hasBlurBehind() const {
+    return std::any_of(mLayers.cbegin(), mLayers.cend(),
+                       [](const Layer& layer) { return layer.getState()->hasBlurBehind(); });
+}
+
 namespace {
 bool contains(const Rect& outer, const Rect& inner) {
     return outer.left <= inner.left && outer.right >= inner.right && outer.top <= inner.top &&
diff --git a/services/surfaceflinger/CompositionEngine/src/planner/Flattener.cpp b/services/surfaceflinger/CompositionEngine/src/planner/Flattener.cpp
index 4453a99..294ec4b 100644
--- a/services/surfaceflinger/CompositionEngine/src/planner/Flattener.cpp
+++ b/services/surfaceflinger/CompositionEngine/src/planner/Flattener.cpp
@@ -327,85 +327,94 @@
     return true;
 }
 
-void Flattener::buildCachedSets(time_point now) {
-    struct Run {
-        Run(std::vector<CachedSet>::const_iterator start, size_t length)
-              : start(start), length(length) {}
-
-        std::vector<CachedSet>::const_iterator start;
-        size_t length;
-    };
-
-    if (mLayers.empty()) {
-        ALOGV("[%s] No layers found, returning", __func__);
-        return;
-    }
-
+std::vector<Flattener::Run> Flattener::findCandidateRuns(time_point now) const {
     std::vector<Run> runs;
     bool isPartOfRun = false;
-
-    // Keep track of the layer that follows a run. It's possible that we will
-    // render it with a hole-punch.
-    const CachedSet* holePunchLayer = nullptr;
+    Run::Builder builder;
+    bool firstLayer = true;
+    bool runHasFirstLayer = false;
 
     for (auto currentSet = mLayers.cbegin(); currentSet != mLayers.cend(); ++currentSet) {
-        if (now - currentSet->getLastUpdate() > kActiveLayerTimeout) {
-            // Layer is inactive
+        const bool layerIsInactive = now - currentSet->getLastUpdate() > kActiveLayerTimeout;
+        const bool layerHasBlur = currentSet->hasBlurBehind();
+        if (layerIsInactive && (firstLayer || runHasFirstLayer || !layerHasBlur)) {
             if (isPartOfRun) {
-                runs.back().length += currentSet->getLayerCount();
+                builder.append(currentSet->getLayerCount());
             } else {
                 // Runs can't start with a non-buffer layer
                 if (currentSet->getFirstLayer().getBuffer() == nullptr) {
                     ALOGV("[%s] Skipping initial non-buffer layer", __func__);
                 } else {
-                    runs.emplace_back(currentSet, currentSet->getLayerCount());
+                    builder.init(currentSet);
+                    if (firstLayer) {
+                        runHasFirstLayer = true;
+                    }
                     isPartOfRun = true;
                 }
             }
         } else if (isPartOfRun) {
-            // Runs must be at least 2 sets long or there's nothing to combine
-            if (runs.back().start->getLayerCount() == runs.back().length) {
-                runs.pop_back();
-            } else {
-                // The prior run contained at least two sets. Currently, we'll
-                // only possibly merge a single run, so only keep track of a
-                // holePunchLayer if this is the first run.
-                if (runs.size() == 1) {
-                    holePunchLayer = &(*currentSet);
-                }
-
-                // TODO(b/185114532: Break out of the loop? We may find more runs, but we
-                // won't do anything with them.
+            builder.setHolePunchCandidate(&(*currentSet));
+            if (auto run = builder.validateAndBuild(); run) {
+                runs.push_back(*run);
             }
 
+            runHasFirstLayer = false;
+            builder.reset();
             isPartOfRun = false;
         }
+
+        firstLayer = false;
     }
 
-    // Check for at least 2 sets one more time in case the set includes the last layer
-    if (isPartOfRun && runs.back().start->getLayerCount() == runs.back().length) {
-        runs.pop_back();
+    // If we're in the middle of a run at the end, we still need to validate and build it.
+    if (isPartOfRun) {
+        if (auto run = builder.validateAndBuild(); run) {
+            runs.push_back(*run);
+        }
     }
 
     ALOGV("[%s] Found %zu candidate runs", __func__, runs.size());
 
+    return runs;
+}
+
+std::optional<Flattener::Run> Flattener::findBestRun(std::vector<Flattener::Run>& runs) const {
     if (runs.empty()) {
+        return std::nullopt;
+    }
+
+    // TODO (b/181192467): Choose the best run, instead of just the first.
+    return runs[0];
+}
+
+void Flattener::buildCachedSets(time_point now) {
+    if (mLayers.empty()) {
+        ALOGV("[%s] No layers found, returning", __func__);
         return;
     }
 
-    mNewCachedSet.emplace(*runs[0].start);
+    std::vector<Run> runs = findCandidateRuns(now);
+
+    std::optional<Run> bestRun = findBestRun(runs);
+
+    if (!bestRun) {
+        return;
+    }
+
+    mNewCachedSet.emplace(*bestRun->getStart());
     mNewCachedSet->setLastUpdate(now);
-    auto currentSet = runs[0].start;
-    while (mNewCachedSet->getLayerCount() < runs[0].length) {
+    auto currentSet = bestRun->getStart();
+    while (mNewCachedSet->getLayerCount() < bestRun->getLayerLength()) {
         ++currentSet;
         mNewCachedSet->append(*currentSet);
     }
 
-    if (mEnableHolePunch && holePunchLayer && holePunchLayer->requiresHolePunch()) {
+    if (mEnableHolePunch && bestRun->getHolePunchCandidate() &&
+        bestRun->getHolePunchCandidate()->requiresHolePunch()) {
         // Add the pip layer to mNewCachedSet, but in a special way - it should
         // replace the buffer with a clear round rect.
-        mNewCachedSet->addHolePunchLayerIfFeasible(*holePunchLayer,
-                                                   runs[0].start == mLayers.cbegin());
+        mNewCachedSet->addHolePunchLayerIfFeasible(*bestRun->getHolePunchCandidate(),
+                                                   bestRun->getStart() == mLayers.cbegin());
     }
 
     // TODO(b/181192467): Actually compute new LayerState vector and corresponding hash for each run
diff --git a/services/surfaceflinger/CompositionEngine/tests/planner/CachedSetTest.cpp b/services/surfaceflinger/CompositionEngine/tests/planner/CachedSetTest.cpp
index a39331c..488f64d 100644
--- a/services/surfaceflinger/CompositionEngine/tests/planner/CachedSetTest.cpp
+++ b/services/surfaceflinger/CompositionEngine/tests/planner/CachedSetTest.cpp
@@ -567,5 +567,30 @@
     }
 }
 
+TEST_F(CachedSetTest, hasBlurBehind) {
+    mTestLayers[1]->layerFECompositionState.backgroundBlurRadius = 1;
+    mTestLayers[1]->layerState->update(&mTestLayers[1]->outputLayer);
+    mTestLayers[2]->layerFECompositionState.blurRegions.push_back(
+            BlurRegion{1, 0, 0, 0, 0, 0, 0, 0, 0, 0});
+    mTestLayers[2]->layerState->update(&mTestLayers[2]->outputLayer);
+
+    CachedSet::Layer& layer1 = *mTestLayers[0]->cachedSetLayer.get();
+    CachedSet::Layer& layer2 = *mTestLayers[1]->cachedSetLayer.get();
+    CachedSet::Layer& layer3 = *mTestLayers[2]->cachedSetLayer.get();
+
+    CachedSet cachedSet1(layer1);
+    CachedSet cachedSet2(layer2);
+    CachedSet cachedSet3(layer3);
+
+    // Cached set 4 will consist of layers 1 and 2, which will contain a blur behind
+    CachedSet cachedSet4(layer1);
+    cachedSet4.addLayer(layer2.getState(), kStartTime);
+
+    EXPECT_FALSE(cachedSet1.hasBlurBehind());
+    EXPECT_TRUE(cachedSet2.hasBlurBehind());
+    EXPECT_TRUE(cachedSet3.hasBlurBehind());
+    EXPECT_TRUE(cachedSet4.hasBlurBehind());
+}
+
 } // namespace
 } // namespace android::compositionengine
diff --git a/services/surfaceflinger/CompositionEngine/tests/planner/FlattenerTest.cpp b/services/surfaceflinger/CompositionEngine/tests/planner/FlattenerTest.cpp
index 25fab49..42096f3 100644
--- a/services/surfaceflinger/CompositionEngine/tests/planner/FlattenerTest.cpp
+++ b/services/surfaceflinger/CompositionEngine/tests/planner/FlattenerTest.cpp
@@ -642,5 +642,149 @@
     EXPECT_EQ(&mTestLayers[2]->outputLayer, peekThroughLayer1);
     EXPECT_EQ(peekThroughLayer1, peekThroughLayer2);
 }
+
+TEST_F(FlattenerTest, flattenLayers_flattensBlurBehindRunIfFirstRun) {
+    auto& layerState1 = mTestLayers[0]->layerState;
+
+    auto& layerState2 = mTestLayers[1]->layerState;
+    mTestLayers[1]->layerFECompositionState.backgroundBlurRadius = 1;
+    layerState2->update(&mTestLayers[1]->outputLayer);
+
+    auto& layerState3 = mTestLayers[2]->layerState;
+    const auto& overrideBuffer1 = layerState1->getOutputLayer()->getState().overrideInfo.buffer;
+    const auto& overrideBuffer2 = layerState2->getOutputLayer()->getState().overrideInfo.buffer;
+    const auto& overrideBuffer3 = layerState3->getOutputLayer()->getState().overrideInfo.buffer;
+
+    const std::vector<const LayerState*> layers = {
+            layerState1.get(),
+            layerState2.get(),
+            layerState3.get(),
+    };
+
+    initializeFlattener(layers);
+
+    // Mark the first two layers inactive, which contain the blur behind
+    mTime += 200ms;
+    layerState3->resetFramesSinceBufferUpdate();
+
+    // layers would be flattened but the buffer would not be overridden
+    EXPECT_CALL(mRenderEngine, drawLayers(_, _, _, _, _, _)).WillOnce(Return(NO_ERROR));
+
+    initializeOverrideBuffer(layers);
+    EXPECT_EQ(getNonBufferHash(layers),
+              mFlattener->flattenLayers(layers, getNonBufferHash(layers), mTime));
+    mFlattener->renderCachedSets(mRenderEngine, mOutputState);
+
+    for (const auto layer : layers) {
+        EXPECT_EQ(nullptr, layer->getOutputLayer()->getState().overrideInfo.buffer);
+    }
+
+    // the new flattened layer is replaced
+    initializeOverrideBuffer(layers);
+    EXPECT_NE(getNonBufferHash(layers),
+              mFlattener->flattenLayers(layers, getNonBufferHash(layers), mTime));
+    mFlattener->renderCachedSets(mRenderEngine, mOutputState);
+    EXPECT_NE(nullptr, overrideBuffer1);
+    EXPECT_EQ(overrideBuffer1, overrideBuffer2);
+    EXPECT_EQ(nullptr, overrideBuffer3);
+}
+
+TEST_F(FlattenerTest, flattenLayers_doesNotFlattenBlurBehindRun) {
+    auto& layerState1 = mTestLayers[0]->layerState;
+
+    auto& layerState2 = mTestLayers[1]->layerState;
+    mTestLayers[1]->layerFECompositionState.backgroundBlurRadius = 1;
+    layerState2->update(&mTestLayers[1]->outputLayer);
+
+    auto& layerState3 = mTestLayers[2]->layerState;
+
+    const std::vector<const LayerState*> layers = {
+            layerState1.get(),
+            layerState2.get(),
+            layerState3.get(),
+    };
+
+    initializeFlattener(layers);
+
+    // Mark the last two layers inactive, which contains the blur layer, but does not contain the
+    // first layer
+    mTime += 200ms;
+    layerState1->resetFramesSinceBufferUpdate();
+
+    // layers would be flattened but the buffer would not be overridden
+    EXPECT_CALL(mRenderEngine, drawLayers(_, _, _, _, _, _)).WillRepeatedly(Return(NO_ERROR));
+
+    initializeOverrideBuffer(layers);
+    EXPECT_EQ(getNonBufferHash(layers),
+              mFlattener->flattenLayers(layers, getNonBufferHash(layers), mTime));
+    mFlattener->renderCachedSets(mRenderEngine, mOutputState);
+
+    for (const auto layer : layers) {
+        EXPECT_EQ(nullptr, layer->getOutputLayer()->getState().overrideInfo.buffer);
+    }
+
+    // nothing is flattened because the last two frames cannot be cached due to containing a blur
+    // layer
+    initializeOverrideBuffer(layers);
+    EXPECT_EQ(getNonBufferHash(layers),
+              mFlattener->flattenLayers(layers, getNonBufferHash(layers), mTime));
+    mFlattener->renderCachedSets(mRenderEngine, mOutputState);
+    for (const auto layer : layers) {
+        EXPECT_EQ(nullptr, layer->getOutputLayer()->getState().overrideInfo.buffer);
+    }
+}
+
+TEST_F(FlattenerTest, flattenLayers_flattenSkipsLayerWithBlurBehind) {
+    auto& layerState1 = mTestLayers[0]->layerState;
+
+    auto& layerStateWithBlurBehind = mTestLayers[1]->layerState;
+    mTestLayers[1]->layerFECompositionState.backgroundBlurRadius = 1;
+    layerStateWithBlurBehind->update(&mTestLayers[1]->outputLayer);
+
+    auto& layerState3 = mTestLayers[2]->layerState;
+    auto& layerState4 = mTestLayers[3]->layerState;
+    const auto& overrideBuffer1 = layerState1->getOutputLayer()->getState().overrideInfo.buffer;
+    const auto& blurOverrideBuffer =
+            layerStateWithBlurBehind->getOutputLayer()->getState().overrideInfo.buffer;
+    const auto& overrideBuffer3 = layerState3->getOutputLayer()->getState().overrideInfo.buffer;
+    const auto& overrideBuffer4 = layerState4->getOutputLayer()->getState().overrideInfo.buffer;
+
+    const std::vector<const LayerState*> layers = {
+            layerState1.get(),
+            layerStateWithBlurBehind.get(),
+            layerState3.get(),
+            layerState4.get(),
+    };
+
+    initializeFlattener(layers);
+
+    // Mark the last three layers inactive, which contains the blur layer, but does not contain the
+    // first layer
+    mTime += 200ms;
+    layerState1->resetFramesSinceBufferUpdate();
+
+    // layers would be flattened but the buffer would not be overridden
+    EXPECT_CALL(mRenderEngine, drawLayers(_, _, _, _, _, _)).WillOnce(Return(NO_ERROR));
+
+    initializeOverrideBuffer(layers);
+    EXPECT_EQ(getNonBufferHash(layers),
+              mFlattener->flattenLayers(layers, getNonBufferHash(layers), mTime));
+    mFlattener->renderCachedSets(mRenderEngine, mOutputState);
+
+    for (const auto layer : layers) {
+        EXPECT_EQ(nullptr, layer->getOutputLayer()->getState().overrideInfo.buffer);
+    }
+
+    // the new flattened layer is replaced
+    initializeOverrideBuffer(layers);
+    EXPECT_NE(getNonBufferHash(layers),
+              mFlattener->flattenLayers(layers, getNonBufferHash(layers), mTime));
+    mFlattener->renderCachedSets(mRenderEngine, mOutputState);
+    EXPECT_EQ(nullptr, overrideBuffer1);
+    EXPECT_EQ(nullptr, blurOverrideBuffer);
+    EXPECT_NE(nullptr, overrideBuffer3);
+    EXPECT_EQ(overrideBuffer3, overrideBuffer4);
+}
+
 } // namespace
 } // namespace android::compositionengine
diff --git a/services/surfaceflinger/CompositionEngine/tests/planner/LayerStateTest.cpp b/services/surfaceflinger/CompositionEngine/tests/planner/LayerStateTest.cpp
index 948c850..a09ce14 100644
--- a/services/surfaceflinger/CompositionEngine/tests/planner/LayerStateTest.cpp
+++ b/services/surfaceflinger/CompositionEngine/tests/planner/LayerStateTest.cpp
@@ -58,6 +58,10 @@
 const GenericLayerMetadataEntry sMetadataValueTwo = GenericLayerMetadataEntry{
         .value = std::vector<uint8_t>({1, 3}),
 };
+const constexpr int32_t sBgBlurRadiusOne = 3;
+const constexpr int32_t sBgBlurRadiusTwo = 4;
+const BlurRegion sBlurRegionOne = BlurRegion{1, 2.f, 3.f, 4.f, 5.f, 6.f, 7, 8, 9, 10};
+const BlurRegion sBlurRegionTwo = BlurRegion{2, 3.f, 4.f, 5.f, 6.f, 7.f, 8, 9, 10, 11};
 
 struct LayerStateTest : public testing::Test {
     LayerStateTest() {
@@ -830,6 +834,114 @@
     EXPECT_TRUE(otherLayerState->compare(*mLayerState));
 }
 
+TEST_F(LayerStateTest, updateBackgroundBlur) {
+    OutputLayerCompositionState outputLayerCompositionState;
+    LayerFECompositionState layerFECompositionState;
+    layerFECompositionState.backgroundBlurRadius = sBgBlurRadiusOne;
+    setupMocksForLayer(mOutputLayer, mLayerFE, outputLayerCompositionState,
+                       layerFECompositionState);
+    mLayerState = std::make_unique<LayerState>(&mOutputLayer);
+
+    mock::OutputLayer newOutputLayer;
+    mock::LayerFE newLayerFE;
+    LayerFECompositionState layerFECompositionStateTwo;
+    layerFECompositionStateTwo.backgroundBlurRadius = sBgBlurRadiusTwo;
+    setupMocksForLayer(newOutputLayer, newLayerFE, outputLayerCompositionState,
+                       layerFECompositionStateTwo);
+    Flags<LayerStateField> updates = mLayerState->update(&newOutputLayer);
+    EXPECT_EQ(Flags<LayerStateField>(LayerStateField::BackgroundBlurRadius), updates);
+}
+
+TEST_F(LayerStateTest, compareBackgroundBlur) {
+    OutputLayerCompositionState outputLayerCompositionState;
+    LayerFECompositionState layerFECompositionState;
+    layerFECompositionState.backgroundBlurRadius = sBgBlurRadiusOne;
+    setupMocksForLayer(mOutputLayer, mLayerFE, outputLayerCompositionState,
+                       layerFECompositionState);
+    mLayerState = std::make_unique<LayerState>(&mOutputLayer);
+    mock::OutputLayer newOutputLayer;
+    mock::LayerFE newLayerFE;
+    LayerFECompositionState layerFECompositionStateTwo;
+    layerFECompositionStateTwo.backgroundBlurRadius = sBgBlurRadiusTwo;
+    setupMocksForLayer(newOutputLayer, newLayerFE, outputLayerCompositionState,
+                       layerFECompositionStateTwo);
+    auto otherLayerState = std::make_unique<LayerState>(&newOutputLayer);
+
+    verifyNonUniqueDifferingFields(*mLayerState, *otherLayerState,
+                                   LayerStateField::BackgroundBlurRadius);
+
+    EXPECT_TRUE(mLayerState->compare(*otherLayerState));
+    EXPECT_TRUE(otherLayerState->compare(*mLayerState));
+}
+
+TEST_F(LayerStateTest, updateBlurRegions) {
+    OutputLayerCompositionState outputLayerCompositionState;
+    LayerFECompositionState layerFECompositionState;
+    layerFECompositionState.blurRegions.push_back(sBlurRegionOne);
+    setupMocksForLayer(mOutputLayer, mLayerFE, outputLayerCompositionState,
+                       layerFECompositionState);
+    mLayerState = std::make_unique<LayerState>(&mOutputLayer);
+
+    mock::OutputLayer newOutputLayer;
+    mock::LayerFE newLayerFE;
+    LayerFECompositionState layerFECompositionStateTwo;
+    layerFECompositionStateTwo.blurRegions.push_back(sBlurRegionTwo);
+    setupMocksForLayer(newOutputLayer, newLayerFE, outputLayerCompositionState,
+                       layerFECompositionStateTwo);
+    Flags<LayerStateField> updates = mLayerState->update(&newOutputLayer);
+    EXPECT_EQ(Flags<LayerStateField>(LayerStateField::BlurRegions), updates);
+}
+
+TEST_F(LayerStateTest, compareBlurRegions) {
+    OutputLayerCompositionState outputLayerCompositionState;
+    LayerFECompositionState layerFECompositionState;
+    layerFECompositionState.blurRegions.push_back(sBlurRegionOne);
+    setupMocksForLayer(mOutputLayer, mLayerFE, outputLayerCompositionState,
+                       layerFECompositionState);
+    mLayerState = std::make_unique<LayerState>(&mOutputLayer);
+    mock::OutputLayer newOutputLayer;
+    mock::LayerFE newLayerFE;
+    LayerFECompositionState layerFECompositionStateTwo;
+    layerFECompositionStateTwo.blurRegions.push_back(sBlurRegionTwo);
+    setupMocksForLayer(newOutputLayer, newLayerFE, outputLayerCompositionState,
+                       layerFECompositionStateTwo);
+    auto otherLayerState = std::make_unique<LayerState>(&newOutputLayer);
+
+    verifyNonUniqueDifferingFields(*mLayerState, *otherLayerState, LayerStateField::BlurRegions);
+
+    EXPECT_TRUE(mLayerState->compare(*otherLayerState));
+    EXPECT_TRUE(otherLayerState->compare(*mLayerState));
+}
+
+TEST_F(LayerStateTest, hasBlurBehind_noBlur_returnsFalse) {
+    OutputLayerCompositionState outputLayerCompositionState;
+    LayerFECompositionState layerFECompositionState;
+    setupMocksForLayer(mOutputLayer, mLayerFE, outputLayerCompositionState,
+                       layerFECompositionState);
+    mLayerState = std::make_unique<LayerState>(&mOutputLayer);
+    EXPECT_FALSE(mLayerState->hasBlurBehind());
+}
+
+TEST_F(LayerStateTest, hasBlurBehind_withBackgroundBlur_returnsTrue) {
+    OutputLayerCompositionState outputLayerCompositionState;
+    LayerFECompositionState layerFECompositionState;
+    layerFECompositionState.backgroundBlurRadius = sBgBlurRadiusOne;
+    setupMocksForLayer(mOutputLayer, mLayerFE, outputLayerCompositionState,
+                       layerFECompositionState);
+    mLayerState = std::make_unique<LayerState>(&mOutputLayer);
+    EXPECT_TRUE(mLayerState->hasBlurBehind());
+}
+
+TEST_F(LayerStateTest, hasBlurBehind_withBlurRegion_returnsTrue) {
+    OutputLayerCompositionState outputLayerCompositionState;
+    LayerFECompositionState layerFECompositionState;
+    layerFECompositionState.blurRegions.push_back(sBlurRegionOne);
+    setupMocksForLayer(mOutputLayer, mLayerFE, outputLayerCompositionState,
+                       layerFECompositionState);
+    mLayerState = std::make_unique<LayerState>(&mOutputLayer);
+    EXPECT_TRUE(mLayerState->hasBlurBehind());
+}
+
 TEST_F(LayerStateTest, dumpDoesNotCrash) {
     OutputLayerCompositionState outputLayerCompositionState;
     LayerFECompositionState layerFECompositionState;