Merge changes I0f0307dc,I3ae3d407 into main

* changes:
  CursorInputMapper: move unit tests to InputMapperUnitTest
  InputMapperUnitTest: extract device creation from SetUp
diff --git a/cmds/dumpstate/dumpstate.cpp b/cmds/dumpstate/dumpstate.cpp
index 83b336c..b99f443 100644
--- a/cmds/dumpstate/dumpstate.cpp
+++ b/cmds/dumpstate/dumpstate.cpp
@@ -178,6 +178,7 @@
 #define PROFILE_DATA_DIR_CUR "/data/misc/profiles/cur"
 #define PROFILE_DATA_DIR_REF "/data/misc/profiles/ref"
 #define XFRM_STAT_PROC_FILE "/proc/net/xfrm_stat"
+#define KERNEL_CONFIG "/proc/config.gz"
 #define WLUTIL "/vendor/xbin/wlutil"
 #define WMTRACE_DATA_DIR "/data/misc/wmtrace"
 #define OTA_METADATA_DIR "/metadata/ota"
@@ -1951,6 +1952,8 @@
     DumpFile("PSI memory", "/proc/pressure/memory");
     DumpFile("PSI io", "/proc/pressure/io");
 
+    ds.AddZipEntry(ZIP_ROOT_DIR + KERNEL_CONFIG, KERNEL_CONFIG);
+
     RunCommand("SDK EXTENSIONS", {SDK_EXT_INFO, "--dump"},
                CommandOptions::WithTimeout(10).Always().DropRoot().Build());
 
diff --git a/libs/binder/tests/Android.bp b/libs/binder/tests/Android.bp
index 041c280..dd2be94 100644
--- a/libs/binder/tests/Android.bp
+++ b/libs/binder/tests/Android.bp
@@ -836,6 +836,7 @@
         ],
         // Adds bugs to hotlist "AIDL fuzzers bugs" on buganizer
         hotlists: ["4637097"],
+        use_for_presubmit: true,
     },
 }
 
diff --git a/libs/renderengine/threaded/RenderEngineThreaded.cpp b/libs/renderengine/threaded/RenderEngineThreaded.cpp
index 367bee8..f58f543 100644
--- a/libs/renderengine/threaded/RenderEngineThreaded.cpp
+++ b/libs/renderengine/threaded/RenderEngineThreaded.cpp
@@ -125,8 +125,10 @@
 }
 
 void RenderEngineThreaded::waitUntilInitialized() const {
-    std::unique_lock<std::mutex> lock(mInitializedMutex);
-    mInitializedCondition.wait(lock, [=] { return mIsInitialized; });
+    if (!mIsInitialized) {
+        std::unique_lock<std::mutex> lock(mInitializedMutex);
+        mInitializedCondition.wait(lock, [this] { return mIsInitialized.load(); });
+    }
 }
 
 std::future<void> RenderEngineThreaded::primeCache(bool shouldPrimeUltraHDR) {
diff --git a/libs/renderengine/threaded/RenderEngineThreaded.h b/libs/renderengine/threaded/RenderEngineThreaded.h
index 74af2bd..3f1e67f 100644
--- a/libs/renderengine/threaded/RenderEngineThreaded.h
+++ b/libs/renderengine/threaded/RenderEngineThreaded.h
@@ -97,7 +97,7 @@
 
     // Used to allow select thread safe methods to be accessed without requiring the
     // method to be invoked on the RenderEngine thread
-    bool mIsInitialized = false;
+    std::atomic_bool mIsInitialized = false;
     mutable std::mutex mInitializedMutex;
     mutable std::condition_variable mInitializedCondition;
 
diff --git a/libs/sensor/SensorManager.cpp b/libs/sensor/SensorManager.cpp
index d112a12..f8ee3fc 100644
--- a/libs/sensor/SensorManager.cpp
+++ b/libs/sensor/SensorManager.cpp
@@ -248,6 +248,22 @@
     return static_cast<ssize_t>(mSensors.size());
 }
 
+ssize_t SensorManager::getDefaultDeviceSensorList(Vector<Sensor> & list) {
+    Mutex::Autolock _l(mLock);
+    status_t err = assertStateLocked();
+    if (err < 0) {
+        return static_cast<ssize_t>(err);
+    }
+
+    if (mDeviceId == DEVICE_ID_DEFAULT) {
+        list = mSensors;
+    } else {
+        list = mSensorServer->getSensorList(mOpPackageName);
+    }
+
+    return static_cast<ssize_t>(list.size());
+}
+
 ssize_t SensorManager::getDynamicSensorList(Vector<Sensor> & dynamicSensors) {
     Mutex::Autolock _l(mLock);
     status_t err = assertStateLocked();
diff --git a/libs/sensor/include/sensor/SensorManager.h b/libs/sensor/include/sensor/SensorManager.h
index e67fac7..49f050a 100644
--- a/libs/sensor/include/sensor/SensorManager.h
+++ b/libs/sensor/include/sensor/SensorManager.h
@@ -58,6 +58,7 @@
     ~SensorManager();
 
     ssize_t getSensorList(Sensor const* const** list);
+    ssize_t getDefaultDeviceSensorList(Vector<Sensor> & list);
     ssize_t getDynamicSensorList(Vector<Sensor>& list);
     ssize_t getDynamicSensorList(Sensor const* const** list);
     ssize_t getRuntimeSensorList(int deviceId, Vector<Sensor>& list);
diff --git a/opengl/tests/gldual/AndroidManifest.xml b/opengl/tests/gldual/AndroidManifest.xml
index a36f4f7..d6335b0 100644
--- a/opengl/tests/gldual/AndroidManifest.xml
+++ b/opengl/tests/gldual/AndroidManifest.xml
@@ -20,8 +20,9 @@
             android:label="@string/gldual_activity">
         <activity android:name="GLDualActivity"
                 android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
-            	android:launchMode="singleTask"
-            	android:configChanges="orientation|keyboardHidden">
+                android:launchMode="singleTask"
+                android:configChanges="orientation|keyboardHidden"
+                android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />
diff --git a/services/inputflinger/InputFilter.cpp b/services/inputflinger/InputFilter.cpp
index 5d87d34..43ebc69 100644
--- a/services/inputflinger/InputFilter.cpp
+++ b/services/inputflinger/InputFilter.cpp
@@ -52,15 +52,15 @@
 }
 
 void InputFilter::notifyInputDevicesChanged(const NotifyInputDevicesChangedArgs& args) {
+    mDeviceInfos.clear();
+    mDeviceInfos.reserve(args.inputDeviceInfos.size());
+    for (auto info : args.inputDeviceInfos) {
+        AidlDeviceInfo& aidlInfo = mDeviceInfos.emplace_back();
+        aidlInfo.deviceId = info.getId();
+        aidlInfo.external = info.isExternal();
+    }
     if (isFilterEnabled()) {
-        std::vector<AidlDeviceInfo> deviceInfos;
-        for (auto info : args.inputDeviceInfos) {
-            AidlDeviceInfo aidlInfo;
-            aidlInfo.deviceId = info.getId();
-            aidlInfo.external = info.isExternal();
-            deviceInfos.push_back(aidlInfo);
-        }
-        LOG_ALWAYS_FATAL_IF(!mInputFilterRust->notifyInputDevicesChanged(deviceInfos).isOk());
+        LOG_ALWAYS_FATAL_IF(!mInputFilterRust->notifyInputDevicesChanged(mDeviceInfos).isOk());
     }
     mNextListener.notify(args);
 }
@@ -74,7 +74,7 @@
         LOG_ALWAYS_FATAL_IF(!mInputFilterRust->notifyKey(notifyKeyArgsToKeyEvent(args)).isOk());
         return;
     }
-    mNextListener.notifyKey(args);
+    mNextListener.notify(args);
 }
 
 void InputFilter::notifyMotion(const NotifyMotionArgs& args) {
@@ -112,7 +112,7 @@
 
     if (mConfig.bounceKeysThresholdNs != threshold) {
         mConfig.bounceKeysThresholdNs = threshold;
-        LOG_ALWAYS_FATAL_IF(!mInputFilterRust->notifyConfigurationChanged(mConfig).isOk());
+        notifyConfigurationChangedLocked();
     }
 }
 
@@ -121,7 +121,14 @@
 
     if (mConfig.stickyKeysEnabled != enabled) {
         mConfig.stickyKeysEnabled = enabled;
-        LOG_ALWAYS_FATAL_IF(!mInputFilterRust->notifyConfigurationChanged(mConfig).isOk());
+        notifyConfigurationChangedLocked();
+    }
+}
+
+void InputFilter::notifyConfigurationChangedLocked() {
+    LOG_ALWAYS_FATAL_IF(!mInputFilterRust->notifyConfigurationChanged(mConfig).isOk());
+    if (isFilterEnabled()) {
+        LOG_ALWAYS_FATAL_IF(!mInputFilterRust->notifyInputDevicesChanged(mDeviceInfos).isOk());
     }
 }
 
diff --git a/services/inputflinger/InputFilter.h b/services/inputflinger/InputFilter.h
index 9fa7a87..a38fbf6 100644
--- a/services/inputflinger/InputFilter.h
+++ b/services/inputflinger/InputFilter.h
@@ -45,6 +45,7 @@
             aidl::com::android::server::inputflinger::IInputFilter::IInputFilterCallbacks;
     using InputFilterConfiguration =
             aidl::com::android::server::inputflinger::InputFilterConfiguration;
+    using AidlDeviceInfo = aidl::com::android::server::inputflinger::DeviceInfo;
 
     explicit InputFilter(InputListenerInterface& listener, IInputFlingerRust&);
     ~InputFilter() override = default;
@@ -65,10 +66,14 @@
     InputListenerInterface& mNextListener;
     std::shared_ptr<InputFilterCallbacks> mCallbacks;
     std::shared_ptr<IInputFilter> mInputFilterRust;
+    // Keep track of connected peripherals, so that if filters are enabled later, we can pass that
+    // info to the filters
+    std::vector<AidlDeviceInfo> mDeviceInfos;
     mutable std::mutex mLock;
     InputFilterConfiguration mConfig GUARDED_BY(mLock);
 
     bool isFilterEnabled();
+    void notifyConfigurationChangedLocked() REQUIRES(mLock);
 };
 
 } // namespace android
diff --git a/services/inputflinger/dispatcher/InputDispatcher.cpp b/services/inputflinger/dispatcher/InputDispatcher.cpp
index 8d0ff4b..ab815ea 100644
--- a/services/inputflinger/dispatcher/InputDispatcher.cpp
+++ b/services/inputflinger/dispatcher/InputDispatcher.cpp
@@ -1121,7 +1121,10 @@
                 }
             }
             if (dropReason == DropReason::NOT_DROPPED && mNextUnblockedEvent) {
-                dropReason = DropReason::BLOCKED;
+                if (!isFromSource(motionEntry->source, AINPUT_SOURCE_CLASS_POINTER)) {
+                    // Only drop events that are focus-dispatched.
+                    dropReason = DropReason::BLOCKED;
+                }
             }
             done = dispatchMotionLocked(currentTime, motionEntry, &dropReason, nextWakeupTime);
             break;
@@ -1392,8 +1395,9 @@
             reason = "inbound event was dropped because of pending overdue app switch";
             break;
         case DropReason::BLOCKED:
-            ALOGI("Dropped event because the current application is not responding and the user "
-                  "has started interacting with a different application.");
+            LOG(INFO) << "Dropping because the current application is not responding and the user "
+                         "has started interacting with a different application: "
+                      << entry.getDescription();
             reason = "inbound event was dropped because the current application is not responding "
                      "and the user has started interacting with a different application";
             break;
@@ -1414,6 +1418,9 @@
     switch (entry.type) {
         case EventEntry::Type::KEY: {
             CancelationOptions options(CancelationOptions::Mode::CANCEL_NON_POINTER_EVENTS, reason);
+            const KeyEntry& keyEntry = static_cast<const KeyEntry&>(entry);
+            options.displayId = keyEntry.displayId;
+            options.deviceId = keyEntry.deviceId;
             synthesizeCancelationEventsForAllConnectionsLocked(options);
             break;
         }
@@ -1421,10 +1428,14 @@
             const MotionEntry& motionEntry = static_cast<const MotionEntry&>(entry);
             if (motionEntry.source & AINPUT_SOURCE_CLASS_POINTER) {
                 CancelationOptions options(CancelationOptions::Mode::CANCEL_POINTER_EVENTS, reason);
+                options.displayId = motionEntry.displayId;
+                options.deviceId = motionEntry.deviceId;
                 synthesizeCancelationEventsForAllConnectionsLocked(options);
             } else {
                 CancelationOptions options(CancelationOptions::Mode::CANCEL_NON_POINTER_EVENTS,
                                            reason);
+                options.displayId = motionEntry.displayId;
+                options.deviceId = motionEntry.deviceId;
                 synthesizeCancelationEventsForAllConnectionsLocked(options);
             }
             break;
@@ -3389,27 +3400,40 @@
 
                 dispatchEntry->resolvedFlags = resolvedFlags;
                 if (resolvedAction != motionEntry.action) {
+                    std::optional<std::vector<PointerProperties>> usingProperties;
+                    std::optional<std::vector<PointerCoords>> usingCoords;
+                    if (resolvedAction == AMOTION_EVENT_ACTION_HOVER_EXIT ||
+                        resolvedAction == AMOTION_EVENT_ACTION_CANCEL) {
+                        // This is a HOVER_EXIT or an ACTION_CANCEL event that was synthesized by
+                        // the dispatcher, and therefore the coordinates of this event are currently
+                        // incorrect. These events should use the coordinates of the last dispatched
+                        // ACTION_MOVE or HOVER_MOVE. We need to query InputState to get this data.
+                        const bool hovering = resolvedAction == AMOTION_EVENT_ACTION_HOVER_EXIT;
+                        std::optional<std::pair<std::vector<PointerProperties>,
+                                                std::vector<PointerCoords>>>
+                                pointerInfo =
+                                        connection->inputState.getPointersOfLastEvent(motionEntry,
+                                                                                      hovering);
+                        if (pointerInfo) {
+                            usingProperties = pointerInfo->first;
+                            usingCoords = pointerInfo->second;
+                        }
+                    }
                     // Generate a new MotionEntry with a new eventId using the resolved action and
                     // flags.
-                    resolvedMotion =
-                            std::make_shared<MotionEntry>(mIdGenerator.nextId(),
-                                                          motionEntry.injectionState,
-                                                          motionEntry.eventTime,
-                                                          motionEntry.deviceId, motionEntry.source,
-                                                          motionEntry.displayId,
-                                                          motionEntry.policyFlags, resolvedAction,
-                                                          motionEntry.actionButton, resolvedFlags,
-                                                          motionEntry.metaState,
-                                                          motionEntry.buttonState,
-                                                          motionEntry.classification,
-                                                          motionEntry.edgeFlags,
-                                                          motionEntry.xPrecision,
-                                                          motionEntry.yPrecision,
-                                                          motionEntry.xCursorPosition,
-                                                          motionEntry.yCursorPosition,
-                                                          motionEntry.downTime,
-                                                          motionEntry.pointerProperties,
-                                                          motionEntry.pointerCoords);
+                    resolvedMotion = std::make_shared<
+                            MotionEntry>(mIdGenerator.nextId(), motionEntry.injectionState,
+                                         motionEntry.eventTime, motionEntry.deviceId,
+                                         motionEntry.source, motionEntry.displayId,
+                                         motionEntry.policyFlags, resolvedAction,
+                                         motionEntry.actionButton, resolvedFlags,
+                                         motionEntry.metaState, motionEntry.buttonState,
+                                         motionEntry.classification, motionEntry.edgeFlags,
+                                         motionEntry.xPrecision, motionEntry.yPrecision,
+                                         motionEntry.xCursorPosition, motionEntry.yCursorPosition,
+                                         motionEntry.downTime,
+                                         usingProperties.value_or(motionEntry.pointerProperties),
+                                         usingCoords.value_or(motionEntry.pointerCoords));
                     if (ATRACE_ENABLED()) {
                         std::string message = StringPrintf("Transmute MotionEvent(id=0x%" PRIx32
                                                            ") to MotionEvent(id=0x%" PRIx32 ").",
@@ -4436,7 +4460,8 @@
     policyFlags |= POLICY_FLAG_TRUSTED;
 
     android::base::Timer t;
-    mPolicy.interceptMotionBeforeQueueing(args.displayId, args.eventTime, policyFlags);
+    mPolicy.interceptMotionBeforeQueueing(args.displayId, args.source, args.action, args.eventTime,
+                                          policyFlags);
     if (t.duration() > SLOW_INTERCEPTION_THRESHOLD) {
         ALOGW("Excessive delay in interceptMotionBeforeQueueing; took %s ms",
               std::to_string(t.duration().count()).c_str());
@@ -4697,7 +4722,9 @@
             if (!(policyFlags & POLICY_FLAG_FILTERED)) {
                 nsecs_t eventTime = motionEvent.getEventTime();
                 android::base::Timer t;
-                mPolicy.interceptMotionBeforeQueueing(displayId, eventTime, /*byref*/ policyFlags);
+                mPolicy.interceptMotionBeforeQueueing(displayId, motionEvent.getSource(),
+                                                      motionEvent.getAction(), eventTime,
+                                                      /*byref*/ policyFlags);
                 if (t.duration() > SLOW_INTERCEPTION_THRESHOLD) {
                     ALOGW("Excessive delay in interceptMotionBeforeQueueing; took %s ms",
                           std::to_string(t.duration().count()).c_str());
diff --git a/services/inputflinger/dispatcher/InputState.cpp b/services/inputflinger/dispatcher/InputState.cpp
index a4ac4fb..1fec9b7 100644
--- a/services/inputflinger/dispatcher/InputState.cpp
+++ b/services/inputflinger/dispatcher/InputState.cpp
@@ -195,6 +195,16 @@
     }
 }
 
+std::optional<std::pair<std::vector<PointerProperties>, std::vector<PointerCoords>>>
+InputState::getPointersOfLastEvent(const MotionEntry& entry, bool hovering) const {
+    ssize_t index = findMotionMemento(entry, hovering);
+    if (index == -1) {
+        return std::nullopt;
+    }
+    return std::make_pair(mMotionMementos[index].pointerProperties,
+                          mMotionMementos[index].pointerCoords);
+}
+
 ssize_t InputState::findKeyMemento(const KeyEntry& entry) const {
     for (size_t i = 0; i < mKeyMementos.size(); i++) {
         const KeyMemento& memento = mKeyMementos[i];
@@ -311,16 +321,6 @@
             return true;
         }
 
-        // Use the previous stream cancellation logic to generate all HOVER_EXIT events.
-        // If this hover event was generated as a result of the pointer leaving the window,
-        // the HOVER_EXIT event should have the same coordinates as the previous
-        // HOVER_MOVE event in this stream. Ensure that all HOVER_EXITs have the same
-        // coordinates as the previous event by cancelling the stream here. With this approach, the
-        // HOVER_EXIT event is generated from the previous event.
-        if (actionMasked == AMOTION_EVENT_ACTION_HOVER_EXIT && lastMemento.hovering) {
-            return true;
-        }
-
         // If the stream changes its source, just cancel the current gesture to be safe. It's
         // possible that the app isn't handling source changes properly
         if (motionEntry.source != lastMemento.source) {
diff --git a/services/inputflinger/dispatcher/InputState.h b/services/inputflinger/dispatcher/InputState.h
index b0e4209..d49469d 100644
--- a/services/inputflinger/dispatcher/InputState.h
+++ b/services/inputflinger/dispatcher/InputState.h
@@ -48,6 +48,15 @@
     // and should be skipped.
     bool trackMotion(const MotionEntry& entry, int32_t flags);
 
+    /**
+     * Return the PointerProperties and the PointerCoords for the last event, if found. Return
+     * std::nullopt if not found. We should not return std::vector<PointerCoords> in isolation,
+     * because the pointers can technically be stored in the vector in any order, so the
+     * PointerProperties are needed to specify the order in which the pointer coords are stored.
+     */
+    std::optional<std::pair<std::vector<PointerProperties>, std::vector<PointerCoords>>>
+    getPointersOfLastEvent(const MotionEntry& entry, bool hovering) const;
+
     // Create cancel events for the previous stream if the current motionEntry requires it.
     std::unique_ptr<EventEntry> cancelConflictingInputStream(const MotionEntry& motionEntry);
 
diff --git a/services/inputflinger/dispatcher/include/InputDispatcherPolicyInterface.h b/services/inputflinger/dispatcher/include/InputDispatcherPolicyInterface.h
index 1c23720..9e6209b 100644
--- a/services/inputflinger/dispatcher/include/InputDispatcherPolicyInterface.h
+++ b/services/inputflinger/dispatcher/include/InputDispatcherPolicyInterface.h
@@ -99,8 +99,8 @@
      * This method is expected to set the POLICY_FLAG_PASS_TO_USER policy flag if the event
      * should be dispatched to applications.
      */
-    virtual void interceptMotionBeforeQueueing(int32_t displayId, nsecs_t when,
-                                               uint32_t& policyFlags) = 0;
+    virtual void interceptMotionBeforeQueueing(int32_t displayId, uint32_t source, int32_t action,
+                                               nsecs_t when, uint32_t& policyFlags) = 0;
 
     /* Allows the policy a chance to intercept a key before dispatching. */
     virtual nsecs_t interceptKeyBeforeDispatching(const sp<IBinder>& token,
diff --git a/services/inputflinger/reader/mapper/TouchpadInputMapper.cpp b/services/inputflinger/reader/mapper/TouchpadInputMapper.cpp
index d331d55..255f02d 100644
--- a/services/inputflinger/reader/mapper/TouchpadInputMapper.cpp
+++ b/services/inputflinger/reader/mapper/TouchpadInputMapper.cpp
@@ -21,9 +21,11 @@
 #include <iterator>
 #include <limits>
 #include <map>
+#include <mutex>
 #include <optional>
 
 #include <android-base/stringprintf.h>
+#include <android-base/thread_annotations.h>
 #include <android/input.h>
 #include <com_android_input_flags.h>
 #include <ftl/enum.h>
@@ -156,13 +158,20 @@
         return sAccumulator;
     }
 
-    void recordFinger(const TouchpadInputMapper::MetricsIdentifier& id) { mCounters[id].fingers++; }
+    void recordFinger(const TouchpadInputMapper::MetricsIdentifier& id) {
+        std::scoped_lock lock(mLock);
+        mCounters[id].fingers++;
+    }
 
-    void recordPalm(const TouchpadInputMapper::MetricsIdentifier& id) { mCounters[id].palms++; }
+    void recordPalm(const TouchpadInputMapper::MetricsIdentifier& id) {
+        std::scoped_lock lock(mLock);
+        mCounters[id].palms++;
+    }
 
     // Checks whether a Gesture struct is for the end of a gesture that we log metrics for, and
     // records it if so.
     void processGesture(const TouchpadInputMapper::MetricsIdentifier& id, const Gesture& gesture) {
+        std::scoped_lock lock(mLock);
         switch (gesture.type) {
             case kGestureTypeFling:
                 if (gesture.details.fling.fling_state == GESTURES_FLING_START) {
@@ -200,15 +209,20 @@
                                                                  void* cookie) {
         LOG_ALWAYS_FATAL_IF(atomTag != android::util::TOUCHPAD_USAGE);
         MetricsAccumulator& accumulator = MetricsAccumulator::getInstance();
-        accumulator.produceAtoms(outEventList);
-        accumulator.resetCounters();
+        accumulator.produceAtomsAndReset(*outEventList);
         return AStatsManager_PULL_SUCCESS;
     }
 
-    void produceAtoms(AStatsEventList* outEventList) const {
+    void produceAtomsAndReset(AStatsEventList& outEventList) {
+        std::scoped_lock lock(mLock);
+        produceAtomsLocked(outEventList);
+        resetCountersLocked();
+    }
+
+    void produceAtomsLocked(AStatsEventList& outEventList) const REQUIRES(mLock) {
         for (auto& [id, counters] : mCounters) {
             auto [busId, vendorId, productId, versionId] = id;
-            addAStatsEvent(outEventList, android::util::TOUCHPAD_USAGE, vendorId, productId,
+            addAStatsEvent(&outEventList, android::util::TOUCHPAD_USAGE, vendorId, productId,
                            versionId, linuxBusToInputDeviceBusEnum(busId, /*isUsi=*/false),
                            counters.fingers, counters.palms, counters.twoFingerSwipeGestures,
                            counters.threeFingerSwipeGestures, counters.fourFingerSwipeGestures,
@@ -216,7 +230,7 @@
         }
     }
 
-    void resetCounters() { mCounters.clear(); }
+    void resetCountersLocked() REQUIRES(mLock) { mCounters.clear(); }
 
     // Stores the counters for a specific touchpad model. Fields have the same meanings as those of
     // the TouchpadUsage atom; see that definition for detailed documentation.
@@ -232,7 +246,10 @@
 
     // Metrics are aggregated by device model and version, so if two devices of the same model and
     // version are connected at once, they will have the same counters.
-    std::map<TouchpadInputMapper::MetricsIdentifier, Counters> mCounters;
+    std::map<TouchpadInputMapper::MetricsIdentifier, Counters> mCounters GUARDED_BY(mLock);
+
+    // Metrics are pulled by a binder thread, so we need to guard them with a mutex.
+    mutable std::mutex mLock;
 };
 
 } // namespace
diff --git a/services/inputflinger/reader/mapper/gestures/GestureConverter.cpp b/services/inputflinger/reader/mapper/gestures/GestureConverter.cpp
index 8daacdb..01e983a 100644
--- a/services/inputflinger/reader/mapper/gestures/GestureConverter.cpp
+++ b/services/inputflinger/reader/mapper/gestures/GestureConverter.cpp
@@ -116,8 +116,6 @@
 void GestureConverter::populateMotionRanges(InputDeviceInfo& info) const {
     info.addMotionRange(AMOTION_EVENT_AXIS_PRESSURE, SOURCE, 0.0f, 1.0f, 0, 0, 0);
 
-    // TODO(b/259547750): set this using the raw axis ranges from the touchpad when pointer capture
-    // is enabled.
     if (!mBoundsInLogicalDisplay.isEmpty()) {
         info.addMotionRange(AMOTION_EVENT_AXIS_X, SOURCE, mBoundsInLogicalDisplay.left,
                             mBoundsInLogicalDisplay.right, 0, 0, 0);
diff --git a/services/inputflinger/tests/FakeInputDispatcherPolicy.h b/services/inputflinger/tests/FakeInputDispatcherPolicy.h
index e9d93af..fb2db06 100644
--- a/services/inputflinger/tests/FakeInputDispatcherPolicy.h
+++ b/services/inputflinger/tests/FakeInputDispatcherPolicy.h
@@ -63,7 +63,7 @@
 
     void interceptKeyBeforeQueueing(const KeyEvent&, uint32_t&) override {}
 
-    void interceptMotionBeforeQueueing(int32_t, nsecs_t, uint32_t&) override {}
+    void interceptMotionBeforeQueueing(int32_t, uint32_t, int32_t, nsecs_t, uint32_t&) override {}
 
     nsecs_t interceptKeyBeforeDispatching(const sp<IBinder>&, const KeyEvent&, uint32_t) override {
         return 0;
diff --git a/services/inputflinger/tests/InputDispatcher_test.cpp b/services/inputflinger/tests/InputDispatcher_test.cpp
index ef07540..c92736e 100644
--- a/services/inputflinger/tests/InputDispatcher_test.cpp
+++ b/services/inputflinger/tests/InputDispatcher_test.cpp
@@ -549,7 +549,7 @@
         }
     }
 
-    void interceptMotionBeforeQueueing(int32_t, nsecs_t, uint32_t&) override {}
+    void interceptMotionBeforeQueueing(int32_t, uint32_t, int32_t, nsecs_t, uint32_t&) override {}
 
     nsecs_t interceptKeyBeforeDispatching(const sp<IBinder>&, const KeyEvent&, uint32_t) override {
         nsecs_t delay = std::chrono::nanoseconds(mInterceptKeyTimeout).count();
@@ -8809,6 +8809,104 @@
     ASSERT_NO_FATAL_FAILURE(mFakePolicy->assertNotifyAnrWasNotCalled());
 }
 
+/**
+ * If we are pruning input queue, we should never drop pointer events. Otherwise, we risk having
+ * an inconsistent event stream inside the dispatcher. In this test, we make sure that the
+ * dispatcher doesn't prune pointer events incorrectly.
+ *
+ * This test reproduces a crash in InputDispatcher.
+ * To reproduce the crash, we need to simulate the conditions for "pruning input queue" to occur.
+ *
+ * Keep the currently focused application (mApplication), and have no focused window.
+ * We set up two additional windows:
+ * 1) The navigation bar window. This simulates the system "NavigationBar", which is used in the
+ * 3-button navigation mode. This window injects a BACK button when it's touched. 2) The application
+ * window. This window is not focusable, but is touchable.
+ *
+ * We first touch the navigation bar, which causes it to inject a key. Since there's no focused
+ * window, the dispatcher doesn't process this key, and all other events inside dispatcher are now
+ * blocked. The dispatcher is waiting for 'mApplication' to add a focused window.
+ *
+ * Now, we touch "Another window". This window is owned by a different application than
+ * 'mApplication'. This causes the dispatcher to stop waiting for 'mApplication' to add a focused
+ * window. Now, the "pruning input queue" behaviour should kick in, and the dispatcher should start
+ * dropping the events from its queue. Ensure that no crash occurs.
+ *
+ * In this test, we are setting long timeouts to prevent ANRs and events dropped due to being stale.
+ * This does not affect the test running time.
+ */
+TEST_F(InputDispatcherMultiWindowAnr, PruningInputQueueShouldNotDropPointerEvents) {
+    std::shared_ptr<FakeApplicationHandle> systemUiApplication =
+            std::make_shared<FakeApplicationHandle>();
+    systemUiApplication->setDispatchingTimeout(3000ms);
+    mFakePolicy->setStaleEventTimeout(3000ms);
+    sp<FakeWindowHandle> navigationBar =
+            sp<FakeWindowHandle>::make(systemUiApplication, mDispatcher, "NavigationBar",
+                                       ADISPLAY_ID_DEFAULT);
+    navigationBar->setFocusable(false);
+    navigationBar->setWatchOutsideTouch(true);
+    navigationBar->setFrame(Rect(0, 0, 100, 100));
+
+    mApplication->setDispatchingTimeout(3000ms);
+    // 'mApplication' is already focused, but we call it again here to make it explicit.
+    mDispatcher->setFocusedApplication(ADISPLAY_ID_DEFAULT, mApplication);
+
+    std::shared_ptr<FakeApplicationHandle> anotherApplication =
+            std::make_shared<FakeApplicationHandle>();
+    sp<FakeWindowHandle> appWindow =
+            sp<FakeWindowHandle>::make(anotherApplication, mDispatcher, "Another window",
+                                       ADISPLAY_ID_DEFAULT);
+    appWindow->setFocusable(false);
+    appWindow->setFrame(Rect(100, 100, 200, 200));
+
+    mDispatcher->onWindowInfosChanged(
+            {{*navigationBar->getInfo(), *appWindow->getInfo()}, {}, 0, 0});
+    // 'mFocusedWindow' is no longer in the dispatcher window list, and therefore loses focus
+    mFocusedWindow->consumeFocusEvent(false);
+
+    // Touch down the navigation bar. It consumes the touch and injects a key into the dispatcher
+    // in response.
+    mDispatcher->notifyMotion(MotionArgsBuilder(ACTION_DOWN, AINPUT_SOURCE_TOUCHSCREEN)
+                                      .pointer(PointerBuilder(0, ToolType::FINGER).x(50).y(50))
+                                      .build());
+    navigationBar->consumeMotionEvent(WithMotionAction(ACTION_DOWN));
+
+    // Key will not be sent anywhere because we have no focused window. It will remain pending.
+    // Pretend we are injecting KEYCODE_BACK, but it doesn't actually matter what key it is.
+    InputEventInjectionResult result =
+            injectKey(*mDispatcher, AKEY_EVENT_ACTION_DOWN, /*repeatCount=*/0, ADISPLAY_ID_DEFAULT,
+                      InputEventInjectionSync::NONE, /*injectionTimeout=*/100ms,
+                      /*allowKeyRepeat=*/false);
+    ASSERT_EQ(InputEventInjectionResult::SUCCEEDED, result);
+
+    // Finish the gesture - lift up finger and inject ACTION_UP key event
+    mDispatcher->notifyMotion(MotionArgsBuilder(ACTION_UP, AINPUT_SOURCE_TOUCHSCREEN)
+                                      .pointer(PointerBuilder(0, ToolType::FINGER).x(50).y(50))
+                                      .build());
+    result = injectKey(*mDispatcher, AKEY_EVENT_ACTION_UP, /*repeatCount=*/0, ADISPLAY_ID_DEFAULT,
+                       InputEventInjectionSync::NONE, /*injectionTimeout=*/100ms,
+                       /*allowKeyRepeat=*/false);
+    ASSERT_EQ(InputEventInjectionResult::SUCCEEDED, result);
+    // The key that was injected is blocking the dispatcher, so the navigation bar shouldn't be
+    // getting any events yet.
+    navigationBar->assertNoEvents();
+
+    // Now touch "Another window". This touch is going to a different application than the one we
+    // are waiting for (which is 'mApplication').
+    // This should cause the dispatcher to drop the pending focus-dispatched events (like the key
+    // trying to be injected) and to continue processing the rest of the events in the original
+    // order.
+    mDispatcher->notifyMotion(MotionArgsBuilder(ACTION_DOWN, AINPUT_SOURCE_TOUCHSCREEN)
+                                      .pointer(PointerBuilder(0, ToolType::FINGER).x(150).y(150))
+                                      .build());
+    navigationBar->consumeMotionEvent(WithMotionAction(ACTION_UP));
+    navigationBar->consumeMotionEvent(WithMotionAction(ACTION_OUTSIDE));
+    appWindow->consumeMotionEvent(WithMotionAction(ACTION_DOWN));
+
+    appWindow->assertNoEvents();
+    navigationBar->assertNoEvents();
+}
+
 // These tests ensure we cannot send touch events to a window that's positioned behind a window
 // that has feature NO_INPUT_CHANNEL.
 // Layout:
diff --git a/services/powermanager/WorkDuration.cpp b/services/powermanager/WorkDuration.cpp
index ef723c2..bd2b10a 100644
--- a/services/powermanager/WorkDuration.cpp
+++ b/services/powermanager/WorkDuration.cpp
@@ -25,8 +25,9 @@
 
 WorkDuration::WorkDuration(int64_t startTimestampNanos, int64_t totalDurationNanos,
                            int64_t cpuDurationNanos, int64_t gpuDurationNanos)
-      : workPeriodStartTimestampNanos(startTimestampNanos),
+      : timestampNanos(0),
         actualTotalDurationNanos(totalDurationNanos),
+        workPeriodStartTimestampNanos(startTimestampNanos),
         actualCpuDurationNanos(cpuDurationNanos),
         actualGpuDurationNanos(gpuDurationNanos) {}
 
diff --git a/services/powermanager/include/android/WorkDuration.h b/services/powermanager/include/android/WorkDuration.h
index 99b5b8b..26a575f 100644
--- a/services/powermanager/include/android/WorkDuration.h
+++ b/services/powermanager/include/android/WorkDuration.h
@@ -61,11 +61,11 @@
         return os;
     }
 
-    int64_t workPeriodStartTimestampNanos;
+    int64_t timestampNanos;
     int64_t actualTotalDurationNanos;
+    int64_t workPeriodStartTimestampNanos;
     int64_t actualCpuDurationNanos;
     int64_t actualGpuDurationNanos;
-    int64_t timestampNanos;
 };
 
 } // namespace android::os
diff --git a/services/surfaceflinger/CompositionEngine/include/compositionengine/CompositionRefreshArgs.h b/services/surfaceflinger/CompositionEngine/include/compositionengine/CompositionRefreshArgs.h
index 1a235e9..18a96f4 100644
--- a/services/surfaceflinger/CompositionEngine/include/compositionengine/CompositionRefreshArgs.h
+++ b/services/surfaceflinger/CompositionEngine/include/compositionengine/CompositionRefreshArgs.h
@@ -24,6 +24,7 @@
 #include <compositionengine/LayerFE.h>
 #include <compositionengine/OutputColorSetting.h>
 #include <math/mat4.h>
+#include <scheduler/interface/ICompositor.h>
 #include <ui/FenceTime.h>
 #include <ui/Transform.h>
 
@@ -89,17 +90,14 @@
     // If set, causes the dirty regions to flash with the delay
     std::optional<std::chrono::microseconds> devOptFlashDirtyRegionsDelay;
 
-    // Optional.
-    // The earliest time to send the present command to the HAL.
-    std::optional<std::chrono::steady_clock::time_point> earliestPresentTime;
-
-    // The expected time for the next present
-    nsecs_t expectedPresentTime{0};
+    scheduler::FrameTargets frameTargets;
 
     // The frameInterval for the next present
-    Fps frameInterval{};
+    // TODO (b/315371484): Calculate per display and store on `FrameTarget`.
+    Fps frameInterval;
 
     // If set, a frame has been scheduled for that time.
+    // TODO (b/255601557): Calculate per display.
     std::optional<std::chrono::steady_clock::time_point> scheduledFrameTime;
 
     std::vector<BorderRenderInfo> borderInfoList;
diff --git a/services/surfaceflinger/CompositionEngine/src/Output.cpp b/services/surfaceflinger/CompositionEngine/src/Output.cpp
index 09c7c99..1c2f6cb 100644
--- a/services/surfaceflinger/CompositionEngine/src/Output.cpp
+++ b/services/surfaceflinger/CompositionEngine/src/Output.cpp
@@ -28,8 +28,11 @@
 #include <compositionengine/impl/OutputLayer.h>
 #include <compositionengine/impl/OutputLayerCompositionState.h>
 #include <compositionengine/impl/planner/Planner.h>
+#include <ftl/algorithm.h>
 #include <ftl/future.h>
 #include <gui/TraceUtils.h>
+#include <scheduler/FrameTargeter.h>
+#include <scheduler/Time.h>
 
 #include <optional>
 #include <thread>
@@ -429,7 +432,28 @@
 
 ftl::Future<std::monostate> Output::present(
         const compositionengine::CompositionRefreshArgs& refreshArgs) {
-    ATRACE_FORMAT("%s for %s", __func__, mNamePlusId.c_str());
+    const auto stringifyExpectedPresentTime = [this, &refreshArgs]() -> std::string {
+        return ftl::Optional(getDisplayId())
+                .and_then(PhysicalDisplayId::tryCast)
+                .and_then([&refreshArgs](PhysicalDisplayId id) {
+                    return refreshArgs.frameTargets.get(id);
+                })
+                .transform([](const auto& frameTargetPtr) {
+                    return frameTargetPtr.get()->expectedPresentTime();
+                })
+                .transform([](TimePoint expectedPresentTime) {
+                    return base::StringPrintf(" vsyncIn %.2fms",
+                                              ticks<std::milli, float>(expectedPresentTime -
+                                                                       TimePoint::now()));
+                })
+                .or_else([] {
+                    // There is no vsync for this output.
+                    return std::make_optional(std::string());
+                })
+                .value();
+    };
+    ATRACE_FORMAT("%s for %s%s", __func__, mNamePlusId.c_str(),
+                  stringifyExpectedPresentTime().c_str());
     ALOGV(__FUNCTION__);
 
     updateColorProfile(refreshArgs);
@@ -853,8 +877,14 @@
         return;
     }
 
-    editState().earliestPresentTime = refreshArgs.earliestPresentTime;
-    editState().expectedPresentTime = refreshArgs.expectedPresentTime;
+    if (auto frameTargetPtrOpt = ftl::Optional(getDisplayId())
+                                         .and_then(PhysicalDisplayId::tryCast)
+                                         .and_then([&refreshArgs](PhysicalDisplayId id) {
+                                             return refreshArgs.frameTargets.get(id);
+                                         })) {
+        editState().earliestPresentTime = frameTargetPtrOpt->get()->earliestPresentTime();
+        editState().expectedPresentTime = frameTargetPtrOpt->get()->expectedPresentTime().ns();
+    }
     editState().frameInterval = refreshArgs.frameInterval;
     editState().powerCallback = refreshArgs.powerCallback;
 
diff --git a/services/surfaceflinger/DisplayDevice.cpp b/services/surfaceflinger/DisplayDevice.cpp
index 7b0aad7..799d62c 100644
--- a/services/surfaceflinger/DisplayDevice.cpp
+++ b/services/surfaceflinger/DisplayDevice.cpp
@@ -222,6 +222,7 @@
                                        const hal::VsyncPeriodChangeConstraints& constraints,
                                        hal::VsyncPeriodChangeTimeline& outTimeline) {
     mPendingModeOpt = std::move(desiredMode);
+    mIsModeSetPending = true;
 
     const auto& mode = *mPendingModeOpt->mode.modePtr;
 
@@ -234,22 +235,9 @@
     return true;
 }
 
-auto DisplayDevice::finalizeModeChange() -> ModeChange {
-    if (!mPendingModeOpt) return NoModeChange{"No pending mode"};
-
-    auto pendingMode = *std::exchange(mPendingModeOpt, std::nullopt);
-    auto& pendingModePtr = pendingMode.mode.modePtr;
-
-    if (!mRefreshRateSelector->displayModes().contains(pendingModePtr->getId())) {
-        return NoModeChange{"Unknown pending mode"};
-    }
-
-    if (getActiveMode().modePtr->getResolution() != pendingModePtr->getResolution()) {
-        return ResolutionChange{std::move(pendingMode)};
-    }
-
-    setActiveMode(pendingModePtr->getId(), pendingModePtr->getVsyncRate(), pendingMode.mode.fps);
-    return RefreshRateChange{std::move(pendingMode)};
+void DisplayDevice::finalizeModeChange(DisplayModeId modeId, Fps vsyncRate, Fps renderFps) {
+    setActiveMode(modeId, vsyncRate, renderFps);
+    mIsModeSetPending = false;
 }
 
 nsecs_t DisplayDevice::getVsyncPeriodFromHWC() const {
@@ -587,14 +575,10 @@
     return mDesiredModeOpt;
 }
 
-auto DisplayDevice::takeDesiredMode() -> DisplayModeRequestOpt {
-    DisplayModeRequestOpt desiredModeOpt;
-    {
-        std::scoped_lock lock(mDesiredModeLock);
-        std::swap(mDesiredModeOpt, desiredModeOpt);
-        mHasDesiredModeTrace = false;
-    }
-    return desiredModeOpt;
+void DisplayDevice::clearDesiredMode() {
+    std::scoped_lock lock(mDesiredModeLock);
+    mDesiredModeOpt.reset();
+    mHasDesiredModeTrace = false;
 }
 
 void DisplayDevice::adjustRefreshRate(Fps pacesetterDisplayRefreshRate) {
diff --git a/services/surfaceflinger/DisplayDevice.h b/services/surfaceflinger/DisplayDevice.h
index 4c09ffd..97b56a2 100644
--- a/services/surfaceflinger/DisplayDevice.h
+++ b/services/surfaceflinger/DisplayDevice.h
@@ -19,7 +19,6 @@
 #include <memory>
 #include <string>
 #include <unordered_map>
-#include <variant>
 
 #include <android-base/thread_annotations.h>
 #include <android/native_window.h>
@@ -196,11 +195,12 @@
     using DisplayModeRequestOpt = ftl::Optional<display::DisplayModeRequest>;
 
     DisplayModeRequestOpt getDesiredMode() const EXCLUDES(mDesiredModeLock);
-    DisplayModeRequestOpt takeDesiredMode() EXCLUDES(mDesiredModeLock);
+    void clearDesiredMode() EXCLUDES(mDesiredModeLock);
 
-    bool isModeSetPending() const REQUIRES(kMainThreadContext) {
-        return mPendingModeOpt.has_value();
+    DisplayModeRequestOpt getPendingMode() const REQUIRES(kMainThreadContext) {
+        return mPendingModeOpt;
     }
+    bool isModeSetPending() const REQUIRES(kMainThreadContext) { return mIsModeSetPending; }
 
     scheduler::FrameRateMode getActiveMode() const REQUIRES(kMainThreadContext) {
         return mRefreshRateSelector->getActiveMode();
@@ -212,25 +212,8 @@
                             hal::VsyncPeriodChangeTimeline& outTimeline)
             REQUIRES(kMainThreadContext);
 
-    struct NoModeChange {
-        const char* reason;
-    };
-
-    struct ResolutionChange {
-        display::DisplayModeRequest activeMode;
-    };
-
-    struct RefreshRateChange {
-        display::DisplayModeRequest activeMode;
-    };
-
-    using ModeChange = std::variant<NoModeChange, ResolutionChange, RefreshRateChange>;
-
-    // Clears the pending DisplayModeRequest, and returns the ModeChange that occurred. If it was a
-    // RefreshRateChange, the pending mode becomes the active mode. If it was a ResolutionChange,
-    // the caller is responsible for resizing the framebuffer to match the active resolution by
-    // recreating the DisplayDevice.
-    ModeChange finalizeModeChange() REQUIRES(kMainThreadContext);
+    void finalizeModeChange(DisplayModeId, Fps vsyncRate, Fps renderFps)
+            REQUIRES(kMainThreadContext);
 
     scheduler::RefreshRateSelector& refreshRateSelector() const { return *mRefreshRateSelector; }
 
@@ -267,8 +250,6 @@
     void dump(utils::Dumper&) const;
 
 private:
-    friend class TestableSurfaceFlinger;
-
     template <size_t N>
     inline std::string concatId(const char (&str)[N]) const {
         return std::string(ftl::Concat(str, ' ', getId().value).str());
@@ -319,15 +300,12 @@
     // This parameter is only used for hdr/sdr ratio overlay
     float mHdrSdrRatio = 1.0f;
 
-    // A DisplayModeRequest flows through three states: desired, pending, and active. Requests
-    // within a frame are merged into a single desired request. Unless cleared, the request is
-    // relayed to HWC on the next frame, and becomes pending. The mode becomes active once HWC
-    // signals the present fence to confirm the mode set.
     mutable std::mutex mDesiredModeLock;
     DisplayModeRequestOpt mDesiredModeOpt GUARDED_BY(mDesiredModeLock);
     TracedOrdinal<bool> mHasDesiredModeTrace GUARDED_BY(mDesiredModeLock);
 
     DisplayModeRequestOpt mPendingModeOpt GUARDED_BY(kMainThreadContext);
+    bool mIsModeSetPending GUARDED_BY(kMainThreadContext) = false;
 };
 
 struct DisplayDeviceState {
diff --git a/services/surfaceflinger/DisplayHardware/HWComposer.cpp b/services/surfaceflinger/DisplayHardware/HWComposer.cpp
index 3ffd8ea..cf1c3c1 100644
--- a/services/surfaceflinger/DisplayHardware/HWComposer.cpp
+++ b/services/surfaceflinger/DisplayHardware/HWComposer.cpp
@@ -77,6 +77,8 @@
 using aidl::android::hardware::graphics::common::HdrConversionStrategy;
 using aidl::android::hardware::graphics::composer3::Capability;
 using aidl::android::hardware::graphics::composer3::DisplayCapability;
+using aidl::android::hardware::graphics::composer3::VrrConfig;
+using namespace std::string_literals;
 namespace hal = android::hardware::graphics::composer::hal;
 
 namespace android {
@@ -89,7 +91,8 @@
       : mComposer(std::move(composer)),
         mMaxVirtualDisplayDimension(static_cast<size_t>(sysprop::max_virtual_display_dimension(0))),
         mUpdateDeviceProductInfoOnHotplugReconnect(
-                sysprop::update_device_product_info_on_hotplug_reconnect(false)) {}
+                sysprop::update_device_product_info_on_hotplug_reconnect(false)),
+        mEnableVrrTimeout(base::GetBoolProperty("debug.sf.vrr_timeout_hint_enabled"s, false)) {}
 
 HWComposer::HWComposer(const std::string& composerServiceName)
       : HWComposer(Hwc2::Composer::create(composerServiceName)) {}
@@ -299,6 +302,10 @@
             hwcMode.dpiY = config.dpi->y;
         }
 
+        if (!mEnableVrrTimeout) {
+            hwcMode.vrrConfig->notifyExpectedPresentConfig = {};
+        }
+
         modes.push_back(hwcMode);
     }
 
diff --git a/services/surfaceflinger/DisplayHardware/HWComposer.h b/services/surfaceflinger/DisplayHardware/HWComposer.h
index 4ca528a..fb32ff4 100644
--- a/services/surfaceflinger/DisplayHardware/HWComposer.h
+++ b/services/surfaceflinger/DisplayHardware/HWComposer.h
@@ -491,6 +491,7 @@
 private:
     // For unit tests
     friend TestableSurfaceFlinger;
+    friend HWComposerTest;
 
     struct DisplayData {
         std::unique_ptr<HWC2::Display> hwcDisplay;
@@ -542,6 +543,7 @@
 
     const size_t mMaxVirtualDisplayDimension;
     const bool mUpdateDeviceProductInfoOnHotplugReconnect;
+    bool mEnableVrrTimeout;
 };
 
 } // namespace impl
diff --git a/services/surfaceflinger/DisplayHardware/Hal.h b/services/surfaceflinger/DisplayHardware/Hal.h
index 31c2833..e3d9622 100644
--- a/services/surfaceflinger/DisplayHardware/Hal.h
+++ b/services/surfaceflinger/DisplayHardware/Hal.h
@@ -169,10 +169,8 @@
         out << "}, ";
         out << "notifyExpectedPresentConfig={";
         if (vrrConfig->notifyExpectedPresentConfig) {
-            out << "notifyExpectedPresentHeadsUpNs="
-                << vrrConfig->notifyExpectedPresentConfig->notifyExpectedPresentHeadsUpNs
-                << ", notifyExpectedPresentTimeoutNs="
-                << vrrConfig->notifyExpectedPresentConfig->notifyExpectedPresentTimeoutNs;
+            out << "headsUpNs=" << vrrConfig->notifyExpectedPresentConfig->headsUpNs
+                << ", timeoutNs=" << vrrConfig->notifyExpectedPresentConfig->timeoutNs;
         }
         out << "}}";
         return out.str();
diff --git a/services/surfaceflinger/Scheduler/EventThread.cpp b/services/surfaceflinger/Scheduler/EventThread.cpp
index 6e862b4..c80c8fd 100644
--- a/services/surfaceflinger/Scheduler/EventThread.cpp
+++ b/services/surfaceflinger/Scheduler/EventThread.cpp
@@ -584,6 +584,9 @@
         case DisplayEventReceiver::DISPLAY_EVENT_HOTPLUG:
             return true;
 
+        case DisplayEventReceiver::DISPLAY_EVENT_HDCP_LEVELS_CHANGE:
+            return true;
+
         case DisplayEventReceiver::DISPLAY_EVENT_MODE_CHANGE: {
             return connection->mEventRegistration.test(
                     gui::ISurfaceComposer::EventRegistration::modeChanged);
diff --git a/services/surfaceflinger/Scheduler/Scheduler.cpp b/services/surfaceflinger/Scheduler/Scheduler.cpp
index bfc47e6..27ca17f 100644
--- a/services/surfaceflinger/Scheduler/Scheduler.cpp
+++ b/services/surfaceflinger/Scheduler/Scheduler.cpp
@@ -51,8 +51,12 @@
 #include "FrameRateOverrideMappings.h"
 #include "FrontEnd/LayerHandle.h"
 #include "OneShotTimer.h"
+#include "RefreshRateStats.h"
+#include "SurfaceFlingerFactory.h"
 #include "SurfaceFlingerProperties.h"
+#include "TimeStats/TimeStats.h"
 #include "VSyncTracker.h"
+#include "VsyncConfiguration.h"
 #include "VsyncController.h"
 #include "VsyncSchedule.h"
 
@@ -67,10 +71,14 @@
 namespace android::scheduler {
 
 Scheduler::Scheduler(ICompositor& compositor, ISchedulerCallback& callback, FeatureFlags features,
-                     sp<VsyncModulator> modulatorPtr, IVsyncTrackerCallback& vsyncTrackerCallback)
-      : impl::MessageQueue(compositor),
+                     surfaceflinger::Factory& factory, Fps activeRefreshRate, TimeStats& timeStats,
+                     IVsyncTrackerCallback& vsyncTrackerCallback)
+      : android::impl::MessageQueue(compositor),
         mFeatures(features),
-        mVsyncModulator(std::move(modulatorPtr)),
+        mVsyncConfiguration(factory.createVsyncConfiguration(activeRefreshRate)),
+        mVsyncModulator(sp<VsyncModulator>::make(mVsyncConfiguration->getCurrentConfigs())),
+        mRefreshRateStats(std::make_unique<RefreshRateStats>(timeStats, activeRefreshRate,
+                                                             hal::PowerMode::OFF)),
         mSchedulerCallback(callback),
         mVsyncTrackerCallback(vsyncTrackerCallback) {}
 
@@ -182,9 +190,9 @@
     const FrameTargeter::BeginFrameArgs beginFrameArgs =
             {.frameBeginTime = SchedulerClock::now(),
              .vsyncId = vsyncId,
-             // TODO(b/255601557): Calculate per display.
              .expectedVsyncTime = expectedVsyncTime,
-             .sfWorkDuration = mVsyncModulator->getVsyncConfig().sfWorkDuration};
+             .sfWorkDuration = mVsyncModulator->getVsyncConfig().sfWorkDuration,
+             .hwcMinWorkDuration = mVsyncConfiguration->getCurrentConfigs().hwcMinWorkDuration};
 
     ftl::NonNull<const Display*> pacesetterPtr = pacesetterPtrLocked();
     pacesetterPtr->targeterPtr->beginFrame(beginFrameArgs, *pacesetterPtr->schedulePtr);
@@ -193,11 +201,20 @@
         FrameTargets targets;
         targets.try_emplace(pacesetterPtr->displayId, &pacesetterPtr->targeterPtr->target());
 
+        // TODO (b/256196556): Followers should use the next VSYNC after the frontrunner, not the
+        // pacesetter.
+        // Update expectedVsyncTime, which may have been adjusted by beginFrame.
+        expectedVsyncTime = pacesetterPtr->targeterPtr->target().expectedPresentTime();
+
         for (const auto& [id, display] : mDisplays) {
             if (id == pacesetterPtr->displayId) continue;
 
+            auto followerBeginFrameArgs = beginFrameArgs;
+            followerBeginFrameArgs.expectedVsyncTime =
+                    display.schedulePtr->vsyncDeadlineAfter(expectedVsyncTime);
+
             FrameTargeter& targeter = *display.targeterPtr;
-            targeter.beginFrame(beginFrameArgs, *display.schedulePtr);
+            targeter.beginFrame(followerBeginFrameArgs, *display.schedulePtr);
             targets.try_emplace(id, &targeter.target());
         }
 
@@ -307,9 +324,10 @@
                                               frametimeline::TokenManager* tokenManager,
                                               std::chrono::nanoseconds workDuration,
                                               std::chrono::nanoseconds readyDuration) {
-    auto eventThread = std::make_unique<impl::EventThread>(cycle == Cycle::Render ? "app" : "appSf",
-                                                           getVsyncSchedule(), tokenManager, *this,
-                                                           workDuration, readyDuration);
+    auto eventThread =
+            std::make_unique<android::impl::EventThread>(cycle == Cycle::Render ? "app" : "appSf",
+                                                         getVsyncSchedule(), tokenManager, *this,
+                                                         workDuration, readyDuration);
 
     auto& handle = cycle == Cycle::Render ? mAppConnectionHandle : mSfConnectionHandle;
     handle = createConnection(std::move(eventThread));
@@ -495,8 +513,23 @@
     thread->setDuration(workDuration, readyDuration);
 }
 
-void Scheduler::setVsyncConfigSet(const VsyncConfigSet& configs, Period vsyncPeriod) {
-    setVsyncConfig(mVsyncModulator->setVsyncConfigSet(configs), vsyncPeriod);
+void Scheduler::updatePhaseConfiguration(Fps refreshRate) {
+    mRefreshRateStats->setRefreshRate(refreshRate);
+    mVsyncConfiguration->setRefreshRateFps(refreshRate);
+    setVsyncConfig(mVsyncModulator->setVsyncConfigSet(mVsyncConfiguration->getCurrentConfigs()),
+                   refreshRate.getPeriod());
+}
+
+void Scheduler::resetPhaseConfiguration(Fps refreshRate) {
+    // Cancel the pending refresh rate change, if any, before updating the phase configuration.
+    mVsyncModulator->cancelRefreshRateChange();
+
+    mVsyncConfiguration->reset();
+    updatePhaseConfiguration(refreshRate);
+}
+
+void Scheduler::setActiveDisplayPowerModeForRefreshRateStats(hal::PowerMode powerMode) {
+    mRefreshRateStats->setPowerMode(powerMode);
 }
 
 void Scheduler::setVsyncConfig(const VsyncConfig& config, Period vsyncPeriod) {
@@ -560,7 +593,7 @@
 
     // On main thread to serialize reads/writes of pending hardware VSYNC state.
     static_cast<void>(
-            schedule([=]() FTL_FAKE_GUARD(mDisplayLock) FTL_FAKE_GUARD(kMainThreadContext) {
+            schedule([=, this]() FTL_FAKE_GUARD(mDisplayLock) FTL_FAKE_GUARD(kMainThreadContext) {
                 ATRACE_NAME(ftl::Concat(whence, ' ', id.value, ' ', enabled).c_str());
 
                 if (const auto displayOpt = mDisplays.get(id)) {
@@ -869,6 +902,12 @@
     mFrameRateOverrideMappings.dump(dumper);
     dumper.eol();
 
+    mVsyncConfiguration->dump(dumper.out());
+    dumper.eol();
+
+    mRefreshRateStats->dump(dumper.out());
+    dumper.eol();
+
     {
         utils::Dumper::Section section(dumper, "Frame Targeting"sv);
 
diff --git a/services/surfaceflinger/Scheduler/Scheduler.h b/services/surfaceflinger/Scheduler/Scheduler.h
index a29d153..f62f1ba 100644
--- a/services/surfaceflinger/Scheduler/Scheduler.h
+++ b/services/surfaceflinger/Scheduler/Scheduler.h
@@ -88,23 +88,30 @@
 namespace android {
 
 class FenceTime;
+class TimeStats;
 
 namespace frametimeline {
 class TokenManager;
 } // namespace frametimeline
 
+namespace surfaceflinger {
+class Factory;
+} // namespace surfaceflinger
+
 namespace scheduler {
 
 using GlobalSignals = RefreshRateSelector::GlobalSignals;
 
+class RefreshRateStats;
+class VsyncConfiguration;
 class VsyncSchedule;
 
 class Scheduler : public IEventThreadCallback, android::impl::MessageQueue {
     using Impl = android::impl::MessageQueue;
 
 public:
-    Scheduler(ICompositor&, ISchedulerCallback&, FeatureFlags, sp<VsyncModulator>,
-              IVsyncTrackerCallback&);
+    Scheduler(ICompositor&, ISchedulerCallback&, FeatureFlags, surfaceflinger::Factory&,
+              Fps activeRefreshRate, TimeStats&, IVsyncTrackerCallback&);
     virtual ~Scheduler();
 
     void startTimers();
@@ -201,7 +208,10 @@
         }
     }
 
-    void setVsyncConfigSet(const VsyncConfigSet&, Period vsyncPeriod);
+    void updatePhaseConfiguration(Fps);
+    void resetPhaseConfiguration(Fps) REQUIRES(kMainThreadContext);
+
+    const VsyncConfiguration& getVsyncConfiguration() const { return *mVsyncConfiguration; }
 
     // Sets the render rate for the scheduler to run at.
     void setRenderRate(PhysicalDisplayId, Fps);
@@ -249,8 +259,10 @@
     // Indicates that touch interaction is taking place.
     void onTouchHint();
 
-    void setDisplayPowerMode(PhysicalDisplayId, hal::PowerMode powerMode)
-            REQUIRES(kMainThreadContext);
+    void setDisplayPowerMode(PhysicalDisplayId, hal::PowerMode) REQUIRES(kMainThreadContext);
+
+    // TODO(b/255635821): Track this per display.
+    void setActiveDisplayPowerModeForRefreshRateStats(hal::PowerMode) REQUIRES(kMainThreadContext);
 
     ConstVsyncSchedulePtr getVsyncSchedule(std::optional<PhysicalDisplayId> = std::nullopt) const
             EXCLUDES(mDisplayLock);
@@ -464,9 +476,14 @@
 
     const FeatureFlags mFeatures;
 
+    // Stores phase offsets configured per refresh rate.
+    const std::unique_ptr<VsyncConfiguration> mVsyncConfiguration;
+
     // Shifts the VSYNC phase during certain transactions and refresh rate changes.
     const sp<VsyncModulator> mVsyncModulator;
 
+    const std::unique_ptr<RefreshRateStats> mRefreshRateStats;
+
     // Used to choose refresh rate if content detection is enabled.
     LayerHistory mLayerHistory;
 
@@ -497,9 +514,7 @@
               : displayId(displayId),
                 selectorPtr(std::move(selectorPtr)),
                 schedulePtr(std::move(schedulePtr)),
-                targeterPtr(std::make_unique<
-                            FrameTargeter>(displayId,
-                                           features.test(Feature::kBackpressureGpuComposition))) {}
+                targeterPtr(std::make_unique<FrameTargeter>(displayId, features)) {}
 
         const PhysicalDisplayId displayId;
 
diff --git a/services/surfaceflinger/Scheduler/VSyncPredictor.cpp b/services/surfaceflinger/Scheduler/VSyncPredictor.cpp
index 33d3652..28e35de 100644
--- a/services/surfaceflinger/Scheduler/VSyncPredictor.cpp
+++ b/services/surfaceflinger/Scheduler/VSyncPredictor.cpp
@@ -405,7 +405,7 @@
             : std::nullopt;
     ALOGV("%s %s: DisplayMode %s notifyExpectedPresentTimeout %s", __func__, to_string(mId).c_str(),
           to_string(*modePtr).c_str(),
-          timeout ? std::to_string(timeout->notifyExpectedPresentTimeoutNs).c_str() : "N/A");
+          timeout ? std::to_string(timeout->timeoutNs).c_str() : "N/A");
     std::lock_guard lock(mMutex);
 
     mDisplayModePtr = modePtr;
diff --git a/services/surfaceflinger/Scheduler/include/scheduler/Features.h b/services/surfaceflinger/Scheduler/include/scheduler/Features.h
index 7c72ac6..52485fb 100644
--- a/services/surfaceflinger/Scheduler/include/scheduler/Features.h
+++ b/services/surfaceflinger/Scheduler/include/scheduler/Features.h
@@ -29,6 +29,7 @@
     kTracePredictedVsync = 1 << 3,
     kBackpressureGpuComposition = 1 << 4,
     kSmallDirtyContentDetection = 1 << 5,
+    kExpectedPresentTime = 1 << 6,
 };
 
 using FeatureFlags = ftl::Flags<Feature>;
diff --git a/services/surfaceflinger/Scheduler/include/scheduler/FrameTargeter.h b/services/surfaceflinger/Scheduler/include/scheduler/FrameTargeter.h
index 70d4846..a5bb6c2 100644
--- a/services/surfaceflinger/Scheduler/include/scheduler/FrameTargeter.h
+++ b/services/surfaceflinger/Scheduler/include/scheduler/FrameTargeter.h
@@ -19,11 +19,13 @@
 #include <array>
 #include <atomic>
 #include <memory>
+#include <optional>
 
 #include <ui/DisplayId.h>
 #include <ui/Fence.h>
 #include <ui/FenceTime.h>
 
+#include <scheduler/Features.h>
 #include <scheduler/Time.h>
 #include <scheduler/VsyncId.h>
 #include <scheduler/interface/CompositeResult.h>
@@ -49,14 +51,11 @@
 
     TimePoint expectedPresentTime() const { return mExpectedPresentTime; }
 
+    std::optional<TimePoint> earliestPresentTime() const { return mEarliestPresentTime; }
+
     // The time of the VSYNC that preceded this frame. See `presentFenceForPastVsync` for details.
     TimePoint pastVsyncTime(Period minFramePeriod) const;
 
-    // Equivalent to `pastVsyncTime` unless running N VSYNCs ahead.
-    TimePoint previousFrameVsyncTime(Period minFramePeriod) const {
-        return mExpectedPresentTime - minFramePeriod;
-    }
-
     // The present fence for the frame that had targeted the most recent VSYNC before this frame.
     // If the target VSYNC for any given frame is more than `vsyncPeriod` in the future, then the
     // VSYNC of at least one previous frame has not yet passed. In other words, this is NOT the
@@ -69,8 +68,6 @@
         return mPresentFences.front().fenceTime;
     }
 
-    bool wouldPresentEarly(Period minFramePeriod) const;
-
     bool isFramePending() const { return mFramePending; }
     bool didMissFrame() const { return mFrameMissed; }
     bool didMissHwcFrame() const { return mHwcFrameMissed && !mGpuFrameMissed; }
@@ -79,9 +76,17 @@
     explicit FrameTarget(const std::string& displayLabel);
     ~FrameTarget() = default;
 
+    bool wouldPresentEarly(Period minFramePeriod) const;
+
+    // Equivalent to `pastVsyncTime` unless running N VSYNCs ahead.
+    TimePoint previousFrameVsyncTime(Period minFramePeriod) const {
+        return mExpectedPresentTime - minFramePeriod;
+    }
+
     VsyncId mVsyncId;
     TimePoint mFrameBeginTime;
     TimePoint mExpectedPresentTime;
+    std::optional<TimePoint> mEarliestPresentTime;
 
     TracedOrdinal<bool> mFramePending;
     TracedOrdinal<bool> mFrameMissed;
@@ -95,6 +100,8 @@
     std::array<FenceWithFenceTime, 2> mPresentFences;
 
 private:
+    friend class FrameTargeterTestBase;
+
     template <int N>
     inline bool targetsVsyncsAhead(Period minFramePeriod) const {
         static_assert(N > 1);
@@ -105,9 +112,10 @@
 // Computes a display's per-frame metrics about past/upcoming targeting of present deadlines.
 class FrameTargeter final : private FrameTarget {
 public:
-    FrameTargeter(PhysicalDisplayId displayId, bool backpressureGpuComposition)
+    FrameTargeter(PhysicalDisplayId displayId, FeatureFlags flags)
           : FrameTarget(to_string(displayId)),
-            mBackpressureGpuComposition(backpressureGpuComposition) {}
+            mBackpressureGpuComposition(flags.test(Feature::kBackpressureGpuComposition)),
+            mSupportsExpectedPresentTime(flags.test(Feature::kExpectedPresentTime)) {}
 
     const FrameTarget& target() const { return *this; }
 
@@ -116,10 +124,14 @@
         VsyncId vsyncId;
         TimePoint expectedVsyncTime;
         Duration sfWorkDuration;
+        Duration hwcMinWorkDuration;
     };
 
     void beginFrame(const BeginFrameArgs&, const IVsyncSource&);
 
+    std::optional<TimePoint> computeEarliestPresentTime(Period minFramePeriod,
+                                                        Duration hwcMinWorkDuration);
+
     // TODO(b/241285191): Merge with FrameTargeter::endFrame.
     FenceTimePtr setPresentFence(sp<Fence>);
 
@@ -128,7 +140,7 @@
     void dump(utils::Dumper&) const;
 
 private:
-    friend class FrameTargeterTest;
+    friend class FrameTargeterTestBase;
 
     // For tests.
     using IsFencePendingFuncPtr = bool (*)(const FenceTimePtr&, int graceTimeMs);
@@ -138,6 +150,7 @@
     static bool isFencePending(const FenceTimePtr&, int graceTimeMs);
 
     const bool mBackpressureGpuComposition;
+    const bool mSupportsExpectedPresentTime;
 
     TimePoint mScheduledPresentTime;
     CompositionCoverageFlags mCompositionCoverage;
diff --git a/services/surfaceflinger/Scheduler/src/FrameTargeter.cpp b/services/surfaceflinger/Scheduler/src/FrameTargeter.cpp
index e80372b..68c277d 100644
--- a/services/surfaceflinger/Scheduler/src/FrameTargeter.cpp
+++ b/services/surfaceflinger/Scheduler/src/FrameTargeter.cpp
@@ -82,6 +82,10 @@
         }
     }
 
+    if (!mSupportsExpectedPresentTime) {
+        mEarliestPresentTime = computeEarliestPresentTime(minFramePeriod, args.hwcMinWorkDuration);
+    }
+
     ATRACE_FORMAT("%s %" PRId64 " vsyncIn %.2fms%s", __func__, ftl::to_underlying(args.vsyncId),
                   ticks<std::milli, float>(mExpectedPresentTime - TimePoint::now()),
                   mExpectedPresentTime == args.expectedVsyncTime ? "" : " (adjusted)");
@@ -121,6 +125,14 @@
     if (mGpuFrameMissed) mGpuFrameMissedCount++;
 }
 
+std::optional<TimePoint> FrameTargeter::computeEarliestPresentTime(Period minFramePeriod,
+                                                                   Duration hwcMinWorkDuration) {
+    if (wouldPresentEarly(minFramePeriod)) {
+        return previousFrameVsyncTime(minFramePeriod) - hwcMinWorkDuration;
+    }
+    return {};
+}
+
 void FrameTargeter::endFrame(const CompositeResult& result) {
     mCompositionCoverage = result.compositionCoverage;
 }
diff --git a/services/surfaceflinger/Scheduler/tests/FrameTargeterTest.cpp b/services/surfaceflinger/Scheduler/tests/FrameTargeterTest.cpp
index c883385..a9abcaf 100644
--- a/services/surfaceflinger/Scheduler/tests/FrameTargeterTest.cpp
+++ b/services/surfaceflinger/Scheduler/tests/FrameTargeterTest.cpp
@@ -44,12 +44,18 @@
 
 } // namespace
 
-class FrameTargeterTest : public testing::Test {
+class FrameTargeterTestBase : public testing::Test {
 public:
+    FrameTargeterTestBase(FeatureFlags flags) : mTargeter(PhysicalDisplayId::fromPort(13), flags) {}
+
     const auto& target() const { return mTargeter.target(); }
 
+    bool wouldPresentEarly(Period minFramePeriod) const {
+        return target().wouldPresentEarly(minFramePeriod);
+    }
+
     struct Frame {
-        Frame(FrameTargeterTest* testPtr, VsyncId vsyncId, TimePoint& frameBeginTime,
+        Frame(FrameTargeterTestBase* testPtr, VsyncId vsyncId, TimePoint& frameBeginTime,
               Duration frameDuration, Fps refreshRate, Fps peakRefreshRate,
               FrameTargeter::IsFencePendingFuncPtr isFencePendingFuncPtr = Frame::fenceSignaled,
               const ftl::Optional<VsyncSource>& vsyncSourceOpt = std::nullopt)
@@ -61,7 +67,8 @@
                                                      .vsyncId = vsyncId,
                                                      .expectedVsyncTime =
                                                              frameBeginTime + frameDuration,
-                                                     .sfWorkDuration = 10ms};
+                                                     .sfWorkDuration = 10ms,
+                                                     .hwcMinWorkDuration = kHwcMinWorkDuration};
 
             testPtr->mTargeter.beginFrame(args,
                                           vsyncSourceOpt
@@ -93,7 +100,7 @@
         static bool fencePending(const FenceTimePtr&, int) { return true; }
         static bool fenceSignaled(const FenceTimePtr&, int) { return false; }
 
-        FrameTargeterTest* const testPtr;
+        FrameTargeterTestBase* const testPtr;
 
         TimePoint& frameBeginTime;
         const Period period;
@@ -102,11 +109,24 @@
         bool ended = false;
     };
 
+    static constexpr Duration kHwcMinWorkDuration = std::chrono::nanoseconds(5ns);
+
 private:
     FenceToFenceTimeMap mFenceMap;
 
-    static constexpr bool kBackpressureGpuComposition = true;
-    FrameTargeter mTargeter{PhysicalDisplayId::fromPort(13), kBackpressureGpuComposition};
+    FrameTargeter mTargeter;
+};
+
+class FrameTargeterTest : public FrameTargeterTestBase {
+public:
+    FrameTargeterTest() : FrameTargeterTestBase(Feature::kBackpressureGpuComposition) {}
+};
+
+class FrameTargeterWithExpectedPresentSupportTest : public FrameTargeterTestBase {
+public:
+    FrameTargeterWithExpectedPresentSupportTest()
+          : FrameTargeterTestBase(FeatureFlags(Feature::kBackpressureGpuComposition) |
+                                  Feature::kExpectedPresentTime) {}
 };
 
 TEST_F(FrameTargeterTest, targetsFrames) {
@@ -208,7 +228,7 @@
 TEST_F(FrameTargeterTest, doesNotDetectEarlyPresentIfNoFence) {
     constexpr Period kPeriod = (60_Hz).getPeriod();
     EXPECT_EQ(target().presentFenceForPastVsync(kPeriod), FenceTime::NO_FENCE);
-    EXPECT_FALSE(target().wouldPresentEarly(kPeriod));
+    EXPECT_FALSE(wouldPresentEarly(kPeriod));
 }
 
 TEST_F(FrameTargeterTest, detectsEarlyPresent) {
@@ -220,7 +240,8 @@
     // The target is not early while past present fences are pending.
     for (int n = 3; n-- > 0;) {
         const Frame frame(this, vsyncId++, frameBeginTime, 10ms, kRefreshRate, kRefreshRate);
-        EXPECT_FALSE(target().wouldPresentEarly(kPeriod));
+        EXPECT_FALSE(wouldPresentEarly(kPeriod));
+        EXPECT_FALSE(target().earliestPresentTime());
     }
 
     // The target is early if the past present fence was signaled.
@@ -228,7 +249,41 @@
     const auto fence = frame.end();
     fence->signalForTest(frameBeginTime.ns());
 
-    EXPECT_TRUE(target().wouldPresentEarly(kPeriod));
+    Frame finalFrame(this, vsyncId++, frameBeginTime, 10ms, kRefreshRate, kRefreshRate);
+
+    // `finalFrame` would present early, so it has an earliest present time.
+    EXPECT_TRUE(wouldPresentEarly(kPeriod));
+    ASSERT_NE(std::nullopt, target().earliestPresentTime());
+    EXPECT_EQ(*target().earliestPresentTime(),
+              target().expectedPresentTime() - kPeriod - kHwcMinWorkDuration);
+}
+
+// Same as `detectsEarlyPresent`, above, but verifies that we do not set an earliest present time
+// when there is expected present time support.
+TEST_F(FrameTargeterWithExpectedPresentSupportTest, detectsEarlyPresent) {
+    VsyncId vsyncId{333};
+    TimePoint frameBeginTime(3000ms);
+    constexpr Fps kRefreshRate = 60_Hz;
+    constexpr Period kPeriod = kRefreshRate.getPeriod();
+
+    // The target is not early while past present fences are pending.
+    for (int n = 3; n-- > 0;) {
+        const Frame frame(this, vsyncId++, frameBeginTime, 10ms, kRefreshRate, kRefreshRate);
+        EXPECT_FALSE(wouldPresentEarly(kPeriod));
+        EXPECT_FALSE(target().earliestPresentTime());
+    }
+
+    // The target is early if the past present fence was signaled.
+    Frame frame(this, vsyncId++, frameBeginTime, 10ms, kRefreshRate, kRefreshRate);
+    const auto fence = frame.end();
+    fence->signalForTest(frameBeginTime.ns());
+
+    Frame finalFrame(this, vsyncId++, frameBeginTime, 10ms, kRefreshRate, kRefreshRate);
+
+    // `finalFrame` would present early, but we have expected present time support, so it has no
+    // earliest present time.
+    EXPECT_TRUE(wouldPresentEarly(kPeriod));
+    ASSERT_EQ(std::nullopt, target().earliestPresentTime());
 }
 
 TEST_F(FrameTargeterTest, detectsEarlyPresentTwoVsyncsAhead) {
@@ -240,7 +295,8 @@
     // The target is not early while past present fences are pending.
     for (int n = 3; n-- > 0;) {
         const Frame frame(this, vsyncId++, frameBeginTime, 10ms, kRefreshRate, kRefreshRate);
-        EXPECT_FALSE(target().wouldPresentEarly(kPeriod));
+        EXPECT_FALSE(wouldPresentEarly(kPeriod));
+        EXPECT_FALSE(target().earliestPresentTime());
     }
 
     Frame frame(this, vsyncId++, frameBeginTime, 10ms, kRefreshRate, kRefreshRate);
@@ -248,12 +304,18 @@
     fence->signalForTest(frameBeginTime.ns());
 
     // The target is two VSYNCs ahead, so the past present fence is still pending.
-    EXPECT_FALSE(target().wouldPresentEarly(kPeriod));
+    EXPECT_FALSE(wouldPresentEarly(kPeriod));
+    EXPECT_FALSE(target().earliestPresentTime());
 
     { const Frame frame(this, vsyncId++, frameBeginTime, 10ms, kRefreshRate, kRefreshRate); }
 
+    Frame finalFrame(this, vsyncId++, frameBeginTime, 10ms, kRefreshRate, kRefreshRate);
+
     // The target is early if the past present fence was signaled.
-    EXPECT_TRUE(target().wouldPresentEarly(kPeriod));
+    EXPECT_TRUE(wouldPresentEarly(kPeriod));
+    ASSERT_NE(std::nullopt, target().earliestPresentTime());
+    EXPECT_EQ(*target().earliestPresentTime(),
+              target().expectedPresentTime() - kPeriod - kHwcMinWorkDuration);
 }
 
 TEST_F(FrameTargeterTest, detectsEarlyPresentThreeVsyncsAhead) {
@@ -264,7 +326,10 @@
     const Frame frame(this, VsyncId{555}, frameBeginTime, 16ms, kRefreshRate, kRefreshRate);
 
     // The target is more than two VSYNCs ahead, but present fences are not tracked that far back.
-    EXPECT_TRUE(target().wouldPresentEarly(kPeriod));
+    EXPECT_TRUE(wouldPresentEarly(kPeriod));
+    EXPECT_TRUE(target().earliestPresentTime());
+    EXPECT_EQ(*target().earliestPresentTime(),
+              target().expectedPresentTime() - kPeriod - kHwcMinWorkDuration);
 }
 
 TEST_F(FrameTargeterTest, detectsMissedFrames) {
diff --git a/services/surfaceflinger/SurfaceFlinger.cpp b/services/surfaceflinger/SurfaceFlinger.cpp
index 138a9bb..c3bfb58 100644
--- a/services/surfaceflinger/SurfaceFlinger.cpp
+++ b/services/surfaceflinger/SurfaceFlinger.cpp
@@ -59,7 +59,6 @@
 #include <ftl/concat.h>
 #include <ftl/fake_guard.h>
 #include <ftl/future.h>
-#include <ftl/match.h>
 #include <ftl/unit.h>
 #include <gui/AidlStatusUtil.h>
 #include <gui/BufferQueue.h>
@@ -1109,7 +1108,7 @@
         outMode.peakRefreshRate = peakFps.getValue();
         outMode.vsyncRate = mode->getVsyncRate().getValue();
 
-        const auto vsyncConfigSet = mVsyncConfiguration->getConfigsForRefreshRate(
+        const auto vsyncConfigSet = mScheduler->getVsyncConfiguration().getConfigsForRefreshRate(
                 Fps::fromValue(outMode.peakRefreshRate));
         outMode.appVsyncOffset = vsyncConfigSet.late.appOffset;
         outMode.sfVsyncOffset = vsyncConfigSet.late.sfOffset;
@@ -1236,21 +1235,20 @@
     return NO_ERROR;
 }
 
-void SurfaceFlinger::setDesiredMode(display::DisplayModeRequest&& desiredMode, bool force) {
-    const auto mode = desiredMode.mode;
-    const auto displayId = mode.modePtr->getPhysicalDisplayId();
-
+void SurfaceFlinger::setDesiredMode(display::DisplayModeRequest&& request, bool force) {
+    const auto displayId = request.mode.modePtr->getPhysicalDisplayId();
     ATRACE_NAME(ftl::Concat(__func__, ' ', displayId.value).c_str());
 
     const auto display = getDisplayDeviceLocked(displayId);
     if (!display) {
-        ALOGW("%s: Unknown display %s", __func__, to_string(displayId).c_str());
+        ALOGW("%s: display is no longer valid", __func__);
         return;
     }
 
-    const bool emitEvent = desiredMode.emitEvent;
+    const auto mode = request.mode;
+    const bool emitEvent = request.emitEvent;
 
-    switch (display->setDesiredMode(std::move(desiredMode), force)) {
+    switch (display->setDesiredMode(std::move(request), force)) {
         case DisplayDevice::DesiredModeAction::InitiateDisplayModeSwitch:
             // DisplayDevice::setDesiredMode updated the render rate, so inform Scheduler.
             mScheduler->setRenderRate(displayId,
@@ -1269,7 +1267,7 @@
             mScheduler->modulateVsync(displayId, &VsyncModulator::onRefreshRateChangeInitiated);
 
             if (displayId == mActiveDisplayId) {
-                updatePhaseConfiguration(mode.fps);
+                mScheduler->updatePhaseConfiguration(mode.fps);
             }
 
             mScheduler->setModeChangePending(true);
@@ -1278,8 +1276,7 @@
             mScheduler->setRenderRate(displayId, mode.fps);
 
             if (displayId == mActiveDisplayId) {
-                updatePhaseConfiguration(mode.fps);
-                mRefreshRateStats->setRefreshRate(mode.fps);
+                mScheduler->updatePhaseConfiguration(mode.fps);
             }
 
             if (emitEvent) {
@@ -1346,56 +1343,61 @@
     const auto displayId = display.getPhysicalId();
     ATRACE_NAME(ftl::Concat(__func__, ' ', displayId.value).c_str());
 
-    ftl::match(
-            display.finalizeModeChange(),
-            [this, displayId](DisplayDevice::RefreshRateChange change) {
-                ftl::FakeGuard guard(mStateLock);
+    const auto pendingModeOpt = display.getPendingMode();
+    if (!pendingModeOpt) {
+        // There is no pending mode change. This can happen if the active
+        // display changed and the mode change happened on a different display.
+        return;
+    }
 
-                if (change.activeMode.emitEvent) {
-                    dispatchDisplayModeChangeEvent(displayId, change.activeMode.mode);
-                }
+    const auto& activeMode = pendingModeOpt->mode;
 
-                applyActiveMode(std::move(change.activeMode));
-            },
-            [&](DisplayDevice::ResolutionChange change) {
-                auto& state = mCurrentState.displays.editValueFor(display.getDisplayToken());
-                // Assign a new sequence ID to recreate the display and so its framebuffer.
-                state.sequenceId = DisplayDeviceState{}.sequenceId;
-                state.physical->activeMode = change.activeMode.mode.modePtr.get();
+    if (display.getActiveMode().modePtr->getResolution() != activeMode.modePtr->getResolution()) {
+        auto& state = mCurrentState.displays.editValueFor(display.getDisplayToken());
+        // We need to generate new sequenceId in order to recreate the display (and this
+        // way the framebuffer).
+        state.sequenceId = DisplayDeviceState{}.sequenceId;
+        state.physical->activeMode = activeMode.modePtr.get();
+        processDisplayChangesLocked();
 
-                ftl::FakeGuard guard1(kMainThreadContext);
-                ftl::FakeGuard guard2(mStateLock);
-                processDisplayChangesLocked();
+        // processDisplayChangesLocked will update all necessary components so we're done here.
+        return;
+    }
 
-                applyActiveMode(std::move(change.activeMode));
-            },
-            [](DisplayDevice::NoModeChange noChange) {
-                // TODO(b/255635821): Remove this case, as it should no longer happen.
-                ALOGE("A mode change was initiated but not finalized: %s", noChange.reason);
-            });
+    display.finalizeModeChange(activeMode.modePtr->getId(), activeMode.modePtr->getVsyncRate(),
+                               activeMode.fps);
+
+    if (displayId == mActiveDisplayId) {
+        mScheduler->updatePhaseConfiguration(activeMode.fps);
+    }
+
+    if (pendingModeOpt->emitEvent) {
+        dispatchDisplayModeChangeEvent(displayId, activeMode);
+    }
 }
 
-void SurfaceFlinger::dropModeRequest(display::DisplayModeRequest&& request) {
-    if (request.mode.modePtr->getPhysicalDisplayId() == mActiveDisplayId) {
+void SurfaceFlinger::dropModeRequest(const sp<DisplayDevice>& display) {
+    display->clearDesiredMode();
+    if (display->getPhysicalId() == mActiveDisplayId) {
         // TODO(b/255635711): Check for pending mode changes on other displays.
         mScheduler->setModeChangePending(false);
     }
 }
 
-void SurfaceFlinger::applyActiveMode(display::DisplayModeRequest&& activeMode) {
-    auto activeModePtr = activeMode.mode.modePtr;
+void SurfaceFlinger::applyActiveMode(const sp<DisplayDevice>& display) {
+    const auto activeModeOpt = display->getDesiredMode();
+    auto activeModePtr = activeModeOpt->mode.modePtr;
     const auto displayId = activeModePtr->getPhysicalDisplayId();
-    const auto renderFps = activeMode.mode.fps;
+    const auto renderFps = activeModeOpt->mode.fps;
 
-    dropModeRequest(std::move(activeMode));
+    dropModeRequest(display);
 
     constexpr bool kAllowToEnable = true;
     mScheduler->resyncToHardwareVsync(displayId, kAllowToEnable, std::move(activeModePtr).take());
     mScheduler->setRenderRate(displayId, renderFps);
 
     if (displayId == mActiveDisplayId) {
-        mRefreshRateStats->setRefreshRate(renderFps);
-        updatePhaseConfiguration(renderFps);
+        mScheduler->updatePhaseConfiguration(renderFps);
     }
 }
 
@@ -1408,50 +1410,50 @@
         const auto display = getDisplayDeviceLocked(id);
         if (!display) continue;
 
-        auto desiredModeOpt = display->takeDesiredMode();
+        auto desiredModeOpt = display->getDesiredMode();
         if (!desiredModeOpt) {
             continue;
         }
 
-        auto desiredMode = std::move(*desiredModeOpt);
-
         if (!shouldApplyRefreshRateSelectorPolicy(*display)) {
-            dropModeRequest(std::move(desiredMode));
+            dropModeRequest(display);
             continue;
         }
 
-        const auto desiredModeId = desiredMode.mode.modePtr->getId();
+        const auto desiredModeId = desiredModeOpt->mode.modePtr->getId();
         const auto displayModePtrOpt = physical.snapshot().displayModes().get(desiredModeId);
 
         if (!displayModePtrOpt) {
-            ALOGW("%s: Unknown mode %d for display %s", __func__, desiredModeId.value(),
-                  to_string(id).c_str());
-            dropModeRequest(std::move(desiredMode));
+            ALOGW("Desired display mode is no longer supported. Mode ID = %d",
+                  desiredModeId.value());
+            dropModeRequest(display);
             continue;
         }
 
-        if (display->getActiveMode() == desiredMode.mode) {
-            dropModeRequest(std::move(desiredMode));
+        ALOGV("%s changing active mode to %d(%s) for display %s", __func__, desiredModeId.value(),
+              to_string(displayModePtrOpt->get()->getVsyncRate()).c_str(),
+              to_string(display->getId()).c_str());
+
+        if (display->getActiveMode() == desiredModeOpt->mode) {
+            applyActiveMode(display);
             continue;
         }
 
-        // The desired mode is different from the active mode. However, the allowed modes might have
-        // changed since setDesiredMode scheduled a mode transition.
-        if (!display->refreshRateSelector().isModeAllowed(desiredMode.mode)) {
-            dropModeRequest(std::move(desiredMode));
+        // Desired active mode was set, it is different than the mode currently in use, however
+        // allowed modes might have changed by the time we process the refresh.
+        // Make sure the desired mode is still allowed
+        if (!display->refreshRateSelector().isModeAllowed(desiredModeOpt->mode)) {
+            dropModeRequest(display);
             continue;
         }
 
-        ALOGV("Mode setting display %s to %d (%s)", to_string(id).c_str(), desiredModeId.value(),
-              to_string(displayModePtrOpt.value().get()->getVsyncRate()).c_str());
-
-        // TODO(b/142753666): Use constraints.
+        // TODO(b/142753666) use constrains
         hal::VsyncPeriodChangeConstraints constraints;
         constraints.desiredTimeNanos = systemTime();
         constraints.seamlessRequired = false;
         hal::VsyncPeriodChangeTimeline outTimeline;
 
-        if (!display->initiateModeChange(std::move(desiredMode), constraints, outTimeline)) {
+        if (!display->initiateModeChange(std::move(*desiredModeOpt), constraints, outTimeline)) {
             continue;
         }
 
@@ -1471,6 +1473,11 @@
     if (displayToUpdateImmediately) {
         const auto display = getDisplayDeviceLocked(*displayToUpdateImmediately);
         finalizeDisplayModeChange(*display);
+
+        const auto desiredModeOpt = display->getDesiredMode();
+        if (desiredModeOpt && display->getActiveMode() == desiredModeOpt->mode) {
+            applyActiveMode(display);
+        }
     }
 }
 
@@ -2674,6 +2681,8 @@
         if (const auto display = getCompositionDisplayLocked(id)) {
             refreshArgs.outputs.push_back(display);
         }
+
+        refreshArgs.frameTargets.try_emplace(id, &targeter->target());
     }
 
     std::vector<DisplayId> displayIds;
@@ -2742,23 +2751,10 @@
         refreshArgs.devOptFlashDirtyRegionsDelay = std::chrono::milliseconds(mDebugFlashDelay);
     }
 
-    const Period minFramePeriod = mScheduler->getVsyncSchedule()->minFramePeriod();
-
-    if (!getHwComposer().getComposer()->isSupported(
-                Hwc2::Composer::OptionalFeature::ExpectedPresentTime) &&
-        pacesetterTarget.wouldPresentEarly(minFramePeriod)) {
-        const auto hwcMinWorkDuration = mVsyncConfiguration->getCurrentConfigs().hwcMinWorkDuration;
-
-        // TODO(b/255601557): Calculate and pass per-display values for each FrameTarget.
-        refreshArgs.earliestPresentTime =
-                pacesetterTarget.previousFrameVsyncTime(minFramePeriod) - hwcMinWorkDuration;
-    }
-
     const TimePoint expectedPresentTime = pacesetterTarget.expectedPresentTime();
     // TODO(b/255601557) Update frameInterval per display
     refreshArgs.frameInterval = mScheduler->getNextFrameInterval(pacesetterId, expectedPresentTime);
     refreshArgs.scheduledFrameTime = mScheduler->getScheduledFrameTime();
-    refreshArgs.expectedPresentTime = expectedPresentTime.ns();
     refreshArgs.hasTrustedPresentationListener = mNumTrustedPresentationListeners > 0;
     {
         auto& notifyExpectedPresentData = mNotifyExpectedPresentMap[pacesetterId];
@@ -3055,7 +3051,8 @@
     const auto schedule = mScheduler->getVsyncSchedule();
     const TimePoint vsyncDeadline = schedule->vsyncDeadlineAfter(presentTime);
     const Period vsyncPeriod = schedule->period();
-    const nsecs_t vsyncPhase = mVsyncConfiguration->getCurrentConfigs().late.sfOffset;
+    const nsecs_t vsyncPhase =
+            mScheduler->getVsyncConfiguration().getCurrentConfigs().late.sfOffset;
 
     const CompositorTiming compositorTiming(vsyncDeadline.ns(), vsyncPeriod.ns(), vsyncPhase,
                                             presentLatency.ns());
@@ -3732,7 +3729,7 @@
 
             // TODO(b/175678251) Call a listener instead.
             if (currentState.physical->hwcDisplayId == getHwComposer().getPrimaryHwcDisplayId()) {
-                resetPhaseConfiguration(display->getActiveMode().fps);
+                mScheduler->resetPhaseConfiguration(display->getActiveMode().fps);
             }
         }
         return;
@@ -3768,15 +3765,6 @@
     }
 }
 
-void SurfaceFlinger::resetPhaseConfiguration(Fps refreshRate) {
-    // Cancel the pending refresh rate change, if any, before updating the phase configuration.
-    mScheduler->vsyncModulator().cancelRefreshRateChange();
-
-    mVsyncConfiguration->reset();
-    updatePhaseConfiguration(refreshRate);
-    mRefreshRateStats->setRefreshRate(refreshRate);
-}
-
 void SurfaceFlinger::processDisplayChangesLocked() {
     // here we take advantage of Vector's copy-on-write semantics to
     // improve performance by skipping the transaction entirely when
@@ -4133,7 +4121,7 @@
         const auto notifyExpectedPresentConfig =
                 modePtr->getVrrConfig()->notifyExpectedPresentConfig;
         if (!notifyExpectedPresentConfig) return std::nullopt;
-        return Period::fromNs(notifyExpectedPresentConfig->notifyExpectedPresentTimeoutNs);
+        return Period::fromNs(notifyExpectedPresentConfig->timeoutNs);
     }();
 
     notifyExpectedPresentIfRequired(modePtr->getPhysicalDisplayId(), vsyncPeriod,
@@ -4193,10 +4181,6 @@
 
     const auto activeMode = display->refreshRateSelector().getActiveMode();
     const Fps activeRefreshRate = activeMode.fps;
-    mRefreshRateStats =
-            std::make_unique<RefreshRateStats>(*mTimeStats, activeRefreshRate, hal::PowerMode::OFF);
-
-    mVsyncConfiguration = getFactory().createVsyncConfiguration(activeRefreshRate);
 
     FeatureFlags features;
 
@@ -4222,12 +4206,14 @@
     if (mBackpressureGpuComposition) {
         features |= Feature::kBackpressureGpuComposition;
     }
-
-    auto modulatorPtr = sp<VsyncModulator>::make(mVsyncConfiguration->getCurrentConfigs());
+    if (getHwComposer().getComposer()->isSupported(
+                Hwc2::Composer::OptionalFeature::ExpectedPresentTime)) {
+        features |= Feature::kExpectedPresentTime;
+    }
 
     mScheduler = std::make_unique<Scheduler>(static_cast<ICompositor&>(*this),
                                              static_cast<ISchedulerCallback&>(*this), features,
-                                             std::move(modulatorPtr),
+                                             getFactory(), activeRefreshRate, *mTimeStats,
                                              static_cast<IVsyncTrackerCallback&>(*this));
     mScheduler->registerDisplay(display->getPhysicalId(), display->holdRefreshRateSelector());
     if (FlagManager::getInstance().vrr_config()) {
@@ -4235,7 +4221,7 @@
     }
     mScheduler->startTimers();
 
-    const auto configs = mVsyncConfiguration->getCurrentConfigs();
+    const auto configs = mScheduler->getVsyncConfiguration().getCurrentConfigs();
 
     mAppConnectionHandle =
             mScheduler->createEventThread(Scheduler::Cycle::Render,
@@ -4257,12 +4243,6 @@
     mFpsReporter = sp<FpsReporter>::make(*mFrameTimeline, *this);
 }
 
-void SurfaceFlinger::updatePhaseConfiguration(Fps refreshRate) {
-    mVsyncConfiguration->setRefreshRateFps(refreshRate);
-    mScheduler->setVsyncConfigSet(mVsyncConfiguration->getCurrentConfigs(),
-                                  refreshRate.getPeriod());
-}
-
 void SurfaceFlinger::doCommitTransactions() {
     ATRACE_CALL();
 
@@ -6017,7 +5997,7 @@
 
     if (displayId == mActiveDisplayId) {
         mTimeStats->setPowerMode(mode);
-        mRefreshRateStats->setPowerMode(mode);
+        mScheduler->setActiveDisplayPowerModeForRefreshRateStats(mode);
     }
 
     mScheduler->setDisplayPowerMode(displayId, mode);
@@ -6182,10 +6162,6 @@
     dumper.dump("debugDisplayModeSetByBackdoor"sv, mDebugDisplayModeSetByBackdoor);
     dumper.eol();
 
-    mRefreshRateStats->dump(result);
-    dumper.eol();
-
-    mVsyncConfiguration->dump(result);
     StringAppendF(&result,
                   "         present offset: %9" PRId64 " ns\t        VSYNC period: %9" PRId64
                   " ns\n\n",
@@ -7395,7 +7371,7 @@
     if (!updateOverlay) return;
 
     // Update the overlay on the main thread to avoid race conditions with
-    // RefreshRateSelector::getActiveMode.
+    // RefreshRateSelector::getActiveMode
     static_cast<void>(mScheduler->schedule([=, this] {
         const auto display = FTL_FAKE_GUARD(mStateLock, getDefaultDisplayDeviceLocked());
         if (!display) {
@@ -8625,7 +8601,8 @@
 }
 
 int SurfaceFlinger::getMaxAcquiredBufferCountForRefreshRate(Fps refreshRate) const {
-    const auto vsyncConfig = mVsyncConfiguration->getConfigsForRefreshRate(refreshRate).late;
+    const auto vsyncConfig =
+            mScheduler->getVsyncConfiguration().getConfigsForRefreshRate(refreshRate).late;
     const auto presentLatency = vsyncConfig.appWorkDuration + vsyncConfig.sfWorkDuration;
     return calculateMaxAcquiredBufferCount(refreshRate, presentLatency);
 }
@@ -8718,7 +8695,7 @@
     mActiveDisplayId = activeDisplay.getPhysicalId();
     activeDisplay.getCompositionDisplay()->setLayerCachingTexturePoolEnabled(true);
 
-    resetPhaseConfiguration(activeDisplay.getActiveMode().fps);
+    mScheduler->resetPhaseConfiguration(activeDisplay.getActiveMode().fps);
 
     // TODO(b/255635711): Check for pending mode changes on other displays.
     mScheduler->setModeChangePending(false);
diff --git a/services/surfaceflinger/SurfaceFlinger.h b/services/surfaceflinger/SurfaceFlinger.h
index 4440c7b..5846214 100644
--- a/services/surfaceflinger/SurfaceFlinger.h
+++ b/services/surfaceflinger/SurfaceFlinger.h
@@ -82,7 +82,6 @@
 #include "MutexUtils.h"
 #include "Scheduler/ISchedulerCallback.h"
 #include "Scheduler/RefreshRateSelector.h"
-#include "Scheduler/RefreshRateStats.h"
 #include "Scheduler/Scheduler.h"
 #include "SurfaceFlingerFactory.h"
 #include "ThreadContext.h"
@@ -727,9 +726,9 @@
     void initiateDisplayModeChanges() REQUIRES(mStateLock, kMainThreadContext);
     void finalizeDisplayModeChange(DisplayDevice&) REQUIRES(mStateLock, kMainThreadContext);
 
-    // TODO(b/241285191): Move to Scheduler.
-    void dropModeRequest(display::DisplayModeRequest&&) REQUIRES(mStateLock);
-    void applyActiveMode(display::DisplayModeRequest&&) REQUIRES(mStateLock);
+    // TODO(b/241285191): Replace DisplayDevice with DisplayModeRequest, and move to Scheduler.
+    void dropModeRequest(const sp<DisplayDevice>&) REQUIRES(mStateLock);
+    void applyActiveMode(const sp<DisplayDevice>&) REQUIRES(mStateLock);
 
     // Called on the main thread in response to setPowerMode()
     void setPowerModeInternal(const sp<DisplayDevice>& display, hal::PowerMode mode)
@@ -785,9 +784,6 @@
 
     void initScheduler(const sp<const DisplayDevice>&) REQUIRES(kMainThreadContext, mStateLock);
 
-    void resetPhaseConfiguration(Fps) REQUIRES(mStateLock, kMainThreadContext);
-    void updatePhaseConfiguration(Fps) REQUIRES(mStateLock);
-
     /*
      * Transactions
      */
@@ -1374,10 +1370,6 @@
     scheduler::ConnectionHandle mAppConnectionHandle;
     scheduler::ConnectionHandle mSfConnectionHandle;
 
-    // Stores phase offsets configured per refresh rate.
-    std::unique_ptr<scheduler::VsyncConfiguration> mVsyncConfiguration;
-
-    std::unique_ptr<scheduler::RefreshRateStats> mRefreshRateStats;
     scheduler::PresentLatencyTracker mPresentLatencyTracker GUARDED_BY(kMainThreadContext);
 
     bool mLumaSampling = true;
diff --git a/services/surfaceflinger/Utils/Dumper.h b/services/surfaceflinger/Utils/Dumper.h
index ee94217..62d2ebb 100644
--- a/services/surfaceflinger/Utils/Dumper.h
+++ b/services/surfaceflinger/Utils/Dumper.h
@@ -35,6 +35,8 @@
 
     void eol() { mOut += '\n'; }
 
+    std::string& out() { return mOut; }
+
     void dump(std::string_view name, std::string_view value = {}) {
         using namespace std::string_view_literals;
 
diff --git a/services/surfaceflinger/fuzzer/surfaceflinger_fuzzers_utils.h b/services/surfaceflinger/fuzzer/surfaceflinger_fuzzers_utils.h
index 4fc39cc..fa79956 100644
--- a/services/surfaceflinger/fuzzer/surfaceflinger_fuzzers_utils.h
+++ b/services/surfaceflinger/fuzzer/surfaceflinger_fuzzers_utils.h
@@ -224,20 +224,13 @@
 
 class TestableScheduler : public Scheduler, private ICompositor {
 public:
-    TestableScheduler(const std::shared_ptr<scheduler::RefreshRateSelector>& selectorPtr,
-                      sp<VsyncModulator> modulatorPtr, ISchedulerCallback& callback,
-                      IVsyncTrackerCallback& vsyncTrackerCallback)
-          : TestableScheduler(std::make_unique<android::mock::VsyncController>(),
-                              std::make_shared<android::mock::VSyncTracker>(), selectorPtr,
-                              std::move(modulatorPtr), callback, vsyncTrackerCallback) {}
-
     TestableScheduler(std::unique_ptr<VsyncController> controller,
                       VsyncSchedule::TrackerPtr tracker,
                       std::shared_ptr<RefreshRateSelector> selectorPtr,
-                      sp<VsyncModulator> modulatorPtr, ISchedulerCallback& callback,
-                      IVsyncTrackerCallback& vsyncTrackerCallback)
-          : Scheduler(*this, callback, Feature::kContentDetection, std::move(modulatorPtr),
-                      vsyncTrackerCallback) {
+                      surfaceflinger::Factory& factory, TimeStats& timeStats,
+                      ISchedulerCallback& callback, IVsyncTrackerCallback& vsyncTrackerCallback)
+          : Scheduler(*this, callback, Feature::kContentDetection, factory,
+                      selectorPtr->getActiveMode().fps, timeStats, vsyncTrackerCallback) {
         const auto displayId = selectorPtr->getActiveMode().modePtr->getPhysicalDisplayId();
         registerDisplayInternal(displayId, std::move(selectorPtr),
                                 std::shared_ptr<VsyncSchedule>(
@@ -613,7 +606,14 @@
             mFlinger->commitTransactions();
             mFlinger->flushTransactionQueues(getFuzzedVsyncId(mFdp));
 
-            scheduler::FrameTargeter frameTargeter(displayId, mFdp.ConsumeBool());
+            scheduler::FeatureFlags flags;
+            if (mFdp.ConsumeBool()) {
+                flags |= scheduler::Feature::kBackpressureGpuComposition;
+            }
+            if (mFdp.ConsumeBool()) {
+                flags |= scheduler::Feature::kExpectedPresentTime;
+            }
+            scheduler::FrameTargeter frameTargeter(displayId, flags);
             mFlinger->onCompositionPresented(displayId, ftl::init::map(displayId, &frameTargeter),
                                              mFdp.ConsumeIntegral<nsecs_t>());
         }
@@ -671,19 +671,11 @@
         }
 
         mRefreshRateSelector = std::make_shared<scheduler::RefreshRateSelector>(modes, kModeId60);
-        const auto fps = mRefreshRateSelector->getActiveMode().modePtr->getVsyncRate();
-        mFlinger->mVsyncConfiguration = mFactory.createVsyncConfiguration(fps);
-
-        mFlinger->mRefreshRateStats =
-                std::make_unique<scheduler::RefreshRateStats>(*mFlinger->mTimeStats, fps,
-                                                              hal::PowerMode::OFF);
-
-        auto modulatorPtr = sp<scheduler::VsyncModulator>::make(
-                mFlinger->mVsyncConfiguration->getCurrentConfigs());
 
         mScheduler = new scheduler::TestableScheduler(std::move(vsyncController),
                                                       std::move(vsyncTracker), mRefreshRateSelector,
-                                                      std::move(modulatorPtr), *(callback ?: this),
+                                                      mFactory, *mFlinger->mTimeStats,
+                                                      *(callback ?: this),
                                                       *(vsyncTrackerCallback ?: this));
 
         mFlinger->mAppConnectionHandle = mScheduler->createConnection(std::move(appEventThread));
diff --git a/services/surfaceflinger/fuzzer/surfaceflinger_scheduler_fuzzer.cpp b/services/surfaceflinger/fuzzer/surfaceflinger_scheduler_fuzzer.cpp
index 649ad25..8a5500f 100644
--- a/services/surfaceflinger/fuzzer/surfaceflinger_scheduler_fuzzer.cpp
+++ b/services/surfaceflinger/fuzzer/surfaceflinger_scheduler_fuzzer.cpp
@@ -24,6 +24,7 @@
 
 #include "Scheduler/OneShotTimer.h"
 #include "Scheduler/RefreshRateSelector.h"
+#include "Scheduler/RefreshRateStats.h"
 #include "Scheduler/VSyncDispatchTimerQueue.h"
 #include "Scheduler/VSyncPredictor.h"
 #include "Scheduler/VSyncReactor.h"
@@ -425,7 +426,15 @@
 }
 
 void SchedulerFuzzer::fuzzFrameTargeter() {
-    scheduler::FrameTargeter frameTargeter(kDisplayId, mFdp.ConsumeBool());
+    scheduler::FeatureFlags flags;
+    if (mFdp.ConsumeBool()) {
+        flags |= scheduler::Feature::kBackpressureGpuComposition;
+    }
+    if (mFdp.ConsumeBool()) {
+        flags |= scheduler::Feature::kExpectedPresentTime;
+    }
+
+    scheduler::FrameTargeter frameTargeter(kDisplayId, flags);
 
     const struct VsyncSource final : scheduler::IVsyncSource {
         explicit VsyncSource(FuzzedDataProvider& fuzzer) : fuzzer(fuzzer) {}
@@ -441,7 +450,8 @@
         frameTargeter.beginFrame({.frameBeginTime = getFuzzedTimePoint(mFdp),
                                   .vsyncId = getFuzzedVsyncId(mFdp),
                                   .expectedVsyncTime = getFuzzedTimePoint(mFdp),
-                                  .sfWorkDuration = getFuzzedDuration(mFdp)},
+                                  .sfWorkDuration = getFuzzedDuration(mFdp),
+                                  .hwcMinWorkDuration = getFuzzedDuration(mFdp)},
                                  vsyncSource);
 
         frameTargeter.setPresentFence(makeFakeFence());
diff --git a/services/surfaceflinger/tests/unittests/Android.bp b/services/surfaceflinger/tests/unittests/Android.bp
index c75f902..5809ea0 100644
--- a/services/surfaceflinger/tests/unittests/Android.bp
+++ b/services/surfaceflinger/tests/unittests/Android.bp
@@ -118,6 +118,7 @@
         "RefreshRateSelectorTest.cpp",
         "RefreshRateStatsTest.cpp",
         "RegionSamplingTest.cpp",
+        "TestableScheduler.cpp",
         "TimeStatsTest.cpp",
         "FrameTracerTest.cpp",
         "TransactionApplicationTest.cpp",
diff --git a/services/surfaceflinger/tests/unittests/DisplayDevice_InitiateModeChange.cpp b/services/surfaceflinger/tests/unittests/DisplayDevice_InitiateModeChange.cpp
index 220001b..c463a92 100644
--- a/services/surfaceflinger/tests/unittests/DisplayDevice_InitiateModeChange.cpp
+++ b/services/surfaceflinger/tests/unittests/DisplayDevice_InitiateModeChange.cpp
@@ -23,13 +23,10 @@
 #include <gmock/gmock.h>
 #include <gtest/gtest.h>
 
-#define EXPECT_DISPLAY_MODE_REQUEST(expected, request)                              \
-    EXPECT_FRAME_RATE_MODE(expected.mode.modePtr, expected.mode.fps, request.mode); \
-    EXPECT_EQ(expected.emitEvent, request.emitEvent)
-
-#define EXPECT_DISPLAY_MODE_REQUEST_OPT(expected, requestOpt) \
-    ASSERT_TRUE(requestOpt);                                  \
-    EXPECT_DISPLAY_MODE_REQUEST(expected, (*requestOpt))
+#define EXPECT_DISPLAY_MODE_REQUEST(expected, requestOpt)                               \
+    ASSERT_TRUE(requestOpt);                                                            \
+    EXPECT_FRAME_RATE_MODE(expected.mode.modePtr, expected.mode.fps, requestOpt->mode); \
+    EXPECT_EQ(expected.emitEvent, requestOpt->emitEvent)
 
 namespace android {
 namespace {
@@ -40,7 +37,6 @@
 class InitiateModeChangeTest : public DisplayTransactionTest {
 public:
     using Action = DisplayDevice::DesiredModeAction;
-
     void SetUp() override {
         injectFakeBufferQueueFactory();
         injectFakeNativeWindowSurfaceFactory();
@@ -88,43 +84,36 @@
 TEST_F(InitiateModeChangeTest, setDesiredMode) {
     EXPECT_EQ(Action::InitiateDisplayModeSwitch,
               mDisplay->setDesiredMode(DisplayModeRequest(kDesiredMode90)));
-    EXPECT_DISPLAY_MODE_REQUEST_OPT(kDesiredMode90, mDisplay->getDesiredMode());
+    EXPECT_DISPLAY_MODE_REQUEST(kDesiredMode90, mDisplay->getDesiredMode());
 
     EXPECT_EQ(Action::None, mDisplay->setDesiredMode(DisplayModeRequest(kDesiredMode120)));
-    EXPECT_DISPLAY_MODE_REQUEST_OPT(kDesiredMode120, mDisplay->getDesiredMode());
+    EXPECT_DISPLAY_MODE_REQUEST(kDesiredMode120, mDisplay->getDesiredMode());
 }
 
-TEST_F(InitiateModeChangeTest, takeDesiredMode) {
+TEST_F(InitiateModeChangeTest, clearDesiredMode) {
     EXPECT_EQ(Action::InitiateDisplayModeSwitch,
               mDisplay->setDesiredMode(DisplayModeRequest(kDesiredMode90)));
-    EXPECT_EQ(kDesiredMode90, mDisplay->getDesiredMode());
+    EXPECT_TRUE(mDisplay->getDesiredMode());
 
-    EXPECT_EQ(kDesiredMode90, mDisplay->takeDesiredMode());
+    mDisplay->clearDesiredMode();
     EXPECT_FALSE(mDisplay->getDesiredMode());
 }
 
-TEST_F(InitiateModeChangeTest, initiateModeChange) FTL_FAKE_GUARD(kMainThreadContext) {
+TEST_F(InitiateModeChangeTest, initiateModeChange) REQUIRES(kMainThreadContext) {
     EXPECT_EQ(Action::InitiateDisplayModeSwitch,
               mDisplay->setDesiredMode(DisplayModeRequest(kDesiredMode90)));
-    EXPECT_DISPLAY_MODE_REQUEST_OPT(kDesiredMode90, mDisplay->getDesiredMode());
+    EXPECT_DISPLAY_MODE_REQUEST(kDesiredMode90, mDisplay->getDesiredMode());
 
     const hal::VsyncPeriodChangeConstraints constraints{
             .desiredTimeNanos = systemTime(),
             .seamlessRequired = false,
     };
     hal::VsyncPeriodChangeTimeline timeline;
+    EXPECT_TRUE(mDisplay->initiateModeChange(*mDisplay->getDesiredMode(), constraints, timeline));
+    EXPECT_DISPLAY_MODE_REQUEST(kDesiredMode90, mDisplay->getPendingMode());
 
-    auto desiredModeOpt = mDisplay->takeDesiredMode();
-    ASSERT_TRUE(desiredModeOpt);
+    mDisplay->clearDesiredMode();
     EXPECT_FALSE(mDisplay->getDesiredMode());
-
-    EXPECT_TRUE(mDisplay->initiateModeChange(std::move(*desiredModeOpt), constraints, timeline));
-
-    auto modeChange = mDisplay->finalizeModeChange();
-
-    auto* changePtr = std::get_if<DisplayDevice::RefreshRateChange>(&modeChange);
-    ASSERT_TRUE(changePtr);
-    EXPECT_DISPLAY_MODE_REQUEST(kDesiredMode90, changePtr->activeMode);
 }
 
 TEST_F(InitiateModeChangeTest, initiateRenderRateSwitch) {
@@ -136,47 +125,27 @@
 TEST_F(InitiateModeChangeTest, initiateDisplayModeSwitch) FTL_FAKE_GUARD(kMainThreadContext) {
     EXPECT_EQ(Action::InitiateDisplayModeSwitch,
               mDisplay->setDesiredMode(DisplayModeRequest(kDesiredMode90)));
-    EXPECT_DISPLAY_MODE_REQUEST_OPT(kDesiredMode90, mDisplay->getDesiredMode());
+    EXPECT_DISPLAY_MODE_REQUEST(kDesiredMode90, mDisplay->getDesiredMode());
 
     const hal::VsyncPeriodChangeConstraints constraints{
             .desiredTimeNanos = systemTime(),
             .seamlessRequired = false,
     };
     hal::VsyncPeriodChangeTimeline timeline;
+    EXPECT_TRUE(mDisplay->initiateModeChange(*mDisplay->getDesiredMode(), constraints, timeline));
+    EXPECT_DISPLAY_MODE_REQUEST(kDesiredMode90, mDisplay->getPendingMode());
 
-    auto desiredModeOpt = mDisplay->takeDesiredMode();
-    ASSERT_TRUE(desiredModeOpt);
-    EXPECT_FALSE(mDisplay->getDesiredMode());
-
-    EXPECT_TRUE(mDisplay->initiateModeChange(std::move(*desiredModeOpt), constraints, timeline));
-
-    auto modeChange = mDisplay->finalizeModeChange();
-    auto* changePtr = std::get_if<DisplayDevice::RefreshRateChange>(&modeChange);
-    ASSERT_TRUE(changePtr);
-    EXPECT_DISPLAY_MODE_REQUEST(kDesiredMode90, changePtr->activeMode);
-
-    // The latest request should override the desired mode.
-    EXPECT_EQ(Action::InitiateDisplayModeSwitch,
-              mDisplay->setDesiredMode(DisplayModeRequest(kDesiredMode60)));
     EXPECT_EQ(Action::None, mDisplay->setDesiredMode(DisplayModeRequest(kDesiredMode120)));
+    ASSERT_TRUE(mDisplay->getDesiredMode());
+    EXPECT_DISPLAY_MODE_REQUEST(kDesiredMode120, mDisplay->getDesiredMode());
 
-    EXPECT_DISPLAY_MODE_REQUEST_OPT(kDesiredMode120, mDisplay->getDesiredMode());
+    EXPECT_DISPLAY_MODE_REQUEST(kDesiredMode90, mDisplay->getPendingMode());
 
-    // No pending mode change.
-    EXPECT_TRUE(
-            std::holds_alternative<DisplayDevice::NoModeChange>(mDisplay->finalizeModeChange()));
+    EXPECT_TRUE(mDisplay->initiateModeChange(*mDisplay->getDesiredMode(), constraints, timeline));
+    EXPECT_DISPLAY_MODE_REQUEST(kDesiredMode120, mDisplay->getPendingMode());
 
-    desiredModeOpt = mDisplay->takeDesiredMode();
-    ASSERT_TRUE(desiredModeOpt);
+    mDisplay->clearDesiredMode();
     EXPECT_FALSE(mDisplay->getDesiredMode());
-
-    EXPECT_TRUE(mDisplay->initiateModeChange(std::move(*desiredModeOpt), constraints, timeline));
-
-    modeChange = mDisplay->finalizeModeChange();
-
-    changePtr = std::get_if<DisplayDevice::RefreshRateChange>(&modeChange);
-    ASSERT_TRUE(changePtr);
-    EXPECT_DISPLAY_MODE_REQUEST(kDesiredMode120, changePtr->activeMode);
 }
 
 } // namespace
diff --git a/services/surfaceflinger/tests/unittests/EventThreadTest.cpp b/services/surfaceflinger/tests/unittests/EventThreadTest.cpp
index 4e8a609..45db0c5 100644
--- a/services/surfaceflinger/tests/unittests/EventThreadTest.cpp
+++ b/services/surfaceflinger/tests/unittests/EventThreadTest.cpp
@@ -55,6 +55,9 @@
 
 constexpr std::chrono::duration VSYNC_PERIOD(16ms);
 
+constexpr int HDCP_V1 = 2;
+constexpr int HDCP_V2 = 3;
+
 } // namespace
 
 class EventThreadTest : public testing::Test, public IEventThreadCallback {
@@ -833,6 +836,19 @@
     expectVSyncCallbackScheduleReceived(true);
 }
 
+TEST_F(EventThreadTest, postHcpLevelsChanged) {
+    setupEventThread();
+
+    mThread->onHdcpLevelsChanged(EXTERNAL_DISPLAY_ID, HDCP_V1, HDCP_V2);
+    auto args = mConnectionEventCallRecorder.waitForCall();
+    ASSERT_TRUE(args.has_value());
+    const auto& event = std::get<0>(args.value());
+    EXPECT_EQ(DisplayEventReceiver::DISPLAY_EVENT_HDCP_LEVELS_CHANGE, event.header.type);
+    EXPECT_EQ(EXTERNAL_DISPLAY_ID, event.header.displayId);
+    EXPECT_EQ(HDCP_V1, event.hdcpLevelsChange.connectedLevel);
+    EXPECT_EQ(HDCP_V2, event.hdcpLevelsChange.maxLevel);
+}
+
 } // namespace
 } // namespace android
 
diff --git a/services/surfaceflinger/tests/unittests/HWComposerTest.cpp b/services/surfaceflinger/tests/unittests/HWComposerTest.cpp
index 6edecff..a5c0657 100644
--- a/services/surfaceflinger/tests/unittests/HWComposerTest.cpp
+++ b/services/surfaceflinger/tests/unittests/HWComposerTest.cpp
@@ -21,6 +21,7 @@
 #undef LOG_TAG
 #define LOG_TAG "LibSurfaceFlingerUnittests"
 
+#include <optional>
 #include <vector>
 
 // StrictMock<T> derives from T and is not marked final, so the destructor of T is expected to be
@@ -82,6 +83,8 @@
         EXPECT_CALL(*mHal, setVsyncEnabled(hwcDisplayId, Hwc2::IComposerClient::Vsync::DISABLE));
         EXPECT_CALL(*mHal, onHotplugConnect(hwcDisplayId));
     }
+
+    void setVrrTimeoutHint(bool status) { mHwc.mEnableVrrTimeout = status; }
 };
 
 TEST_F(HWComposerTest, isHeadless) {
@@ -323,6 +326,7 @@
         EXPECT_TRUE(mHwc.getModes(info->id, kMaxFrameIntervalNs).empty());
     }
     {
+        setVrrTimeoutHint(true);
         constexpr int32_t kWidth = 480;
         constexpr int32_t kHeight = 720;
         constexpr int32_t kConfigGroup = 1;
@@ -330,10 +334,8 @@
         const hal::VrrConfig vrrConfig =
                 hal::VrrConfig{.minFrameIntervalNs = static_cast<Fps>(120_Hz).getPeriodNsecs(),
                                .notifyExpectedPresentConfig = hal::VrrConfig::
-                                       NotifyExpectedPresentConfig{.notifyExpectedPresentHeadsUpNs =
-                                                                           ms2ns(30),
-                                                                   .notifyExpectedPresentTimeoutNs =
-                                                                           ms2ns(30)}};
+                                       NotifyExpectedPresentConfig{.headsUpNs = ms2ns(30),
+                                                                   .timeoutNs = ms2ns(30)}};
         hal::DisplayConfiguration displayConfiguration{.configId = kConfigId,
                                                        .width = kWidth,
                                                        .height = kHeight,
@@ -363,9 +365,9 @@
         displayConfiguration.dpi = {kDpi, kDpi};
 
         EXPECT_CALL(*mHal, getDisplayConfigurations(kHwcDisplayId, _, _))
-                .WillOnce(DoAll(SetArgPointee<2>(std::vector<hal::DisplayConfiguration>{
-                                        displayConfiguration}),
-                                Return(HalError::NONE)));
+                .WillRepeatedly(DoAll(SetArgPointee<2>(std::vector<hal::DisplayConfiguration>{
+                                              displayConfiguration}),
+                                      Return(HalError::NONE)));
 
         modes = mHwc.getModes(info->id, kMaxFrameIntervalNs);
         EXPECT_EQ(modes.size(), size_t{1});
@@ -377,6 +379,10 @@
         EXPECT_EQ(modes.front().vrrConfig, vrrConfig);
         EXPECT_EQ(modes.front().dpiX, kDpi);
         EXPECT_EQ(modes.front().dpiY, kDpi);
+
+        setVrrTimeoutHint(false);
+        modes = mHwc.getModes(info->id, kMaxFrameIntervalNs);
+        EXPECT_EQ(modes.front().vrrConfig->notifyExpectedPresentConfig, std::nullopt);
     }
 }
 
diff --git a/services/surfaceflinger/tests/unittests/LayerHistoryIntegrationTest.cpp b/services/surfaceflinger/tests/unittests/LayerHistoryIntegrationTest.cpp
index e9d2319..734fddb 100644
--- a/services/surfaceflinger/tests/unittests/LayerHistoryIntegrationTest.cpp
+++ b/services/surfaceflinger/tests/unittests/LayerHistoryIntegrationTest.cpp
@@ -167,10 +167,10 @@
     mock::SchedulerCallback mSchedulerCallback;
     mock::VsyncTrackerCallback mVsyncTrackerCallback;
 
-    TestableScheduler* mScheduler =
-            new TestableScheduler(mSelector, mSchedulerCallback, mVsyncTrackerCallback);
-
     TestableSurfaceFlinger mFlinger;
+
+    TestableScheduler* mScheduler =
+            new TestableScheduler(mSelector, mFlinger, mSchedulerCallback, mVsyncTrackerCallback);
 };
 
 namespace {
diff --git a/services/surfaceflinger/tests/unittests/LayerHistoryTest.cpp b/services/surfaceflinger/tests/unittests/LayerHistoryTest.cpp
index 0ae3ca3..9456e37 100644
--- a/services/surfaceflinger/tests/unittests/LayerHistoryTest.cpp
+++ b/services/surfaceflinger/tests/unittests/LayerHistoryTest.cpp
@@ -148,10 +148,9 @@
     mock::SchedulerCallback mSchedulerCallback;
 
     mock::VsyncTrackerCallback mVsyncTrackerCallback;
-    TestableScheduler* mScheduler =
-            new TestableScheduler(mSelector, mSchedulerCallback, mVsyncTrackerCallback);
-
     TestableSurfaceFlinger mFlinger;
+    TestableScheduler* mScheduler =
+            new TestableScheduler(mSelector, mFlinger, mSchedulerCallback, mVsyncTrackerCallback);
 };
 
 namespace {
diff --git a/services/surfaceflinger/tests/unittests/LayerInfoTest.cpp b/services/surfaceflinger/tests/unittests/LayerInfoTest.cpp
index 07a522a..22cfbd8 100644
--- a/services/surfaceflinger/tests/unittests/LayerInfoTest.cpp
+++ b/services/surfaceflinger/tests/unittests/LayerInfoTest.cpp
@@ -65,9 +65,9 @@
                                                   DisplayModeId(0));
     mock::SchedulerCallback mSchedulerCallback;
     mock::VsyncTrackerCallback mVsyncTrackerCallback;
-    TestableScheduler* mScheduler =
-            new TestableScheduler(mSelector, mSchedulerCallback, mVsyncTrackerCallback);
     TestableSurfaceFlinger mFlinger;
+    TestableScheduler* mScheduler =
+            new TestableScheduler(mSelector, mFlinger, mSchedulerCallback, mVsyncTrackerCallback);
 };
 
 namespace {
diff --git a/services/surfaceflinger/tests/unittests/SchedulerTest.cpp b/services/surfaceflinger/tests/unittests/SchedulerTest.cpp
index 7fdca71..6986689 100644
--- a/services/surfaceflinger/tests/unittests/SchedulerTest.cpp
+++ b/services/surfaceflinger/tests/unittests/SchedulerTest.cpp
@@ -96,15 +96,14 @@
 
     mock::SchedulerCallback mSchedulerCallback;
     mock::VsyncTrackerCallback mVsyncTrackerCallback;
+    TestableSurfaceFlinger mFlinger;
     TestableScheduler* mScheduler =
-            new TestableScheduler{mSelector, mSchedulerCallback, mVsyncTrackerCallback};
+            new TestableScheduler{mSelector, mFlinger, mSchedulerCallback, mVsyncTrackerCallback};
     surfaceflinger::frontend::LayerHierarchyBuilder mLayerHierarchyBuilder;
 
     ConnectionHandle mConnectionHandle;
     MockEventThread* mEventThread;
     sp<MockEventThreadConnection> mEventThreadConnection;
-
-    TestableSurfaceFlinger mFlinger;
 };
 
 SchedulerTest::SchedulerTest() {
@@ -575,7 +574,8 @@
     TestableScheduler scheduler{std::make_unique<android::mock::VsyncController>(),
                                 vrrTracker,
                                 vrrSelectorPtr,
-                                sp<VsyncModulator>::make(VsyncConfigSet{}),
+                                mFlinger.getFactory(),
+                                mFlinger.getTimeStats(),
                                 mSchedulerCallback,
                                 mVsyncTrackerCallback};
 
diff --git a/services/surfaceflinger/tests/unittests/SurfaceFlinger_DisplayModeSwitching.cpp b/services/surfaceflinger/tests/unittests/SurfaceFlinger_DisplayModeSwitching.cpp
index 3291dc3..8b16a8a 100644
--- a/services/surfaceflinger/tests/unittests/SurfaceFlinger_DisplayModeSwitching.cpp
+++ b/services/surfaceflinger/tests/unittests/SurfaceFlinger_DisplayModeSwitching.cpp
@@ -179,7 +179,7 @@
 
     Mock::VerifyAndClearExpectations(mComposer);
 
-    EXPECT_FALSE(mDisplay->getDesiredMode());
+    EXPECT_TRUE(mDisplay->getDesiredMode());
     EXPECT_EQ(mDisplay->getActiveMode().modePtr->getId(), kModeId60);
 
     // Verify that the next commit will complete the mode change and send
@@ -263,13 +263,11 @@
 
     mFlinger.commit();
 
-    // The 120 Hz mode should be pending.
-    EXPECT_FALSE(mDisplay->getDesiredMode());
-    EXPECT_EQ(mDisplay->getActiveMode().modePtr->getId(), kModeId90);
+    ASSERT_TRUE(mDisplay->getDesiredMode());
+    EXPECT_EQ(mDisplay->getDesiredMode()->mode.modePtr->getId(), kModeId120);
 
     mFlinger.commit();
 
-    // The 120 Hz mode should be active.
     EXPECT_FALSE(mDisplay->getDesiredMode());
     EXPECT_EQ(mDisplay->getActiveMode().modePtr->getId(), kModeId120);
 }
@@ -326,7 +324,7 @@
     EXPECT_EQ(mDisplay->getActiveMode().modePtr->getId(), kModeId90_4K);
 }
 
-MATCHER_P2(HasDesiredMode, flinger, modeId, "") {
+MATCHER_P2(ModeSwitchingTo, flinger, modeId, "") {
     if (!arg->getDesiredMode()) {
         *result_listener << "No desired mode";
         return false;
@@ -345,33 +343,12 @@
     return true;
 }
 
-MATCHER_P(HasPendingMode, modeId, "") {
-    const auto pendingOpt = TestableSurfaceFlinger::getPendingMode(arg);
-
-    if (!pendingOpt) {
-        *result_listener << "No pending mode";
-        return false;
-    }
-
-    if (pendingOpt->mode.modePtr->getId() != modeId) {
-        *result_listener << "Unexpected pending mode " << modeId;
-        return false;
-    }
-
-    return true;
-}
-
-MATCHER_P(HasActiveMode, modeId, "") {
+MATCHER_P(ModeSettledTo, modeId, "") {
     if (const auto desiredOpt = arg->getDesiredMode()) {
         *result_listener << "Unsettled desired mode " << desiredOpt->mode.modePtr->getId();
         return false;
     }
 
-    if (const auto pendingOpt = TestableSurfaceFlinger::getPendingMode(arg)) {
-        *result_listener << "Unsettled pending mode " << pendingOpt->mode.modePtr->getId();
-        return false;
-    }
-
     ftl::FakeGuard guard(kMainThreadContext);
 
     if (arg->getActiveMode().modePtr->getId() != modeId) {
@@ -388,14 +365,14 @@
     EXPECT_TRUE(innerDisplay->isPoweredOn());
     EXPECT_FALSE(outerDisplay->isPoweredOn());
 
-    EXPECT_THAT(innerDisplay, HasActiveMode(kModeId60));
-    EXPECT_THAT(outerDisplay, HasActiveMode(kModeId120));
+    EXPECT_THAT(innerDisplay, ModeSettledTo(kModeId60));
+    EXPECT_THAT(outerDisplay, ModeSettledTo(kModeId120));
 
     // Only the inner display is powered on.
     mFlinger.onActiveDisplayChanged(nullptr, *innerDisplay);
 
-    EXPECT_THAT(innerDisplay, HasActiveMode(kModeId60));
-    EXPECT_THAT(outerDisplay, HasActiveMode(kModeId120));
+    EXPECT_THAT(innerDisplay, ModeSettledTo(kModeId60));
+    EXPECT_THAT(outerDisplay, ModeSettledTo(kModeId120));
 
     EXPECT_EQ(NO_ERROR,
               mFlinger.setDesiredDisplayModeSpecs(innerDisplay->getDisplayToken().promote(),
@@ -407,8 +384,8 @@
                                                   mock::createDisplayModeSpecs(kModeId60.value(),
                                                                                false, 0.f, 120.f)));
 
-    EXPECT_THAT(innerDisplay, HasDesiredMode(&mFlinger, kModeId90));
-    EXPECT_THAT(outerDisplay, HasActiveMode(kModeId120));
+    EXPECT_THAT(innerDisplay, ModeSwitchingTo(&mFlinger, kModeId90));
+    EXPECT_THAT(outerDisplay, ModeSettledTo(kModeId120));
 
     const VsyncPeriodChangeTimeline timeline{.refreshRequired = true};
     EXPECT_CALL(*mComposer,
@@ -418,13 +395,13 @@
 
     mFlinger.commit();
 
-    EXPECT_THAT(innerDisplay, HasPendingMode(kModeId90));
-    EXPECT_THAT(outerDisplay, HasActiveMode(kModeId120));
+    EXPECT_THAT(innerDisplay, ModeSwitchingTo(&mFlinger, kModeId90));
+    EXPECT_THAT(outerDisplay, ModeSettledTo(kModeId120));
 
     mFlinger.commit();
 
-    EXPECT_THAT(innerDisplay, HasActiveMode(kModeId90));
-    EXPECT_THAT(outerDisplay, HasActiveMode(kModeId120));
+    EXPECT_THAT(innerDisplay, ModeSettledTo(kModeId90));
+    EXPECT_THAT(outerDisplay, ModeSettledTo(kModeId120));
 
     innerDisplay->setPowerMode(hal::PowerMode::OFF);
     outerDisplay->setPowerMode(hal::PowerMode::ON);
@@ -432,8 +409,8 @@
     // Only the outer display is powered on.
     mFlinger.onActiveDisplayChanged(innerDisplay.get(), *outerDisplay);
 
-    EXPECT_THAT(innerDisplay, HasActiveMode(kModeId90));
-    EXPECT_THAT(outerDisplay, HasDesiredMode(&mFlinger, kModeId60));
+    EXPECT_THAT(innerDisplay, ModeSettledTo(kModeId90));
+    EXPECT_THAT(outerDisplay, ModeSwitchingTo(&mFlinger, kModeId60));
 
     EXPECT_CALL(*mComposer,
                 setActiveConfigWithConstraints(kOuterDisplayHwcId,
@@ -442,13 +419,13 @@
 
     mFlinger.commit();
 
-    EXPECT_THAT(innerDisplay, HasActiveMode(kModeId90));
-    EXPECT_THAT(outerDisplay, HasPendingMode(kModeId60));
+    EXPECT_THAT(innerDisplay, ModeSettledTo(kModeId90));
+    EXPECT_THAT(outerDisplay, ModeSwitchingTo(&mFlinger, kModeId60));
 
     mFlinger.commit();
 
-    EXPECT_THAT(innerDisplay, HasActiveMode(kModeId90));
-    EXPECT_THAT(outerDisplay, HasActiveMode(kModeId60));
+    EXPECT_THAT(innerDisplay, ModeSettledTo(kModeId90));
+    EXPECT_THAT(outerDisplay, ModeSettledTo(kModeId60));
 }
 
 TEST_F(DisplayModeSwitchingTest, innerAndOuterDisplay) {
@@ -457,16 +434,16 @@
     EXPECT_TRUE(innerDisplay->isPoweredOn());
     EXPECT_FALSE(outerDisplay->isPoweredOn());
 
-    EXPECT_THAT(innerDisplay, HasActiveMode(kModeId60));
-    EXPECT_THAT(outerDisplay, HasActiveMode(kModeId120));
+    EXPECT_THAT(innerDisplay, ModeSettledTo(kModeId60));
+    EXPECT_THAT(outerDisplay, ModeSettledTo(kModeId120));
 
     outerDisplay->setPowerMode(hal::PowerMode::ON);
 
     // Both displays are powered on.
     mFlinger.onActiveDisplayChanged(nullptr, *innerDisplay);
 
-    EXPECT_THAT(innerDisplay, HasActiveMode(kModeId60));
-    EXPECT_THAT(outerDisplay, HasActiveMode(kModeId120));
+    EXPECT_THAT(innerDisplay, ModeSettledTo(kModeId60));
+    EXPECT_THAT(outerDisplay, ModeSettledTo(kModeId120));
 
     EXPECT_EQ(NO_ERROR,
               mFlinger.setDesiredDisplayModeSpecs(innerDisplay->getDisplayToken().promote(),
@@ -478,8 +455,8 @@
                                                   mock::createDisplayModeSpecs(kModeId60.value(),
                                                                                false, 0.f, 120.f)));
 
-    EXPECT_THAT(innerDisplay, HasDesiredMode(&mFlinger, kModeId90));
-    EXPECT_THAT(outerDisplay, HasDesiredMode(&mFlinger, kModeId60));
+    EXPECT_THAT(innerDisplay, ModeSwitchingTo(&mFlinger, kModeId90));
+    EXPECT_THAT(outerDisplay, ModeSwitchingTo(&mFlinger, kModeId60));
 
     const VsyncPeriodChangeTimeline timeline{.refreshRequired = true};
     EXPECT_CALL(*mComposer,
@@ -494,25 +471,25 @@
 
     mFlinger.commit();
 
-    EXPECT_THAT(innerDisplay, HasPendingMode(kModeId90));
-    EXPECT_THAT(outerDisplay, HasPendingMode(kModeId60));
+    EXPECT_THAT(innerDisplay, ModeSwitchingTo(&mFlinger, kModeId90));
+    EXPECT_THAT(outerDisplay, ModeSwitchingTo(&mFlinger, kModeId60));
 
     mFlinger.commit();
 
-    EXPECT_THAT(innerDisplay, HasActiveMode(kModeId90));
-    EXPECT_THAT(outerDisplay, HasActiveMode(kModeId60));
+    EXPECT_THAT(innerDisplay, ModeSettledTo(kModeId90));
+    EXPECT_THAT(outerDisplay, ModeSettledTo(kModeId60));
 }
 
 TEST_F(DisplayModeSwitchingTest, powerOffDuringModeSet) {
     EXPECT_TRUE(mDisplay->isPoweredOn());
-    EXPECT_THAT(mDisplay, HasActiveMode(kModeId60));
+    EXPECT_THAT(mDisplay, ModeSettledTo(kModeId60));
 
     EXPECT_EQ(NO_ERROR,
               mFlinger.setDesiredDisplayModeSpecs(mDisplay->getDisplayToken().promote(),
                                                   mock::createDisplayModeSpecs(kModeId90.value(),
                                                                                false, 0.f, 120.f)));
 
-    EXPECT_THAT(mDisplay, HasDesiredMode(&mFlinger, kModeId90));
+    EXPECT_THAT(mDisplay, ModeSwitchingTo(&mFlinger, kModeId90));
 
     // Power off the display before the mode has been set.
     mDisplay->setPowerMode(hal::PowerMode::OFF);
@@ -527,11 +504,11 @@
 
     // Powering off should not abort the mode set.
     EXPECT_FALSE(mDisplay->isPoweredOn());
-    EXPECT_THAT(mDisplay, HasPendingMode(kModeId90));
+    EXPECT_THAT(mDisplay, ModeSwitchingTo(&mFlinger, kModeId90));
 
     mFlinger.commit();
 
-    EXPECT_THAT(mDisplay, HasActiveMode(kModeId90));
+    EXPECT_THAT(mDisplay, ModeSettledTo(kModeId90));
 }
 
 TEST_F(DisplayModeSwitchingTest, powerOffDuringConcurrentModeSet) {
@@ -540,16 +517,16 @@
     EXPECT_TRUE(innerDisplay->isPoweredOn());
     EXPECT_FALSE(outerDisplay->isPoweredOn());
 
-    EXPECT_THAT(innerDisplay, HasActiveMode(kModeId60));
-    EXPECT_THAT(outerDisplay, HasActiveMode(kModeId120));
+    EXPECT_THAT(innerDisplay, ModeSettledTo(kModeId60));
+    EXPECT_THAT(outerDisplay, ModeSettledTo(kModeId120));
 
     outerDisplay->setPowerMode(hal::PowerMode::ON);
 
     // Both displays are powered on.
     mFlinger.onActiveDisplayChanged(nullptr, *innerDisplay);
 
-    EXPECT_THAT(innerDisplay, HasActiveMode(kModeId60));
-    EXPECT_THAT(outerDisplay, HasActiveMode(kModeId120));
+    EXPECT_THAT(innerDisplay, ModeSettledTo(kModeId60));
+    EXPECT_THAT(outerDisplay, ModeSettledTo(kModeId120));
 
     EXPECT_EQ(NO_ERROR,
               mFlinger.setDesiredDisplayModeSpecs(innerDisplay->getDisplayToken().promote(),
@@ -561,8 +538,8 @@
                                                   mock::createDisplayModeSpecs(kModeId60.value(),
                                                                                false, 0.f, 120.f)));
 
-    EXPECT_THAT(innerDisplay, HasDesiredMode(&mFlinger, kModeId90));
-    EXPECT_THAT(outerDisplay, HasDesiredMode(&mFlinger, kModeId60));
+    EXPECT_THAT(innerDisplay, ModeSwitchingTo(&mFlinger, kModeId90));
+    EXPECT_THAT(outerDisplay, ModeSwitchingTo(&mFlinger, kModeId60));
 
     // Power off the outer display before the mode has been set.
     outerDisplay->setPowerMode(hal::PowerMode::OFF);
@@ -576,13 +553,13 @@
     mFlinger.commit();
 
     // Powering off the inactive display should abort the mode set.
-    EXPECT_THAT(innerDisplay, HasPendingMode(kModeId90));
-    EXPECT_THAT(outerDisplay, HasActiveMode(kModeId120));
+    EXPECT_THAT(innerDisplay, ModeSwitchingTo(&mFlinger, kModeId90));
+    EXPECT_THAT(outerDisplay, ModeSettledTo(kModeId120));
 
     mFlinger.commit();
 
-    EXPECT_THAT(innerDisplay, HasActiveMode(kModeId90));
-    EXPECT_THAT(outerDisplay, HasActiveMode(kModeId120));
+    EXPECT_THAT(innerDisplay, ModeSettledTo(kModeId90));
+    EXPECT_THAT(outerDisplay, ModeSettledTo(kModeId120));
 
     innerDisplay->setPowerMode(hal::PowerMode::OFF);
     outerDisplay->setPowerMode(hal::PowerMode::ON);
@@ -598,13 +575,13 @@
     mFlinger.commit();
 
     // The mode set should resume once the display becomes active.
-    EXPECT_THAT(innerDisplay, HasActiveMode(kModeId90));
-    EXPECT_THAT(outerDisplay, HasPendingMode(kModeId60));
+    EXPECT_THAT(innerDisplay, ModeSettledTo(kModeId90));
+    EXPECT_THAT(outerDisplay, ModeSwitchingTo(&mFlinger, kModeId60));
 
     mFlinger.commit();
 
-    EXPECT_THAT(innerDisplay, HasActiveMode(kModeId90));
-    EXPECT_THAT(outerDisplay, HasActiveMode(kModeId60));
+    EXPECT_THAT(innerDisplay, ModeSettledTo(kModeId90));
+    EXPECT_THAT(outerDisplay, ModeSettledTo(kModeId60));
 }
 
 } // namespace
diff --git a/services/surfaceflinger/tests/unittests/TestableScheduler.cpp b/services/surfaceflinger/tests/unittests/TestableScheduler.cpp
new file mode 100644
index 0000000..e0b7366
--- /dev/null
+++ b/services/surfaceflinger/tests/unittests/TestableScheduler.cpp
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2023 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 "TestableScheduler.h"
+#include "TestableSurfaceFlinger.h"
+
+namespace android::scheduler {
+
+TestableScheduler::TestableScheduler(RefreshRateSelectorPtr selectorPtr,
+                                     TestableSurfaceFlinger& testableSurfaceFlinger,
+                                     ISchedulerCallback& callback,
+                                     IVsyncTrackerCallback& vsyncTrackerCallback)
+      : TestableScheduler(std::make_unique<android::mock::VsyncController>(),
+                          std::make_shared<android::mock::VSyncTracker>(), std::move(selectorPtr),
+                          testableSurfaceFlinger.getFactory(),
+                          testableSurfaceFlinger.getTimeStats(), callback, vsyncTrackerCallback) {}
+
+} // namespace android::scheduler
diff --git a/services/surfaceflinger/tests/unittests/TestableScheduler.h b/services/surfaceflinger/tests/unittests/TestableScheduler.h
index 2a1b88e..6213713 100644
--- a/services/surfaceflinger/tests/unittests/TestableScheduler.h
+++ b/services/surfaceflinger/tests/unittests/TestableScheduler.h
@@ -32,25 +32,27 @@
 #include "mock/MockVSyncTracker.h"
 #include "mock/MockVsyncController.h"
 
+namespace android {
+class TestableSurfaceFlinger;
+} // namespace android
+
 namespace android::scheduler {
 
 class TestableScheduler : public Scheduler, private ICompositor {
 public:
-    TestableScheduler(RefreshRateSelectorPtr selectorPtr, ISchedulerCallback& callback,
-                      IVsyncTrackerCallback& vsyncTrackerCallback)
-          : TestableScheduler(std::make_unique<mock::VsyncController>(),
-                              std::make_shared<mock::VSyncTracker>(), std::move(selectorPtr),
-                              sp<VsyncModulator>::make(VsyncConfigSet{}), callback,
-                              vsyncTrackerCallback) {}
+    TestableScheduler(RefreshRateSelectorPtr selectorPtr,
+                      TestableSurfaceFlinger& testableSurfaceFlinger, ISchedulerCallback& callback,
+                      IVsyncTrackerCallback& vsyncTrackerCallback);
 
     TestableScheduler(std::unique_ptr<VsyncController> controller,
                       std::shared_ptr<VSyncTracker> tracker, RefreshRateSelectorPtr selectorPtr,
-                      sp<VsyncModulator> modulatorPtr, ISchedulerCallback& schedulerCallback,
+                      surfaceflinger::Factory& factory, TimeStats& timeStats,
+                      ISchedulerCallback& schedulerCallback,
                       IVsyncTrackerCallback& vsyncTrackerCallback)
           : Scheduler(*this, schedulerCallback,
                       (FeatureFlags)Feature::kContentDetection |
                               Feature::kSmallDirtyContentDetection,
-                      std::move(modulatorPtr), vsyncTrackerCallback) {
+                      factory, selectorPtr->getActiveMode().fps, timeStats, vsyncTrackerCallback) {
         const auto displayId = selectorPtr->getActiveMode().modePtr->getPhysicalDisplayId();
         registerDisplay(displayId, std::move(selectorPtr), std::move(controller),
                         std::move(tracker));
diff --git a/services/surfaceflinger/tests/unittests/TestableSurfaceFlinger.h b/services/surfaceflinger/tests/unittests/TestableSurfaceFlinger.h
index 16815be..f00eacc 100644
--- a/services/surfaceflinger/tests/unittests/TestableSurfaceFlinger.h
+++ b/services/surfaceflinger/tests/unittests/TestableSurfaceFlinger.h
@@ -216,6 +216,10 @@
 
     using DisplayModesVariant = std::variant<DefaultDisplayMode, RefreshRateSelectorPtr>;
 
+    surfaceflinger::Factory& getFactory() { return mFactory; }
+
+    TimeStats& getTimeStats() { return *mFlinger->mTimeStats; }
+
     void setupScheduler(
             std::unique_ptr<scheduler::VsyncController> vsyncController,
             std::shared_ptr<scheduler::VSyncTracker> vsyncTracker,
@@ -234,13 +238,6 @@
                 },
                 [](RefreshRateSelectorPtr selectorPtr) { return selectorPtr; });
 
-        const auto fps = selectorPtr->getActiveMode().fps;
-        mFlinger->mVsyncConfiguration = mFactory.createVsyncConfiguration(fps);
-
-        mFlinger->mRefreshRateStats =
-                std::make_unique<scheduler::RefreshRateStats>(*mFlinger->mTimeStats, fps,
-                                                              hal::PowerMode::OFF);
-
         mTokenManager = std::make_unique<frametimeline::impl::TokenManager>();
 
         using ISchedulerCallback = scheduler::ISchedulerCallback;
@@ -254,23 +251,21 @@
                 ? static_cast<VsyncTrackerCallback&>(mNoOpVsyncTrackerCallback)
                 : static_cast<VsyncTrackerCallback&>(mVsyncTrackerCallback);
 
-        auto modulatorPtr = sp<scheduler::VsyncModulator>::make(
-                mFlinger->mVsyncConfiguration->getCurrentConfigs());
-
         if (useNiceMock) {
             mScheduler =
                     new testing::NiceMock<scheduler::TestableScheduler>(std::move(vsyncController),
                                                                         std::move(vsyncTracker),
                                                                         std::move(selectorPtr),
-                                                                        std::move(modulatorPtr),
+                                                                        mFactory,
+                                                                        *mFlinger->mTimeStats,
                                                                         schedulerCallback,
                                                                         vsyncTrackerCallback);
         } else {
             mScheduler = new scheduler::TestableScheduler(std::move(vsyncController),
                                                           std::move(vsyncTracker),
-                                                          std::move(selectorPtr),
-                                                          std::move(modulatorPtr),
-                                                          schedulerCallback, vsyncTrackerCallback);
+                                                          std::move(selectorPtr), mFactory,
+                                                          *mFlinger->mTimeStats, schedulerCallback,
+                                                          vsyncTrackerCallback);
         }
 
         mScheduler->initVsync(mScheduler->getVsyncSchedule()->getDispatch(), *mTokenManager, 0ms);
@@ -391,13 +386,14 @@
         LOG_ALWAYS_FATAL_IF(!displayIdOpt);
         const auto displayId = *displayIdOpt;
 
-        constexpr bool kBackpressureGpuComposition = true;
-        scheduler::FrameTargeter frameTargeter(displayId, kBackpressureGpuComposition);
+        scheduler::FrameTargeter frameTargeter(displayId,
+                                               scheduler::Feature::kBackpressureGpuComposition);
 
         frameTargeter.beginFrame({.frameBeginTime = frameTime,
                                   .vsyncId = vsyncId,
                                   .expectedVsyncTime = expectedVsyncTime,
-                                  .sfWorkDuration = 10ms},
+                                  .sfWorkDuration = 10ms,
+                                  .hwcMinWorkDuration = 10ms},
                                  *mScheduler->getVsyncSchedule());
 
         scheduler::FrameTargets targets;
@@ -484,10 +480,6 @@
         return mFlinger->setDisplayBrightness(display, brightness);
     }
 
-    static const auto& getPendingMode(const sp<DisplayDevice>& display) {
-        return display->mPendingModeOpt;
-    }
-
     // Allow reading display state without locking, as if called on the SF main thread.
     auto setPowerModeInternal(const sp<DisplayDevice>& display,
                               hal::PowerMode mode) NO_THREAD_SAFETY_ANALYSIS {
diff --git a/services/surfaceflinger/tests/unittests/VSyncPredictorTest.cpp b/services/surfaceflinger/tests/unittests/VSyncPredictorTest.cpp
index 9afaabe..961ba57 100644
--- a/services/surfaceflinger/tests/unittests/VSyncPredictorTest.cpp
+++ b/services/surfaceflinger/tests/unittests/VSyncPredictorTest.cpp
@@ -662,7 +662,7 @@
 
     const auto refreshRate = Fps::fromPeriodNsecs(mPeriod);
     NotifyExpectedPresentConfig notifyExpectedPresentConfig;
-    notifyExpectedPresentConfig.notifyExpectedPresentTimeoutNs = Period::fromNs(30).ns();
+    notifyExpectedPresentConfig.timeoutNs = Period::fromNs(30).ns();
 
     hal::VrrConfig vrrConfig;
     vrrConfig.notifyExpectedPresentConfig = notifyExpectedPresentConfig;
diff --git a/vulkan/libvulkan/swapchain.cpp b/vulkan/libvulkan/swapchain.cpp
index 0df5e77..6b3c379 100644
--- a/vulkan/libvulkan/swapchain.cpp
+++ b/vulkan/libvulkan/swapchain.cpp
@@ -1762,6 +1762,8 @@
     }
 
     int query_value;
+    // TODO: Now that we are calling into GPDSC2 directly, this query may be redundant
+    //       the call to std::max(min_buffer_count, num_images) may be redundant as well
     err = window->query(window, NATIVE_WINDOW_MIN_UNDEQUEUED_BUFFERS,
                         &query_value);
     if (err != android::OK || query_value < 0) {
@@ -1778,12 +1780,33 @@
     // with extra images (which they can't actually use!).
     const uint32_t min_buffer_count = min_undequeued_buffers + 1;
 
-    uint32_t num_images;
-    if (create_info->presentMode  == VK_PRESENT_MODE_MAILBOX_KHR) {
-        num_images = std::max(3u, create_info->minImageCount);
-    } else {
-        num_images = create_info->minImageCount;
-    }
+    // Call into GPDSC2 to get the minimum and maximum allowable buffer count for the surface of
+    // interest. This step is only necessary if the app requests a number of images
+    // (create_info->minImageCount) that is less or more than the surface capabilities.
+    // An app should be calling GPDSC2 and using those values to set create_info, but in the
+    // event that the app has hard-coded image counts an error can occur
+    VkSurfacePresentModeEXT present_mode = {
+        VK_STRUCTURE_TYPE_SURFACE_PRESENT_MODE_EXT,
+        nullptr,
+        create_info->presentMode
+    };
+    VkPhysicalDeviceSurfaceInfo2KHR surface_info2 = {
+        VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SURFACE_INFO_2_KHR,
+        &present_mode,
+        create_info->surface
+    };
+    VkSurfaceCapabilities2KHR surface_capabilities2 = {
+        VK_STRUCTURE_TYPE_SURFACE_CAPABILITIES_2_KHR,
+        nullptr,
+        {},
+    };
+    result = GetPhysicalDeviceSurfaceCapabilities2KHR(GetData(device).driver_physical_device,
+            &surface_info2, &surface_capabilities2);
+
+    uint32_t num_images = create_info->minImageCount;
+    num_images = std::clamp(num_images,
+            surface_capabilities2.surfaceCapabilities.minImageCount,
+            surface_capabilities2.surfaceCapabilities.maxImageCount);
 
     const uint32_t buffer_count = std::max(min_buffer_count, num_images);
     err = native_window_set_buffer_count(window, buffer_count);