Merge "Revert "Revert "Load native GLES driver when specified.""" into udc-qpr-dev
diff --git a/data/etc/input/motion_predictor_config.xml b/data/etc/input/motion_predictor_config.xml
index 03dfd63..39772ae 100644
--- a/data/etc/input/motion_predictor_config.xml
+++ b/data/etc/input/motion_predictor_config.xml
@@ -16,5 +16,20 @@
 <motion-predictor>
   <!-- The time interval (ns) between the model's predictions. -->
   <prediction-interval>4166666</prediction-interval>  <!-- 4.167 ms = ~240 Hz -->
+  <!-- The noise floor (px) for predicted distances.
+
+       As the model is trained stochastically, there is some expected minimum
+       variability in its output. This can be a UX issue when the input device
+       is moving slowly and the variability is large relative to the magnitude
+       of the motion. In these cases, it is better to inhibit the prediction,
+       rather than show noisy predictions (and there is little benefit to
+       prediction anyway).
+
+       The value for this parameter should at least be close to the maximum
+       predicted distance when the input device is held stationary (i.e. the
+       expected minimum variability), and perhaps a little larger to capture
+       the UX issue mentioned above.
+  -->
+  <distance-noise-floor>0.2</distance-noise-floor>
 </motion-predictor>
 
diff --git a/data/etc/input/motion_predictor_model.tflite b/data/etc/input/motion_predictor_model.tflite
index 10b3c8b..45fc162 100644
--- a/data/etc/input/motion_predictor_model.tflite
+++ b/data/etc/input/motion_predictor_model.tflite
Binary files differ
diff --git a/include/input/TfLiteMotionPredictor.h b/include/input/TfLiteMotionPredictor.h
index fbd6026..2edc138 100644
--- a/include/input/TfLiteMotionPredictor.h
+++ b/include/input/TfLiteMotionPredictor.h
@@ -99,6 +99,14 @@
 // A TFLite model for generating motion predictions.
 class TfLiteMotionPredictorModel {
 public:
+    struct Config {
+        // The time between predictions.
+        nsecs_t predictionInterval = 0;
+        // The noise floor for predictions.
+        // Distances (r) less than this should be discarded as noise.
+        float distanceNoiseFloor = 0;
+    };
+
     // Creates a model from an encoded Flatbuffer model.
     static std::unique_ptr<TfLiteMotionPredictorModel> create();
 
@@ -110,8 +118,7 @@
     // Returns the length of the model's output buffers.
     size_t outputLength() const;
 
-    // Returns the time interval between predictions.
-    nsecs_t predictionInterval() const { return mPredictionInterval; }
+    const Config& config() const { return mConfig; }
 
     // Executes the model.
     // Returns true if the model successfully executed and the output tensors can be read.
@@ -132,7 +139,7 @@
 
 private:
     explicit TfLiteMotionPredictorModel(std::unique_ptr<android::base::MappedFile> model,
-                                        nsecs_t predictionInterval);
+                                        Config config);
 
     void allocateTensors();
     void attachInputTensors();
@@ -154,7 +161,7 @@
     std::unique_ptr<tflite::Interpreter> mInterpreter;
     tflite::SignatureRunner* mRunner = nullptr;
 
-    const nsecs_t mPredictionInterval = 0;
+    const Config mConfig = {};
 };
 
 } // namespace android
diff --git a/libs/gui/Android.bp b/libs/gui/Android.bp
index bf34987..3c8df2b 100644
--- a/libs/gui/Android.bp
+++ b/libs/gui/Android.bp
@@ -73,6 +73,7 @@
         "android/gui/FocusRequest.aidl",
         "android/gui/InputApplicationInfo.aidl",
         "android/gui/IWindowInfosListener.aidl",
+        "android/gui/IWindowInfosPublisher.aidl",
         "android/gui/IWindowInfosReportedListener.aidl",
         "android/gui/WindowInfo.aidl",
         "android/gui/WindowInfosUpdate.aidl",
@@ -90,6 +91,7 @@
         "android/gui/FocusRequest.aidl",
         "android/gui/InputApplicationInfo.aidl",
         "android/gui/IWindowInfosListener.aidl",
+        "android/gui/IWindowInfosPublisher.aidl",
         "android/gui/IWindowInfosReportedListener.aidl",
         "android/gui/WindowInfosUpdate.aidl",
         "android/gui/WindowInfo.aidl",
diff --git a/libs/gui/WindowInfosListenerReporter.cpp b/libs/gui/WindowInfosListenerReporter.cpp
index 76e7b6e..0929b8e 100644
--- a/libs/gui/WindowInfosListenerReporter.cpp
+++ b/libs/gui/WindowInfosListenerReporter.cpp
@@ -22,7 +22,6 @@
 namespace android {
 
 using gui::DisplayInfo;
-using gui::IWindowInfosReportedListener;
 using gui::WindowInfo;
 using gui::WindowInfosListener;
 using gui::aidl_utils::statusTFromBinderStatus;
@@ -40,8 +39,13 @@
     {
         std::scoped_lock lock(mListenersMutex);
         if (mWindowInfosListeners.empty()) {
-            binder::Status s = surfaceComposer->addWindowInfosListener(this);
+            gui::WindowInfosListenerInfo listenerInfo;
+            binder::Status s = surfaceComposer->addWindowInfosListener(this, &listenerInfo);
             status = statusTFromBinderStatus(s);
+            if (status == OK) {
+                mWindowInfosPublisher = std::move(listenerInfo.windowInfosPublisher);
+                mListenerId = listenerInfo.listenerId;
+            }
         }
 
         if (status == OK) {
@@ -85,8 +89,7 @@
 }
 
 binder::Status WindowInfosListenerReporter::onWindowInfosChanged(
-        const gui::WindowInfosUpdate& update,
-        const sp<IWindowInfosReportedListener>& windowInfosReportedListener) {
+        const gui::WindowInfosUpdate& update) {
     std::unordered_set<sp<WindowInfosListener>, gui::SpHash<WindowInfosListener>>
             windowInfosListeners;
 
@@ -104,9 +107,7 @@
         listener->onWindowInfosChanged(update);
     }
 
-    if (windowInfosReportedListener) {
-        windowInfosReportedListener->onWindowInfosReported();
-    }
+    mWindowInfosPublisher->ackWindowInfosReceived(update.vsyncId, mListenerId);
 
     return binder::Status::ok();
 }
@@ -114,7 +115,10 @@
 void WindowInfosListenerReporter::reconnect(const sp<gui::ISurfaceComposer>& composerService) {
     std::scoped_lock lock(mListenersMutex);
     if (!mWindowInfosListeners.empty()) {
-        composerService->addWindowInfosListener(this);
+        gui::WindowInfosListenerInfo listenerInfo;
+        composerService->addWindowInfosListener(this, &listenerInfo);
+        mWindowInfosPublisher = std::move(listenerInfo.windowInfosPublisher);
+        mListenerId = listenerInfo.listenerId;
     }
 }
 
diff --git a/libs/gui/aidl/android/gui/ISurfaceComposer.aidl b/libs/gui/aidl/android/gui/ISurfaceComposer.aidl
index ec3266c..539a1c1 100644
--- a/libs/gui/aidl/android/gui/ISurfaceComposer.aidl
+++ b/libs/gui/aidl/android/gui/ISurfaceComposer.aidl
@@ -40,12 +40,14 @@
 import android.gui.ISurfaceComposerClient;
 import android.gui.ITunnelModeEnabledListener;
 import android.gui.IWindowInfosListener;
+import android.gui.IWindowInfosPublisher;
 import android.gui.LayerCaptureArgs;
 import android.gui.LayerDebugInfo;
 import android.gui.OverlayProperties;
 import android.gui.PullAtomData;
 import android.gui.ARect;
 import android.gui.StaticDisplayInfo;
+import android.gui.WindowInfosListenerInfo;
 
 /** @hide */
 interface ISurfaceComposer {
@@ -500,7 +502,7 @@
      */
     int getMaxAcquiredBufferCount();
 
-    void addWindowInfosListener(IWindowInfosListener windowInfosListener);
+    WindowInfosListenerInfo addWindowInfosListener(IWindowInfosListener windowInfosListener);
 
     void removeWindowInfosListener(IWindowInfosListener windowInfosListener);
 
diff --git a/libs/gui/aidl/android/gui/WindowInfosListenerInfo.aidl b/libs/gui/aidl/android/gui/WindowInfosListenerInfo.aidl
new file mode 100644
index 0000000..0ca13b7
--- /dev/null
+++ b/libs/gui/aidl/android/gui/WindowInfosListenerInfo.aidl
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 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.
+ */
+
+package android.gui;
+
+import android.gui.IWindowInfosPublisher;
+
+/** @hide */
+parcelable WindowInfosListenerInfo {
+    long listenerId;
+    IWindowInfosPublisher windowInfosPublisher;
+}
\ No newline at end of file
diff --git a/libs/gui/android/gui/IWindowInfosListener.aidl b/libs/gui/android/gui/IWindowInfosListener.aidl
index 400229d..07cb5ed 100644
--- a/libs/gui/android/gui/IWindowInfosListener.aidl
+++ b/libs/gui/android/gui/IWindowInfosListener.aidl
@@ -16,11 +16,9 @@
 
 package android.gui;
 
-import android.gui.IWindowInfosReportedListener;
 import android.gui.WindowInfosUpdate;
 
 /** @hide */
 oneway interface IWindowInfosListener {
-    void onWindowInfosChanged(
-        in WindowInfosUpdate update, in @nullable IWindowInfosReportedListener windowInfosReportedListener);
+    void onWindowInfosChanged(in WindowInfosUpdate update);
 }
diff --git a/libs/gui/android/gui/IWindowInfosPublisher.aidl b/libs/gui/android/gui/IWindowInfosPublisher.aidl
new file mode 100644
index 0000000..5a9c328
--- /dev/null
+++ b/libs/gui/android/gui/IWindowInfosPublisher.aidl
@@ -0,0 +1,23 @@
+/**
+ * Copyright (c) 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.
+ */
+
+package android.gui;
+
+/** @hide */
+oneway interface IWindowInfosPublisher
+{
+    void ackWindowInfosReceived(long vsyncId, long listenerId);
+}
diff --git a/libs/gui/fuzzer/libgui_fuzzer_utils.h b/libs/gui/fuzzer/libgui_fuzzer_utils.h
index 8c003d8..4c7d056 100644
--- a/libs/gui/fuzzer/libgui_fuzzer_utils.h
+++ b/libs/gui/fuzzer/libgui_fuzzer_utils.h
@@ -153,8 +153,8 @@
     MOCK_METHOD(binder::Status, setOverrideFrameRate, (int32_t, float), (override));
     MOCK_METHOD(binder::Status, getGpuContextPriority, (int32_t*), (override));
     MOCK_METHOD(binder::Status, getMaxAcquiredBufferCount, (int32_t*), (override));
-    MOCK_METHOD(binder::Status, addWindowInfosListener, (const sp<gui::IWindowInfosListener>&),
-                (override));
+    MOCK_METHOD(binder::Status, addWindowInfosListener,
+                (const sp<gui::IWindowInfosListener>&, gui::WindowInfosListenerInfo*), (override));
     MOCK_METHOD(binder::Status, removeWindowInfosListener, (const sp<gui::IWindowInfosListener>&),
                 (override));
     MOCK_METHOD(binder::Status, getOverlaySupport, (gui::OverlayProperties*), (override));
diff --git a/libs/gui/include/gui/ISurfaceComposer.h b/libs/gui/include/gui/ISurfaceComposer.h
index 7c150d5..3ff6735 100644
--- a/libs/gui/include/gui/ISurfaceComposer.h
+++ b/libs/gui/include/gui/ISurfaceComposer.h
@@ -26,6 +26,7 @@
 #include <android/gui/IScreenCaptureListener.h>
 #include <android/gui/ITunnelModeEnabledListener.h>
 #include <android/gui/IWindowInfosListener.h>
+#include <android/gui/IWindowInfosPublisher.h>
 #include <binder/IBinder.h>
 #include <binder/IInterface.h>
 #include <gui/ITransactionCompletedListener.h>
diff --git a/libs/gui/include/gui/WindowInfosListenerReporter.h b/libs/gui/include/gui/WindowInfosListenerReporter.h
index 38cb108..684e21a 100644
--- a/libs/gui/include/gui/WindowInfosListenerReporter.h
+++ b/libs/gui/include/gui/WindowInfosListenerReporter.h
@@ -18,7 +18,7 @@
 
 #include <android/gui/BnWindowInfosListener.h>
 #include <android/gui/ISurfaceComposer.h>
-#include <android/gui/IWindowInfosReportedListener.h>
+#include <android/gui/IWindowInfosPublisher.h>
 #include <binder/IBinder.h>
 #include <gui/SpHash.h>
 #include <gui/WindowInfosListener.h>
@@ -30,8 +30,7 @@
 class WindowInfosListenerReporter : public gui::BnWindowInfosListener {
 public:
     static sp<WindowInfosListenerReporter> getInstance();
-    binder::Status onWindowInfosChanged(const gui::WindowInfosUpdate& update,
-                                        const sp<gui::IWindowInfosReportedListener>&) override;
+    binder::Status onWindowInfosChanged(const gui::WindowInfosUpdate& update) override;
     status_t addWindowInfosListener(
             const sp<gui::WindowInfosListener>& windowInfosListener,
             const sp<gui::ISurfaceComposer>&,
@@ -47,5 +46,8 @@
 
     std::vector<gui::WindowInfo> mLastWindowInfos GUARDED_BY(mListenersMutex);
     std::vector<gui::DisplayInfo> mLastDisplayInfos GUARDED_BY(mListenersMutex);
+
+    sp<gui::IWindowInfosPublisher> mWindowInfosPublisher;
+    int64_t mListenerId;
 };
 } // namespace android
diff --git a/libs/gui/tests/Surface_test.cpp b/libs/gui/tests/Surface_test.cpp
index 90c0a63..567604d 100644
--- a/libs/gui/tests/Surface_test.cpp
+++ b/libs/gui/tests/Surface_test.cpp
@@ -1002,7 +1002,8 @@
     }
 
     binder::Status addWindowInfosListener(
-            const sp<gui::IWindowInfosListener>& /*windowInfosListener*/) override {
+            const sp<gui::IWindowInfosListener>& /*windowInfosListener*/,
+            gui::WindowInfosListenerInfo* /*outInfo*/) override {
         return binder::Status::ok();
     }
 
diff --git a/libs/input/MotionPredictor.cpp b/libs/input/MotionPredictor.cpp
index 68e6888..c2ea35c 100644
--- a/libs/input/MotionPredictor.cpp
+++ b/libs/input/MotionPredictor.cpp
@@ -138,7 +138,8 @@
     // Pass input event to the MetricsManager.
     if (!mMetricsManager) {
         mMetricsManager =
-                std::make_optional<MotionPredictorMetricsManager>(mModel->predictionInterval(),
+                std::make_optional<MotionPredictorMetricsManager>(mModel->config()
+                                                                          .predictionInterval,
                                                                   mModel->outputLength());
     }
     mMetricsManager->onRecord(event);
@@ -184,8 +185,18 @@
     const int64_t futureTime = timestamp + mPredictionTimestampOffsetNanos;
 
     for (int i = 0; i < predictedR.size() && predictionTime <= futureTime; ++i) {
-        // TODO(b/266747654): Stop predictions if confidence and/or predicted pressure are below
-        // some thresholds.
+        if (predictedR[i] < mModel->config().distanceNoiseFloor) {
+            // Stop predicting when the predicted output is below the model's noise floor.
+            //
+            // We assume that all subsequent predictions in the batch are unreliable because later
+            // predictions are conditional on earlier predictions, and a state of noise is not a
+            // good basis for prediction.
+            //
+            // The UX trade-off is that this potentially sacrifices some predictions when the input
+            // device starts to speed up, but avoids producing noisy predictions as it slows down.
+            break;
+        }
+        // TODO(b/266747654): Stop predictions if confidence is < some threshold.
 
         const TfLiteMotionPredictorSample::Point predictedPoint =
                 convertPrediction(axisFrom, axisTo, predictedR[i], predictedPhi[i]);
@@ -197,7 +208,7 @@
         coords.setAxisValue(AMOTION_EVENT_AXIS_Y, predictedPoint.y);
         coords.setAxisValue(AMOTION_EVENT_AXIS_PRESSURE, predictedPressure[i]);
 
-        predictionTime += mModel->predictionInterval();
+        predictionTime += mModel->config().predictionInterval;
         if (i == 0) {
             hasPredictions = true;
             prediction->initialize(InputEvent::nextId(), event.getDeviceId(), event.getSource(),
diff --git a/libs/input/TfLiteMotionPredictor.cpp b/libs/input/TfLiteMotionPredictor.cpp
index 9f4aaa8..5984b4d3 100644
--- a/libs/input/TfLiteMotionPredictor.cpp
+++ b/libs/input/TfLiteMotionPredictor.cpp
@@ -100,6 +100,16 @@
     return value;
 }
 
+float parseXMLFloat(const tinyxml2::XMLElement& configRoot, const char* elementName) {
+    const tinyxml2::XMLElement* element = configRoot.FirstChildElement(elementName);
+    LOG_ALWAYS_FATAL_IF(!element, "Could not find '%s' element", elementName);
+
+    float value = 0;
+    LOG_ALWAYS_FATAL_IF(element->QueryFloatText(&value) != tinyxml2::XML_SUCCESS,
+                        "Failed to parse %s: %s", elementName, element->GetText());
+    return value;
+}
+
 // A TFLite ErrorReporter that logs to logcat.
 class LoggingErrorReporter : public tflite::ErrorReporter {
 public:
@@ -152,6 +162,7 @@
                          ::tflite::ops::builtin::Register_CONCATENATION());
     resolver->AddBuiltin(::tflite::BuiltinOperator_FULLY_CONNECTED,
                          ::tflite::ops::builtin::Register_FULLY_CONNECTED());
+    resolver->AddBuiltin(::tflite::BuiltinOperator_GELU, ::tflite::ops::builtin::Register_GELU());
     return resolver;
 }
 
@@ -208,13 +219,7 @@
     float phi = 0;
     float orientation = 0;
 
-    // Ignore the sample if there is no movement. These samples can occur when there's change to a
-    // property other than the coordinates and pollute the input to the model.
-    if (r == 0) {
-        return;
-    }
-
-    if (!mAxisFrom) { // Second point.
+    if (!mAxisFrom && r > 0) { // Second point.
         // We can only determine the distance from the first point, and not any
         // angle. However, if the second point forms an axis, the orientation can
         // be transformed relative to that axis.
@@ -235,8 +240,10 @@
     }
 
     // Update the axis for the next point.
-    mAxisFrom = mAxisTo;
-    mAxisTo = sample;
+    if (r > 0) {
+        mAxisFrom = mAxisTo;
+        mAxisTo = sample;
+    }
 
     // Push the current sample onto the end of the input buffers.
     mInputR.pushBack(r);
@@ -272,15 +279,18 @@
     // Parse configuration file.
     const tinyxml2::XMLElement* configRoot = configDocument.FirstChildElement("motion-predictor");
     LOG_ALWAYS_FATAL_IF(!configRoot);
-    const nsecs_t predictionInterval = parseXMLInt64(*configRoot, "prediction-interval");
+    Config config{
+            .predictionInterval = parseXMLInt64(*configRoot, "prediction-interval"),
+            .distanceNoiseFloor = parseXMLFloat(*configRoot, "distance-noise-floor"),
+    };
 
     return std::unique_ptr<TfLiteMotionPredictorModel>(
-            new TfLiteMotionPredictorModel(std::move(modelBuffer), predictionInterval));
+            new TfLiteMotionPredictorModel(std::move(modelBuffer), std::move(config)));
 }
 
 TfLiteMotionPredictorModel::TfLiteMotionPredictorModel(
-        std::unique_ptr<android::base::MappedFile> model, nsecs_t predictionInterval)
-      : mFlatBuffer(std::move(model)), mPredictionInterval(predictionInterval) {
+        std::unique_ptr<android::base::MappedFile> model, Config config)
+      : mFlatBuffer(std::move(model)), mConfig(std::move(config)) {
     CHECK(mFlatBuffer);
     mErrorReporter = std::make_unique<LoggingErrorReporter>();
     mModel = tflite::FlatBufferModel::VerifyAndBuildFromBuffer(mFlatBuffer->data(),
diff --git a/libs/input/tests/MotionPredictor_test.cpp b/libs/input/tests/MotionPredictor_test.cpp
index 7a62f5e..4ac7ae9 100644
--- a/libs/input/tests/MotionPredictor_test.cpp
+++ b/libs/input/tests/MotionPredictor_test.cpp
@@ -72,11 +72,20 @@
     ASSERT_FALSE(predictor.isPredictionAvailable(/*deviceId=*/1, AINPUT_SOURCE_TOUCHSCREEN));
 }
 
+TEST(MotionPredictorTest, StationaryNoiseFloor) {
+    MotionPredictor predictor(/*predictionTimestampOffsetNanos=*/1,
+                              []() { return true /*enable prediction*/; });
+    predictor.record(getMotionEvent(DOWN, 0, 1, 30ms));
+    predictor.record(getMotionEvent(MOVE, 0, 1, 35ms)); // No movement.
+    std::unique_ptr<MotionEvent> predicted = predictor.predict(40 * NSEC_PER_MSEC);
+    ASSERT_EQ(nullptr, predicted);
+}
+
 TEST(MotionPredictorTest, Offset) {
     MotionPredictor predictor(/*predictionTimestampOffsetNanos=*/1,
                               []() { return true /*enable prediction*/; });
     predictor.record(getMotionEvent(DOWN, 0, 1, 30ms));
-    predictor.record(getMotionEvent(MOVE, 0, 2, 35ms));
+    predictor.record(getMotionEvent(MOVE, 0, 5, 35ms)); // Move enough to overcome the noise floor.
     std::unique_ptr<MotionEvent> predicted = predictor.predict(40 * NSEC_PER_MSEC);
     ASSERT_NE(nullptr, predicted);
     ASSERT_GE(predicted->getEventTime(), 41);
diff --git a/libs/renderengine/skia/SkiaRenderEngine.cpp b/libs/renderengine/skia/SkiaRenderEngine.cpp
index 18d9864..0212e08 100644
--- a/libs/renderengine/skia/SkiaRenderEngine.cpp
+++ b/libs/renderengine/skia/SkiaRenderEngine.cpp
@@ -713,7 +713,9 @@
     SkCanvas* canvas = dstCanvas;
     SkiaCapture::OffscreenState offscreenCaptureState;
     const LayerSettings* blurCompositionLayer = nullptr;
-    if (mBlurFilter) {
+
+    // TODO (b/270314344): Enable blurs in protected context.
+    if (mBlurFilter && !mInProtectedContext) {
         bool requiresCompositionLayer = false;
         for (const auto& layer : layers) {
             // if the layer doesn't have blur or it is not visible then continue
@@ -807,7 +809,8 @@
         const auto [bounds, roundRectClip] =
                 getBoundsAndClip(layer.geometry.boundaries, layer.geometry.roundedCornersCrop,
                                  layer.geometry.roundedCornersRadius);
-        if (mBlurFilter && layerHasBlur(layer, ctModifiesAlpha)) {
+        // TODO (b/270314344): Enable blurs in protected context.
+        if (mBlurFilter && layerHasBlur(layer, ctModifiesAlpha) && !mInProtectedContext) {
             std::unordered_map<uint32_t, sk_sp<SkImage>> cachedBlurs;
 
             // if multiple layers have blur, then we need to take a snapshot now because
diff --git a/services/inputflinger/include/InputReaderBase.h b/services/inputflinger/include/InputReaderBase.h
index a93a2ea..88e1d7d 100644
--- a/services/inputflinger/include/InputReaderBase.h
+++ b/services/inputflinger/include/InputReaderBase.h
@@ -447,6 +447,9 @@
             const std::string& inputDeviceDescriptor, ui::Rotation surfaceRotation) = 0;
     /* Notifies the input reader policy that a stylus gesture has started. */
     virtual void notifyStylusGestureStarted(int32_t deviceId, nsecs_t eventTime) = 0;
+
+    /* Returns true if any InputConnection is currently active. */
+    virtual bool isInputMethodConnectionActive() = 0;
 };
 
 } // namespace android
diff --git a/services/inputflinger/reader/InputReader.cpp b/services/inputflinger/reader/InputReader.cpp
index ea95f78..08600b2 100644
--- a/services/inputflinger/reader/InputReader.cpp
+++ b/services/inputflinger/reader/InputReader.cpp
@@ -1040,6 +1040,16 @@
     return mReader->getLedMetaStateLocked();
 }
 
+void InputReader::ContextImpl::setPreventingTouchpadTaps(bool prevent) {
+    // lock is already held by the input loop
+    mReader->mPreventingTouchpadTaps = prevent;
+}
+
+bool InputReader::ContextImpl::isPreventingTouchpadTaps() {
+    // lock is already held by the input loop
+    return mReader->mPreventingTouchpadTaps;
+}
+
 void InputReader::ContextImpl::disableVirtualKeysUntil(nsecs_t time) {
     // lock is already held by the input loop
     mReader->disableVirtualKeysUntilLocked(time);
diff --git a/services/inputflinger/reader/include/InputReader.h b/services/inputflinger/reader/include/InputReader.h
index 9112913..01ec7c1 100644
--- a/services/inputflinger/reader/include/InputReader.h
+++ b/services/inputflinger/reader/include/InputReader.h
@@ -155,6 +155,9 @@
         int32_t getNextId() NO_THREAD_SAFETY_ANALYSIS override;
         void updateLedMetaState(int32_t metaState) REQUIRES(mReader->mLock) override;
         int32_t getLedMetaState() REQUIRES(mReader->mLock) REQUIRES(mLock) override;
+        void setPreventingTouchpadTaps(bool prevent) REQUIRES(mReader->mLock)
+                REQUIRES(mLock) override;
+        bool isPreventingTouchpadTaps() REQUIRES(mReader->mLock) REQUIRES(mLock) override;
     } mContext;
 
     friend class ContextImpl;
@@ -185,6 +188,9 @@
     std::unordered_map<std::shared_ptr<InputDevice>, std::vector<int32_t> /*eventHubId*/>
             mDeviceToEventHubIdsMap GUARDED_BY(mLock);
 
+    // true if tap-to-click on touchpad currently disabled
+    bool mPreventingTouchpadTaps GUARDED_BY(mLock){false};
+
     // low-level input event decoding and device management
     [[nodiscard]] std::list<NotifyArgs> processEventsLocked(const RawEvent* rawEvents, size_t count)
             REQUIRES(mLock);
diff --git a/services/inputflinger/reader/include/InputReaderContext.h b/services/inputflinger/reader/include/InputReaderContext.h
index 0beace1..aed7563 100644
--- a/services/inputflinger/reader/include/InputReaderContext.h
+++ b/services/inputflinger/reader/include/InputReaderContext.h
@@ -62,6 +62,9 @@
 
     virtual void updateLedMetaState(int32_t metaState) = 0;
     virtual int32_t getLedMetaState() = 0;
+
+    virtual void setPreventingTouchpadTaps(bool prevent) = 0;
+    virtual bool isPreventingTouchpadTaps() = 0;
 };
 
 } // namespace android
diff --git a/services/inputflinger/reader/mapper/KeyboardInputMapper.cpp b/services/inputflinger/reader/mapper/KeyboardInputMapper.cpp
index 7388752..5c42e10 100644
--- a/services/inputflinger/reader/mapper/KeyboardInputMapper.cpp
+++ b/services/inputflinger/reader/mapper/KeyboardInputMapper.cpp
@@ -242,6 +242,7 @@
             keyDown.downTime = when;
             mKeyDowns.push_back(keyDown);
         }
+        onKeyDownProcessed();
     } else {
         // Remove key down.
         if (keyDownIndex) {
@@ -419,4 +420,19 @@
     return out;
 }
 
+void KeyboardInputMapper::onKeyDownProcessed() {
+    InputReaderContext& context = *getContext();
+    if (context.isPreventingTouchpadTaps()) {
+        // avoid pinging java service unnecessarily
+        return;
+    }
+    // Ignore meta keys or multiple simultaneous down keys as they are likely to be keyboard
+    // shortcuts
+    bool shouldHideCursor = mKeyDowns.size() == 1 && !isMetaKey(mKeyDowns[0].keyCode);
+    if (shouldHideCursor && context.getPolicy()->isInputMethodConnectionActive()) {
+        context.fadePointer();
+        context.setPreventingTouchpadTaps(true);
+    }
+}
+
 } // namespace android
diff --git a/services/inputflinger/reader/mapper/KeyboardInputMapper.h b/services/inputflinger/reader/mapper/KeyboardInputMapper.h
index cd3d3c4..96044eb 100644
--- a/services/inputflinger/reader/mapper/KeyboardInputMapper.h
+++ b/services/inputflinger/reader/mapper/KeyboardInputMapper.h
@@ -104,6 +104,7 @@
     void updateLedStateForModifier(LedState& ledState, int32_t led, int32_t modifier, bool reset);
     std::optional<DisplayViewport> findViewport(const InputReaderConfiguration& readerConfig);
     [[nodiscard]] std::list<NotifyArgs> cancelAllDownKeys(nsecs_t when);
+    void onKeyDownProcessed();
 };
 
 } // namespace android
diff --git a/services/inputflinger/reader/mapper/gestures/GestureConverter.cpp b/services/inputflinger/reader/mapper/gestures/GestureConverter.cpp
index e826341..3abf2bd 100644
--- a/services/inputflinger/reader/mapper/gestures/GestureConverter.cpp
+++ b/services/inputflinger/reader/mapper/gestures/GestureConverter.cpp
@@ -153,6 +153,9 @@
                                               const Gesture& gesture) {
     float deltaX = gesture.details.move.dx;
     float deltaY = gesture.details.move.dy;
+    if (std::abs(deltaX) > 0 || std::abs(deltaY) > 0) {
+        enableTapToClick();
+    }
     rotateDelta(mOrientation, &deltaX, &deltaY);
 
     mPointerController->setPresentation(PointerControllerInterface::Presentation::POINTER);
@@ -191,6 +194,15 @@
     coords.setAxisValue(AMOTION_EVENT_AXIS_Y, yCursorPosition);
     coords.setAxisValue(AMOTION_EVENT_AXIS_RELATIVE_X, 0);
     coords.setAxisValue(AMOTION_EVENT_AXIS_RELATIVE_Y, 0);
+
+    if (mReaderContext.isPreventingTouchpadTaps()) {
+        enableTapToClick();
+        if (gesture.details.buttons.is_tap) {
+            // return early to prevent this tap
+            return out;
+        }
+    }
+
     const uint32_t buttonsPressed = gesture.details.buttons.down;
     bool pointerDown = isPointerDown(mButtonState) ||
             buttonsPressed &
@@ -337,6 +349,9 @@
                 // magnitude, which will also result in the pointer icon being updated.
                 // TODO(b/282023644): Add a signal in libgestures for when a stable contact has been
                 //  initiated with a touchpad.
+                if (!mReaderContext.isPreventingTouchpadTaps()) {
+                    enableTapToClick();
+                }
                 return {handleMove(when, readTime,
                                    Gesture(kGestureMove, gesture.start_time, gesture.end_time,
                                            /*dx=*/0.f,
@@ -545,7 +560,7 @@
             /* policyFlags= */ POLICY_FLAG_WAKE,
             action,
             /* actionButton= */ actionButton,
-            /* flags= */ 0,
+            /* flags= */ action == AMOTION_EVENT_ACTION_CANCEL ? AMOTION_EVENT_FLAG_CANCELED : 0,
             mReaderContext.getGlobalMetaState(),
             buttonState,
             mCurrentClassification,
@@ -561,4 +576,8 @@
             /* videoFrames= */ {}};
 }
 
+void GestureConverter::enableTapToClick() {
+    mReaderContext.setPreventingTouchpadTaps(false);
+}
+
 } // namespace android
diff --git a/services/inputflinger/reader/mapper/gestures/GestureConverter.h b/services/inputflinger/reader/mapper/gestures/GestureConverter.h
index b613b88..3ea3790 100644
--- a/services/inputflinger/reader/mapper/gestures/GestureConverter.h
+++ b/services/inputflinger/reader/mapper/gestures/GestureConverter.h
@@ -78,6 +78,8 @@
                                     const PointerCoords* pointerCoords, float xCursorPosition,
                                     float yCursorPosition);
 
+    void enableTapToClick();
+
     const int32_t mDeviceId;
     InputReaderContext& mReaderContext;
     std::shared_ptr<PointerControllerInterface> mPointerController;
diff --git a/services/inputflinger/tests/Android.bp b/services/inputflinger/tests/Android.bp
index 300bb85..1585fdd 100644
--- a/services/inputflinger/tests/Android.bp
+++ b/services/inputflinger/tests/Android.bp
@@ -62,6 +62,7 @@
         "SyncQueue_test.cpp",
         "TestInputListener.cpp",
         "TouchpadInputMapper_test.cpp",
+        "KeyboardInputMapper_test.cpp",
         "UinputDevice.cpp",
         "UnwantedInteractionBlocker_test.cpp",
     ],
diff --git a/services/inputflinger/tests/FakeInputReaderPolicy.cpp b/services/inputflinger/tests/FakeInputReaderPolicy.cpp
index 3486d0f..30222bf 100644
--- a/services/inputflinger/tests/FakeInputReaderPolicy.cpp
+++ b/services/inputflinger/tests/FakeInputReaderPolicy.cpp
@@ -209,6 +209,14 @@
     mConfig.stylusPointerIconEnabled = enabled;
 }
 
+void FakeInputReaderPolicy::setIsInputMethodConnectionActive(bool active) {
+    mIsInputMethodConnectionActive = active;
+}
+
+bool FakeInputReaderPolicy::isInputMethodConnectionActive() {
+    return mIsInputMethodConnectionActive;
+}
+
 void FakeInputReaderPolicy::getReaderConfiguration(InputReaderConfiguration* outConfig) {
     *outConfig = mConfig;
 }
diff --git a/services/inputflinger/tests/FakeInputReaderPolicy.h b/services/inputflinger/tests/FakeInputReaderPolicy.h
index 85ff01a..78bb2c3 100644
--- a/services/inputflinger/tests/FakeInputReaderPolicy.h
+++ b/services/inputflinger/tests/FakeInputReaderPolicy.h
@@ -77,6 +77,8 @@
     void setVelocityControlParams(const VelocityControlParameters& params);
     void setStylusButtonMotionEventsEnabled(bool enabled);
     void setStylusPointerIconEnabled(bool enabled);
+    void setIsInputMethodConnectionActive(bool active);
+    bool isInputMethodConnectionActive() override;
 
 private:
     void getReaderConfiguration(InputReaderConfiguration* outConfig) override;
@@ -99,6 +101,7 @@
     std::vector<DisplayViewport> mViewports;
     TouchAffineTransformation transform;
     std::optional<int32_t /*deviceId*/> mStylusGestureNotified GUARDED_BY(mLock){};
+    bool mIsInputMethodConnectionActive{false};
 
     uint32_t mNextPointerCaptureSequenceNumber{0};
 };
diff --git a/services/inputflinger/tests/GestureConverter_test.cpp b/services/inputflinger/tests/GestureConverter_test.cpp
index 482a266..4df0f69 100644
--- a/services/inputflinger/tests/GestureConverter_test.cpp
+++ b/services/inputflinger/tests/GestureConverter_test.cpp
@@ -983,4 +983,226 @@
     ASSERT_TRUE(mFakePointerController->isPointerShown());
 }
 
+TEST_F(GestureConverterTest, Tap) {
+    // Tap should produce button press/release events
+    InputDeviceContext deviceContext(*mDevice, EVENTHUB_ID);
+    GestureConverter converter(*mReader->getContext(), deviceContext, DEVICE_ID);
+
+    Gesture flingGesture(kGestureFling, ARBITRARY_GESTURE_TIME, ARBITRARY_GESTURE_TIME, /* vx= */ 0,
+                         /* vy= */ 0, GESTURES_FLING_TAP_DOWN);
+    std::list<NotifyArgs> args = converter.handleGesture(ARBITRARY_TIME, READ_TIME, flingGesture);
+
+    ASSERT_EQ(1u, args.size());
+    ASSERT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_MOVE),
+                      WithCoords(POINTER_X, POINTER_Y), WithRelativeMotion(0, 0),
+                      WithToolType(ToolType::FINGER), WithButtonState(0), WithPressure(0.0f)));
+
+    Gesture tapGesture(kGestureButtonsChange, ARBITRARY_GESTURE_TIME, ARBITRARY_GESTURE_TIME,
+                       /* down= */ GESTURES_BUTTON_LEFT,
+                       /* up= */ GESTURES_BUTTON_LEFT, /* is_tap= */ true);
+    args = converter.handleGesture(ARBITRARY_TIME, READ_TIME, tapGesture);
+
+    ASSERT_EQ(5u, args.size());
+    ASSERT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_DOWN), WithCoords(POINTER_X, POINTER_Y),
+                      WithRelativeMotion(0.f, 0.f), WithToolType(ToolType::FINGER),
+                      WithButtonState(AMOTION_EVENT_BUTTON_PRIMARY), WithPressure(1.0f)));
+    args.pop_front();
+    ASSERT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_BUTTON_PRESS),
+                      WithActionButton(AMOTION_EVENT_BUTTON_PRIMARY),
+                      WithButtonState(AMOTION_EVENT_BUTTON_PRIMARY),
+                      WithCoords(POINTER_X, POINTER_Y), WithRelativeMotion(0.f, 0.f),
+                      WithToolType(ToolType::FINGER), WithButtonState(AMOTION_EVENT_BUTTON_PRIMARY),
+                      WithPressure(1.0f)));
+    args.pop_front();
+    ASSERT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_BUTTON_RELEASE),
+                      WithActionButton(AMOTION_EVENT_BUTTON_PRIMARY), WithButtonState(0),
+                      WithCoords(POINTER_X, POINTER_Y), WithRelativeMotion(0.f, 0.f),
+                      WithToolType(ToolType::FINGER), WithButtonState(0), WithPressure(1.0f)));
+    args.pop_front();
+    ASSERT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_UP), WithCoords(POINTER_X, POINTER_Y),
+                      WithRelativeMotion(0.f, 0.f), WithToolType(ToolType::FINGER),
+                      WithButtonState(0), WithPressure(0.0f)));
+    args.pop_front();
+    ASSERT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_MOVE),
+                      WithCoords(POINTER_X, POINTER_Y), WithRelativeMotion(0, 0),
+                      WithToolType(ToolType::FINGER), WithButtonState(0), WithPressure(0.0f)));
+}
+
+TEST_F(GestureConverterTest, Click) {
+    // Click should produce button press/release events
+    InputDeviceContext deviceContext(*mDevice, EVENTHUB_ID);
+    GestureConverter converter(*mReader->getContext(), deviceContext, DEVICE_ID);
+
+    Gesture flingGesture(kGestureFling, ARBITRARY_GESTURE_TIME, ARBITRARY_GESTURE_TIME, /* vx= */ 0,
+                         /* vy= */ 0, GESTURES_FLING_TAP_DOWN);
+    std::list<NotifyArgs> args = converter.handleGesture(ARBITRARY_TIME, READ_TIME, flingGesture);
+
+    ASSERT_EQ(1u, args.size());
+    ASSERT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_MOVE),
+                      WithCoords(POINTER_X, POINTER_Y), WithRelativeMotion(0, 0),
+                      WithToolType(ToolType::FINGER), WithButtonState(0), WithPressure(0.0f)));
+
+    Gesture buttonDownGesture(kGestureButtonsChange, ARBITRARY_GESTURE_TIME, ARBITRARY_GESTURE_TIME,
+                              /* down= */ GESTURES_BUTTON_LEFT,
+                              /* up= */ GESTURES_BUTTON_NONE, /* is_tap= */ false);
+    args = converter.handleGesture(ARBITRARY_TIME, READ_TIME, buttonDownGesture);
+
+    ASSERT_EQ(2u, args.size());
+    ASSERT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_DOWN), WithCoords(POINTER_X, POINTER_Y),
+                      WithRelativeMotion(0.f, 0.f), WithToolType(ToolType::FINGER),
+                      WithButtonState(AMOTION_EVENT_BUTTON_PRIMARY), WithPressure(1.0f)));
+    args.pop_front();
+    ASSERT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_BUTTON_PRESS),
+                      WithActionButton(AMOTION_EVENT_BUTTON_PRIMARY),
+                      WithButtonState(AMOTION_EVENT_BUTTON_PRIMARY),
+                      WithCoords(POINTER_X, POINTER_Y), WithRelativeMotion(0.f, 0.f),
+                      WithToolType(ToolType::FINGER), WithButtonState(AMOTION_EVENT_BUTTON_PRIMARY),
+                      WithPressure(1.0f)));
+
+    Gesture buttonUpGesture(kGestureButtonsChange, ARBITRARY_GESTURE_TIME, ARBITRARY_GESTURE_TIME,
+                            /* down= */ GESTURES_BUTTON_NONE,
+                            /* up= */ GESTURES_BUTTON_LEFT, /* is_tap= */ false);
+    args = converter.handleGesture(ARBITRARY_TIME, READ_TIME, buttonUpGesture);
+
+    ASSERT_EQ(3u, args.size());
+    ASSERT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_BUTTON_RELEASE),
+                      WithActionButton(AMOTION_EVENT_BUTTON_PRIMARY), WithButtonState(0),
+                      WithCoords(POINTER_X, POINTER_Y), WithRelativeMotion(0.f, 0.f),
+                      WithToolType(ToolType::FINGER), WithButtonState(0), WithPressure(1.0f)));
+    args.pop_front();
+    ASSERT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_UP), WithCoords(POINTER_X, POINTER_Y),
+                      WithRelativeMotion(0.f, 0.f), WithToolType(ToolType::FINGER),
+                      WithButtonState(0), WithPressure(0.0f)));
+    args.pop_front();
+    ASSERT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_MOVE),
+                      WithCoords(POINTER_X, POINTER_Y), WithRelativeMotion(0, 0),
+                      WithToolType(ToolType::FINGER), WithButtonState(0), WithPressure(0.0f)));
+}
+
+TEST_F(GestureConverterTest, TapWithTapToClickDisabled) {
+    // Tap should be ignored when disabled
+    mReader->getContext()->setPreventingTouchpadTaps(true);
+
+    InputDeviceContext deviceContext(*mDevice, EVENTHUB_ID);
+    GestureConverter converter(*mReader->getContext(), deviceContext, DEVICE_ID);
+
+    Gesture flingGesture(kGestureFling, ARBITRARY_GESTURE_TIME, ARBITRARY_GESTURE_TIME, /* vx= */ 0,
+                         /* vy= */ 0, GESTURES_FLING_TAP_DOWN);
+    std::list<NotifyArgs> args = converter.handleGesture(ARBITRARY_TIME, READ_TIME, flingGesture);
+
+    ASSERT_EQ(1u, args.size());
+    ASSERT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_MOVE),
+                      WithCoords(POINTER_X, POINTER_Y), WithRelativeMotion(0, 0),
+                      WithToolType(ToolType::FINGER), WithButtonState(0), WithPressure(0.0f)));
+    args.pop_front();
+
+    Gesture tapGesture(kGestureButtonsChange, ARBITRARY_GESTURE_TIME, ARBITRARY_GESTURE_TIME,
+                       /* down= */ GESTURES_BUTTON_LEFT,
+                       /* up= */ GESTURES_BUTTON_LEFT, /* is_tap= */ true);
+    args = converter.handleGesture(ARBITRARY_TIME, READ_TIME, tapGesture);
+
+    // no events should be generated
+    ASSERT_EQ(0u, args.size());
+
+    // Future taps should be re-enabled
+    ASSERT_FALSE(mReader->getContext()->isPreventingTouchpadTaps());
+}
+
+TEST_F(GestureConverterTest, ClickWithTapToClickDisabled) {
+    // Click should still produce button press/release events
+    mReader->getContext()->setPreventingTouchpadTaps(true);
+
+    InputDeviceContext deviceContext(*mDevice, EVENTHUB_ID);
+    GestureConverter converter(*mReader->getContext(), deviceContext, DEVICE_ID);
+
+    Gesture flingGesture(kGestureFling, ARBITRARY_GESTURE_TIME, ARBITRARY_GESTURE_TIME, /* vx= */ 0,
+                         /* vy= */ 0, GESTURES_FLING_TAP_DOWN);
+    std::list<NotifyArgs> args = converter.handleGesture(ARBITRARY_TIME, READ_TIME, flingGesture);
+
+    ASSERT_EQ(1u, args.size());
+    ASSERT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_MOVE),
+                      WithCoords(POINTER_X, POINTER_Y), WithRelativeMotion(0, 0),
+                      WithToolType(ToolType::FINGER), WithButtonState(0), WithPressure(0.0f)));
+
+    Gesture buttonDownGesture(kGestureButtonsChange, ARBITRARY_GESTURE_TIME, ARBITRARY_GESTURE_TIME,
+                              /* down= */ GESTURES_BUTTON_LEFT,
+                              /* up= */ GESTURES_BUTTON_NONE, /* is_tap= */ false);
+    args = converter.handleGesture(ARBITRARY_TIME, READ_TIME, buttonDownGesture);
+    ASSERT_EQ(2u, args.size());
+
+    ASSERT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_DOWN), WithCoords(POINTER_X, POINTER_Y),
+                      WithRelativeMotion(0.f, 0.f), WithToolType(ToolType::FINGER),
+                      WithButtonState(AMOTION_EVENT_BUTTON_PRIMARY), WithPressure(1.0f)));
+    args.pop_front();
+    ASSERT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_BUTTON_PRESS),
+                      WithActionButton(AMOTION_EVENT_BUTTON_PRIMARY),
+                      WithButtonState(AMOTION_EVENT_BUTTON_PRIMARY),
+                      WithCoords(POINTER_X, POINTER_Y), WithRelativeMotion(0.f, 0.f),
+                      WithToolType(ToolType::FINGER), WithButtonState(AMOTION_EVENT_BUTTON_PRIMARY),
+                      WithPressure(1.0f)));
+
+    Gesture buttonUpGesture(kGestureButtonsChange, ARBITRARY_GESTURE_TIME, ARBITRARY_GESTURE_TIME,
+                            /* down= */ GESTURES_BUTTON_NONE,
+                            /* up= */ GESTURES_BUTTON_LEFT, /* is_tap= */ false);
+    args = converter.handleGesture(ARBITRARY_TIME, READ_TIME, buttonUpGesture);
+
+    ASSERT_EQ(3u, args.size());
+    ASSERT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_BUTTON_RELEASE),
+                      WithActionButton(AMOTION_EVENT_BUTTON_PRIMARY), WithButtonState(0),
+                      WithCoords(POINTER_X, POINTER_Y), WithRelativeMotion(0.f, 0.f),
+                      WithToolType(ToolType::FINGER), WithButtonState(0), WithPressure(1.0f)));
+    args.pop_front();
+    ASSERT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_UP), WithCoords(POINTER_X, POINTER_Y),
+                      WithRelativeMotion(0.f, 0.f), WithToolType(ToolType::FINGER),
+                      WithButtonState(0), WithPressure(0.0f)));
+    args.pop_front();
+    ASSERT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_MOVE),
+                      WithCoords(POINTER_X, POINTER_Y), WithRelativeMotion(0, 0),
+                      WithToolType(ToolType::FINGER), WithButtonState(0), WithPressure(0.0f)));
+
+    // Future taps should be re-enabled
+    ASSERT_FALSE(mReader->getContext()->isPreventingTouchpadTaps());
+}
+
+TEST_F(GestureConverterTest, MoveEnablesTapToClick) {
+    // initially disable tap-to-click
+    mReader->getContext()->setPreventingTouchpadTaps(true);
+
+    InputDeviceContext deviceContext(*mDevice, EVENTHUB_ID);
+    GestureConverter converter(*mReader->getContext(), deviceContext, DEVICE_ID);
+
+    Gesture moveGesture(kGestureMove, ARBITRARY_GESTURE_TIME, ARBITRARY_GESTURE_TIME, -5, 10);
+    std::list<NotifyArgs> args = converter.handleGesture(ARBITRARY_TIME, READ_TIME, moveGesture);
+    ASSERT_EQ(1u, args.size());
+
+    ASSERT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_MOVE),
+                      WithCoords(POINTER_X - 5, POINTER_Y + 10), WithRelativeMotion(-5, 10),
+                      WithToolType(ToolType::FINGER), WithButtonState(0), WithPressure(0.0f)));
+
+    ASSERT_NO_FATAL_FAILURE(mFakePointerController->assertPosition(POINTER_X - 5, POINTER_Y + 10));
+
+    // Future taps should be re-enabled
+    ASSERT_FALSE(mReader->getContext()->isPreventingTouchpadTaps());
+}
+
 } // namespace android
diff --git a/services/inputflinger/tests/InstrumentedInputReader.h b/services/inputflinger/tests/InstrumentedInputReader.h
index 7f8d556..fef58ec 100644
--- a/services/inputflinger/tests/InstrumentedInputReader.h
+++ b/services/inputflinger/tests/InstrumentedInputReader.h
@@ -103,12 +103,16 @@
             mExternalStylusDevices = devices;
         }
 
+        void setPreventingTouchpadTaps(bool prevent) override { mPreventingTouchpadTaps = prevent; }
+        bool isPreventingTouchpadTaps() override { return mPreventingTouchpadTaps; }
+
     private:
         int32_t mGlobalMetaState;
         bool mUpdateGlobalMetaStateWasCalled;
         int32_t mGeneration;
         std::optional<nsecs_t> mRequestedTimeout;
         std::vector<InputDeviceInfo> mExternalStylusDevices;
+        bool mPreventingTouchpadTaps{false};
     } mFakeContext;
 
     friend class InputReaderTest;
diff --git a/services/inputflinger/tests/InterfaceMocks.h b/services/inputflinger/tests/InterfaceMocks.h
index d720a90..b6720c5 100644
--- a/services/inputflinger/tests/InterfaceMocks.h
+++ b/services/inputflinger/tests/InterfaceMocks.h
@@ -49,6 +49,9 @@
 
     MOCK_METHOD(void, updateLedMetaState, (int32_t metaState), (override));
     MOCK_METHOD(int32_t, getLedMetaState, (), (override));
+
+    MOCK_METHOD(void, setPreventingTouchpadTaps, (bool prevent), (override));
+    MOCK_METHOD(bool, isPreventingTouchpadTaps, (), (override));
 };
 
 class MockEventHubInterface : public EventHubInterface {
diff --git a/services/inputflinger/tests/KeyboardInputMapper_test.cpp b/services/inputflinger/tests/KeyboardInputMapper_test.cpp
new file mode 100644
index 0000000..08a5559
--- /dev/null
+++ b/services/inputflinger/tests/KeyboardInputMapper_test.cpp
@@ -0,0 +1,152 @@
+/*
+ * 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 "KeyboardInputMapper.h"
+
+#include <gtest/gtest.h>
+
+#include "InputMapperTest.h"
+#include "InterfaceMocks.h"
+
+#define TAG "KeyboardInputMapper_test"
+
+namespace android {
+
+using testing::_;
+using testing::DoAll;
+using testing::Return;
+using testing::SetArgPointee;
+
+/**
+ * Unit tests for KeyboardInputMapper.
+ */
+class KeyboardInputMapperUnitTest : public InputMapperUnitTest {
+protected:
+    sp<FakeInputReaderPolicy> mFakePolicy;
+    const std::unordered_map<int32_t, int32_t> mKeyCodeMap{{KEY_0, AKEYCODE_0},
+                                                           {KEY_A, AKEYCODE_A},
+                                                           {KEY_LEFTCTRL, AKEYCODE_CTRL_LEFT},
+                                                           {KEY_LEFTALT, AKEYCODE_ALT_LEFT},
+                                                           {KEY_RIGHTALT, AKEYCODE_ALT_RIGHT},
+                                                           {KEY_LEFTSHIFT, AKEYCODE_SHIFT_LEFT},
+                                                           {KEY_RIGHTSHIFT, AKEYCODE_SHIFT_RIGHT},
+                                                           {KEY_FN, AKEYCODE_FUNCTION},
+                                                           {KEY_LEFTCTRL, AKEYCODE_CTRL_LEFT},
+                                                           {KEY_RIGHTCTRL, AKEYCODE_CTRL_RIGHT},
+                                                           {KEY_LEFTMETA, AKEYCODE_META_LEFT},
+                                                           {KEY_RIGHTMETA, AKEYCODE_META_RIGHT},
+                                                           {KEY_CAPSLOCK, AKEYCODE_CAPS_LOCK},
+                                                           {KEY_NUMLOCK, AKEYCODE_NUM_LOCK},
+                                                           {KEY_SCROLLLOCK, AKEYCODE_SCROLL_LOCK}};
+
+    void SetUp() override {
+        InputMapperUnitTest::SetUp();
+
+        // set key-codes expected in tests
+        for (const auto& [scanCode, outKeycode] : mKeyCodeMap) {
+            EXPECT_CALL(mMockEventHub, mapKey(EVENTHUB_ID, scanCode, _, _, _, _, _))
+                    .WillRepeatedly(DoAll(SetArgPointee<4>(outKeycode), Return(NO_ERROR)));
+        }
+
+        mFakePolicy = sp<FakeInputReaderPolicy>::make();
+        EXPECT_CALL(mMockInputReaderContext, getPolicy).WillRepeatedly(Return(mFakePolicy.get()));
+
+        mMapper = createInputMapper<KeyboardInputMapper>(*mDeviceContext, mReaderConfiguration,
+                                                         AINPUT_SOURCE_KEYBOARD,
+                                                         AINPUT_KEYBOARD_TYPE_ALPHABETIC);
+    }
+
+    void testPointerVisibilityForKeys(const std::vector<int32_t>& keyCodes, bool expectVisible) {
+        EXPECT_CALL(mMockInputReaderContext, fadePointer)
+                .Times(expectVisible ? 0 : keyCodes.size());
+        for (int32_t keyCode : keyCodes) {
+            process(EV_KEY, keyCode, 1);
+            process(EV_SYN, SYN_REPORT, 0);
+            process(EV_KEY, keyCode, 0);
+            process(EV_SYN, SYN_REPORT, 0);
+        }
+    }
+
+    void testTouchpadTapStateForKeys(const std::vector<int32_t>& keyCodes,
+                                     const bool expectPrevent) {
+        EXPECT_CALL(mMockInputReaderContext, isPreventingTouchpadTaps).Times(keyCodes.size());
+        if (expectPrevent) {
+            EXPECT_CALL(mMockInputReaderContext, setPreventingTouchpadTaps(true))
+                    .Times(keyCodes.size());
+        }
+        for (int32_t keyCode : keyCodes) {
+            process(EV_KEY, keyCode, 1);
+            process(EV_SYN, SYN_REPORT, 0);
+            process(EV_KEY, keyCode, 0);
+            process(EV_SYN, SYN_REPORT, 0);
+        }
+    }
+};
+
+/**
+ * Pointer visibility should remain unaffected if there is no active Input Method Connection
+ */
+TEST_F(KeyboardInputMapperUnitTest, KeystrokesWithoutIMeConnectionDoesNotHidePointer) {
+    testPointerVisibilityForKeys({KEY_0, KEY_A, KEY_LEFTCTRL}, /* expectVisible= */ true);
+}
+
+/**
+ * Pointer should hide if there is a active Input Method Connection
+ */
+TEST_F(KeyboardInputMapperUnitTest, AlphanumericKeystrokesWithIMeConnectionHidePointer) {
+    mFakePolicy->setIsInputMethodConnectionActive(true);
+    testPointerVisibilityForKeys({KEY_0, KEY_A}, /* expectVisible= */ false);
+}
+
+/**
+ * Pointer visibility should remain unaffected by meta keys even if Input Method Connection is
+ * active
+ */
+TEST_F(KeyboardInputMapperUnitTest, MetaKeystrokesWithIMeConnectionDoesNotHidePointer) {
+    mFakePolicy->setIsInputMethodConnectionActive(true);
+    std::vector<int32_t> metaKeys{KEY_LEFTALT,   KEY_RIGHTALT, KEY_LEFTSHIFT, KEY_RIGHTSHIFT,
+                                  KEY_FN,        KEY_LEFTCTRL, KEY_RIGHTCTRL, KEY_LEFTMETA,
+                                  KEY_RIGHTMETA, KEY_CAPSLOCK, KEY_NUMLOCK,   KEY_SCROLLLOCK};
+    testPointerVisibilityForKeys(metaKeys, /* expectVisible= */ true);
+}
+
+/**
+ * Touchpad tap should not be disabled if there is no active Input Method Connection
+ */
+TEST_F(KeyboardInputMapperUnitTest, KeystrokesWithoutIMeConnectionDontDisableTouchpadTap) {
+    testTouchpadTapStateForKeys({KEY_0, KEY_A, KEY_LEFTCTRL}, /* expectPrevent= */ false);
+}
+
+/**
+ * Touchpad tap should be disabled if there is a active Input Method Connection
+ */
+TEST_F(KeyboardInputMapperUnitTest, AlphanumericKeystrokesWithIMeConnectionDisableTouchpadTap) {
+    mFakePolicy->setIsInputMethodConnectionActive(true);
+    testTouchpadTapStateForKeys({KEY_0, KEY_A}, /* expectPrevent= */ true);
+}
+
+/**
+ * Touchpad tap should not be disabled by meta keys even if Input Method Connection is active
+ */
+TEST_F(KeyboardInputMapperUnitTest, MetaKeystrokesWithIMeConnectionDontDisableTouchpadTap) {
+    mFakePolicy->setIsInputMethodConnectionActive(true);
+    std::vector<int32_t> metaKeys{KEY_LEFTALT,   KEY_RIGHTALT, KEY_LEFTSHIFT, KEY_RIGHTSHIFT,
+                                  KEY_FN,        KEY_LEFTCTRL, KEY_RIGHTCTRL, KEY_LEFTMETA,
+                                  KEY_RIGHTMETA, KEY_CAPSLOCK, KEY_NUMLOCK,   KEY_SCROLLLOCK};
+    testTouchpadTapStateForKeys(metaKeys, /* expectPrevent= */ false);
+}
+
+} // namespace android
diff --git a/services/inputflinger/tests/fuzzers/MapperHelpers.h b/services/inputflinger/tests/fuzzers/MapperHelpers.h
index 1e44e0f..1ecaa64 100644
--- a/services/inputflinger/tests/fuzzers/MapperHelpers.h
+++ b/services/inputflinger/tests/fuzzers/MapperHelpers.h
@@ -289,6 +289,7 @@
     }
     void setTouchAffineTransformation(const TouchAffineTransformation t) { mTransform = t; }
     void notifyStylusGestureStarted(int32_t, nsecs_t) {}
+    bool isInputMethodConnectionActive() override { return mFdp->ConsumeBool(); }
 };
 
 class FuzzInputListener : public virtual InputListenerInterface {
@@ -339,6 +340,9 @@
     void updateLedMetaState(int32_t metaState) override{};
     int32_t getLedMetaState() override { return mFdp->ConsumeIntegral<int32_t>(); };
     void notifyStylusGestureStarted(int32_t, nsecs_t) {}
+
+    void setPreventingTouchpadTaps(bool prevent) {}
+    bool isPreventingTouchpadTaps() { return mFdp->ConsumeBool(); };
 };
 
 } // namespace android
diff --git a/services/sensorservice/AidlSensorHalWrapper.cpp b/services/sensorservice/AidlSensorHalWrapper.cpp
index f5b360f..e60db93 100644
--- a/services/sensorservice/AidlSensorHalWrapper.cpp
+++ b/services/sensorservice/AidlSensorHalWrapper.cpp
@@ -308,8 +308,12 @@
     }
 
     int32_t token;
-    mSensors->configDirectReport(sensorHandle, channelHandle, rate, &token);
-    return token;
+    status_t status = convertToStatus(
+        mSensors->configDirectReport(sensorHandle, channelHandle, rate, &token));
+    if (status == OK && rate != ISensors::RateLevel::STOP) {
+        status = static_cast<status_t>(token);
+    }
+    return status;
 }
 
 void AidlSensorHalWrapper::writeWakeLockHandled(uint32_t count) {
diff --git a/services/surfaceflinger/BackgroundExecutor.cpp b/services/surfaceflinger/BackgroundExecutor.cpp
index 6ddf790..5a1ec6f 100644
--- a/services/surfaceflinger/BackgroundExecutor.cpp
+++ b/services/surfaceflinger/BackgroundExecutor.cpp
@@ -20,6 +20,7 @@
 #define ATRACE_TAG ATRACE_TAG_GRAPHICS
 
 #include <utils/Log.h>
+#include <mutex>
 
 #include "BackgroundExecutor.h"
 
@@ -60,4 +61,17 @@
     LOG_ALWAYS_FATAL_IF(sem_post(&mSemaphore), "sem_post failed");
 }
 
+void BackgroundExecutor::flushQueue() {
+    std::mutex mutex;
+    std::condition_variable cv;
+    bool flushComplete = false;
+    sendCallbacks({[&]() {
+        std::scoped_lock lock{mutex};
+        flushComplete = true;
+        cv.notify_one();
+    }});
+    std::unique_lock<std::mutex> lock{mutex};
+    cv.wait(lock, [&]() { return flushComplete; });
+}
+
 } // namespace android
diff --git a/services/surfaceflinger/BackgroundExecutor.h b/services/surfaceflinger/BackgroundExecutor.h
index 0fae5a5..66b7d7a 100644
--- a/services/surfaceflinger/BackgroundExecutor.h
+++ b/services/surfaceflinger/BackgroundExecutor.h
@@ -34,6 +34,7 @@
     // Queues callbacks onto a work queue to be executed by a background thread.
     // This is safe to call from multiple threads.
     void sendCallbacks(Callbacks&& tasks);
+    void flushQueue();
 
 private:
     sem_t mSemaphore;
diff --git a/services/surfaceflinger/Scheduler/RefreshRateSelector.cpp b/services/surfaceflinger/Scheduler/RefreshRateSelector.cpp
index f136e9f..c44e22e 100644
--- a/services/surfaceflinger/Scheduler/RefreshRateSelector.cpp
+++ b/services/surfaceflinger/Scheduler/RefreshRateSelector.cpp
@@ -302,6 +302,19 @@
 
     if (layer.vote == LayerVoteType::ExplicitExactOrMultiple ||
         layer.vote == LayerVoteType::Heuristic) {
+        using fps_approx_ops::operator<;
+        if (refreshRate < 60_Hz) {
+            const bool favorsAtLeast60 =
+                    std::find_if(mFrameRatesThatFavorsAtLeast60.begin(),
+                                 mFrameRatesThatFavorsAtLeast60.end(), [&](Fps fps) {
+                                     using fps_approx_ops::operator==;
+                                     return fps == layer.desiredRefreshRate;
+                                 }) != mFrameRatesThatFavorsAtLeast60.end();
+            if (favorsAtLeast60) {
+                return 0;
+            }
+        }
+
         const float multiplier = refreshRate.getValue() / layer.desiredRefreshRate.getValue();
 
         // We only want to score this layer as a fractional pair if the content is not
@@ -1221,10 +1234,19 @@
                     (supportsFrameRateOverride() || ranges.render.includes(mode.getFps()));
         };
 
-        const auto frameRateModes = createFrameRateModes(filterModes, ranges.render);
+        auto frameRateModes = createFrameRateModes(filterModes, ranges.render);
+        if (frameRateModes.empty()) {
+            ALOGW("No matching frame rate modes for %s range. policy: %s", rangeName,
+                  policy->toString().c_str());
+            // TODO(b/292105422): Ideally DisplayManager should not send render ranges smaller than
+            // the min supported. See b/292047939.
+            //  For not we just ignore the render ranges.
+            frameRateModes = createFrameRateModes(filterModes, {});
+        }
         LOG_ALWAYS_FATAL_IF(frameRateModes.empty(),
-                            "No matching frame rate modes for %s range. policy: %s", rangeName,
-                            policy->toString().c_str());
+                            "No matching frame rate modes for %s range even after ignoring the "
+                            "render range. policy: %s",
+                            rangeName, policy->toString().c_str());
 
         const auto stringifyModes = [&] {
             std::string str;
diff --git a/services/surfaceflinger/Scheduler/RefreshRateSelector.h b/services/surfaceflinger/Scheduler/RefreshRateSelector.h
index 5052e6e..7af8d03 100644
--- a/services/surfaceflinger/Scheduler/RefreshRateSelector.h
+++ b/services/surfaceflinger/Scheduler/RefreshRateSelector.h
@@ -18,6 +18,7 @@
 
 #include <algorithm>
 #include <numeric>
+#include <set>
 #include <type_traits>
 #include <utility>
 #include <variant>
@@ -500,6 +501,12 @@
     const std::vector<Fps> mKnownFrameRates;
 
     const Config mConfig;
+
+    // A list of known frame rates that favors at least 60Hz if there is no exact match display
+    // refresh rate
+    const std::vector<Fps> mFrameRatesThatFavorsAtLeast60 = {23.976_Hz, 25_Hz, 29.97_Hz, 50_Hz,
+                                                             59.94_Hz};
+
     Config::FrameRateOverride mFrameRateOverrideConfig;
 
     struct GetRankedFrameRatesCache {
diff --git a/services/surfaceflinger/Scheduler/Scheduler.cpp b/services/surfaceflinger/Scheduler/Scheduler.cpp
index 41639b6..0dca21e 100644
--- a/services/surfaceflinger/Scheduler/Scheduler.cpp
+++ b/services/surfaceflinger/Scheduler/Scheduler.cpp
@@ -25,6 +25,7 @@
 #include <android/hardware/configstore/1.0/ISurfaceFlingerConfigs.h>
 #include <android/hardware/configstore/1.1/ISurfaceFlingerConfigs.h>
 #include <configstore/Utils.h>
+#include <ftl/concat.h>
 #include <ftl/enum.h>
 #include <ftl/fake_guard.h>
 #include <ftl/small_map.h>
@@ -130,8 +131,8 @@
     auto [pacesetterVsyncSchedule, isNew] = [&]() FTL_FAKE_GUARD(kMainThreadContext) {
         std::scoped_lock lock(mDisplayLock);
         const bool isNew = mDisplays
-                                   .emplace_or_replace(displayId, std::move(selectorPtr),
-                                                       std::move(schedulePtr))
+                                   .emplace_or_replace(displayId, displayId, std::move(selectorPtr),
+                                                       std::move(schedulePtr), mFeatures)
                                    .second;
 
         return std::make_pair(promotePacesetterDisplayLocked(), isNew);
@@ -171,21 +172,43 @@
 
 void Scheduler::onFrameSignal(ICompositor& compositor, VsyncId vsyncId,
                               TimePoint expectedVsyncTime) {
-    mPacesetterFrameTargeter.beginFrame({.frameBeginTime = SchedulerClock::now(),
-                                         .vsyncId = vsyncId,
-                                         .expectedVsyncTime = expectedVsyncTime,
-                                         .sfWorkDuration =
-                                                 mVsyncModulator->getVsyncConfig().sfWorkDuration},
-                                        *getVsyncSchedule());
+    const FrameTargeter::BeginFrameArgs beginFrameArgs =
+            {.frameBeginTime = SchedulerClock::now(),
+             .vsyncId = vsyncId,
+             // TODO(b/255601557): Calculate per display.
+             .expectedVsyncTime = expectedVsyncTime,
+             .sfWorkDuration = mVsyncModulator->getVsyncConfig().sfWorkDuration};
 
-    if (!compositor.commit(mPacesetterFrameTargeter.target())) {
-        return;
+    LOG_ALWAYS_FATAL_IF(!mPacesetterDisplayId);
+    const auto pacesetterId = *mPacesetterDisplayId;
+    const auto pacesetterOpt = mDisplays.get(pacesetterId);
+
+    FrameTargeter& pacesetterTargeter = *pacesetterOpt->get().targeterPtr;
+    pacesetterTargeter.beginFrame(beginFrameArgs, *pacesetterOpt->get().schedulePtr);
+
+    if (!compositor.commit(pacesetterTargeter.target())) return;
+
+    // TODO(b/256196556): Choose the frontrunner display.
+    FrameTargeters targeters;
+    targeters.try_emplace(pacesetterId, &pacesetterTargeter);
+
+    for (auto& [id, display] : mDisplays) {
+        if (id == pacesetterId) continue;
+
+        FrameTargeter& targeter = *display.targeterPtr;
+        targeter.beginFrame(beginFrameArgs, *display.schedulePtr);
+
+        targeters.try_emplace(id, &targeter);
     }
 
-    const auto compositeResult = compositor.composite(mPacesetterFrameTargeter);
+    const auto resultsPerDisplay = compositor.composite(pacesetterId, targeters);
     compositor.sample();
 
-    mPacesetterFrameTargeter.endFrame(compositeResult);
+    for (const auto& [id, targeter] : targeters) {
+        const auto resultOpt = resultsPerDisplay.get(id);
+        LOG_ALWAYS_FATAL_IF(!resultOpt);
+        targeter->endFrame(*resultOpt);
+    }
 }
 
 std::optional<Fps> Scheduler::getFrameRateOverride(uid_t uid) const {
@@ -534,8 +557,16 @@
 }
 
 void Scheduler::addPresentFence(PhysicalDisplayId id, std::shared_ptr<FenceTime> fence) {
-    auto schedule = getVsyncSchedule(id);
-    LOG_ALWAYS_FATAL_IF(!schedule);
+    const auto scheduleOpt =
+            (ftl::FakeGuard(mDisplayLock), mDisplays.get(id)).and_then([](const Display& display) {
+                return display.powerMode == hal::PowerMode::OFF
+                        ? std::nullopt
+                        : std::make_optional(display.schedulePtr);
+            });
+
+    if (!scheduleOpt) return;
+    const auto& schedule = scheduleOpt->get();
+
     if (const bool needMoreSignals = schedule->getController().addPresentFence(std::move(fence))) {
         schedule->enableHardwareVsync();
     } else {
@@ -724,7 +755,23 @@
     mFrameRateOverrideMappings.dump(dumper);
     dumper.eol();
 
-    mPacesetterFrameTargeter.dump(dumper);
+    {
+        utils::Dumper::Section section(dumper, "Frame Targeting"sv);
+
+        std::scoped_lock lock(mDisplayLock);
+        ftl::FakeGuard guard(kMainThreadContext);
+
+        for (const auto& [id, display] : mDisplays) {
+            utils::Dumper::Section
+                    section(dumper,
+                            id == mPacesetterDisplayId
+                                    ? ftl::Concat("Pacesetter Display ", id.value).c_str()
+                                    : ftl::Concat("Follower Display ", id.value).c_str());
+
+            display.targeterPtr->dump(dumper);
+            dumper.eol();
+        }
+    }
 }
 
 void Scheduler::dumpVsync(std::string& out) const {
diff --git a/services/surfaceflinger/Scheduler/Scheduler.h b/services/surfaceflinger/Scheduler/Scheduler.h
index 17e9cea..b913700 100644
--- a/services/surfaceflinger/Scheduler/Scheduler.h
+++ b/services/surfaceflinger/Scheduler/Scheduler.h
@@ -220,7 +220,7 @@
     // otherwise.
     bool addResyncSample(PhysicalDisplayId, nsecs_t timestamp,
                          std::optional<nsecs_t> hwcVsyncPeriod);
-    void addPresentFence(PhysicalDisplayId, std::shared_ptr<FenceTime>) EXCLUDES(mDisplayLock)
+    void addPresentFence(PhysicalDisplayId, std::shared_ptr<FenceTime>)
             REQUIRES(kMainThreadContext);
 
     // Layers are registered on creation, and unregistered when the weak reference expires.
@@ -250,7 +250,14 @@
         return std::const_pointer_cast<VsyncSchedule>(std::as_const(*this).getVsyncSchedule(idOpt));
     }
 
-    const FrameTarget& pacesetterFrameTarget() { return mPacesetterFrameTargeter.target(); }
+    TimePoint expectedPresentTimeForPacesetter() const EXCLUDES(mDisplayLock) {
+        std::scoped_lock lock(mDisplayLock);
+        return pacesetterDisplayLocked()
+                .transform([](const Display& display) {
+                    return display.targeterPtr->target().expectedPresentTime();
+                })
+                .value_or(TimePoint());
+    }
 
     // Returns true if a given vsync timestamp is considered valid vsync
     // for a given uid
@@ -306,7 +313,8 @@
     enum class TouchState { Inactive, Active };
 
     // impl::MessageQueue overrides:
-    void onFrameSignal(ICompositor&, VsyncId, TimePoint expectedVsyncTime) override;
+    void onFrameSignal(ICompositor&, VsyncId, TimePoint expectedVsyncTime) override
+            REQUIRES(kMainThreadContext, mDisplayLock);
 
     // Create a connection on the given EventThread.
     ConnectionHandle createConnection(std::unique_ptr<EventThread>);
@@ -429,13 +437,24 @@
     // must lock for writes but not reads. See also mPolicyLock for locking order.
     mutable std::mutex mDisplayLock;
 
+    using FrameTargeterPtr = std::unique_ptr<FrameTargeter>;
+
     struct Display {
-        Display(RefreshRateSelectorPtr selectorPtr, VsyncSchedulePtr schedulePtr)
-              : selectorPtr(std::move(selectorPtr)), schedulePtr(std::move(schedulePtr)) {}
+        Display(PhysicalDisplayId displayId, RefreshRateSelectorPtr selectorPtr,
+                VsyncSchedulePtr schedulePtr, FeatureFlags features)
+              : displayId(displayId),
+                selectorPtr(std::move(selectorPtr)),
+                schedulePtr(std::move(schedulePtr)),
+                targeterPtr(std::make_unique<
+                            FrameTargeter>(displayId,
+                                           features.test(Feature::kBackpressureGpuComposition))) {}
+
+        const PhysicalDisplayId displayId;
 
         // Effectively const except in move constructor.
         RefreshRateSelectorPtr selectorPtr;
         VsyncSchedulePtr schedulePtr;
+        FrameTargeterPtr targeterPtr;
 
         hal::PowerMode powerMode = hal::PowerMode::OFF;
     };
@@ -449,8 +468,6 @@
     ftl::Optional<PhysicalDisplayId> mPacesetterDisplayId GUARDED_BY(mDisplayLock)
             GUARDED_BY(kMainThreadContext);
 
-    FrameTargeter mPacesetterFrameTargeter{mFeatures.test(Feature::kBackpressureGpuComposition)};
-
     ftl::Optional<DisplayRef> pacesetterDisplayLocked() REQUIRES(mDisplayLock) {
         return static_cast<const Scheduler*>(this)->pacesetterDisplayLocked().transform(
                 [](const Display& display) { return std::ref(const_cast<Display&>(display)); });
diff --git a/services/surfaceflinger/Scheduler/include/scheduler/FrameTargeter.h b/services/surfaceflinger/Scheduler/include/scheduler/FrameTargeter.h
index 85f2e64..ae74205 100644
--- a/services/surfaceflinger/Scheduler/include/scheduler/FrameTargeter.h
+++ b/services/surfaceflinger/Scheduler/include/scheduler/FrameTargeter.h
@@ -20,6 +20,7 @@
 #include <atomic>
 #include <memory>
 
+#include <ui/DisplayId.h>
 #include <ui/Fence.h>
 #include <ui/FenceTime.h>
 
@@ -75,16 +76,17 @@
     bool didMissHwcFrame() const { return mHwcFrameMissed && !mGpuFrameMissed; }
 
 protected:
+    explicit FrameTarget(const std::string& displayLabel);
     ~FrameTarget() = default;
 
     VsyncId mVsyncId;
     TimePoint mFrameBeginTime;
     TimePoint mExpectedPresentTime;
 
-    TracedOrdinal<bool> mFramePending{"PrevFramePending", false};
-    TracedOrdinal<bool> mFrameMissed{"PrevFrameMissed", false};
-    TracedOrdinal<bool> mHwcFrameMissed{"PrevHwcFrameMissed", false};
-    TracedOrdinal<bool> mGpuFrameMissed{"PrevGpuFrameMissed", false};
+    TracedOrdinal<bool> mFramePending;
+    TracedOrdinal<bool> mFrameMissed;
+    TracedOrdinal<bool> mHwcFrameMissed;
+    TracedOrdinal<bool> mGpuFrameMissed;
 
     struct FenceWithFenceTime {
         sp<Fence> fence = Fence::NO_FENCE;
@@ -103,8 +105,9 @@
 // Computes a display's per-frame metrics about past/upcoming targeting of present deadlines.
 class FrameTargeter final : private FrameTarget {
 public:
-    explicit FrameTargeter(bool backpressureGpuComposition)
-          : mBackpressureGpuComposition(backpressureGpuComposition) {}
+    FrameTargeter(PhysicalDisplayId displayId, bool backpressureGpuComposition)
+          : FrameTarget(to_string(displayId)),
+            mBackpressureGpuComposition(backpressureGpuComposition) {}
 
     const FrameTarget& target() const { return *this; }
 
diff --git a/services/surfaceflinger/Scheduler/include/scheduler/interface/CompositeResult.h b/services/surfaceflinger/Scheduler/include/scheduler/interface/CompositeResult.h
index f795f1f..87c704e 100644
--- a/services/surfaceflinger/Scheduler/include/scheduler/interface/CompositeResult.h
+++ b/services/surfaceflinger/Scheduler/include/scheduler/interface/CompositeResult.h
@@ -16,6 +16,9 @@
 
 #pragma once
 
+#include <ui/DisplayId.h>
+#include <ui/DisplayMap.h>
+
 #include <scheduler/interface/CompositionCoverage.h>
 
 namespace android {
@@ -24,4 +27,6 @@
     CompositionCoverageFlags compositionCoverage;
 };
 
+using CompositeResultsPerDisplay = ui::PhysicalDisplayMap<PhysicalDisplayId, CompositeResult>;
+
 } // namespace android
diff --git a/services/surfaceflinger/Scheduler/include/scheduler/interface/CompositionCoverage.h b/services/surfaceflinger/Scheduler/include/scheduler/interface/CompositionCoverage.h
index 3d0f1a9..767462d 100644
--- a/services/surfaceflinger/Scheduler/include/scheduler/interface/CompositionCoverage.h
+++ b/services/surfaceflinger/Scheduler/include/scheduler/interface/CompositionCoverage.h
@@ -19,6 +19,8 @@
 #include <cstdint>
 
 #include <ftl/flags.h>
+#include <ui/DisplayId.h>
+#include <ui/DisplayMap.h>
 
 namespace android {
 
@@ -34,4 +36,14 @@
 
 using CompositionCoverageFlags = ftl::Flags<CompositionCoverage>;
 
+using CompositionCoveragePerDisplay = ui::DisplayMap<DisplayId, CompositionCoverageFlags>;
+
+inline CompositionCoverageFlags multiDisplayUnion(const CompositionCoveragePerDisplay& displays) {
+    CompositionCoverageFlags coverage;
+    for (const auto& [id, flags] : displays) {
+        coverage |= flags;
+    }
+    return coverage;
+}
+
 } // namespace android
diff --git a/services/surfaceflinger/Scheduler/include/scheduler/interface/ICompositor.h b/services/surfaceflinger/Scheduler/include/scheduler/interface/ICompositor.h
index 2696076..6fe813a 100644
--- a/services/surfaceflinger/Scheduler/include/scheduler/interface/ICompositor.h
+++ b/services/surfaceflinger/Scheduler/include/scheduler/interface/ICompositor.h
@@ -16,6 +16,9 @@
 
 #pragma once
 
+#include <ui/DisplayId.h>
+#include <ui/DisplayMap.h>
+
 #include <scheduler/Time.h>
 #include <scheduler/VsyncId.h>
 #include <scheduler/interface/CompositeResult.h>
@@ -26,6 +29,8 @@
 class FrameTarget;
 class FrameTargeter;
 
+using FrameTargeters = ui::PhysicalDisplayMap<PhysicalDisplayId, scheduler::FrameTargeter*>;
+
 } // namespace scheduler
 
 struct ICompositor {
@@ -38,7 +43,8 @@
 
     // Composites a frame for each display. CompositionEngine performs GPU and/or HAL composition
     // via RenderEngine and the Composer HAL, respectively.
-    virtual CompositeResult composite(scheduler::FrameTargeter&) = 0;
+    virtual CompositeResultsPerDisplay composite(PhysicalDisplayId pacesetterId,
+                                                 const scheduler::FrameTargeters&) = 0;
 
     // Samples the composited frame via RegionSamplingThread.
     virtual void sample() = 0;
diff --git a/services/surfaceflinger/Scheduler/src/FrameTargeter.cpp b/services/surfaceflinger/Scheduler/src/FrameTargeter.cpp
index 7138afd..7a18654 100644
--- a/services/surfaceflinger/Scheduler/src/FrameTargeter.cpp
+++ b/services/surfaceflinger/Scheduler/src/FrameTargeter.cpp
@@ -21,6 +21,12 @@
 
 namespace android::scheduler {
 
+FrameTarget::FrameTarget(const std::string& displayLabel)
+      : mFramePending("PrevFramePending " + displayLabel, false),
+        mFrameMissed("PrevFrameMissed " + displayLabel, false),
+        mHwcFrameMissed("PrevHwcFrameMissed " + displayLabel, false),
+        mGpuFrameMissed("PrevGpuFrameMissed " + displayLabel, false) {}
+
 TimePoint FrameTarget::pastVsyncTime(Period vsyncPeriod) const {
     // TODO(b/267315508): Generalize to N VSYNCs.
     const int shift = static_cast<int>(targetsVsyncsAhead<2>(vsyncPeriod));
@@ -130,10 +136,6 @@
 }
 
 void FrameTargeter::dump(utils::Dumper& dumper) const {
-    using namespace std::string_view_literals;
-
-    utils::Dumper::Section section(dumper, "Frame Targeting"sv);
-
     // There are scripts and tests that expect this (rather than "name=value") format.
     dumper.dump({}, "Total missed frame count: " + std::to_string(mFrameMissedCount));
     dumper.dump({}, "HWC missed frame count: " + std::to_string(mHwcFrameMissedCount));
diff --git a/services/surfaceflinger/Scheduler/tests/FrameTargeterTest.cpp b/services/surfaceflinger/Scheduler/tests/FrameTargeterTest.cpp
index 908f214..1e038d1 100644
--- a/services/surfaceflinger/Scheduler/tests/FrameTargeterTest.cpp
+++ b/services/surfaceflinger/Scheduler/tests/FrameTargeterTest.cpp
@@ -96,7 +96,7 @@
     FenceToFenceTimeMap mFenceMap;
 
     static constexpr bool kBackpressureGpuComposition = true;
-    FrameTargeter mTargeter{kBackpressureGpuComposition};
+    FrameTargeter mTargeter{PhysicalDisplayId::fromPort(13), kBackpressureGpuComposition};
 };
 
 TEST_F(FrameTargeterTest, targetsFrames) {
diff --git a/services/surfaceflinger/SurfaceFlinger.cpp b/services/surfaceflinger/SurfaceFlinger.cpp
index d3489ff..de0034b 100644
--- a/services/surfaceflinger/SurfaceFlinger.cpp
+++ b/services/surfaceflinger/SurfaceFlinger.cpp
@@ -2162,7 +2162,7 @@
     }
 }
 
-void SurfaceFlinger::configure() FTL_FAKE_GUARD(kMainThreadContext) {
+void SurfaceFlinger::configure() {
     Mutex::Autolock lock(mStateLock);
     if (configureLocked()) {
         setTransactionFlags(eDisplayTransactionNeeded);
@@ -2336,8 +2336,7 @@
     return mustComposite;
 }
 
-bool SurfaceFlinger::commit(const scheduler::FrameTarget& pacesetterFrameTarget)
-        FTL_FAKE_GUARD(kMainThreadContext) {
+bool SurfaceFlinger::commit(const scheduler::FrameTarget& pacesetterFrameTarget) {
     const VsyncId vsyncId = pacesetterFrameTarget.vsyncId();
     ATRACE_NAME(ftl::Concat(__func__, ' ', ftl::to_underlying(vsyncId)).c_str());
 
@@ -2474,31 +2473,42 @@
     return mustComposite && CC_LIKELY(mBootStage != BootStage::BOOTLOADER);
 }
 
-CompositeResult SurfaceFlinger::composite(scheduler::FrameTargeter& pacesetterFrameTargeter)
-        FTL_FAKE_GUARD(kMainThreadContext) {
-    const scheduler::FrameTarget& pacesetterFrameTarget = pacesetterFrameTargeter.target();
+CompositeResultsPerDisplay SurfaceFlinger::composite(
+        PhysicalDisplayId pacesetterId, const scheduler::FrameTargeters& frameTargeters) {
+    const scheduler::FrameTarget& pacesetterTarget =
+            frameTargeters.get(pacesetterId)->get()->target();
 
-    const VsyncId vsyncId = pacesetterFrameTarget.vsyncId();
+    const VsyncId vsyncId = pacesetterTarget.vsyncId();
     ATRACE_NAME(ftl::Concat(__func__, ' ', ftl::to_underlying(vsyncId)).c_str());
 
     compositionengine::CompositionRefreshArgs refreshArgs;
     refreshArgs.powerCallback = this;
     const auto& displays = FTL_FAKE_GUARD(mStateLock, mDisplays);
     refreshArgs.outputs.reserve(displays.size());
+
+    // Add outputs for physical displays.
+    for (const auto& [id, targeter] : frameTargeters) {
+        ftl::FakeGuard guard(mStateLock);
+
+        if (const auto display = getCompositionDisplayLocked(id)) {
+            refreshArgs.outputs.push_back(display);
+        }
+    }
+
     std::vector<DisplayId> displayIds;
     for (const auto& [_, display] : displays) {
         displayIds.push_back(display->getId());
         display->tracePowerMode();
 
+        // Add outputs for virtual displays.
         if (display->isVirtual()) {
             const Fps refreshRate = display->getAdjustedRefreshRate();
-            if (refreshRate.isValid() &&
-                !mScheduler->isVsyncInPhase(pacesetterFrameTarget.frameBeginTime(), refreshRate)) {
-                continue;
+
+            if (!refreshRate.isValid() ||
+                mScheduler->isVsyncInPhase(pacesetterTarget.frameBeginTime(), refreshRate)) {
+                refreshArgs.outputs.push_back(display->getCompositionDisplay());
             }
         }
-
-        refreshArgs.outputs.push_back(display->getCompositionDisplay());
     }
     mPowerAdvisor->setDisplays(displayIds);
 
@@ -2558,15 +2568,16 @@
 
     if (!getHwComposer().getComposer()->isSupported(
                 Hwc2::Composer::OptionalFeature::ExpectedPresentTime) &&
-        pacesetterFrameTarget.wouldPresentEarly(vsyncPeriod)) {
+        pacesetterTarget.wouldPresentEarly(vsyncPeriod)) {
         const auto hwcMinWorkDuration = mVsyncConfiguration->getCurrentConfigs().hwcMinWorkDuration;
 
+        // TODO(b/255601557): Calculate and pass per-display values for each FrameTarget.
         refreshArgs.earliestPresentTime =
-                pacesetterFrameTarget.previousFrameVsyncTime(vsyncPeriod) - hwcMinWorkDuration;
+                pacesetterTarget.previousFrameVsyncTime(vsyncPeriod) - hwcMinWorkDuration;
     }
 
     refreshArgs.scheduledFrameTime = mScheduler->getScheduledFrameTime();
-    refreshArgs.expectedPresentTime = pacesetterFrameTarget.expectedPresentTime().ns();
+    refreshArgs.expectedPresentTime = pacesetterTarget.expectedPresentTime().ns();
     refreshArgs.hasTrustedPresentationListener = mNumTrustedPresentationListeners > 0;
 
     // Store the present time just before calling to the composition engine so we could notify
@@ -2592,14 +2603,14 @@
         }
     }
 
-    mTimeStats->recordFrameDuration(pacesetterFrameTarget.frameBeginTime().ns(), systemTime());
+    mTimeStats->recordFrameDuration(pacesetterTarget.frameBeginTime().ns(), systemTime());
 
     // Send a power hint after presentation is finished.
     if (mPowerHintSessionEnabled) {
         // Now that the current frame has been presented above, PowerAdvisor needs the present time
         // of the previous frame (whose fence is signaled by now) to determine how long the HWC had
         // waited on that fence to retire before presenting.
-        const auto& previousPresentFence = pacesetterFrameTarget.presentFenceForPreviousFrame();
+        const auto& previousPresentFence = pacesetterTarget.presentFenceForPreviousFrame();
 
         mPowerAdvisor->setSfPresentTiming(TimePoint::fromNs(previousPresentFence->getSignalTime()),
                                           TimePoint::now());
@@ -2610,23 +2621,27 @@
         scheduleComposite(FrameHint::kNone);
     }
 
-    postComposition(pacesetterFrameTargeter, presentTime);
+    postComposition(pacesetterId, frameTargeters, presentTime);
 
-    const bool hadGpuComposited = mCompositionCoverage.test(CompositionCoverage::Gpu);
+    const bool hadGpuComposited =
+            multiDisplayUnion(mCompositionCoverage).test(CompositionCoverage::Gpu);
     mCompositionCoverage.clear();
 
     TimeStats::ClientCompositionRecord clientCompositionRecord;
+
     for (const auto& [_, display] : displays) {
         const auto& state = display->getCompositionDisplay()->getState();
+        CompositionCoverageFlags& flags =
+                mCompositionCoverage.try_emplace(display->getId()).first->second;
 
         if (state.usesDeviceComposition) {
-            mCompositionCoverage |= CompositionCoverage::Hwc;
+            flags |= CompositionCoverage::Hwc;
         }
 
         if (state.reusedClientComposition) {
-            mCompositionCoverage |= CompositionCoverage::GpuReuse;
+            flags |= CompositionCoverage::GpuReuse;
         } else if (state.usesClientComposition) {
-            mCompositionCoverage |= CompositionCoverage::Gpu;
+            flags |= CompositionCoverage::Gpu;
         }
 
         clientCompositionRecord.predicted |=
@@ -2635,10 +2650,11 @@
                 (state.strategyPrediction == CompositionStrategyPredictionState::SUCCESS);
     }
 
-    const bool hasGpuComposited = mCompositionCoverage.test(CompositionCoverage::Gpu);
+    const auto coverage = multiDisplayUnion(mCompositionCoverage);
+    const bool hasGpuComposited = coverage.test(CompositionCoverage::Gpu);
 
     clientCompositionRecord.hadClientComposition = hasGpuComposited;
-    clientCompositionRecord.reused = mCompositionCoverage.test(CompositionCoverage::GpuReuse);
+    clientCompositionRecord.reused = coverage.test(CompositionCoverage::GpuReuse);
     clientCompositionRecord.changed = hadGpuComposited != hasGpuComposited;
 
     mTimeStats->pushCompositionStrategyState(clientCompositionRecord);
@@ -2647,13 +2663,13 @@
 
     // TODO(b/160583065): Enable skip validation when SF caches all client composition layers.
     const bool hasGpuUseOrReuse =
-            mCompositionCoverage.any(CompositionCoverage::Gpu | CompositionCoverage::GpuReuse);
+            coverage.any(CompositionCoverage::Gpu | CompositionCoverage::GpuReuse);
     mScheduler->modulateVsync({}, &VsyncModulator::onDisplayRefresh, hasGpuUseOrReuse);
 
     mLayersWithQueuedFrames.clear();
     if (mLayerTracingEnabled && mLayerTracing.flagIsSet(LayerTracing::TRACE_COMPOSITION)) {
         // This will block and should only be used for debugging.
-        addToLayerTracing(mVisibleRegionsDirty, pacesetterFrameTarget.frameBeginTime(), vsyncId);
+        addToLayerTracing(mVisibleRegionsDirty, pacesetterTarget.frameBeginTime(), vsyncId);
     }
 
     if (mVisibleRegionsDirty) mHdrLayerInfoChanged = true;
@@ -2667,7 +2683,16 @@
         mPowerAdvisor->setCompositeEnd(TimePoint::now());
     }
 
-    return {mCompositionCoverage};
+    CompositeResultsPerDisplay resultsPerDisplay;
+
+    // Filter out virtual displays.
+    for (const auto& [id, coverage] : mCompositionCoverage) {
+        if (const auto idOpt = PhysicalDisplayId::tryCast(id)) {
+            resultsPerDisplay.try_emplace(*idOpt, CompositeResult{coverage});
+        }
+    }
+
+    return resultsPerDisplay;
 }
 
 void SurfaceFlinger::updateLayerGeometry() {
@@ -2751,35 +2776,56 @@
     return ui::ROTATION_0;
 }
 
-void SurfaceFlinger::postComposition(scheduler::FrameTargeter& pacesetterFrameTargeter,
+void SurfaceFlinger::postComposition(PhysicalDisplayId pacesetterId,
+                                     const scheduler::FrameTargeters& frameTargeters,
                                      nsecs_t presentStartTime) {
     ATRACE_CALL();
     ALOGV(__func__);
 
-    const auto* defaultDisplay = FTL_FAKE_GUARD(mStateLock, getDefaultDisplayDeviceLocked()).get();
+    ui::PhysicalDisplayMap<PhysicalDisplayId, std::shared_ptr<FenceTime>> presentFences;
+    ui::PhysicalDisplayMap<PhysicalDisplayId, const sp<Fence>> gpuCompositionDoneFences;
 
-    std::shared_ptr<FenceTime> glCompositionDoneFenceTime;
-    if (defaultDisplay &&
-        defaultDisplay->getCompositionDisplay()->getState().usesClientComposition) {
-        glCompositionDoneFenceTime =
-                std::make_shared<FenceTime>(defaultDisplay->getCompositionDisplay()
-                                                    ->getRenderSurface()
-                                                    ->getClientTargetAcquireFence());
-    } else {
-        glCompositionDoneFenceTime = FenceTime::NO_FENCE;
+    for (const auto& [id, targeter] : frameTargeters) {
+        auto presentFence = getHwComposer().getPresentFence(id);
+
+        if (id == pacesetterId) {
+            mTransactionCallbackInvoker.addPresentFence(presentFence);
+        }
+
+        if (auto fenceTime = targeter->setPresentFence(std::move(presentFence));
+            fenceTime->isValid()) {
+            presentFences.try_emplace(id, std::move(fenceTime));
+        }
+
+        ftl::FakeGuard guard(mStateLock);
+        if (const auto display = getCompositionDisplayLocked(id);
+            display && display->getState().usesClientComposition) {
+            gpuCompositionDoneFences
+                    .try_emplace(id, display->getRenderSurface()->getClientTargetAcquireFence());
+        }
     }
 
-    auto presentFence = defaultDisplay
-            ? getHwComposer().getPresentFence(defaultDisplay->getPhysicalId())
-            : Fence::NO_FENCE;
+    const auto pacesetterDisplay = FTL_FAKE_GUARD(mStateLock, getDisplayDeviceLocked(pacesetterId));
 
-    auto presentFenceTime = pacesetterFrameTargeter.setPresentFence(presentFence);
+    std::shared_ptr<FenceTime> pacesetterPresentFenceTime =
+            presentFences.get(pacesetterId)
+                    .transform([](const FenceTimePtr& ptr) { return ptr; })
+                    .value_or(FenceTime::NO_FENCE);
+
+    std::shared_ptr<FenceTime> pacesetterGpuCompositionDoneFenceTime =
+            gpuCompositionDoneFences.get(pacesetterId)
+                    .transform([](sp<Fence> fence) {
+                        return std::make_shared<FenceTime>(std::move(fence));
+                    })
+                    .value_or(FenceTime::NO_FENCE);
+
     const TimePoint presentTime = TimePoint::now();
 
     // Set presentation information before calling Layer::releasePendingBuffer, such that jank
     // information from previous' frame classification is already available when sending jank info
     // to clients, so they get jank classification as early as possible.
-    mFrameTimeline->setSfPresent(presentTime.ns(), presentFenceTime, glCompositionDoneFenceTime);
+    mFrameTimeline->setSfPresent(presentTime.ns(), pacesetterPresentFenceTime,
+                                 pacesetterGpuCompositionDoneFenceTime);
 
     // We use the CompositionEngine::getLastFrameRefreshTimestamp() which might
     // be sampled a little later than when we started doing work for this frame,
@@ -2787,9 +2833,9 @@
     const TimePoint compositeTime =
             TimePoint::fromNs(mCompositionEngine->getLastFrameRefreshTimestamp());
     const Duration presentLatency =
-            !getHwComposer().hasCapability(Capability::PRESENT_FENCE_IS_NOT_RELIABLE)
-            ? mPresentLatencyTracker.trackPendingFrame(compositeTime, presentFenceTime)
-            : Duration::zero();
+            getHwComposer().hasCapability(Capability::PRESENT_FENCE_IS_NOT_RELIABLE)
+            ? Duration::zero()
+            : mPresentLatencyTracker.trackPendingFrame(compositeTime, pacesetterPresentFenceTime);
 
     const auto schedule = mScheduler->getVsyncSchedule();
     const TimePoint vsyncDeadline = schedule->vsyncDeadlineAfter(presentTime);
@@ -2826,8 +2872,8 @@
     mLayersWithBuffersRemoved.clear();
 
     for (const auto& layer: mLayersWithQueuedFrames) {
-        layer->onPostComposition(defaultDisplay, glCompositionDoneFenceTime, presentFenceTime,
-                                 compositorTiming);
+        layer->onPostComposition(pacesetterDisplay.get(), pacesetterGpuCompositionDoneFenceTime,
+                                 pacesetterPresentFenceTime, compositorTiming);
         layer->releasePendingBuffer(presentTime.ns());
     }
 
@@ -2890,34 +2936,28 @@
 
     mHdrLayerInfoChanged = false;
 
-    mTransactionCallbackInvoker.addPresentFence(std::move(presentFence));
     mTransactionCallbackInvoker.sendCallbacks(false /* onCommitOnly */);
     mTransactionCallbackInvoker.clearCompletedTransactions();
 
     mTimeStats->incrementTotalFrames();
-    mTimeStats->setPresentFenceGlobal(presentFenceTime);
+    mTimeStats->setPresentFenceGlobal(pacesetterPresentFenceTime);
 
-    {
+    for (auto&& [id, presentFence] : presentFences) {
         ftl::FakeGuard guard(mStateLock);
-        for (const auto& [id, physicalDisplay] : mPhysicalDisplays) {
-            if (auto displayDevice = getDisplayDeviceLocked(id);
-                displayDevice && displayDevice->isPoweredOn() && physicalDisplay.isInternal()) {
-                auto presentFenceTimeI = defaultDisplay && defaultDisplay->getPhysicalId() == id
-                        ? std::move(presentFenceTime)
-                        : std::make_shared<FenceTime>(getHwComposer().getPresentFence(id));
-                if (presentFenceTimeI->isValid()) {
-                    mScheduler->addPresentFence(id, std::move(presentFenceTimeI));
-                }
-            }
+        const bool isInternalDisplay =
+                mPhysicalDisplays.get(id).transform(&PhysicalDisplay::isInternal).value_or(false);
+
+        if (isInternalDisplay) {
+            mScheduler->addPresentFence(id, std::move(presentFence));
         }
     }
 
-    const bool isDisplayConnected =
-            defaultDisplay && getHwComposer().isConnected(defaultDisplay->getPhysicalId());
+    const bool hasPacesetterDisplay =
+            pacesetterDisplay && getHwComposer().isConnected(pacesetterId);
 
     if (!hasSyncFramework) {
-        if (isDisplayConnected && defaultDisplay->isPoweredOn()) {
-            mScheduler->enableHardwareVsync(defaultDisplay->getPhysicalId());
+        if (hasPacesetterDisplay && pacesetterDisplay->isPoweredOn()) {
+            mScheduler->enableHardwareVsync(pacesetterId);
         }
     }
 
@@ -2925,7 +2965,7 @@
     const size_t appConnections = mScheduler->getEventThreadConnectionCount(mAppConnectionHandle);
     mTimeStats->recordDisplayEventConnectionCount(sfConnections + appConnections);
 
-    if (isDisplayConnected && !defaultDisplay->isPoweredOn()) {
+    if (hasPacesetterDisplay && !pacesetterDisplay->isPoweredOn()) {
         getRenderEngine().cleanupPostRender();
         return;
     }
@@ -4204,7 +4244,7 @@
     const auto& transaction = *flushState.transaction;
 
     const TimePoint desiredPresentTime = TimePoint::fromNs(transaction.desiredPresentTime);
-    const TimePoint expectedPresentTime = mScheduler->pacesetterFrameTarget().expectedPresentTime();
+    const TimePoint expectedPresentTime = mScheduler->expectedPresentTimeForPacesetter();
 
     using TransactionReadiness = TransactionHandler::TransactionReadiness;
 
@@ -6088,8 +6128,7 @@
                   ftl::to_underlying(windowInfosDebug.maxSendDelayVsyncId));
     StringAppendF(&result, "  max send delay (ns): %" PRId64 " ns\n",
                   windowInfosDebug.maxSendDelayDuration);
-    StringAppendF(&result, "  unsent messages: %" PRIu32 "\n",
-                  windowInfosDebug.pendingMessageCount);
+    StringAppendF(&result, "  unsent messages: %zu\n", windowInfosDebug.pendingMessageCount);
     result.append("\n");
 }
 
@@ -7352,7 +7391,10 @@
                                       renderArea->getHintForSeamlessTransition());
             sdrWhitePointNits = state.sdrWhitePointNits;
             displayBrightnessNits = state.displayBrightnessNits;
-            if (sdrWhitePointNits > 1.0f) {
+            // Only clamp the display brightness if this is not a seamless transition. Otherwise
+            // for seamless transitions it's important to match the current display state as the
+            // buffer will be shown under these same conditions, and we want to avoid any flickers
+            if (sdrWhitePointNits > 1.0f && !renderArea->getHintForSeamlessTransition()) {
                 // Restrict the amount of HDR "headroom" in the screenshot to avoid over-dimming
                 // the SDR portion. 2.0 chosen by experimentation
                 constexpr float kMaxScreenshotHeadroom = 2.0f;
@@ -7926,9 +7968,9 @@
                                    forceApplyPolicy);
 }
 
-status_t SurfaceFlinger::addWindowInfosListener(
-        const sp<IWindowInfosListener>& windowInfosListener) {
-    mWindowInfosListenerInvoker->addWindowInfosListener(windowInfosListener);
+status_t SurfaceFlinger::addWindowInfosListener(const sp<IWindowInfosListener>& windowInfosListener,
+                                                gui::WindowInfosListenerInfo* outInfo) {
+    mWindowInfosListenerInvoker->addWindowInfosListener(windowInfosListener, outInfo);
     setTransactionFlags(eInputInfoUpdateNeeded);
     return NO_ERROR;
 }
@@ -9011,7 +9053,8 @@
 }
 
 binder::Status SurfaceComposerAIDL::addWindowInfosListener(
-        const sp<gui::IWindowInfosListener>& windowInfosListener) {
+        const sp<gui::IWindowInfosListener>& windowInfosListener,
+        gui::WindowInfosListenerInfo* outInfo) {
     status_t status;
     const int pid = IPCThreadState::self()->getCallingPid();
     const int uid = IPCThreadState::self()->getCallingUid();
@@ -9019,7 +9062,7 @@
     // WindowInfosListeners
     if (uid == AID_SYSTEM || uid == AID_GRAPHICS ||
         checkPermission(sAccessSurfaceFlinger, pid, uid)) {
-        status = mFlinger->addWindowInfosListener(windowInfosListener);
+        status = mFlinger->addWindowInfosListener(windowInfosListener, outInfo);
     } else {
         status = PERMISSION_DENIED;
     }
diff --git a/services/surfaceflinger/SurfaceFlinger.h b/services/surfaceflinger/SurfaceFlinger.h
index e27d21f..f1759a5 100644
--- a/services/surfaceflinger/SurfaceFlinger.h
+++ b/services/surfaceflinger/SurfaceFlinger.h
@@ -613,7 +613,8 @@
 
     status_t getMaxAcquiredBufferCount(int* buffers) const;
 
-    status_t addWindowInfosListener(const sp<gui::IWindowInfosListener>& windowInfosListener);
+    status_t addWindowInfosListener(const sp<gui::IWindowInfosListener>& windowInfosListener,
+                                    gui::WindowInfosListenerInfo* outResult);
     status_t removeWindowInfosListener(
             const sp<gui::IWindowInfosListener>& windowInfosListener) const;
 
@@ -632,9 +633,12 @@
     void onRefreshRateChangedDebug(const RefreshRateChangedDebugData&) override;
 
     // ICompositor overrides:
-    void configure() override;
-    bool commit(const scheduler::FrameTarget&) override;
-    CompositeResult composite(scheduler::FrameTargeter&) override;
+    void configure() override REQUIRES(kMainThreadContext);
+    bool commit(const scheduler::FrameTarget&) override REQUIRES(kMainThreadContext);
+    CompositeResultsPerDisplay composite(PhysicalDisplayId pacesetterId,
+                                         const scheduler::FrameTargeters&) override
+            REQUIRES(kMainThreadContext);
+
     void sample() override;
 
     // ISchedulerCallback overrides:
@@ -883,6 +887,14 @@
         return findDisplay([id](const auto& display) { return display.getId() == id; });
     }
 
+    std::shared_ptr<compositionengine::Display> getCompositionDisplayLocked(DisplayId id) const
+            REQUIRES(mStateLock) {
+        if (const auto display = getDisplayDeviceLocked(id)) {
+            return display->getCompositionDisplay();
+        }
+        return nullptr;
+    }
+
     // Returns the primary display or (for foldables) the active display, assuming that the inner
     // and outer displays have mutually exclusive power states.
     sp<const DisplayDevice> getDefaultDisplayDeviceLocked() const REQUIRES(mStateLock) {
@@ -956,8 +968,8 @@
     /*
      * Compositing
      */
-    void postComposition(scheduler::FrameTargeter&, nsecs_t presentStartTime)
-            REQUIRES(kMainThreadContext);
+    void postComposition(PhysicalDisplayId pacesetterId, const scheduler::FrameTargeters&,
+                         nsecs_t presentStartTime) REQUIRES(kMainThreadContext);
 
     /*
      * Display management
@@ -1289,7 +1301,7 @@
 
     std::unique_ptr<compositionengine::CompositionEngine> mCompositionEngine;
 
-    CompositionCoverageFlags mCompositionCoverage;
+    CompositionCoveragePerDisplay mCompositionCoverage;
 
     // mMaxRenderTargetSize is only set once in init() so it doesn't need to be protected by
     // any mutex.
@@ -1526,8 +1538,8 @@
     binder::Status setOverrideFrameRate(int32_t uid, float frameRate) override;
     binder::Status getGpuContextPriority(int32_t* outPriority) override;
     binder::Status getMaxAcquiredBufferCount(int32_t* buffers) override;
-    binder::Status addWindowInfosListener(
-            const sp<gui::IWindowInfosListener>& windowInfosListener) override;
+    binder::Status addWindowInfosListener(const sp<gui::IWindowInfosListener>& windowInfosListener,
+                                          gui::WindowInfosListenerInfo* outInfo) override;
     binder::Status removeWindowInfosListener(
             const sp<gui::IWindowInfosListener>& windowInfosListener) override;
 
diff --git a/services/surfaceflinger/WindowInfosListenerInvoker.cpp b/services/surfaceflinger/WindowInfosListenerInvoker.cpp
index 20699ef..7062a4e 100644
--- a/services/surfaceflinger/WindowInfosListenerInvoker.cpp
+++ b/services/surfaceflinger/WindowInfosListenerInvoker.cpp
@@ -14,7 +14,9 @@
  * limitations under the License.
  */
 
-#include <ftl/small_vector.h>
+#include <android/gui/BnWindowInfosPublisher.h>
+#include <android/gui/IWindowInfosPublisher.h>
+#include <android/gui/WindowInfosListenerInfo.h>
 #include <gui/ISurfaceComposer.h>
 #include <gui/TraceUtils.h>
 #include <gui/WindowInfosUpdate.h>
@@ -23,162 +25,130 @@
 #include "BackgroundExecutor.h"
 #include "WindowInfosListenerInvoker.h"
 
+#undef ATRACE_TAG
+#define ATRACE_TAG ATRACE_TAG_GRAPHICS
+
 namespace android {
 
 using gui::DisplayInfo;
 using gui::IWindowInfosListener;
 using gui::WindowInfo;
 
-using WindowInfosListenerVector = ftl::SmallVector<const sp<gui::IWindowInfosListener>, 3>;
+void WindowInfosListenerInvoker::addWindowInfosListener(sp<IWindowInfosListener> listener,
+                                                        gui::WindowInfosListenerInfo* outInfo) {
+    int64_t listenerId = mNextListenerId++;
+    outInfo->listenerId = listenerId;
+    outInfo->windowInfosPublisher = sp<gui::IWindowInfosPublisher>::fromExisting(this);
 
-struct WindowInfosReportedListenerInvoker : gui::BnWindowInfosReportedListener,
-                                            IBinder::DeathRecipient {
-    WindowInfosReportedListenerInvoker(WindowInfosListenerVector windowInfosListeners,
-                                       WindowInfosReportedListenerSet windowInfosReportedListeners)
-          : mCallbacksPending(windowInfosListeners.size()),
-            mWindowInfosListeners(std::move(windowInfosListeners)),
-            mWindowInfosReportedListeners(std::move(windowInfosReportedListeners)) {}
-
-    binder::Status onWindowInfosReported() override {
-        if (--mCallbacksPending == 0) {
-            for (const auto& listener : mWindowInfosReportedListeners) {
+    BackgroundExecutor::getInstance().sendCallbacks(
+            {[this, listener = std::move(listener), listenerId]() {
+                ATRACE_NAME("WindowInfosListenerInvoker::addWindowInfosListener");
                 sp<IBinder> asBinder = IInterface::asBinder(listener);
-                if (asBinder->isBinderAlive()) {
-                    listener->onWindowInfosReported();
-                }
-            }
-
-            auto wpThis = wp<WindowInfosReportedListenerInvoker>::fromExisting(this);
-            for (const auto& listener : mWindowInfosListeners) {
-                sp<IBinder> binder = IInterface::asBinder(listener);
-                binder->unlinkToDeath(wpThis);
-            }
-        }
-        return binder::Status::ok();
-    }
-
-    void binderDied(const wp<IBinder>&) { onWindowInfosReported(); }
-
-private:
-    std::atomic<size_t> mCallbacksPending;
-    static constexpr size_t kStaticCapacity = 3;
-    const WindowInfosListenerVector mWindowInfosListeners;
-    WindowInfosReportedListenerSet mWindowInfosReportedListeners;
-};
-
-void WindowInfosListenerInvoker::addWindowInfosListener(sp<IWindowInfosListener> listener) {
-    sp<IBinder> asBinder = IInterface::asBinder(listener);
-    asBinder->linkToDeath(sp<DeathRecipient>::fromExisting(this));
-
-    std::scoped_lock lock(mListenersMutex);
-    mWindowInfosListeners.try_emplace(asBinder, std::move(listener));
+                asBinder->linkToDeath(sp<DeathRecipient>::fromExisting(this));
+                mWindowInfosListeners.try_emplace(asBinder,
+                                                  std::make_pair(listenerId, std::move(listener)));
+            }});
 }
 
 void WindowInfosListenerInvoker::removeWindowInfosListener(
         const sp<IWindowInfosListener>& listener) {
-    sp<IBinder> asBinder = IInterface::asBinder(listener);
-
-    std::scoped_lock lock(mListenersMutex);
-    asBinder->unlinkToDeath(sp<DeathRecipient>::fromExisting(this));
-    mWindowInfosListeners.erase(asBinder);
+    BackgroundExecutor::getInstance().sendCallbacks({[this, listener]() {
+        ATRACE_NAME("WindowInfosListenerInvoker::removeWindowInfosListener");
+        sp<IBinder> asBinder = IInterface::asBinder(listener);
+        asBinder->unlinkToDeath(sp<DeathRecipient>::fromExisting(this));
+        mWindowInfosListeners.erase(asBinder);
+    }});
 }
 
 void WindowInfosListenerInvoker::binderDied(const wp<IBinder>& who) {
-    std::scoped_lock lock(mListenersMutex);
-    mWindowInfosListeners.erase(who);
+    BackgroundExecutor::getInstance().sendCallbacks({[this, who]() {
+        ATRACE_NAME("WindowInfosListenerInvoker::binderDied");
+        auto it = mWindowInfosListeners.find(who);
+        int64_t listenerId = it->second.first;
+        mWindowInfosListeners.erase(who);
+
+        std::vector<int64_t> vsyncIds;
+        for (auto& [vsyncId, state] : mUnackedState) {
+            if (std::find(state.unackedListenerIds.begin(), state.unackedListenerIds.end(),
+                          listenerId) != state.unackedListenerIds.end()) {
+                vsyncIds.push_back(vsyncId);
+            }
+        }
+
+        for (int64_t vsyncId : vsyncIds) {
+            ackWindowInfosReceived(vsyncId, listenerId);
+        }
+    }});
 }
 
 void WindowInfosListenerInvoker::windowInfosChanged(
         gui::WindowInfosUpdate update, WindowInfosReportedListenerSet reportedListeners,
         bool forceImmediateCall) {
-    WindowInfosListenerVector listeners;
-    {
-        std::scoped_lock lock{mMessagesMutex};
+    if (!mDelayInfo) {
+        mDelayInfo = DelayInfo{
+                .vsyncId = update.vsyncId,
+                .frameTime = update.timestamp,
+        };
+    }
 
-        if (!mDelayInfo) {
-            mDelayInfo = DelayInfo{
-                    .vsyncId = update.vsyncId,
-                    .frameTime = update.timestamp,
-            };
-        }
+    // If there are unacked messages and this isn't a forced call, then return immediately.
+    // If a forced window infos change doesn't happen first, the update will be sent after
+    // the WindowInfosReportedListeners are called. If a forced window infos change happens or
+    // if there are subsequent delayed messages before this update is sent, then this message
+    // will be dropped and the listeners will only be called with the latest info. This is done
+    // to reduce the amount of binder memory used.
+    if (!mUnackedState.empty() && !forceImmediateCall) {
+        mDelayedUpdate = std::move(update);
+        mReportedListeners.merge(reportedListeners);
+        return;
+    }
 
-        // If there are unacked messages and this isn't a forced call, then return immediately.
-        // If a forced window infos change doesn't happen first, the update will be sent after
-        // the WindowInfosReportedListeners are called. If a forced window infos change happens or
-        // if there are subsequent delayed messages before this update is sent, then this message
-        // will be dropped and the listeners will only be called with the latest info. This is done
-        // to reduce the amount of binder memory used.
-        if (mActiveMessageCount > 0 && !forceImmediateCall) {
-            mDelayedUpdate = std::move(update);
-            mReportedListeners.merge(reportedListeners);
-            return;
-        }
+    if (mDelayedUpdate) {
+        mDelayedUpdate.reset();
+    }
 
-        if (mDelayedUpdate) {
-            mDelayedUpdate.reset();
-        }
-
-        {
-            std::scoped_lock lock{mListenersMutex};
-            for (const auto& [_, listener] : mWindowInfosListeners) {
-                listeners.push_back(listener);
-            }
-        }
-        if (CC_UNLIKELY(listeners.empty())) {
-            mReportedListeners.merge(reportedListeners);
-            mDelayInfo.reset();
-            return;
-        }
-
-        reportedListeners.insert(sp<WindowInfosListenerInvoker>::fromExisting(this));
-        reportedListeners.merge(mReportedListeners);
-        mReportedListeners.clear();
-
-        mActiveMessageCount++;
-        updateMaxSendDelay();
+    if (CC_UNLIKELY(mWindowInfosListeners.empty())) {
+        mReportedListeners.merge(reportedListeners);
         mDelayInfo.reset();
+        return;
     }
 
-    auto reportedInvoker =
-            sp<WindowInfosReportedListenerInvoker>::make(listeners, std::move(reportedListeners));
+    reportedListeners.merge(mReportedListeners);
+    mReportedListeners.clear();
 
-    for (const auto& listener : listeners) {
-        sp<IBinder> asBinder = IInterface::asBinder(listener);
+    // Update mUnackedState to include the message we're about to send
+    auto [it, _] = mUnackedState.try_emplace(update.vsyncId,
+                                             UnackedState{.reportedListeners =
+                                                                  std::move(reportedListeners)});
+    auto& unackedState = it->second;
+    for (auto& pair : mWindowInfosListeners) {
+        int64_t listenerId = pair.second.first;
+        unackedState.unackedListenerIds.push_back(listenerId);
+    }
 
-        // linkToDeath is used here to ensure that the windowInfosReportedListeners
-        // are called even if one of the windowInfosListeners dies before
-        // calling onWindowInfosReported.
-        asBinder->linkToDeath(reportedInvoker);
+    mDelayInfo.reset();
+    updateMaxSendDelay();
 
-        auto status = listener->onWindowInfosChanged(update, reportedInvoker);
+    // Call the listeners
+    for (auto& pair : mWindowInfosListeners) {
+        auto& [listenerId, listener] = pair.second;
+        auto status = listener->onWindowInfosChanged(update);
         if (!status.isOk()) {
-            reportedInvoker->onWindowInfosReported();
+            ackWindowInfosReceived(update.vsyncId, listenerId);
         }
     }
 }
 
-binder::Status WindowInfosListenerInvoker::onWindowInfosReported() {
-    BackgroundExecutor::getInstance().sendCallbacks({[this]() {
-        gui::WindowInfosUpdate update;
-        {
-            std::scoped_lock lock{mMessagesMutex};
-            mActiveMessageCount--;
-            if (!mDelayedUpdate || mActiveMessageCount > 0) {
-                return;
-            }
-            update = std::move(*mDelayedUpdate);
-            mDelayedUpdate.reset();
-        }
-        windowInfosChanged(std::move(update), {}, false);
-    }});
-    return binder::Status::ok();
-}
-
 WindowInfosListenerInvoker::DebugInfo WindowInfosListenerInvoker::getDebugInfo() {
-    std::scoped_lock lock{mMessagesMutex};
-    updateMaxSendDelay();
-    mDebugInfo.pendingMessageCount = mActiveMessageCount;
-    return mDebugInfo;
+    DebugInfo result;
+    BackgroundExecutor::getInstance().sendCallbacks({[&, this]() {
+        ATRACE_NAME("WindowInfosListenerInvoker::getDebugInfo");
+        updateMaxSendDelay();
+        result = mDebugInfo;
+        result.pendingMessageCount = mUnackedState.size();
+    }});
+    BackgroundExecutor::getInstance().flushQueue();
+    return result;
 }
 
 void WindowInfosListenerInvoker::updateMaxSendDelay() {
@@ -192,4 +162,41 @@
     }
 }
 
+binder::Status WindowInfosListenerInvoker::ackWindowInfosReceived(int64_t vsyncId,
+                                                                  int64_t listenerId) {
+    BackgroundExecutor::getInstance().sendCallbacks({[this, vsyncId, listenerId]() {
+        ATRACE_NAME("WindowInfosListenerInvoker::ackWindowInfosReceived");
+        auto it = mUnackedState.find(vsyncId);
+        if (it == mUnackedState.end()) {
+            return;
+        }
+
+        auto& state = it->second;
+        state.unackedListenerIds.unstable_erase(std::find(state.unackedListenerIds.begin(),
+                                                          state.unackedListenerIds.end(),
+                                                          listenerId));
+        if (!state.unackedListenerIds.empty()) {
+            return;
+        }
+
+        WindowInfosReportedListenerSet reportedListeners{std::move(state.reportedListeners)};
+        mUnackedState.erase(vsyncId);
+
+        for (const auto& reportedListener : reportedListeners) {
+            sp<IBinder> asBinder = IInterface::asBinder(reportedListener);
+            if (asBinder->isBinderAlive()) {
+                reportedListener->onWindowInfosReported();
+            }
+        }
+
+        if (!mDelayedUpdate || !mUnackedState.empty()) {
+            return;
+        }
+        gui::WindowInfosUpdate update{std::move(*mDelayedUpdate)};
+        mDelayedUpdate.reset();
+        windowInfosChanged(std::move(update), {}, false);
+    }});
+    return binder::Status::ok();
+}
+
 } // namespace android
diff --git a/services/surfaceflinger/WindowInfosListenerInvoker.h b/services/surfaceflinger/WindowInfosListenerInvoker.h
index bc465a3..f36b0ed 100644
--- a/services/surfaceflinger/WindowInfosListenerInvoker.h
+++ b/services/surfaceflinger/WindowInfosListenerInvoker.h
@@ -19,11 +19,12 @@
 #include <optional>
 #include <unordered_set>
 
-#include <android/gui/BnWindowInfosReportedListener.h>
+#include <android/gui/BnWindowInfosPublisher.h>
 #include <android/gui/IWindowInfosListener.h>
 #include <android/gui/IWindowInfosReportedListener.h>
 #include <binder/IBinder.h>
 #include <ftl/small_map.h>
+#include <ftl/small_vector.h>
 #include <gui/SpHash.h>
 #include <utils/Mutex.h>
 
@@ -35,22 +36,22 @@
         std::unordered_set<sp<gui::IWindowInfosReportedListener>,
                            gui::SpHash<gui::IWindowInfosReportedListener>>;
 
-class WindowInfosListenerInvoker : public gui::BnWindowInfosReportedListener,
+class WindowInfosListenerInvoker : public gui::BnWindowInfosPublisher,
                                    public IBinder::DeathRecipient {
 public:
-    void addWindowInfosListener(sp<gui::IWindowInfosListener>);
+    void addWindowInfosListener(sp<gui::IWindowInfosListener>, gui::WindowInfosListenerInfo*);
     void removeWindowInfosListener(const sp<gui::IWindowInfosListener>& windowInfosListener);
 
     void windowInfosChanged(gui::WindowInfosUpdate update,
                             WindowInfosReportedListenerSet windowInfosReportedListeners,
                             bool forceImmediateCall);
 
-    binder::Status onWindowInfosReported() override;
+    binder::Status ackWindowInfosReceived(int64_t, int64_t) override;
 
     struct DebugInfo {
         VsyncId maxSendDelayVsyncId;
         nsecs_t maxSendDelayDuration;
-        uint32_t pendingMessageCount;
+        size_t pendingMessageCount;
     };
     DebugInfo getDebugInfo();
 
@@ -58,24 +59,28 @@
     void binderDied(const wp<IBinder>& who) override;
 
 private:
-    std::mutex mListenersMutex;
-
     static constexpr size_t kStaticCapacity = 3;
-    ftl::SmallMap<wp<IBinder>, const sp<gui::IWindowInfosListener>, kStaticCapacity>
-            mWindowInfosListeners GUARDED_BY(mListenersMutex);
+    std::atomic<int64_t> mNextListenerId{0};
+    ftl::SmallMap<wp<IBinder>, const std::pair<int64_t, sp<gui::IWindowInfosListener>>,
+                  kStaticCapacity>
+            mWindowInfosListeners;
 
-    std::mutex mMessagesMutex;
-    uint32_t mActiveMessageCount GUARDED_BY(mMessagesMutex) = 0;
-    std::optional<gui::WindowInfosUpdate> mDelayedUpdate GUARDED_BY(mMessagesMutex);
+    std::optional<gui::WindowInfosUpdate> mDelayedUpdate;
     WindowInfosReportedListenerSet mReportedListeners;
 
-    DebugInfo mDebugInfo GUARDED_BY(mMessagesMutex);
+    struct UnackedState {
+        ftl::SmallVector<int64_t, kStaticCapacity> unackedListenerIds;
+        WindowInfosReportedListenerSet reportedListeners;
+    };
+    ftl::SmallMap<int64_t /* vsyncId */, UnackedState, 5> mUnackedState;
+
+    DebugInfo mDebugInfo;
     struct DelayInfo {
         int64_t vsyncId;
         nsecs_t frameTime;
     };
-    std::optional<DelayInfo> mDelayInfo GUARDED_BY(mMessagesMutex);
-    void updateMaxSendDelay() REQUIRES(mMessagesMutex);
+    std::optional<DelayInfo> mDelayInfo;
+    void updateMaxSendDelay();
 };
 
 } // namespace android
diff --git a/services/surfaceflinger/fuzzer/surfaceflinger_fuzzers_utils.h b/services/surfaceflinger/fuzzer/surfaceflinger_fuzzers_utils.h
index 0c9a16b..36095b4 100644
--- a/services/surfaceflinger/fuzzer/surfaceflinger_fuzzers_utils.h
+++ b/services/surfaceflinger/fuzzer/surfaceflinger_fuzzers_utils.h
@@ -287,7 +287,10 @@
     // ICompositor overrides:
     void configure() override {}
     bool commit(const scheduler::FrameTarget&) override { return false; }
-    CompositeResult composite(scheduler::FrameTargeter&) override { return {}; }
+    CompositeResultsPerDisplay composite(PhysicalDisplayId,
+                                         const scheduler::FrameTargeters&) override {
+        return {};
+    }
     void sample() override {}
 
     // MessageQueue overrides:
@@ -474,25 +477,25 @@
                                            &outWideColorGamutPixelFormat);
     }
 
-    void overrideHdrTypes(sp<IBinder> &display, FuzzedDataProvider *fdp) {
+    void overrideHdrTypes(const sp<IBinder>& display, FuzzedDataProvider* fdp) {
         std::vector<ui::Hdr> hdrTypes;
         hdrTypes.push_back(fdp->PickValueInArray(kHdrTypes));
         mFlinger->overrideHdrTypes(display, hdrTypes);
     }
 
-    void getDisplayedContentSample(sp<IBinder> &display, FuzzedDataProvider *fdp) {
+    void getDisplayedContentSample(const sp<IBinder>& display, FuzzedDataProvider* fdp) {
         DisplayedFrameStats outDisplayedFrameStats;
         mFlinger->getDisplayedContentSample(display, fdp->ConsumeIntegral<uint64_t>(),
                                             fdp->ConsumeIntegral<uint64_t>(),
                                             &outDisplayedFrameStats);
     }
 
-    void getDisplayStats(sp<IBinder> &display) {
+    void getDisplayStats(const sp<IBinder>& display) {
         android::DisplayStatInfo stats;
         mFlinger->getDisplayStats(display, &stats);
     }
 
-    void getDisplayState(sp<IBinder> &display) {
+    void getDisplayState(const sp<IBinder>& display) {
         ui::DisplayState displayState;
         mFlinger->getDisplayState(display, &displayState);
     }
@@ -506,12 +509,12 @@
         android::ui::DynamicDisplayInfo dynamicDisplayInfo;
         mFlinger->getDynamicDisplayInfoFromId(displayId, &dynamicDisplayInfo);
     }
-    void getDisplayNativePrimaries(sp<IBinder> &display) {
+    void getDisplayNativePrimaries(const sp<IBinder>& display) {
         android::ui::DisplayPrimaries displayPrimaries;
         mFlinger->getDisplayNativePrimaries(display, displayPrimaries);
     }
 
-    void getDesiredDisplayModeSpecs(sp<IBinder> &display) {
+    void getDesiredDisplayModeSpecs(const sp<IBinder>& display) {
         gui::DisplayModeSpecs _;
         mFlinger->getDesiredDisplayModeSpecs(display, &_);
     }
@@ -523,7 +526,7 @@
         return ids.front();
     }
 
-    std::pair<sp<IBinder>, int64_t> fuzzBoot(FuzzedDataProvider *fdp) {
+    std::pair<sp<IBinder>, PhysicalDisplayId> fuzzBoot(FuzzedDataProvider* fdp) {
         mFlinger->callingThreadHasUnscopedSurfaceFlingerAccess(fdp->ConsumeBool());
         const sp<Client> client = sp<Client>::make(mFlinger);
 
@@ -550,13 +553,13 @@
 
         mFlinger->bootFinished();
 
-        return {display, physicalDisplayId.value};
+        return {display, physicalDisplayId};
     }
 
     void fuzzSurfaceFlinger(const uint8_t *data, size_t size) {
         FuzzedDataProvider mFdp(data, size);
 
-        auto [display, displayId] = fuzzBoot(&mFdp);
+        const auto [display, displayId] = fuzzBoot(&mFdp);
 
         sp<IGraphicBufferProducer> bufferProducer = sp<mock::GraphicBufferProducer>::make();
 
@@ -564,8 +567,8 @@
 
         getDisplayStats(display);
         getDisplayState(display);
-        getStaticDisplayInfo(displayId);
-        getDynamicDisplayInfo(displayId);
+        getStaticDisplayInfo(displayId.value);
+        getDynamicDisplayInfo(displayId.value);
         getDisplayNativePrimaries(display);
 
         mFlinger->setAutoLowLatencyMode(display, mFdp.ConsumeBool());
@@ -605,8 +608,9 @@
             mFlinger->commitTransactions();
             mFlinger->flushTransactionQueues(getFuzzedVsyncId(mFdp));
 
-            scheduler::FrameTargeter frameTargeter(mFdp.ConsumeBool());
-            mFlinger->postComposition(frameTargeter, mFdp.ConsumeIntegral<nsecs_t>());
+            scheduler::FrameTargeter frameTargeter(displayId, mFdp.ConsumeBool());
+            mFlinger->postComposition(displayId, ftl::init::map(displayId, &frameTargeter),
+                                      mFdp.ConsumeIntegral<nsecs_t>());
         }
 
         mFlinger->setTransactionFlags(mFdp.ConsumeIntegral<uint32_t>());
diff --git a/services/surfaceflinger/fuzzer/surfaceflinger_scheduler_fuzzer.cpp b/services/surfaceflinger/fuzzer/surfaceflinger_scheduler_fuzzer.cpp
index b1fd06f..4d1a5ff 100644
--- a/services/surfaceflinger/fuzzer/surfaceflinger_scheduler_fuzzer.cpp
+++ b/services/surfaceflinger/fuzzer/surfaceflinger_scheduler_fuzzer.cpp
@@ -50,7 +50,7 @@
 
 constexpr uint16_t kRandomStringLength = 256;
 constexpr std::chrono::duration kSyncPeriod(16ms);
-constexpr PhysicalDisplayId DEFAULT_DISPLAY_ID = PhysicalDisplayId::fromPort(42u);
+constexpr PhysicalDisplayId kDisplayId = PhysicalDisplayId::fromPort(42u);
 
 template <typename T>
 void dump(T* component, FuzzedDataProvider* fdp) {
@@ -177,9 +177,8 @@
     uint16_t now = mFdp.ConsumeIntegral<uint16_t>();
     uint16_t historySize = mFdp.ConsumeIntegralInRange<uint16_t>(1, UINT16_MAX);
     uint16_t minimumSamplesForPrediction = mFdp.ConsumeIntegralInRange<uint16_t>(1, UINT16_MAX);
-    scheduler::VSyncPredictor tracker{DEFAULT_DISPLAY_ID,
-                                      mFdp.ConsumeIntegral<uint16_t>() /*period*/, historySize,
-                                      minimumSamplesForPrediction,
+    scheduler::VSyncPredictor tracker{kDisplayId, mFdp.ConsumeIntegral<uint16_t>() /*period*/,
+                                      historySize, minimumSamplesForPrediction,
                                       mFdp.ConsumeIntegral<uint32_t>() /*outlierTolerancePercent*/};
     uint16_t period = mFdp.ConsumeIntegral<uint16_t>();
     tracker.setPeriod(period);
@@ -251,7 +250,7 @@
 
 void SchedulerFuzzer::fuzzVSyncReactor() {
     std::shared_ptr<FuzzImplVSyncTracker> vSyncTracker = std::make_shared<FuzzImplVSyncTracker>();
-    scheduler::VSyncReactor reactor(DEFAULT_DISPLAY_ID,
+    scheduler::VSyncReactor reactor(kDisplayId,
                                     std::make_unique<ClockWrapper>(
                                             std::make_shared<FuzzImplClock>()),
                                     *vSyncTracker, mFdp.ConsumeIntegral<uint8_t>() /*pendingLimit*/,
@@ -408,7 +407,7 @@
 }
 
 void SchedulerFuzzer::fuzzFrameTargeter() {
-    scheduler::FrameTargeter frameTargeter(mFdp.ConsumeBool());
+    scheduler::FrameTargeter frameTargeter(kDisplayId, mFdp.ConsumeBool());
 
     const struct VsyncSource final : scheduler::IVsyncSource {
         explicit VsyncSource(FuzzedDataProvider& fuzzer) : fuzzer(fuzzer) {}
diff --git a/services/surfaceflinger/tests/unittests/MessageQueueTest.cpp b/services/surfaceflinger/tests/unittests/MessageQueueTest.cpp
index 359e2ab..1dcf222 100644
--- a/services/surfaceflinger/tests/unittests/MessageQueueTest.cpp
+++ b/services/surfaceflinger/tests/unittests/MessageQueueTest.cpp
@@ -36,7 +36,10 @@
 struct NoOpCompositor final : ICompositor {
     void configure() override {}
     bool commit(const scheduler::FrameTarget&) override { return false; }
-    CompositeResult composite(scheduler::FrameTargeter&) override { return {}; }
+    CompositeResultsPerDisplay composite(PhysicalDisplayId,
+                                         const scheduler::FrameTargeters&) override {
+        return {};
+    }
     void sample() override {}
 } gNoOpCompositor;
 
diff --git a/services/surfaceflinger/tests/unittests/RefreshRateSelectorTest.cpp b/services/surfaceflinger/tests/unittests/RefreshRateSelectorTest.cpp
index d63e187..aaf55fb 100644
--- a/services/surfaceflinger/tests/unittests/RefreshRateSelectorTest.cpp
+++ b/services/surfaceflinger/tests/unittests/RefreshRateSelectorTest.cpp
@@ -222,6 +222,7 @@
             makeModes(kMode60, kMode90, kMode72_G1, kMode120_G1, kMode30_G1, kMode25_G1, kMode50);
     static inline const DisplayModes kModes_60_120 = makeModes(kMode60, kMode120);
     static inline const DisplayModes kModes_1_5_10 = makeModes(kMode1, kMode5, kMode10);
+    static inline const DisplayModes kModes_60_90_120 = makeModes(kMode60, kMode90, kMode120);
 
     // This is a typical TV configuration.
     static inline const DisplayModes kModes_24_25_30_50_60_Frac =
@@ -1413,7 +1414,9 @@
         ss << "ExplicitDefault " << desired;
         lr.name = ss.str();
 
-        EXPECT_EQ(expected, selector.getBestFrameRateMode(layers)->getFps());
+        const auto bestFps = selector.getBestFrameRateMode(layers)->getFps();
+        EXPECT_EQ(expected, bestFps)
+                << "expected " << expected << " for " << desired << " but got " << bestFps;
     }
 }
 
@@ -1422,7 +1425,7 @@
     std::vector<LayerRequirement> layers = {{.weight = 1.f}};
     auto& lr = layers[0];
 
-    // Test that 23.976 will choose 24 if 23.976 is not supported
+    // Test that 23.976 will prefer 60 over 59.94 and 30
     {
         auto selector = createSelector(makeModes(kMode24, kMode25, kMode30, kMode30Frac, kMode60,
                                                  kMode60Frac),
@@ -1431,7 +1434,7 @@
         lr.vote = LayerVoteType::ExplicitExactOrMultiple;
         lr.desiredRefreshRate = 23.976_Hz;
         lr.name = "ExplicitExactOrMultiple 23.976 Hz";
-        EXPECT_EQ(kModeId24, selector.getBestFrameRateMode(layers)->getId());
+        EXPECT_EQ(kModeId60, selector.getBestFrameRateMode(layers)->getId());
     }
 
     // Test that 24 will choose 23.976 if 24 is not supported
@@ -1456,13 +1459,13 @@
         EXPECT_EQ(kModeId60Frac, selector.getBestFrameRateMode(layers)->getId());
     }
 
-    // Test that 29.97 will choose 30 if 59.94 is not supported
+    // Test that 29.97 will choose 60 if 59.94 is not supported
     {
         auto selector = createSelector(makeModes(kMode30, kMode60), kModeId60);
 
         lr.desiredRefreshRate = 29.97_Hz;
         lr.name = "ExplicitExactOrMultiple 29.97 Hz";
-        EXPECT_EQ(kModeId30, selector.getBestFrameRateMode(layers)->getId());
+        EXPECT_EQ(kModeId60, selector.getBestFrameRateMode(layers)->getId());
     }
 
     // Test that 59.94 will choose 60 if 59.94 is not supported
@@ -2516,6 +2519,71 @@
     EXPECT_FALSE(RefreshRateSelector::isFractionalPairOrMultiple(29.97_Hz, 59.94_Hz));
 }
 
+TEST_P(RefreshRateSelectorTest, test23976Chooses120) {
+    auto selector = createSelector(kModes_60_90_120, kModeId120);
+    std::vector<LayerRequirement> layers = {{.weight = 1.f}};
+    layers[0].name = "23.976 ExplicitExactOrMultiple";
+    layers[0].vote = LayerVoteType::ExplicitExactOrMultiple;
+    layers[0].desiredRefreshRate = 23.976_Hz;
+    EXPECT_FRAME_RATE_MODE(kMode120, 120_Hz, selector.getBestScoredFrameRate(layers).frameRateMode);
+}
+
+TEST_P(RefreshRateSelectorTest, test23976Chooses60IfThresholdIs120) {
+    auto selector =
+            createSelector(kModes_60_90_120, kModeId120, {.frameRateMultipleThreshold = 120});
+    std::vector<LayerRequirement> layers = {{.weight = 1.f}};
+    layers[0].name = "23.976 ExplicitExactOrMultiple";
+    layers[0].vote = LayerVoteType::ExplicitExactOrMultiple;
+    layers[0].desiredRefreshRate = 23.976_Hz;
+    EXPECT_FRAME_RATE_MODE(kMode60, 60_Hz, selector.getBestScoredFrameRate(layers).frameRateMode);
+}
+
+TEST_P(RefreshRateSelectorTest, test25Chooses60) {
+    auto selector = createSelector(kModes_60_90_120, kModeId120);
+    std::vector<LayerRequirement> layers = {{.weight = 1.f}};
+    layers[0].name = "25 ExplicitExactOrMultiple";
+    layers[0].vote = LayerVoteType::ExplicitExactOrMultiple;
+    layers[0].desiredRefreshRate = 25.00_Hz;
+    EXPECT_FRAME_RATE_MODE(kMode60, 60_Hz, selector.getBestScoredFrameRate(layers).frameRateMode);
+}
+
+TEST_P(RefreshRateSelectorTest, test2997Chooses60) {
+    auto selector = createSelector(kModes_60_90_120, kModeId120);
+    std::vector<LayerRequirement> layers = {{.weight = 1.f}};
+    layers[0].name = "29.97 ExplicitExactOrMultiple";
+    layers[0].vote = LayerVoteType::ExplicitExactOrMultiple;
+    layers[0].desiredRefreshRate = 29.97_Hz;
+    EXPECT_FRAME_RATE_MODE(kMode60, 60_Hz, selector.getBestScoredFrameRate(layers).frameRateMode);
+}
+
+TEST_P(RefreshRateSelectorTest, test50Chooses120) {
+    auto selector = createSelector(kModes_60_90_120, kModeId120);
+    std::vector<LayerRequirement> layers = {{.weight = 1.f}};
+    layers[0].name = "50 ExplicitExactOrMultiple";
+    layers[0].vote = LayerVoteType::ExplicitExactOrMultiple;
+    layers[0].desiredRefreshRate = 50.00_Hz;
+    EXPECT_FRAME_RATE_MODE(kMode120, 120_Hz, selector.getBestScoredFrameRate(layers).frameRateMode);
+}
+
+TEST_P(RefreshRateSelectorTest, test50Chooses60IfThresholdIs120) {
+    auto selector =
+            createSelector(kModes_60_90_120, kModeId120, {.frameRateMultipleThreshold = 120});
+    std::vector<LayerRequirement> layers = {{.weight = 1.f}};
+    layers[0].name = "50 ExplicitExactOrMultiple";
+    layers[0].vote = LayerVoteType::ExplicitExactOrMultiple;
+    layers[0].desiredRefreshRate = 50.00_Hz;
+    EXPECT_FRAME_RATE_MODE(kMode60, 60_Hz, selector.getBestScoredFrameRate(layers).frameRateMode);
+}
+
+TEST_P(RefreshRateSelectorTest, test5994Chooses60) {
+    auto selector = createSelector(kModes_60_90_120, kModeId120);
+    std::vector<LayerRequirement> layers = {{.weight = 1.f}};
+    layers[0].name = "59.94 ExplicitExactOrMultiple";
+    layers[0].vote = LayerVoteType::ExplicitExactOrMultiple;
+    layers[0].desiredRefreshRate = 59.94_Hz;
+    EXPECT_FRAME_RATE_MODE(kMode60, 60_Hz, selector.getBestScoredFrameRate(layers).frameRateMode);
+}
+
 TEST_P(RefreshRateSelectorTest, getFrameRateOverrides_noLayers) {
     auto selector = createSelector(kModes_30_60_72_90_120, kModeId120);
 
@@ -3042,5 +3110,20 @@
     EXPECT_FRAME_RATE_MODE(kMode60, 60_Hz, selector.getBestScoredFrameRate(layers).frameRateMode);
 }
 
+TEST_P(RefreshRateSelectorTest, frameRateIsLowerThanMinSupported) {
+    if (GetParam() != Config::FrameRateOverride::Enabled) {
+        return;
+    }
+
+    auto selector = createSelector(kModes_60_90, kModeId60);
+
+    constexpr Fps kMin = RefreshRateSelector::kMinSupportedFrameRate;
+    constexpr FpsRanges kLowerThanMin = {{60_Hz, 90_Hz}, {kMin / 2, kMin / 2}};
+
+    EXPECT_EQ(SetPolicyResult::Changed,
+              selector.setDisplayManagerPolicy(
+                      {DisplayModeId(kModeId60), kLowerThanMin, kLowerThanMin}));
+}
+
 } // namespace
 } // namespace android::scheduler
diff --git a/services/surfaceflinger/tests/unittests/TestableScheduler.h b/services/surfaceflinger/tests/unittests/TestableScheduler.h
index aac11c0..a978984 100644
--- a/services/surfaceflinger/tests/unittests/TestableScheduler.h
+++ b/services/surfaceflinger/tests/unittests/TestableScheduler.h
@@ -174,7 +174,10 @@
     // ICompositor overrides:
     void configure() override {}
     bool commit(const scheduler::FrameTarget&) override { return false; }
-    CompositeResult composite(scheduler::FrameTargeter&) override { return {}; }
+    CompositeResultsPerDisplay composite(PhysicalDisplayId,
+                                         const scheduler::FrameTargeters&) override {
+        return {};
+    }
     void sample() override {}
 };
 
diff --git a/services/surfaceflinger/tests/unittests/TestableSurfaceFlinger.h b/services/surfaceflinger/tests/unittests/TestableSurfaceFlinger.h
index 833984f..da482d5 100644
--- a/services/surfaceflinger/tests/unittests/TestableSurfaceFlinger.h
+++ b/services/surfaceflinger/tests/unittests/TestableSurfaceFlinger.h
@@ -353,7 +353,10 @@
      * Forwarding for functions being tested
      */
 
-    void configure() { mFlinger->configure(); }
+    void configure() {
+        ftl::FakeGuard guard(kMainThreadContext);
+        mFlinger->configure();
+    }
 
     void configureAndCommit() {
         configure();
@@ -362,8 +365,14 @@
 
     void commit(TimePoint frameTime, VsyncId vsyncId, TimePoint expectedVsyncTime,
                 bool composite = false) {
+        ftl::FakeGuard guard(kMainThreadContext);
+
+        const auto displayIdOpt = mScheduler->pacesetterDisplayId();
+        LOG_ALWAYS_FATAL_IF(!displayIdOpt);
+        const auto displayId = *displayIdOpt;
+
         constexpr bool kBackpressureGpuComposition = true;
-        scheduler::FrameTargeter frameTargeter(kBackpressureGpuComposition);
+        scheduler::FrameTargeter frameTargeter(displayId, kBackpressureGpuComposition);
 
         frameTargeter.beginFrame({.frameBeginTime = frameTime,
                                   .vsyncId = vsyncId,
@@ -374,7 +383,7 @@
         mFlinger->commit(frameTargeter.target());
 
         if (composite) {
-            mFlinger->composite(frameTargeter);
+            mFlinger->composite(displayId, ftl::init::map(displayId, &frameTargeter));
         }
     }
 
diff --git a/services/surfaceflinger/tests/unittests/WindowInfosListenerInvokerTest.cpp b/services/surfaceflinger/tests/unittests/WindowInfosListenerInvokerTest.cpp
index af4971b..c7b845e 100644
--- a/services/surfaceflinger/tests/unittests/WindowInfosListenerInvokerTest.cpp
+++ b/services/surfaceflinger/tests/unittests/WindowInfosListenerInvokerTest.cpp
@@ -15,35 +15,23 @@
     WindowInfosListenerInvokerTest() : mInvoker(sp<WindowInfosListenerInvoker>::make()) {}
 
     ~WindowInfosListenerInvokerTest() {
-        std::mutex mutex;
-        std::condition_variable cv;
-        bool flushComplete = false;
         // Flush the BackgroundExecutor thread to ensure any scheduled tasks are complete.
         // Otherwise, references those tasks hold may go out of scope before they are done
         // executing.
-        BackgroundExecutor::getInstance().sendCallbacks({[&]() {
-            std::scoped_lock lock{mutex};
-            flushComplete = true;
-            cv.notify_one();
-        }});
-        std::unique_lock<std::mutex> lock{mutex};
-        cv.wait(lock, [&]() { return flushComplete; });
+        BackgroundExecutor::getInstance().flushQueue();
     }
 
     sp<WindowInfosListenerInvoker> mInvoker;
 };
 
-using WindowInfosUpdateConsumer = std::function<void(const gui::WindowInfosUpdate&,
-                                                     const sp<gui::IWindowInfosReportedListener>&)>;
+using WindowInfosUpdateConsumer = std::function<void(const gui::WindowInfosUpdate&)>;
 
 class Listener : public gui::BnWindowInfosListener {
 public:
     Listener(WindowInfosUpdateConsumer consumer) : mConsumer(std::move(consumer)) {}
 
-    binder::Status onWindowInfosChanged(
-            const gui::WindowInfosUpdate& update,
-            const sp<gui::IWindowInfosReportedListener>& reportedListener) override {
-        mConsumer(update, reportedListener);
+    binder::Status onWindowInfosChanged(const gui::WindowInfosUpdate& update) override {
+        mConsumer(update);
         return binder::Status::ok();
     }
 
@@ -58,15 +46,17 @@
 
     int callCount = 0;
 
-    mInvoker->addWindowInfosListener(
-            sp<Listener>::make([&](const gui::WindowInfosUpdate&,
-                                   const sp<gui::IWindowInfosReportedListener>& reportedListener) {
-                std::scoped_lock lock{mutex};
-                callCount++;
-                cv.notify_one();
+    gui::WindowInfosListenerInfo listenerInfo;
+    mInvoker->addWindowInfosListener(sp<Listener>::make([&](const gui::WindowInfosUpdate& update) {
+                                         std::scoped_lock lock{mutex};
+                                         callCount++;
+                                         cv.notify_one();
 
-                reportedListener->onWindowInfosReported();
-            }));
+                                         listenerInfo.windowInfosPublisher
+                                                 ->ackWindowInfosReceived(update.vsyncId,
+                                                                          listenerInfo.listenerId);
+                                     }),
+                                     &listenerInfo);
 
     BackgroundExecutor::getInstance().sendCallbacks(
             {[this]() { mInvoker->windowInfosChanged({}, {}, false); }});
@@ -81,21 +71,27 @@
     std::mutex mutex;
     std::condition_variable cv;
 
-    int callCount = 0;
-    const int expectedCallCount = 3;
+    size_t callCount = 0;
+    const size_t expectedCallCount = 3;
+    std::vector<gui::WindowInfosListenerInfo> listenerInfos{expectedCallCount,
+                                                            gui::WindowInfosListenerInfo{}};
 
-    for (int i = 0; i < expectedCallCount; i++) {
-        mInvoker->addWindowInfosListener(sp<Listener>::make(
-                [&](const gui::WindowInfosUpdate&,
-                    const sp<gui::IWindowInfosReportedListener>& reportedListener) {
-                    std::scoped_lock lock{mutex};
-                    callCount++;
-                    if (callCount == expectedCallCount) {
-                        cv.notify_one();
-                    }
+    for (size_t i = 0; i < expectedCallCount; i++) {
+        mInvoker->addWindowInfosListener(sp<Listener>::make([&, &listenerInfo = listenerInfos[i]](
+                                                                    const gui::WindowInfosUpdate&
+                                                                            update) {
+                                             std::scoped_lock lock{mutex};
+                                             callCount++;
+                                             if (callCount == expectedCallCount) {
+                                                 cv.notify_one();
+                                             }
 
-                    reportedListener->onWindowInfosReported();
-                }));
+                                             listenerInfo.windowInfosPublisher
+                                                     ->ackWindowInfosReceived(update.vsyncId,
+                                                                              listenerInfo
+                                                                                      .listenerId);
+                                         }),
+                                         &listenerInfos[i]);
     }
 
     BackgroundExecutor::getInstance().sendCallbacks(
@@ -114,17 +110,20 @@
 
     int callCount = 0;
 
-    // Simulate a slow ack by not calling the WindowInfosReportedListener.
-    mInvoker->addWindowInfosListener(sp<Listener>::make(
-            [&](const gui::WindowInfosUpdate&, const sp<gui::IWindowInfosReportedListener>&) {
-                std::scoped_lock lock{mutex};
-                callCount++;
-                cv.notify_one();
-            }));
+    // Simulate a slow ack by not calling IWindowInfosPublisher.ackWindowInfosReceived
+    gui::WindowInfosListenerInfo listenerInfo;
+    mInvoker->addWindowInfosListener(sp<Listener>::make([&](const gui::WindowInfosUpdate&) {
+                                         std::scoped_lock lock{mutex};
+                                         callCount++;
+                                         cv.notify_one();
+                                     }),
+                                     &listenerInfo);
 
     BackgroundExecutor::getInstance().sendCallbacks({[&]() {
-        mInvoker->windowInfosChanged({}, {}, false);
-        mInvoker->windowInfosChanged({}, {}, false);
+        mInvoker->windowInfosChanged(gui::WindowInfosUpdate{{}, {}, /* vsyncId= */ 0, 0}, {},
+                                     false);
+        mInvoker->windowInfosChanged(gui::WindowInfosUpdate{{}, {}, /* vsyncId= */ 1, 0}, {},
+                                     false);
     }});
 
     {
@@ -134,7 +133,7 @@
     EXPECT_EQ(callCount, 1);
 
     // Ack the first message.
-    mInvoker->onWindowInfosReported();
+    listenerInfo.windowInfosPublisher->ackWindowInfosReceived(0, listenerInfo.listenerId);
 
     {
         std::unique_lock lock{mutex};
@@ -152,19 +151,21 @@
     int callCount = 0;
     const int expectedCallCount = 2;
 
-    // Simulate a slow ack by not calling the WindowInfosReportedListener.
-    mInvoker->addWindowInfosListener(sp<Listener>::make(
-            [&](const gui::WindowInfosUpdate&, const sp<gui::IWindowInfosReportedListener>&) {
-                std::scoped_lock lock{mutex};
-                callCount++;
-                if (callCount == expectedCallCount) {
-                    cv.notify_one();
-                }
-            }));
+    // Simulate a slow ack by not calling IWindowInfosPublisher.ackWindowInfosReceived
+    gui::WindowInfosListenerInfo listenerInfo;
+    mInvoker->addWindowInfosListener(sp<Listener>::make([&](const gui::WindowInfosUpdate&) {
+                                         std::scoped_lock lock{mutex};
+                                         callCount++;
+                                         if (callCount == expectedCallCount) {
+                                             cv.notify_one();
+                                         }
+                                     }),
+                                     &listenerInfo);
 
     BackgroundExecutor::getInstance().sendCallbacks({[&]() {
-        mInvoker->windowInfosChanged({}, {}, false);
-        mInvoker->windowInfosChanged({}, {}, true);
+        mInvoker->windowInfosChanged(gui::WindowInfosUpdate{{}, {}, /* vsyncId= */ 0, 0}, {},
+                                     false);
+        mInvoker->windowInfosChanged(gui::WindowInfosUpdate{{}, {}, /* vsyncId= */ 1, 0}, {}, true);
     }});
 
     {
@@ -182,14 +183,14 @@
 
     int64_t lastUpdateId = -1;
 
-    // Simulate a slow ack by not calling the WindowInfosReportedListener.
-    mInvoker->addWindowInfosListener(
-            sp<Listener>::make([&](const gui::WindowInfosUpdate& update,
-                                   const sp<gui::IWindowInfosReportedListener>&) {
-                std::scoped_lock lock{mutex};
-                lastUpdateId = update.vsyncId;
-                cv.notify_one();
-            }));
+    // Simulate a slow ack by not calling IWindowInfosPublisher.ackWindowInfosReceived
+    gui::WindowInfosListenerInfo listenerInfo;
+    mInvoker->addWindowInfosListener(sp<Listener>::make([&](const gui::WindowInfosUpdate& update) {
+                                         std::scoped_lock lock{mutex};
+                                         lastUpdateId = update.vsyncId;
+                                         cv.notify_one();
+                                     }),
+                                     &listenerInfo);
 
     BackgroundExecutor::getInstance().sendCallbacks({[&]() {
         mInvoker->windowInfosChanged({{}, {}, /* vsyncId= */ 1, 0}, {}, false);
@@ -204,7 +205,7 @@
     EXPECT_EQ(lastUpdateId, 1);
 
     // Ack the first message. The third update should be sent.
-    mInvoker->onWindowInfosReported();
+    listenerInfo.windowInfosPublisher->ackWindowInfosReceived(1, listenerInfo.listenerId);
 
     {
         std::unique_lock lock{mutex};
@@ -225,14 +226,17 @@
     // delayed.
     BackgroundExecutor::getInstance().sendCallbacks({[&]() {
         mInvoker->windowInfosChanged({}, {}, false);
-        mInvoker->addWindowInfosListener(sp<Listener>::make(
-                [&](const gui::WindowInfosUpdate&, const sp<gui::IWindowInfosReportedListener>&) {
-                    std::scoped_lock lock{mutex};
-                    callCount++;
-                    cv.notify_one();
-                }));
-        mInvoker->windowInfosChanged({}, {}, false);
+        gui::WindowInfosListenerInfo listenerInfo;
+        mInvoker->addWindowInfosListener(sp<Listener>::make([&](const gui::WindowInfosUpdate&) {
+                                             std::scoped_lock lock{mutex};
+                                             callCount++;
+                                             cv.notify_one();
+                                         }),
+                                         &listenerInfo);
     }});
+    BackgroundExecutor::getInstance().flushQueue();
+    BackgroundExecutor::getInstance().sendCallbacks(
+            {[&]() { mInvoker->windowInfosChanged({}, {}, false); }});
 
     {
         std::unique_lock lock{mutex};