Support skipping CachedSet rendering based on invalidate() proximity

Add tunable sysprops which allow for skipping rendering a new
CachedSet if we're too close to an invalidate(), so that rendering the
CachedSet does not cause jank.

The sysprops control:
1. Whether we support this type of deferred rendering
2. How close to a rendering deadline we're allowed to be
3. Max number of times we can skip before we give up and render anyway.

Bug: 188678587
Test: builds, boots
Test: perfetto
Change-Id: I9801c25da1d9178faa8fa28b9bd8f9d180db7b11
diff --git a/services/surfaceflinger/CompositionEngine/include/compositionengine/CompositionRefreshArgs.h b/services/surfaceflinger/CompositionEngine/include/compositionengine/CompositionRefreshArgs.h
index 289cb11..29937fb 100644
--- a/services/surfaceflinger/CompositionEngine/include/compositionengine/CompositionRefreshArgs.h
+++ b/services/surfaceflinger/CompositionEngine/include/compositionengine/CompositionRefreshArgs.h
@@ -82,6 +82,9 @@
 
     // The earliest time to send the present command to the HAL
     std::chrono::steady_clock::time_point earliestPresentTime;
+
+    // The predicted next invalidation time
+    std::optional<std::chrono::steady_clock::time_point> nextInvalidateTime;
 };
 
 } // namespace android::compositionengine
diff --git a/services/surfaceflinger/CompositionEngine/include/compositionengine/Output.h b/services/surfaceflinger/CompositionEngine/include/compositionengine/Output.h
index 257974f..1416b1e 100644
--- a/services/surfaceflinger/CompositionEngine/include/compositionengine/Output.h
+++ b/services/surfaceflinger/CompositionEngine/include/compositionengine/Output.h
@@ -286,7 +286,7 @@
     virtual std::optional<base::unique_fd> composeSurfaces(
             const Region&, const compositionengine::CompositionRefreshArgs& refreshArgs) = 0;
     virtual void postFramebuffer() = 0;
-    virtual void renderCachedSets() = 0;
+    virtual void renderCachedSets(const CompositionRefreshArgs&) = 0;
     virtual void chooseCompositionStrategy() = 0;
     virtual bool getSkipColorTransform() const = 0;
     virtual FrameFences presentAndGetFrameFences() = 0;
diff --git a/services/surfaceflinger/CompositionEngine/include/compositionengine/impl/Output.h b/services/surfaceflinger/CompositionEngine/include/compositionengine/impl/Output.h
index f10ff25..f832084 100644
--- a/services/surfaceflinger/CompositionEngine/include/compositionengine/impl/Output.h
+++ b/services/surfaceflinger/CompositionEngine/include/compositionengine/impl/Output.h
@@ -93,7 +93,7 @@
     std::optional<base::unique_fd> composeSurfaces(
             const Region&, const compositionengine::CompositionRefreshArgs& refreshArgs) override;
     void postFramebuffer() override;
-    void renderCachedSets() override;
+    void renderCachedSets(const CompositionRefreshArgs&) override;
     void cacheClientCompositionRequests(uint32_t) override;
 
     // Testing
diff --git a/services/surfaceflinger/CompositionEngine/include/compositionengine/impl/planner/CachedSet.h b/services/surfaceflinger/CompositionEngine/include/compositionengine/impl/planner/CachedSet.h
index a4356c5..7cb0f6b 100644
--- a/services/surfaceflinger/CompositionEngine/include/compositionengine/impl/planner/CachedSet.h
+++ b/services/surfaceflinger/CompositionEngine/include/compositionengine/impl/planner/CachedSet.h
@@ -98,6 +98,7 @@
         mDrawFence = nullptr;
         mBlurLayer = nullptr;
         mHolePunchLayer = nullptr;
+        mSkipCount = 0;
 
         mLayers.insert(mLayers.end(), other.mLayers.cbegin(), other.mLayers.cend());
         Region boundingRegion;
@@ -107,6 +108,8 @@
         mVisibleRegion.orSelf(other.mVisibleRegion);
     }
     void incrementAge() { ++mAge; }
+    void incrementSkipCount() { mSkipCount++; }
+    size_t getSkipCount() { return mSkipCount; }
 
     // Renders the cached set with the supplied output composition state.
     void render(renderengine::RenderEngine& re, TexturePool& texturePool,
@@ -155,6 +158,7 @@
     Rect mBounds = Rect::EMPTY_RECT;
     Region mVisibleRegion;
     size_t mAge = 0;
+    size_t mSkipCount = 0;
 
     // TODO(b/190411067): This is a shared pointer only because CachedSets are copied into different
     // containers in the Flattener. Logically this should have unique ownership otherwise.
diff --git a/services/surfaceflinger/CompositionEngine/include/compositionengine/impl/planner/Flattener.h b/services/surfaceflinger/CompositionEngine/include/compositionengine/impl/planner/Flattener.h
index 94a169e..7534548 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 <chrono>
 #include <numeric>
 #include <vector>
 
@@ -37,7 +38,35 @@
 
 class Flattener {
 public:
-    Flattener(renderengine::RenderEngine& renderEngine, bool enableHolePunch = false);
+    struct CachedSetRenderSchedulingTunables {
+        // This default assumes that rendering a cached set takes about 3ms. That time is then cut
+        // in half - the next frame using the cached set would have the same workload, meaning that
+        // composition cost is the same. This is best illustrated with the following example:
+        //
+        // Suppose we're at a 120hz cadence so SurfaceFlinger is budgeted 8.3ms per-frame. If
+        // renderCachedSets costs 3ms, then two consecutive frames have timings:
+        //
+        // First frame: Start at 0ms, end at 6.8ms.
+        // renderCachedSets: Start at 6.8ms, end at 9.8ms.
+        // Second frame: Start at 9.8ms, end at 16.6ms.
+        //
+        // Now the second frame won't render a cached set afterwards, but the first frame didn't
+        // really steal time from the second frame.
+        static const constexpr std::chrono::nanoseconds kDefaultCachedSetRenderDuration = 1500us;
+
+        static const constexpr size_t kDefaultMaxDeferRenderAttempts = 240;
+
+        // Duration allocated for rendering a cached set. If we don't have enough time for rendering
+        // a cached set, then rendering is deferred to another frame.
+        const std::chrono::nanoseconds cachedSetRenderDuration;
+        // Maximum of times that we defer rendering a cached set. If we defer rendering a cached set
+        // too many times, then render it anyways so that future frames would benefit from the
+        // flattened cached set.
+        const size_t maxDeferRenderAttempts;
+    };
+    Flattener(renderengine::RenderEngine& renderEngine, bool enableHolePunch = false,
+              std::optional<CachedSetRenderSchedulingTunables> cachedSetRenderSchedulingTunables =
+                      std::nullopt);
 
     void setDisplaySize(ui::Size size) {
         mDisplaySize = size;
@@ -48,16 +77,14 @@
                                 std::chrono::steady_clock::time_point now);
 
     // Renders the newest cached sets with the supplied output composition state
-    void renderCachedSets(const OutputCompositionState& outputState);
+    void renderCachedSets(const OutputCompositionState& outputState,
+                          std::optional<std::chrono::steady_clock::time_point> renderDeadline);
 
     void dump(std::string& result) const;
     void dumpLayers(std::string& result) const;
 
     const std::optional<CachedSet>& getNewCachedSetForTesting() const { return mNewCachedSet; }
 
-protected:
-    std::optional<CachedSet> mNewCachedSet;
-
 private:
     size_t calculateDisplayCost(const std::vector<const LayerState*>& layers) const;
 
@@ -149,9 +176,15 @@
 
     renderengine::RenderEngine& mRenderEngine;
     const bool mEnableHolePunch;
+    const std::optional<CachedSetRenderSchedulingTunables> mCachedSetRenderSchedulingTunables;
 
     TexturePool mTexturePool;
 
+protected:
+    // mNewCachedSet must be destroyed before mTexturePool is.
+    std::optional<CachedSet> mNewCachedSet;
+
+private:
     ui::Size mDisplaySize;
 
     NonBufferHash mCurrentGeometry;
diff --git a/services/surfaceflinger/CompositionEngine/include/compositionengine/impl/planner/Planner.h b/services/surfaceflinger/CompositionEngine/include/compositionengine/impl/planner/Planner.h
index fd1ddfc..be34153 100644
--- a/services/surfaceflinger/CompositionEngine/include/compositionengine/impl/planner/Planner.h
+++ b/services/surfaceflinger/CompositionEngine/include/compositionengine/impl/planner/Planner.h
@@ -58,8 +58,11 @@
     void reportFinalPlan(
             compositionengine::Output::OutputLayersEnumerator<compositionengine::Output>&& layers);
 
-    // The planner will call to the Flattener to render any pending cached set
-    void renderCachedSets(const OutputCompositionState& outputState);
+    // The planner will call to the Flattener to render any pending cached set.
+    // Rendering a pending cached set is optional: if the renderDeadline is not far enough in the
+    // future then the planner may opt to skip rendering the cached set.
+    void renderCachedSets(const OutputCompositionState& outputState,
+                          std::optional<std::chrono::steady_clock::time_point> renderDeadline);
 
     void dump(const Vector<String16>& args, std::string&);
 
diff --git a/services/surfaceflinger/CompositionEngine/include/compositionengine/mock/Output.h b/services/surfaceflinger/CompositionEngine/include/compositionengine/mock/Output.h
index 4b4d375..8e777e3 100644
--- a/services/surfaceflinger/CompositionEngine/include/compositionengine/mock/Output.h
+++ b/services/surfaceflinger/CompositionEngine/include/compositionengine/mock/Output.h
@@ -109,7 +109,7 @@
     MOCK_CONST_METHOD0(getSkipColorTransform, bool());
 
     MOCK_METHOD0(postFramebuffer, void());
-    MOCK_METHOD0(renderCachedSets, void());
+    MOCK_METHOD1(renderCachedSets, void(const CompositionRefreshArgs&));
     MOCK_METHOD0(presentAndGetFrameFences, compositionengine::Output::FrameFences());
 
     MOCK_METHOD3(generateClientCompositionRequests,
diff --git a/services/surfaceflinger/CompositionEngine/src/Output.cpp b/services/surfaceflinger/CompositionEngine/src/Output.cpp
index cd2f742..67bb149 100644
--- a/services/surfaceflinger/CompositionEngine/src/Output.cpp
+++ b/services/surfaceflinger/CompositionEngine/src/Output.cpp
@@ -434,7 +434,7 @@
     devOptRepaintFlash(refreshArgs);
     finishFrame(refreshArgs);
     postFramebuffer();
-    renderCachedSets();
+    renderCachedSets(refreshArgs);
 }
 
 void Output::rebuildLayerStacks(const compositionengine::CompositionRefreshArgs& refreshArgs,
@@ -1312,9 +1312,9 @@
     mReleasedLayers.clear();
 }
 
-void Output::renderCachedSets() {
+void Output::renderCachedSets(const CompositionRefreshArgs& refreshArgs) {
     if (mPlanner) {
-        mPlanner->renderCachedSets(getState());
+        mPlanner->renderCachedSets(getState(), refreshArgs.nextInvalidateTime);
     }
 }
 
diff --git a/services/surfaceflinger/CompositionEngine/src/planner/CachedSet.cpp b/services/surfaceflinger/CompositionEngine/src/planner/CachedSet.cpp
index 69e8c7d..c1d525b 100644
--- a/services/surfaceflinger/CompositionEngine/src/planner/CachedSet.cpp
+++ b/services/surfaceflinger/CompositionEngine/src/planner/CachedSet.cpp
@@ -277,6 +277,7 @@
         mOutputSpace.orientation = outputState.framebufferSpace.orientation;
         mOutputDataspace = outputDataspace;
         mOrientation = orientation;
+        mSkipCount = 0;
     } else {
         mTexture.reset();
     }
diff --git a/services/surfaceflinger/CompositionEngine/src/planner/Flattener.cpp b/services/surfaceflinger/CompositionEngine/src/planner/Flattener.cpp
index 192c411..2bcaf60 100644
--- a/services/surfaceflinger/CompositionEngine/src/planner/Flattener.cpp
+++ b/services/surfaceflinger/CompositionEngine/src/planner/Flattener.cpp
@@ -23,7 +23,7 @@
 #include <compositionengine/impl/planner/Flattener.h>
 #include <compositionengine/impl/planner/LayerState.h>
 
-#include <utils/Trace.h>
+#include <gui/TraceUtils.h>
 
 using time_point = std::chrono::steady_clock::time_point;
 using namespace std::chrono_literals;
@@ -60,9 +60,12 @@
 
 } // namespace
 
-Flattener::Flattener(renderengine::RenderEngine& renderEngine, bool enableHolePunch)
+Flattener::Flattener(
+        renderengine::RenderEngine& renderEngine, bool enableHolePunch,
+        std::optional<CachedSetRenderSchedulingTunables> cachedSetRenderSchedulingTunables)
       : mRenderEngine(renderEngine),
         mEnableHolePunch(enableHolePunch),
+        mCachedSetRenderSchedulingTunables(cachedSetRenderSchedulingTunables),
         mTexturePool(mRenderEngine) {
     const int timeoutInMs =
             base::GetIntProperty(std::string("debug.sf.layer_caching_active_layer_timeout_ms"), 0);
@@ -105,12 +108,45 @@
     return hash;
 }
 
-void Flattener::renderCachedSets(const OutputCompositionState& outputState) {
+void Flattener::renderCachedSets(
+        const OutputCompositionState& outputState,
+        std::optional<std::chrono::steady_clock::time_point> renderDeadline) {
     ATRACE_CALL();
-    if (!mNewCachedSet || mNewCachedSet->hasRenderedBuffer()) {
+
+    if (!mNewCachedSet) {
         return;
     }
 
+    // Ensure that a cached set has a valid buffer first
+    if (mNewCachedSet->hasRenderedBuffer()) {
+        ATRACE_NAME("mNewCachedSet->hasRenderedBuffer()");
+        return;
+    }
+
+    const auto now = std::chrono::steady_clock::now();
+
+    // If we have a render deadline, and the flattener is configured to skip rendering if we don't
+    // have enough time, then we skip rendering the cached set if we think that we'll steal too much
+    // time from the next frame.
+    if (renderDeadline && mCachedSetRenderSchedulingTunables) {
+        if (const auto estimatedRenderFinish =
+                    now + mCachedSetRenderSchedulingTunables->cachedSetRenderDuration;
+            estimatedRenderFinish > *renderDeadline) {
+            mNewCachedSet->incrementSkipCount();
+
+            if (mNewCachedSet->getSkipCount() <=
+                mCachedSetRenderSchedulingTunables->maxDeferRenderAttempts) {
+                ATRACE_FORMAT("DeadlinePassed: exceeded deadline by: %d us",
+                              std::chrono::duration_cast<std::chrono::microseconds>(
+                                      estimatedRenderFinish - *renderDeadline)
+                                      .count());
+                return;
+            } else {
+                ATRACE_NAME("DeadlinePassed: exceeded max skips");
+            }
+        }
+    }
+
     mNewCachedSet->render(mRenderEngine, mTexturePool, outputState);
 }
 
diff --git a/services/surfaceflinger/CompositionEngine/src/planner/Planner.cpp b/services/surfaceflinger/CompositionEngine/src/planner/Planner.cpp
index 711a634..be2510f 100644
--- a/services/surfaceflinger/CompositionEngine/src/planner/Planner.cpp
+++ b/services/surfaceflinger/CompositionEngine/src/planner/Planner.cpp
@@ -26,16 +26,43 @@
 #include <compositionengine/impl/planner/Planner.h>
 
 #include <utils/Trace.h>
+#include <chrono>
 
 namespace android::compositionengine::impl::planner {
 
+namespace {
+
+std::optional<Flattener::CachedSetRenderSchedulingTunables> buildFlattenerTuneables() {
+    if (!base::GetBoolProperty(std::string("debug.sf.enable_cached_set_render_scheduling"), true)) {
+        return std::nullopt;
+    }
+
+    auto renderDuration = std::chrono::nanoseconds(
+            base::GetUintProperty<uint64_t>(std::string("debug.sf.cached_set_render_duration_ns"),
+                                            Flattener::CachedSetRenderSchedulingTunables::
+                                                    kDefaultCachedSetRenderDuration.count()));
+
+    auto maxDeferRenderAttempts = base::GetUintProperty<
+            size_t>(std::string("debug.sf.cached_set_max_defer_render_attmpts"),
+                    Flattener::CachedSetRenderSchedulingTunables::kDefaultMaxDeferRenderAttempts);
+
+    return std::make_optional<Flattener::CachedSetRenderSchedulingTunables>(
+            Flattener::CachedSetRenderSchedulingTunables{
+                    .cachedSetRenderDuration = renderDuration,
+                    .maxDeferRenderAttempts = maxDeferRenderAttempts,
+            });
+}
+
+} // namespace
+
 Planner::Planner(renderengine::RenderEngine& renderEngine)
       // Implicitly, layer caching must also be enabled for the hole punch or
       // predictor to have any effect.
       // E.g., setprop debug.sf.enable_layer_caching 1, or
       // adb shell service call SurfaceFlinger 1040 i32 1 [i64 <display ID>]
       : mFlattener(renderEngine,
-                   base::GetBoolProperty(std::string("debug.sf.enable_hole_punch_pip"), true)) {
+                   base::GetBoolProperty(std::string("debug.sf.enable_hole_punch_pip"), true),
+                   buildFlattenerTuneables()) {
     mPredictorEnabled =
             base::GetBoolProperty(std::string("debug.sf.enable_planner_prediction"), false);
 }
@@ -161,9 +188,11 @@
                             finalPlan);
 }
 
-void Planner::renderCachedSets(const OutputCompositionState& outputState) {
+void Planner::renderCachedSets(
+        const OutputCompositionState& outputState,
+        std::optional<std::chrono::steady_clock::time_point> renderDeadline) {
     ATRACE_CALL();
-    mFlattener.renderCachedSets(outputState);
+    mFlattener.renderCachedSets(outputState, renderDeadline);
 }
 
 void Planner::dump(const Vector<String16>& args, std::string& result) {
diff --git a/services/surfaceflinger/CompositionEngine/tests/OutputTest.cpp b/services/surfaceflinger/CompositionEngine/tests/OutputTest.cpp
index c381081..742b155 100644
--- a/services/surfaceflinger/CompositionEngine/tests/OutputTest.cpp
+++ b/services/surfaceflinger/CompositionEngine/tests/OutputTest.cpp
@@ -1767,7 +1767,7 @@
         MOCK_METHOD1(devOptRepaintFlash, void(const compositionengine::CompositionRefreshArgs&));
         MOCK_METHOD1(finishFrame, void(const compositionengine::CompositionRefreshArgs&));
         MOCK_METHOD0(postFramebuffer, void());
-        MOCK_METHOD0(renderCachedSets, void());
+        MOCK_METHOD1(renderCachedSets, void(const compositionengine::CompositionRefreshArgs&));
     };
 
     StrictMock<OutputPartialMock> mOutput;
@@ -1787,7 +1787,7 @@
     EXPECT_CALL(mOutput, devOptRepaintFlash(Ref(args)));
     EXPECT_CALL(mOutput, finishFrame(Ref(args)));
     EXPECT_CALL(mOutput, postFramebuffer());
-    EXPECT_CALL(mOutput, renderCachedSets());
+    EXPECT_CALL(mOutput, renderCachedSets(Ref(args)));
 
     mOutput.present(args);
 }
diff --git a/services/surfaceflinger/CompositionEngine/tests/planner/CachedSetTest.cpp b/services/surfaceflinger/CompositionEngine/tests/planner/CachedSetTest.cpp
index b15e4f3..449ae21 100644
--- a/services/surfaceflinger/CompositionEngine/tests/planner/CachedSetTest.cpp
+++ b/services/surfaceflinger/CompositionEngine/tests/planner/CachedSetTest.cpp
@@ -223,6 +223,16 @@
     EXPECT_EQ(2u, cachedSet.getAge());
 }
 
+TEST_F(CachedSetTest, incrementSkipCount) {
+    CachedSet::Layer& layer = *mTestLayers[0]->cachedSetLayer.get();
+    CachedSet cachedSet(layer);
+    EXPECT_EQ(0u, cachedSet.getSkipCount());
+    cachedSet.incrementSkipCount();
+    EXPECT_EQ(1u, cachedSet.getSkipCount());
+    cachedSet.incrementSkipCount();
+    EXPECT_EQ(2u, cachedSet.getSkipCount());
+}
+
 TEST_F(CachedSetTest, hasBufferUpdate_NoUpdate) {
     CachedSet::Layer& layer1 = *mTestLayers[0]->cachedSetLayer.get();
     CachedSet::Layer& layer2 = *mTestLayers[1]->cachedSetLayer.get();
@@ -257,6 +267,8 @@
     CachedSet cachedSet1(layer1);
     CachedSet cachedSet2(layer2);
     cachedSet1.addLayer(layer3.getState(), kStartTime + 10ms);
+    cachedSet1.incrementSkipCount();
+    EXPECT_EQ(1u, cachedSet1.getSkipCount());
     cachedSet1.append(cachedSet2);
 
     EXPECT_EQ(kStartTime, cachedSet1.getLastUpdate());
@@ -268,6 +280,8 @@
     EXPECT_TRUE(cachedSet1.getVisibleRegion().hasSameRects(expectedRegion));
     EXPECT_EQ(3u, cachedSet1.getLayerCount());
     EXPECT_EQ(0u, cachedSet1.getAge());
+    EXPECT_EQ(0u, cachedSet1.getSkipCount());
+
     expectNoBuffer(cachedSet1);
     // TODO(b/181192080): check that getNonBufferHash returns the correct hash value
     // EXPECT_EQ(android::hashCombine(layer1.getHash(), layer2.getHash()),
diff --git a/services/surfaceflinger/CompositionEngine/tests/planner/FlattenerTest.cpp b/services/surfaceflinger/CompositionEngine/tests/planner/FlattenerTest.cpp
index e176c98..334b855 100644
--- a/services/surfaceflinger/CompositionEngine/tests/planner/FlattenerTest.cpp
+++ b/services/surfaceflinger/CompositionEngine/tests/planner/FlattenerTest.cpp
@@ -24,6 +24,7 @@
 #include <renderengine/ExternalTexture.h>
 #include <renderengine/LayerSettings.h>
 #include <renderengine/mock/RenderEngine.h>
+#include <chrono>
 
 namespace android::compositionengine {
 using namespace std::chrono_literals;
@@ -46,22 +47,28 @@
 
 class TestableFlattener : public Flattener {
 public:
-    TestableFlattener(renderengine::RenderEngine& renderEngine, bool enableHolePunch)
-          : Flattener(renderEngine, enableHolePunch) {}
+    TestableFlattener(renderengine::RenderEngine& renderEngine, bool enableHolePunch,
+                      std::optional<Flattener::CachedSetRenderSchedulingTunables>
+                              cachedSetRenderSchedulingTunables = std::nullopt)
+          : Flattener(renderEngine, enableHolePunch, cachedSetRenderSchedulingTunables) {}
     const std::optional<CachedSet>& getNewCachedSetForTesting() const { return mNewCachedSet; }
 };
 
 class FlattenerTest : public testing::Test {
 public:
-    FlattenerTest() : mFlattener(std::make_unique<TestableFlattener>(mRenderEngine, true)) {}
+    FlattenerTest() : FlattenerTest(std::nullopt) {}
     void SetUp() override;
 
 protected:
+    FlattenerTest(std::optional<Flattener::CachedSetRenderSchedulingTunables>
+                          cachedSetRenderSchedulingTunables)
+          : mFlattener(std::make_unique<TestableFlattener>(mRenderEngine, true,
+                                                           cachedSetRenderSchedulingTunables)) {}
     void initializeOverrideBuffer(const std::vector<const LayerState*>& layers);
     void initializeFlattener(const std::vector<const LayerState*>& layers);
     void expectAllLayersFlattened(const std::vector<const LayerState*>& layers);
 
-    // mRenderEngine is held as a reference in mFlattener, so mFlattener must be destroyed first.
+    // mRenderEngine is held as a reference in mFlattener, so explicitly destroy mFlattener first.
     renderengine::mock::RenderEngine mRenderEngine;
     std::unique_ptr<TestableFlattener> mFlattener;
 
@@ -148,13 +155,13 @@
     initializeOverrideBuffer(layers);
     EXPECT_EQ(getNonBufferHash(layers),
               mFlattener->flattenLayers(layers, getNonBufferHash(layers), mTime));
-    mFlattener->renderCachedSets(mOutputState);
+    mFlattener->renderCachedSets(mOutputState, std::nullopt);
 
     // same geometry, update the internal layer stack
     initializeOverrideBuffer(layers);
     EXPECT_EQ(getNonBufferHash(layers),
               mFlattener->flattenLayers(layers, getNonBufferHash(layers), mTime));
-    mFlattener->renderCachedSets(mOutputState);
+    mFlattener->renderCachedSets(mOutputState, std::nullopt);
 }
 
 void FlattenerTest::expectAllLayersFlattened(const std::vector<const LayerState*>& layers) {
@@ -164,7 +171,7 @@
     initializeOverrideBuffer(layers);
     EXPECT_EQ(getNonBufferHash(layers),
               mFlattener->flattenLayers(layers, getNonBufferHash(layers), mTime));
-    mFlattener->renderCachedSets(mOutputState);
+    mFlattener->renderCachedSets(mOutputState, std::nullopt);
 
     for (const auto layer : layers) {
         EXPECT_EQ(nullptr, layer->getOutputLayer()->getState().overrideInfo.buffer);
@@ -174,7 +181,7 @@
     initializeOverrideBuffer(layers);
     EXPECT_NE(getNonBufferHash(layers),
               mFlattener->flattenLayers(layers, getNonBufferHash(layers), mTime));
-    mFlattener->renderCachedSets(mOutputState);
+    mFlattener->renderCachedSets(mOutputState, std::nullopt);
 
     const auto buffer = layers[0]->getOutputLayer()->getState().overrideInfo.buffer;
     EXPECT_NE(nullptr, buffer);
@@ -209,7 +216,7 @@
     initializeOverrideBuffer(layers);
     EXPECT_EQ(getNonBufferHash(layers),
               mFlattener->flattenLayers(layers, getNonBufferHash(layers), mTime));
-    mFlattener->renderCachedSets(mOutputState);
+    mFlattener->renderCachedSets(mOutputState, std::nullopt);
 }
 
 TEST_F(FlattenerTest, flattenLayers_basicFlatten) {
@@ -255,7 +262,7 @@
     initializeOverrideBuffer(layers);
     EXPECT_NE(getNonBufferHash(layers),
               mFlattener->flattenLayers(layers, getNonBufferHash(layers), mTime));
-    mFlattener->renderCachedSets(mOutputState);
+    mFlattener->renderCachedSets(mOutputState, std::nullopt);
 
     EXPECT_NE(nullptr, overrideBuffer1);
     EXPECT_EQ(overrideBuffer1, overrideBuffer2);
@@ -360,7 +367,7 @@
     initializeOverrideBuffer(layers);
     EXPECT_EQ(getNonBufferHash(layers),
               mFlattener->flattenLayers(layers, getNonBufferHash(layers), mTime));
-    mFlattener->renderCachedSets(mOutputState);
+    mFlattener->renderCachedSets(mOutputState, std::nullopt);
 
     EXPECT_EQ(nullptr, overrideBuffer1);
     EXPECT_EQ(nullptr, overrideBuffer2);
@@ -397,7 +404,7 @@
     initializeOverrideBuffer(layers);
     EXPECT_EQ(getNonBufferHash(layers),
               mFlattener->flattenLayers(layers, getNonBufferHash(layers), mTime));
-    mFlattener->renderCachedSets(mOutputState);
+    mFlattener->renderCachedSets(mOutputState, std::nullopt);
 
     EXPECT_EQ(nullptr, overrideBuffer1);
     EXPECT_EQ(nullptr, overrideBuffer2);
@@ -406,7 +413,7 @@
     initializeOverrideBuffer(layers);
     EXPECT_NE(getNonBufferHash(layers),
               mFlattener->flattenLayers(layers, getNonBufferHash(layers), mTime));
-    mFlattener->renderCachedSets(mOutputState);
+    mFlattener->renderCachedSets(mOutputState, std::nullopt);
 
     EXPECT_EQ(nullptr, overrideBuffer1);
     EXPECT_NE(nullptr, overrideBuffer2);
@@ -419,7 +426,7 @@
     initializeOverrideBuffer(layers);
     EXPECT_NE(getNonBufferHash(layers),
               mFlattener->flattenLayers(layers, getNonBufferHash(layers), mTime));
-    mFlattener->renderCachedSets(mOutputState);
+    mFlattener->renderCachedSets(mOutputState, std::nullopt);
 
     EXPECT_EQ(nullptr, overrideBuffer1);
     EXPECT_NE(nullptr, overrideBuffer2);
@@ -428,7 +435,7 @@
     initializeOverrideBuffer(layers);
     EXPECT_NE(getNonBufferHash(layers),
               mFlattener->flattenLayers(layers, getNonBufferHash(layers), mTime));
-    mFlattener->renderCachedSets(mOutputState);
+    mFlattener->renderCachedSets(mOutputState, std::nullopt);
 
     EXPECT_NE(nullptr, overrideBuffer1);
     EXPECT_EQ(overrideBuffer1, overrideBuffer2);
@@ -470,7 +477,7 @@
     initializeOverrideBuffer(layers);
     EXPECT_EQ(getNonBufferHash(layers),
               mFlattener->flattenLayers(layers, getNonBufferHash(layers), mTime));
-    mFlattener->renderCachedSets(mOutputState);
+    mFlattener->renderCachedSets(mOutputState, std::nullopt);
 
     EXPECT_EQ(nullptr, overrideBuffer1);
     EXPECT_EQ(nullptr, overrideBuffer2);
@@ -484,7 +491,7 @@
     EXPECT_NE(getNonBufferHash(layers),
               mFlattener->flattenLayers(layers, getNonBufferHash(layers), mTime));
     mOutputState.framebufferSpace.orientation = ui::ROTATION_90;
-    mFlattener->renderCachedSets(mOutputState);
+    mFlattener->renderCachedSets(mOutputState, std::nullopt);
 
     EXPECT_NE(nullptr, overrideBuffer1);
     EXPECT_EQ(overrideBuffer1, overrideBuffer2);
@@ -497,7 +504,7 @@
     EXPECT_NE(getNonBufferHash(layers),
               mFlattener->flattenLayers(layers, getNonBufferHash(layers), mTime));
     mOutputState.framebufferSpace.orientation = ui::ROTATION_180;
-    mFlattener->renderCachedSets(mOutputState);
+    mFlattener->renderCachedSets(mOutputState, std::nullopt);
 
     EXPECT_NE(nullptr, overrideBuffer1);
     EXPECT_EQ(overrideBuffer1, overrideBuffer2);
@@ -512,7 +519,7 @@
     initializeOverrideBuffer(layers);
     EXPECT_NE(getNonBufferHash(layers),
               mFlattener->flattenLayers(layers, getNonBufferHash(layers), mTime));
-    mFlattener->renderCachedSets(mOutputState);
+    mFlattener->renderCachedSets(mOutputState, std::nullopt);
 
     EXPECT_NE(nullptr, overrideBuffer1);
     EXPECT_EQ(overrideBuffer1, overrideBuffer2);
@@ -524,7 +531,7 @@
     EXPECT_NE(getNonBufferHash(layers),
               mFlattener->flattenLayers(layers, getNonBufferHash(layers), mTime));
     mOutputState.framebufferSpace.orientation = ui::ROTATION_270;
-    mFlattener->renderCachedSets(mOutputState);
+    mFlattener->renderCachedSets(mOutputState, std::nullopt);
 
     EXPECT_NE(nullptr, overrideBuffer1);
     EXPECT_EQ(overrideBuffer1, overrideBuffer2);
@@ -563,7 +570,7 @@
 
     // This will render a CachedSet.
     EXPECT_CALL(mRenderEngine, drawLayers(_, _, _, _, _, _)).WillOnce(Return(NO_ERROR));
-    mFlattener->renderCachedSets(mOutputState);
+    mFlattener->renderCachedSets(mOutputState, std::nullopt);
 
     // We've rendered a CachedSet, but we haven't merged it in.
     EXPECT_EQ(nullptr, overrideBuffer1);
@@ -576,7 +583,7 @@
     initializeOverrideBuffer(layers);
     EXPECT_NE(getNonBufferHash(layers),
               mFlattener->flattenLayers(layers, getNonBufferHash(layers), mTime));
-    mFlattener->renderCachedSets(mOutputState);
+    mFlattener->renderCachedSets(mOutputState, std::nullopt);
 
     EXPECT_NE(nullptr, overrideBuffer1);
     EXPECT_EQ(overrideBuffer1, overrideBuffer2);
@@ -625,7 +632,7 @@
 
     // This will render a CachedSet.
     EXPECT_CALL(mRenderEngine, drawLayers(_, _, _, _, _, _)).WillOnce(Return(NO_ERROR));
-    mFlattener->renderCachedSets(mOutputState);
+    mFlattener->renderCachedSets(mOutputState, std::nullopt);
 
     // We've rendered a CachedSet, but we haven't merged it in.
     EXPECT_EQ(nullptr, overrideBuffer1);
@@ -638,7 +645,7 @@
     initializeOverrideBuffer(layers);
     EXPECT_NE(getNonBufferHash(layers),
               mFlattener->flattenLayers(layers, getNonBufferHash(layers), mTime));
-    mFlattener->renderCachedSets(mOutputState);
+    mFlattener->renderCachedSets(mOutputState, std::nullopt);
 
     EXPECT_NE(nullptr, overrideBuffer1);
     EXPECT_EQ(overrideBuffer1, overrideBuffer2);
@@ -682,7 +689,7 @@
     initializeOverrideBuffer(layers);
     EXPECT_EQ(getNonBufferHash(layers),
               mFlattener->flattenLayers(layers, getNonBufferHash(layers), mTime));
-    mFlattener->renderCachedSets(mOutputState);
+    mFlattener->renderCachedSets(mOutputState, std::nullopt);
 
     for (const auto layer : layers) {
         EXPECT_EQ(nullptr, layer->getOutputLayer()->getState().overrideInfo.buffer);
@@ -692,7 +699,7 @@
     initializeOverrideBuffer(layers);
     EXPECT_NE(getNonBufferHash(layers),
               mFlattener->flattenLayers(layers, getNonBufferHash(layers), mTime));
-    mFlattener->renderCachedSets(mOutputState);
+    mFlattener->renderCachedSets(mOutputState, std::nullopt);
     EXPECT_NE(nullptr, overrideBuffer1);
     EXPECT_EQ(overrideBuffer1, overrideBuffer2);
     EXPECT_EQ(nullptr, overrideBuffer3);
@@ -726,7 +733,7 @@
     initializeOverrideBuffer(layers);
     EXPECT_EQ(getNonBufferHash(layers),
               mFlattener->flattenLayers(layers, getNonBufferHash(layers), mTime));
-    mFlattener->renderCachedSets(mOutputState);
+    mFlattener->renderCachedSets(mOutputState, std::nullopt);
 
     for (const auto layer : layers) {
         EXPECT_EQ(nullptr, layer->getOutputLayer()->getState().overrideInfo.buffer);
@@ -737,7 +744,7 @@
     initializeOverrideBuffer(layers);
     EXPECT_EQ(getNonBufferHash(layers),
               mFlattener->flattenLayers(layers, getNonBufferHash(layers), mTime));
-    mFlattener->renderCachedSets(mOutputState);
+    mFlattener->renderCachedSets(mOutputState, std::nullopt);
     for (const auto layer : layers) {
         EXPECT_EQ(nullptr, layer->getOutputLayer()->getState().overrideInfo.buffer);
     }
@@ -778,7 +785,7 @@
     initializeOverrideBuffer(layers);
     EXPECT_EQ(getNonBufferHash(layers),
               mFlattener->flattenLayers(layers, getNonBufferHash(layers), mTime));
-    mFlattener->renderCachedSets(mOutputState);
+    mFlattener->renderCachedSets(mOutputState, std::nullopt);
 
     for (const auto layer : layers) {
         EXPECT_EQ(nullptr, layer->getOutputLayer()->getState().overrideInfo.buffer);
@@ -788,7 +795,7 @@
     initializeOverrideBuffer(layers);
     EXPECT_NE(getNonBufferHash(layers),
               mFlattener->flattenLayers(layers, getNonBufferHash(layers), mTime));
-    mFlattener->renderCachedSets(mOutputState);
+    mFlattener->renderCachedSets(mOutputState, std::nullopt);
     EXPECT_EQ(nullptr, overrideBuffer1);
     EXPECT_EQ(nullptr, blurOverrideBuffer);
     EXPECT_NE(nullptr, overrideBuffer3);
@@ -825,7 +832,7 @@
     initializeOverrideBuffer(layers);
     EXPECT_EQ(getNonBufferHash(layers),
               mFlattener->flattenLayers(layers, getNonBufferHash(layers), mTime));
-    mFlattener->renderCachedSets(mOutputState);
+    mFlattener->renderCachedSets(mOutputState, std::nullopt);
 
     const auto& cachedSet = mFlattener->getNewCachedSetForTesting();
     ASSERT_NE(std::nullopt, cachedSet);
@@ -839,7 +846,7 @@
     initializeOverrideBuffer(layers);
     EXPECT_NE(getNonBufferHash(layers),
               mFlattener->flattenLayers(layers, getNonBufferHash(layers), mTime));
-    mFlattener->renderCachedSets(mOutputState);
+    mFlattener->renderCachedSets(mOutputState, std::nullopt);
     EXPECT_NE(nullptr, overrideBuffer1);
     EXPECT_EQ(overrideBuffer2, overrideBuffer1);
     EXPECT_EQ(nullptr, blurOverrideBuffer);
@@ -866,7 +873,7 @@
     initializeOverrideBuffer(layers);
     EXPECT_EQ(getNonBufferHash(layers),
               mFlattener->flattenLayers(layers, getNonBufferHash(layers), mTime));
-    mFlattener->renderCachedSets(mOutputState);
+    mFlattener->renderCachedSets(mOutputState, std::nullopt);
 
     EXPECT_EQ(nullptr, overrideBuffer1);
     EXPECT_EQ(nullptr, overrideBuffer2);
@@ -874,16 +881,61 @@
     // Simulate attempting to render prior to merging the new cached set with the layer stack.
     // Here we should not try to re-render.
     EXPECT_CALL(mRenderEngine, drawLayers(_, _, _, _, _, _)).Times(0);
-    mFlattener->renderCachedSets(mOutputState);
+    mFlattener->renderCachedSets(mOutputState, std::nullopt);
 
     // We provide the override buffer now that it's rendered
     EXPECT_NE(getNonBufferHash(layers),
               mFlattener->flattenLayers(layers, getNonBufferHash(layers), mTime));
-    mFlattener->renderCachedSets(mOutputState);
+    mFlattener->renderCachedSets(mOutputState, std::nullopt);
 
     EXPECT_NE(nullptr, overrideBuffer1);
     EXPECT_EQ(overrideBuffer2, overrideBuffer1);
 }
 
+const constexpr std::chrono::nanoseconds kCachedSetRenderDuration = 0ms;
+const constexpr size_t kMaxDeferRenderAttempts = 2;
+
+class FlattenerRenderSchedulingTest : public FlattenerTest {
+public:
+    FlattenerRenderSchedulingTest()
+          : FlattenerTest(
+                    Flattener::CachedSetRenderSchedulingTunables{.cachedSetRenderDuration =
+                                                                         kCachedSetRenderDuration,
+                                                                 .maxDeferRenderAttempts =
+                                                                         kMaxDeferRenderAttempts}) {
+    }
+};
+
+TEST_F(FlattenerRenderSchedulingTest, flattenLayers_renderCachedSets_defersUpToMaxAttempts) {
+    auto& layerState1 = mTestLayers[0]->layerState;
+    auto& layerState2 = mTestLayers[1]->layerState;
+
+    const std::vector<const LayerState*> layers = {
+            layerState1.get(),
+            layerState2.get(),
+    };
+
+    initializeFlattener(layers);
+
+    // Mark the layers inactive
+    mTime += 200ms;
+
+    initializeOverrideBuffer(layers);
+    EXPECT_EQ(getNonBufferHash(layers),
+              mFlattener->flattenLayers(layers, getNonBufferHash(layers), mTime));
+
+    for (size_t i = 0; i < kMaxDeferRenderAttempts; i++) {
+        EXPECT_CALL(mRenderEngine, drawLayers(_, _, _, _, _, _)).Times(0);
+        mFlattener->renderCachedSets(mOutputState,
+                                     std::chrono::steady_clock::now() -
+                                             (kCachedSetRenderDuration + 10ms));
+    }
+
+    EXPECT_CALL(mRenderEngine, drawLayers(_, _, _, _, _, _)).WillOnce(Return(NO_ERROR));
+    mFlattener->renderCachedSets(mOutputState,
+                                 std::chrono::steady_clock::now() -
+                                         (kCachedSetRenderDuration + 10ms));
+}
+
 } // namespace
 } // namespace android::compositionengine
diff --git a/services/surfaceflinger/SurfaceFlinger.cpp b/services/surfaceflinger/SurfaceFlinger.cpp
index a4b6fef..a109f3b 100644
--- a/services/surfaceflinger/SurfaceFlinger.cpp
+++ b/services/surfaceflinger/SurfaceFlinger.cpp
@@ -2040,6 +2040,7 @@
     }
 
     refreshArgs.earliestPresentTime = mScheduler->getPreviousVsyncFrom(mExpectedPresentTime);
+    refreshArgs.nextInvalidateTime = mEventQueue->nextExpectedInvalidate();
 
     mGeometryInvalid = false;