Add border API to surface control

See go/sf-box-shadows-api for more details

Bug: b/367464660
Flag: com.android.window.flags.enable_border_settings
Test: atest SurfaceFlinger_test
Change-Id: I1190edb97693004d9f46058fd0165451470a65b3
diff --git a/services/surfaceflinger/CompositionEngine/include/compositionengine/LayerFECompositionState.h b/services/surfaceflinger/CompositionEngine/include/compositionengine/LayerFECompositionState.h
index fb8fed0..34b0bb5 100644
--- a/services/surfaceflinger/CompositionEngine/include/compositionengine/LayerFECompositionState.h
+++ b/services/surfaceflinger/CompositionEngine/include/compositionengine/LayerFECompositionState.h
@@ -18,6 +18,7 @@
 
 #include <cstdint>
 
+#include <android/gui/BorderSettings.h>
 #include <android/gui/CachingHint.h>
 #include <gui/DisplayLuts.h>
 #include <gui/HdrMetadata.h>
@@ -141,6 +142,9 @@
 
     ShadowSettings shadowSettings;
 
+    // The settings to configure the outline of a layer.
+    gui::BorderSettings borderSettings;
+
     // List of regions that require blur
     std::vector<BlurRegion> blurRegions;
 
diff --git a/services/surfaceflinger/CompositionEngine/src/LayerFECompositionState.cpp b/services/surfaceflinger/CompositionEngine/src/LayerFECompositionState.cpp
index 348111d..294b167 100644
--- a/services/surfaceflinger/CompositionEngine/src/LayerFECompositionState.cpp
+++ b/services/surfaceflinger/CompositionEngine/src/LayerFECompositionState.cpp
@@ -70,6 +70,9 @@
     out.append("      ");
     dumpVal(out, "shadowLength", shadowSettings.length);
 
+    out.append("      ");
+    dumpVal(out, "borderSettings", borderSettings.toString());
+
     out.append("\n      ");
     dumpVal(out, "blend", toString(blendMode), blendMode);
     dumpVal(out, "alpha", alpha);
diff --git a/services/surfaceflinger/CompositionEngine/src/OutputLayer.cpp b/services/surfaceflinger/CompositionEngine/src/OutputLayer.cpp
index ea36011..e4793a4 100644
--- a/services/surfaceflinger/CompositionEngine/src/OutputLayer.cpp
+++ b/services/surfaceflinger/CompositionEngine/src/OutputLayer.cpp
@@ -237,6 +237,16 @@
         geomLayerBounds.bottom += outset;
     }
 
+    // Similar to above
+    if (layerState.forceClientComposition && layerState.borderSettings.strokeWidth > 0.0f) {
+        // Antialiasing should never add more than 2 pixels.
+        const auto outset = layerState.borderSettings.strokeWidth + 2;
+        geomLayerBounds.left -= outset;
+        geomLayerBounds.top -= outset;
+        geomLayerBounds.right += outset;
+        geomLayerBounds.bottom += outset;
+    }
+
     geomLayerBounds = layerTransform.transform(geomLayerBounds);
     FloatRect frame = reduce(geomLayerBounds, activeTransparentRegion);
     frame = frame.intersect(outputState.layerStackSpace.getContent().toFloatRect());
diff --git a/services/surfaceflinger/CompositionEngine/tests/OutputLayerTest.cpp b/services/surfaceflinger/CompositionEngine/tests/OutputLayerTest.cpp
index ca262ee..2f531f1 100644
--- a/services/surfaceflinger/CompositionEngine/tests/OutputLayerTest.cpp
+++ b/services/surfaceflinger/CompositionEngine/tests/OutputLayerTest.cpp
@@ -355,6 +355,26 @@
     EXPECT_THAT(calculateOutputDisplayFrame(), expected);
 }
 
+TEST_F(OutputLayerDisplayFrameTest, outlineExpandsDisplayFrame) {
+    const int kStrokeWidth = 3;
+    mLayerFEState.borderSettings.strokeWidth = kStrokeWidth;
+    mLayerFEState.forceClientComposition = true;
+
+    mLayerFEState.geomLayerBounds = FloatRect{100.f, 100.f, 200.f, 200.f};
+    Rect expected{mLayerFEState.geomLayerBounds};
+    expected.inset(-kStrokeWidth - 2, -kStrokeWidth - 2, -kStrokeWidth - 2, -kStrokeWidth - 2);
+    EXPECT_THAT(calculateOutputDisplayFrame(), expected);
+}
+TEST_F(OutputLayerDisplayFrameTest, outlineExpandsDisplayFrame_onlyIfForcingClientComposition) {
+    const int kStrokeWidth = 3;
+    mLayerFEState.borderSettings.strokeWidth = kStrokeWidth;
+    mLayerFEState.forceClientComposition = false;
+
+    mLayerFEState.geomLayerBounds = FloatRect{100.f, 100.f, 200.f, 200.f};
+    Rect expected{mLayerFEState.geomLayerBounds};
+    EXPECT_THAT(calculateOutputDisplayFrame(), expected);
+}
+
 /*
  * OutputLayer::calculateOutputRelativeBufferTransform()
  */
diff --git a/services/surfaceflinger/FrontEnd/LayerSnapshot.cpp b/services/surfaceflinger/FrontEnd/LayerSnapshot.cpp
index 964a970..3aa2e98 100644
--- a/services/surfaceflinger/FrontEnd/LayerSnapshot.cpp
+++ b/services/surfaceflinger/FrontEnd/LayerSnapshot.cpp
@@ -179,8 +179,12 @@
     return backgroundBlurRadius > 0 || blurRegions.size() > 0;
 }
 
+bool LayerSnapshot::hasOutline() const {
+    return borderSettings.strokeWidth > 0;
+}
+
 bool LayerSnapshot::hasEffect() const {
-    return fillsColor() || drawShadows() || hasBlur();
+    return fillsColor() || drawShadows() || hasBlur() || hasOutline();
 }
 
 bool LayerSnapshot::hasSomethingToDraw() const {
@@ -253,6 +257,7 @@
         reason << " buffer=" << externalTexture->getId() << " frame=" << frameNumber;
     if (fillsColor() || color.a > 0.0f) reason << " color{" << color << "}";
     if (drawShadows()) reason << " shadowSettings.length=" << shadowSettings.length;
+    if (hasOutline()) reason << "borderSettings=" << borderSettings.toString();
     if (backgroundBlurRadius > 0) reason << " backgroundBlurRadius=" << backgroundBlurRadius;
     if (blurRegions.size() > 0) reason << " blurRegions.size()=" << blurRegions.size();
     if (contentDirty) reason << " contentDirty";
@@ -410,7 +415,9 @@
     if (forceUpdate || requested.what & layer_state_t::eShadowRadiusChanged) {
         shadowSettings.length = requested.shadowRadius;
     }
-
+    if (forceUpdate || requested.what & layer_state_t::eBorderSettingsChanged) {
+        borderSettings = requested.borderSettings;
+    }
     if (forceUpdate || requested.what & layer_state_t::eFrameRateSelectionPriority) {
         frameRateSelectionPriority = requested.frameRateSelectionPriority;
     }
@@ -508,9 +515,9 @@
                 (layer_state_t::eBufferChanged | layer_state_t::eDataspaceChanged |
                  layer_state_t::eApiChanged | layer_state_t::eShadowRadiusChanged |
                  layer_state_t::eBlurRegionsChanged | layer_state_t::eStretchChanged |
-                 layer_state_t::eEdgeExtensionChanged)) {
+                 layer_state_t::eEdgeExtensionChanged | layer_state_t::eBorderSettingsChanged)) {
         forceClientComposition = shadowSettings.length > 0 || stretchEffect.hasEffect() ||
-                edgeExtensionEffect.hasEffect();
+                edgeExtensionEffect.hasEffect() || borderSettings.strokeWidth > 0;
     }
 
     if (forceUpdate ||
diff --git a/services/surfaceflinger/FrontEnd/LayerSnapshot.h b/services/surfaceflinger/FrontEnd/LayerSnapshot.h
index 69120bd..eca9718 100644
--- a/services/surfaceflinger/FrontEnd/LayerSnapshot.h
+++ b/services/surfaceflinger/FrontEnd/LayerSnapshot.h
@@ -149,6 +149,7 @@
     bool hasBlur() const;
     bool hasBufferOrSidebandStream() const;
     bool hasEffect() const;
+    bool hasOutline() const;
     bool hasSomethingToDraw() const;
     bool isContentOpaque() const;
     bool isHiddenByPolicy() const;
diff --git a/services/surfaceflinger/FrontEnd/LayerSnapshotBuilder.cpp b/services/surfaceflinger/FrontEnd/LayerSnapshotBuilder.cpp
index 28a6031..91b54af 100644
--- a/services/surfaceflinger/FrontEnd/LayerSnapshotBuilder.cpp
+++ b/services/surfaceflinger/FrontEnd/LayerSnapshotBuilder.cpp
@@ -939,6 +939,18 @@
     }
 
     if (forceUpdate ||
+        snapshot.clientChanges &
+                (layer_state_t::eBorderSettingsChanged | layer_state_t::eAlphaChanged)) {
+        snapshot.borderSettings = requested.borderSettings;
+
+        // Multiply outline alpha by snapshot alpha.
+        uint32_t c = static_cast<uint32_t>(snapshot.borderSettings.color);
+        float alpha = snapshot.alpha * (c >> 24) / 255.0f;
+        uint32_t a = static_cast<uint32_t>(alpha * 255 + 0.5f);
+        snapshot.borderSettings.color = static_cast<int32_t>((c & ~0xff000000) | (a << 24));
+    }
+
+    if (forceUpdate ||
         snapshot.changes.any(RequestedLayerState::Changes::Geometry |
                              RequestedLayerState::Changes::Input)) {
         updateInput(snapshot, requested, parentSnapshot, path, args);
@@ -946,7 +958,9 @@
 
     // computed snapshot properties
     snapshot.forceClientComposition = snapshot.shadowSettings.length > 0 ||
-            snapshot.stretchEffect.hasEffect() || snapshot.edgeExtensionEffect.hasEffect();
+            snapshot.stretchEffect.hasEffect() || snapshot.edgeExtensionEffect.hasEffect() ||
+            snapshot.borderSettings.strokeWidth > 0;
+
     snapshot.contentOpaque = snapshot.isContentOpaque();
     snapshot.isOpaque = snapshot.contentOpaque && !snapshot.roundedCorner.hasRoundedCorners() &&
             snapshot.color.a == 1.f;
diff --git a/services/surfaceflinger/LayerFE.cpp b/services/surfaceflinger/LayerFE.cpp
index 5e076bd..3cd432c 100644
--- a/services/surfaceflinger/LayerFE.cpp
+++ b/services/surfaceflinger/LayerFE.cpp
@@ -113,6 +113,8 @@
     // set the shadow for the layer if needed
     prepareShadowClientComposition(*layerSettings, targetSettings.viewport);
 
+    layerSettings->borderSettings = mSnapshot->borderSettings;
+
     return layerSettings;
 }
 
@@ -120,6 +122,7 @@
         compositionengine::LayerFE::ClientCompositionTargetSettings& targetSettings) const {
     SFTRACE_CALL();
     compositionengine::LayerFE::LayerSettings layerSettings;
+    layerSettings.geometry.originalBounds = mSnapshot->geomLayerBounds;
     layerSettings.geometry.boundaries =
             reduce(mSnapshot->geomLayerBounds, mSnapshot->transparentRegionHint);
     layerSettings.geometry.positionTransform = mSnapshot->geomLayerTransform.asMatrix4();
@@ -205,7 +208,7 @@
     if (targetSettings.realContentIsVisible && fillsColor()) {
         // Set color for color fill settings.
         layerSettings.source.solidColor = mSnapshot->color.rgb;
-    } else if (hasBlur() || drawShadows()) {
+    } else if (hasBlur() || drawShadows() || hasOutline()) {
         layerSettings.skipContentDraw = true;
     }
 }
@@ -392,6 +395,10 @@
     return mSnapshot->backgroundBlurRadius > 0 || mSnapshot->blurRegions.size() > 0;
 }
 
+bool LayerFE::hasOutline() const {
+    return mSnapshot->borderSettings.strokeWidth > 0;
+}
+
 bool LayerFE::drawShadows() const {
     return mSnapshot->shadowSettings.length > 0.f &&
             (mSnapshot->shadowSettings.ambientColor.a > 0 ||
diff --git a/services/surfaceflinger/LayerFE.h b/services/surfaceflinger/LayerFE.h
index b89b6b4..b897a90 100644
--- a/services/surfaceflinger/LayerFE.h
+++ b/services/surfaceflinger/LayerFE.h
@@ -83,12 +83,13 @@
             compositionengine::LayerFE::LayerSettings&,
             compositionengine::LayerFE::ClientCompositionTargetSettings&) const;
 
-    bool hasEffect() const { return fillsColor() || drawShadows() || hasBlur(); }
+    bool hasEffect() const { return fillsColor() || drawShadows() || hasBlur() || hasOutline(); }
     bool hasBufferOrSidebandStream() const;
 
     bool fillsColor() const;
     bool hasBlur() const;
     bool drawShadows() const;
+    bool hasOutline() const;
 
     const sp<GraphicBuffer> getBuffer() const;
 
diff --git a/services/surfaceflinger/common/Android.bp b/services/surfaceflinger/common/Android.bp
index 13f6577..c68513e 100644
--- a/services/surfaceflinger/common/Android.bp
+++ b/services/surfaceflinger/common/Android.bp
@@ -22,6 +22,7 @@
     ],
     static_libs: [
         "librenderengine_includes",
+        "libgui_window_info_static",
     ],
     srcs: [
         "FlagManager.cpp",
diff --git a/services/surfaceflinger/tests/Android.bp b/services/surfaceflinger/tests/Android.bp
index b5f7a74..37f3aa7 100644
--- a/services/surfaceflinger/tests/Android.bp
+++ b/services/surfaceflinger/tests/Android.bp
@@ -63,7 +63,10 @@
         "VirtualDisplay_test.cpp",
         "WindowInfosListener_test.cpp",
     ],
-    data: ["SurfaceFlinger_test.filter"],
+    data: [
+        "SurfaceFlinger_test.filter",
+        "testdata/*",
+    ],
     static_libs: [
         "android.hardware.graphics.composer@2.1",
         "libsurfaceflinger_common",
@@ -76,6 +79,7 @@
         "libcutils",
         "libEGL",
         "libGLESv2",
+        "libjnigraphics",
         "libgui",
         "liblog",
         "libnativewindow",
@@ -83,6 +87,7 @@
         "libui",
         "libutils",
         "server_configurable_flags",
+        "libc++",
     ],
     header_libs: [
         "libnativewindow_headers",
diff --git a/services/surfaceflinger/tests/AndroidTest.xml b/services/surfaceflinger/tests/AndroidTest.xml
index ad43cdc..b199ddb 100644
--- a/services/surfaceflinger/tests/AndroidTest.xml
+++ b/services/surfaceflinger/tests/AndroidTest.xml
@@ -14,6 +14,11 @@
      limitations under the License.
 -->
 <configuration description="Config for SurfaceFlinger_test">
+    <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+        <option name="throw-if-cmd-fail" value="true" />
+        <option name="run-command" value="mkdir -p /data/local/tmp/SurfaceFlinger_test_screenshots" />
+        <option name="teardown-command" value="rm -fr /data/local/tmp/SurfaceFlinger_test_screenshots"/>
+    </target_preparer>
     <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
         <option name="cleanup" value="true" />
         <option name="push" value="SurfaceFlinger_test->/data/local/tmp/SurfaceFlinger_test" />
@@ -27,4 +32,8 @@
         <option name="native-test-device-path" value="/data/local/tmp" />
         <option name="module-name" value="SurfaceFlinger_test" />
     </test>
-</configuration>
+    <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
+        <option name = "pull-pattern-keys" value = ".*png" />
+        <option name = "directory-keys" value = "/data/local/tmp/SurfaceFlinger_test_screenshots" />
+    </metrics_collector>
+</configuration>
\ No newline at end of file
diff --git a/services/surfaceflinger/tests/LayerTypeAndRenderTypeTransaction_test.cpp b/services/surfaceflinger/tests/LayerTypeAndRenderTypeTransaction_test.cpp
index 151611c..ada9862 100644
--- a/services/surfaceflinger/tests/LayerTypeAndRenderTypeTransaction_test.cpp
+++ b/services/surfaceflinger/tests/LayerTypeAndRenderTypeTransaction_test.cpp
@@ -662,6 +662,93 @@
     }
 }
 
+TEST_P(LayerTypeAndRenderTypeTransactionTest, SetBorderSettings) {
+    sp<SurfaceControl> parent;
+    sp<SurfaceControl> child;
+    const uint32_t size = 64;
+    const uint32_t parentSize = size * 3;
+    ASSERT_NO_FATAL_FAILURE(parent = createLayer("parent", parentSize, parentSize));
+    ASSERT_NO_FATAL_FAILURE(fillLayerColor(parent, Color::RED, parentSize, parentSize));
+    ASSERT_NO_FATAL_FAILURE(child = createLayer("child", size, size));
+    ASSERT_NO_FATAL_FAILURE(fillLayerColor(child, Color::GREEN, size, size));
+
+    gui::BorderSettings outline;
+    outline.strokeWidth = 3;
+    outline.color = 0xff0000ff;
+    Transaction()
+            .setCrop(parent, Rect(0, 0, parentSize, parentSize))
+            .reparent(child, parent)
+            .setPosition(child, size, size)
+            .setCornerRadius(child, 20.0f)
+            .setBorderSettings(child, outline)
+            .apply(true);
+
+    {
+        auto shot = getScreenCapture();
+
+        shot->expectBufferMatchesImageFromFile(Rect(0, 0, parentSize, parentSize),
+                                               "testdata/SetBorderSettings_Opaque.png");
+    }
+
+    {
+        Transaction().setAlpha(child, 0.5f).apply(true);
+        auto shot = getScreenCapture();
+
+        shot->expectBufferMatchesImageFromFile(Rect(0, 0, parentSize, parentSize),
+                                               "testdata/SetBorderSettings_HalfAlpha.png");
+    }
+
+    {
+        Transaction().setAlpha(child, 0.0f).apply(true);
+
+        auto shot = getScreenCapture();
+
+        shot->expectBufferMatchesImageFromFile(Rect(0, 0, parentSize, parentSize),
+                                               "testdata/SetBorderSettings_ZeroAlpha.png");
+    }
+
+    {
+        Transaction()
+                .setAlpha(child, 1.0f)
+                .setCrop(parent, Rect(0, 0, parentSize / 2, parentSize))
+                .apply(true);
+
+        auto shot = getScreenCapture();
+
+        shot->expectBufferMatchesImageFromFile(Rect(0, 0, parentSize, parentSize),
+                                               "testdata/SetBorderSettings_Cropped.png");
+    }
+
+    {
+        outline.color = 0xff0000ff;
+        outline.strokeWidth = 1;
+        Transaction()
+                .setCrop(parent, Rect(0, 0, parentSize, parentSize))
+                .setBorderSettings(child, outline)
+                .apply(true);
+
+        auto shot = getScreenCapture();
+
+        shot->expectBufferMatchesImageFromFile(Rect(0, 0, parentSize, parentSize),
+                                               "testdata/SetBorderSettings_StrokeWidth1.png");
+    }
+
+    {
+        outline.color = 0x440000ff;
+        outline.strokeWidth = 3;
+        Transaction()
+                .setCrop(parent, Rect(0, 0, parentSize, parentSize))
+                .setBorderSettings(child, outline)
+                .apply(true);
+
+        auto shot = getScreenCapture();
+
+        shot->expectBufferMatchesImageFromFile(Rect(0, 0, parentSize, parentSize),
+                                               "testdata/"
+                                               "SetBorderSettings_StrokeColorWithAlpha.png");
+    }
+}
+
 TEST_P(LayerTypeAndRenderTypeTransactionTest, SetBackgroundBlurRadiusSimple) {
     if (!deviceSupportsBlurs()) GTEST_SKIP();
     if (!deviceUsesSkiaRenderEngine()) GTEST_SKIP();
diff --git a/services/surfaceflinger/tests/common/LayerLifecycleManagerHelper.h b/services/surfaceflinger/tests/common/LayerLifecycleManagerHelper.h
index 82390ac..1bee27b 100644
--- a/services/surfaceflinger/tests/common/LayerLifecycleManagerHelper.h
+++ b/services/surfaceflinger/tests/common/LayerLifecycleManagerHelper.h
@@ -504,6 +504,17 @@
         mLifecycleManager.applyTransactions(transactions);
     }
 
+    void setBorderSettings(uint32_t id, gui::BorderSettings settings) {
+        std::vector<QueuedTransactionState> transactions;
+        transactions.emplace_back();
+        transactions.back().states.push_back({});
+
+        transactions.back().states.front().state.what = layer_state_t::eBorderSettingsChanged;
+        transactions.back().states.front().layerId = id;
+        transactions.back().states.front().state.borderSettings = settings;
+        mLifecycleManager.applyTransactions(transactions);
+    }
+
     void setTrustedOverlay(uint32_t id, gui::TrustedOverlay trustedOverlay) {
         std::vector<QueuedTransactionState> transactions;
         transactions.emplace_back();
diff --git a/services/surfaceflinger/tests/testdata/SetBorderSettings_Cropped.png b/services/surfaceflinger/tests/testdata/SetBorderSettings_Cropped.png
new file mode 100644
index 0000000..b52d517
--- /dev/null
+++ b/services/surfaceflinger/tests/testdata/SetBorderSettings_Cropped.png
Binary files differ
diff --git a/services/surfaceflinger/tests/testdata/SetBorderSettings_HalfAlpha.png b/services/surfaceflinger/tests/testdata/SetBorderSettings_HalfAlpha.png
new file mode 100644
index 0000000..e1ab54b
--- /dev/null
+++ b/services/surfaceflinger/tests/testdata/SetBorderSettings_HalfAlpha.png
Binary files differ
diff --git a/services/surfaceflinger/tests/testdata/SetBorderSettings_Opaque.png b/services/surfaceflinger/tests/testdata/SetBorderSettings_Opaque.png
new file mode 100644
index 0000000..bbaf0af
--- /dev/null
+++ b/services/surfaceflinger/tests/testdata/SetBorderSettings_Opaque.png
Binary files differ
diff --git a/services/surfaceflinger/tests/testdata/SetBorderSettings_StrokeColorWithAlpha.png b/services/surfaceflinger/tests/testdata/SetBorderSettings_StrokeColorWithAlpha.png
new file mode 100644
index 0000000..0fe2ed8
--- /dev/null
+++ b/services/surfaceflinger/tests/testdata/SetBorderSettings_StrokeColorWithAlpha.png
Binary files differ
diff --git a/services/surfaceflinger/tests/testdata/SetBorderSettings_StrokeWidth1.png b/services/surfaceflinger/tests/testdata/SetBorderSettings_StrokeWidth1.png
new file mode 100644
index 0000000..3ee5ac6
--- /dev/null
+++ b/services/surfaceflinger/tests/testdata/SetBorderSettings_StrokeWidth1.png
Binary files differ
diff --git a/services/surfaceflinger/tests/testdata/SetBorderSettings_ZeroAlpha.png b/services/surfaceflinger/tests/testdata/SetBorderSettings_ZeroAlpha.png
new file mode 100644
index 0000000..e5e8850
--- /dev/null
+++ b/services/surfaceflinger/tests/testdata/SetBorderSettings_ZeroAlpha.png
Binary files differ
diff --git a/services/surfaceflinger/tests/unittests/LayerSnapshotTest.cpp b/services/surfaceflinger/tests/unittests/LayerSnapshotTest.cpp
index 07356b9..d045eb8 100644
--- a/services/surfaceflinger/tests/unittests/LayerSnapshotTest.cpp
+++ b/services/surfaceflinger/tests/unittests/LayerSnapshotTest.cpp
@@ -1546,6 +1546,14 @@
     EXPECT_EQ(getSnapshot(1)->shadowSettings.length, SHADOW_RADIUS);
 }
 
+TEST_F(LayerSnapshotTest, setBorderSettings) {
+    gui::BorderSettings settings;
+    settings.strokeWidth = 5;
+    setBorderSettings(1, settings);
+    UPDATE_AND_VERIFY(mSnapshotBuilder, STARTING_ZORDER);
+    EXPECT_EQ(getSnapshot(1)->borderSettings.strokeWidth, settings.strokeWidth);
+}
+
 TEST_F(LayerSnapshotTest, setTrustedOverlayForNonVisibleInput) {
     hideLayer(1);
     setTrustedOverlay(1, gui::TrustedOverlay::ENABLED);
diff --git a/services/surfaceflinger/tests/utils/ScreenshotUtils.h b/services/surfaceflinger/tests/utils/ScreenshotUtils.h
index 0bedcd1..02c3ecd 100644
--- a/services/surfaceflinger/tests/utils/ScreenshotUtils.h
+++ b/services/surfaceflinger/tests/utils/ScreenshotUtils.h
@@ -15,15 +15,23 @@
  */
 #pragma once
 
+#include <android-base/file.h>
+#include <android/bitmap.h>
+#include <android/data_space.h>
+#include <android/imagedecoder.h>
 #include <gui/AidlUtil.h>
 #include <gui/SyncScreenCaptureListener.h>
 #include <private/gui/ComposerServiceAIDL.h>
 #include <ui/FenceResult.h>
+#include <ui/PixelFormat.h>
 #include <ui/Rect.h>
 #include <utils/String8.h>
 #include <functional>
 #include "TransactionUtils.h"
 
+#include <filesystem>
+#include <fstream>
+
 namespace android {
 
 using gui::aidl_utils::statusTFromBinderStatus;
@@ -174,6 +182,146 @@
         }
     }
 
+    static void writePng(const std::filesystem::path& path, const void* pixels, uint32_t width,
+                         uint32_t height, uint32_t stride) {
+        AndroidBitmapInfo info{
+                .width = width,
+                .height = height,
+                .stride = stride,
+                .format = ANDROID_BITMAP_FORMAT_RGBA_8888,
+                .flags = ANDROID_BITMAP_FLAGS_ALPHA_OPAQUE,
+        };
+
+        std::ofstream file(path, std::ios::binary);
+        ASSERT_TRUE(file.is_open());
+
+        auto writeFunc = [](void* filePtr, const void* data, size_t size) -> bool {
+            auto file = reinterpret_cast<std::ofstream*>(filePtr);
+            file->write(reinterpret_cast<const char*>(data), size);
+            return file->good();
+        };
+
+        int compressResult = AndroidBitmap_compress(&info, ADATASPACE_SRGB, pixels,
+                                                    ANDROID_BITMAP_COMPRESS_FORMAT_PNG,
+                                                    /*(ignored) quality=*/100, &file, writeFunc);
+        ASSERT_EQ(compressResult, ANDROID_BITMAP_RESULT_SUCCESS);
+        file.close();
+    }
+
+    static void readImage(const std::filesystem::path& filename, std::vector<uint8_t>& outBytes,
+                          int& outWidth, int& outHeight) {
+        std::ifstream file(filename, std::ios::binary | std::ios::ate);
+        ASSERT_TRUE(file.is_open()) << "Failed to open " << filename;
+
+        size_t fileSize = file.tellg();
+        file.seekg(0, std::ios::beg);
+        std::vector<char> fileData(fileSize);
+        file.read(fileData.data(), fileSize);
+        file.close();
+
+        AImageDecoder* decoder = nullptr;
+        int createResult = AImageDecoder_createFromBuffer(fileData.data(), fileSize, &decoder);
+
+        ASSERT_EQ(createResult, ANDROID_IMAGE_DECODER_SUCCESS);
+
+        const AImageDecoderHeaderInfo* headerInfo = AImageDecoder_getHeaderInfo(decoder);
+        outWidth = AImageDecoderHeaderInfo_getWidth(headerInfo);
+        outHeight = AImageDecoderHeaderInfo_getHeight(headerInfo);
+        int32_t format = AImageDecoderHeaderInfo_getAndroidBitmapFormat(headerInfo);
+        ASSERT_EQ(format, ANDROID_BITMAP_FORMAT_RGBA_8888);
+
+        size_t stride = outWidth * 4; // Assuming RGBA format
+        size_t bufferSize = stride * outHeight;
+
+        outBytes.resize(bufferSize);
+        int decodeResult = AImageDecoder_decodeImage(decoder, outBytes.data(), stride, bufferSize);
+        ASSERT_EQ(decodeResult, ANDROID_IMAGE_DECODER_SUCCESS);
+        AImageDecoder_delete(decoder);
+    }
+
+    static void writeGraphicBufferToPng(const std::string& path, const sp<GraphicBuffer>& buffer) {
+        base::unique_fd fd{open(path.c_str(), O_WRONLY | O_CREAT, S_IWUSR)};
+        ASSERT_GE(fd.get(), 0);
+
+        void* pixels = nullptr;
+        int32_t stride = 0;
+        auto lockStatus = buffer->lock(GRALLOC_USAGE_SW_READ_OFTEN, &pixels,
+                                       nullptr /*outBytesPerPixel*/, &stride);
+        ASSERT_GE(lockStatus, 0);
+
+        writePng(path, pixels, buffer->getWidth(), buffer->getHeight(), stride);
+
+        auto unlockStatus = buffer->unlock();
+        ASSERT_GE(unlockStatus, 0);
+    }
+
+    // Tries to read an image from executable directory
+    // If the test fails, the screenshot is written to $TMPDIR
+    void expectBufferMatchesImageFromFile(const Rect& rect,
+                                          const std::filesystem::path& pathRelativeToExeDir) {
+        ASSERT_NE(nullptr, mOutBuffer);
+        ASSERT_EQ(HAL_PIXEL_FORMAT_RGBA_8888, mOutBuffer->getPixelFormat());
+
+        int bufferWidth = int32_t(mOutBuffer->getWidth());
+        int bufferHeight = int32_t(mOutBuffer->getHeight());
+        int bufferStride = mOutBuffer->getStride() * 4;
+
+        std::vector<uint8_t> imagePixels;
+        int imageWidth;
+        int imageHeight;
+        readImage(android::base::GetExecutableDirectory() / pathRelativeToExeDir, imagePixels,
+                  imageWidth, imageHeight);
+        int imageStride = 4 * imageWidth;
+
+        ASSERT_TRUE(rect.isValid());
+
+        ASSERT_GE(rect.left, 0);
+        ASSERT_GE(rect.bottom, 0);
+
+        ASSERT_LE(rect.right, bufferWidth);
+        ASSERT_LE(rect.bottom, bufferHeight);
+
+        ASSERT_LE(rect.right, imageWidth);
+        ASSERT_LE(rect.bottom, imageHeight);
+
+        int tolerance = 4; // arbitrary
+        for (int32_t y = rect.top; y < rect.bottom; y++) {
+            for (int32_t x = rect.left; x < rect.right; x++) {
+                const uint8_t* bufferPixel = mPixels + y * bufferStride + x * 4;
+                const uint8_t* imagePixel =
+                        imagePixels.data() + (y - rect.top) * imageStride + (x - rect.left) * 4;
+
+                int dr = bufferPixel[0] - imagePixel[0];
+                int dg = bufferPixel[1] - imagePixel[1];
+                int db = bufferPixel[2] - imagePixel[2];
+                int da = bufferPixel[3] - imagePixel[3];
+                int dist = std::abs(dr) + std::abs(dg) + std::abs(db) + std::abs(da);
+
+                bool pixelMatches = dist < tolerance;
+
+                if (!pixelMatches) {
+                    std::filesystem::path outFilename = pathRelativeToExeDir.filename();
+                    outFilename.replace_extension();
+                    outFilename += "_actual.png";
+                    std::filesystem::path outPath = std::filesystem::temp_directory_path() /
+                            "SurfaceFlinger_test_screenshots" / outFilename;
+                    writeGraphicBufferToPng(outPath, mOutBuffer);
+
+                    ASSERT_TRUE(pixelMatches)
+                            << String8::format("pixel @ (%3d, %3d): "
+                                               "expected [%3d, %3d, %3d, %3d], got [%3d, %3d, %3d, "
+                                               "%3d], "
+                                               "wrote screenshot to '%s'",
+                                               x, y, imagePixel[0], imagePixel[1], imagePixel[2],
+                                               imagePixel[3], bufferPixel[0], bufferPixel[1],
+                                               bufferPixel[2], bufferPixel[3], outPath.c_str())
+                                       .c_str();
+                    return;
+                }
+            }
+        }
+    }
+
     Color getPixelColor(uint32_t x, uint32_t y) {
         if (!mOutBuffer || mOutBuffer->getPixelFormat() != HAL_PIXEL_FORMAT_RGBA_8888) {
             return {0, 0, 0, 0};