Skip to content

[17] Fix SurfaceFlinger virtual display, UDFPS and FPS regressions - #8

Open
WegTeam wants to merge 38 commits into
ProjectInfinity-X:17from
WegTeam:fix/native-17-regressions
Open

WegTeam wants to merge 38 commits into
ProjectInfinity-X:17from
WegTeam:fix/native-17-regressions

Conversation

@WegTeam

@WegTeam WegTeam commented Oct 18, 2026

Copy link
Copy Markdown

Targets 17, based on b52ea442520793475c7f7b43e653c0806968a5e8.

The legacy virtual-display path drops renderer usage requests and cancels recomposed frames when HWC returns Fence::NO_FENCE. UDFPS traversal also exits before checking higher layers, and frame-rate overrides outside a small whitelist silently become 90 Hz.

  • Read consumer usage through the 64-bit producer API and keep it in uint64_t, including vendor bits above bit 31. Combine it with renderer requests and preserve the existing secure-encoder protection policy. Handle usage-query failure without reading an uninitialized value.
  • Queue recomposed frames with NO_FENCE, which represents completed work. Continue canceling unchanged frames to avoid a composition loop.
  • Continue scanning above the UDFPS touch layer so later sideband/protected layers can disable background composition and higher blur/sampling requests determine the correct boundary.
  • Convert positive FRAME_RATE_CATEGORY_MIN/HIGH values with Fps::fromValue, preserving values such as 24, 72 and 165 Hz. Reject nonpositive definitions at compile time.

Added nine Android unit tests: five for legacy display usage and frame delivery, three for layers above UDFPS, and one for configured frame-rate constants. The new test source is included by the existing *.cpp unit-test source glob.

Validation:

  • A focused C++20 host harness compiled and executed the actual changed method bodies and FPS definitions with host doubles: 19 behavioral checks passed in each of three configurations (defaults, 72/165, 24/144).
  • The same harness against the original 17 sources reproduced 8, 10 and 9 failed checks respectively.
  • Changed C++ lines were formatted using the repository's clang-format style; git diff --check passed.

The host harness does not compile or link full SurfaceFlinger, Android Binder/BufferQueue, or HWC implementations. The added Android tests and full Soong build were not run in this standalone Windows checkout. Device-side validation is still required (libcompositionengine_test and libsurfaceflinger_unittest, plus real virtual-display/UDFPS testing). No claim is made that all bugs in frameworks_native have been eliminated.

kdrag0n and others added 30 commits September 28, 2026 18:47
Unlinking death recipients (from linkToDeath) is a normal part of Binder
operation, and doing it when a BpBinder's refcount has reached 0 is not
much different.

This log message is constantly spamming when swiping as part of a back
navigation gesture:

04-05 22:29:41.402   655  3888 I BpBinder: onLastStrongRef automatically unlinking death recipients: <uncached descriptor>
04-05 22:29:41.413   655  3888 I BpBinder: onLastStrongRef automatically unlinking death recipients: <uncached descriptor>
04-05 22:29:41.424   655  3888 I BpBinder: onLastStrongRef automatically unlinking death recipients: <uncached descriptor>
04-05 22:29:41.435   655   681 I BpBinder: onLastStrongRef automatically unlinking death recipients: <uncached descriptor>
04-05 22:29:41.447   655  3888 I BpBinder: onLastStrongRef automatically unlinking death recipients: <uncached descriptor>

Logging is relatively expensive on Android and the log spam isn't
useful, so suppress this log message by setting it to the verbose log
level.

Change-Id: I774d0c59ca6f70a4e2ed33b9fac3fb5b86d8ff0a
Signed-off-by: Pranav Vashi <neobuddy89@gmail.com>
libwfdnative compiled for 14 QPR1 depends on this symbol.

Change-Id: I7bc5ee3fb7760a9f470023cd0daa215b2e194dc1
Signed-off-by: Dmitrii <bankersenator@gmail.com>
Layer dataspace is initialized as unknown by default, unless
overriden by the buffer source. We don't require expensive
rendering for color conversion when the dataspace is unknown.

Change-Id: I079c520f63a65c77ba3162664656e607eafff991
Signed-off-by: Dmitrii <bankersenator@gmail.com>
partially reverts: 1fd9bfc

Signed-off-by: minaripenguin <minaripenguin@users.noreply.github.com>
Signed-off-by: Pranav Vashi <neobuddy89@gmail.com>
Signed-off-by: Dmitrii <bankersenator@gmail.com>
CRs-Fixed: 3785154
Signed-off-by: Pranav Vashi <neobuddy89@gmail.com>
Signed-off-by: Dmitrii <bankersenator@gmail.com>
[Pulkit077]: Adapt to A14 QPR1
[SamarV-121]: Adapt to A14 QPR3

Taken from CLO (QSSI 13). Some Qualcomm devices can still
benefit from disabling backpressure propagation by setting:

debug.sf.disable_backpressure=1

Signed-off-by: Pulkit077 <pulkitagarwal2k1@gmail.com>
Signed-off-by: Pranav Vashi <neobuddy89@gmail.com>
Signed-off-by: cjh1249131356 <cjh1249131356@gmail.com>
Change-Id: I401c3082455ff42b15c71f963cb1a4aeee44b1a3
Signed-off-by: rmp22 <195054967+rmp22@users.noreply.github.com>
Change-Id: I5d1eef7a493451afc9f75df274ec4aed8e8df87c
Signed-off-by: rmp22 <195054967+rmp22@users.noreply.github.com>
Change-Id: I28c3508c7722461c853e4fcff4d3280c6bbcf670
No layers definitely doesn't mean new changes happened; so there's no
need to enforce the maximum refresh rate. Maintain current refresh rate
instead.

[gotenksIN: adapt to 14]

Change-Id: I31c9edc1a8ff3ff8340afd2f731df66755ffc9e0
Co-authored-by: Omkar Chandorkar <gotenksIN@aospa.co>
Signed-off-by: Omkar Chandorkar <gotenksIN@aospa.co>
Signed-off-by: Dmitrii <bankersenator@gmail.com>
presentTime is missing when the user isn't touching the screen. To make
the estimation more accurate regardless of the user's input, remove this
check.

Change-Id: I27c55989b7f3d5ef5efe6b0d709135122bb6ba87
Signed-off-by: Juhyung Park <qkrwngud825@gmail.com>
Signed-off-by: Dmitrii <bankersenator@gmail.com>
30 frames for 300ms is quite enough for approximated calculation.

As an added bonus, the device reacts quicker when a higher refresh rate
should be used.

Change-Id: Id8930ab7343cc4e974b78929615e6efdd1c2b27e
Signed-off-by: Juhyung Park <qkrwngud825@gmail.com>
Signed-off-by: Dmitrii <bankersenator@gmail.com>
Change-Id: I28a2e93d0718e5950948b87a6c6abf4341753552
Signed-off-by: Pranav Vashi <neobuddy89@gmail.com>
- From Xiaomi Android 13 Sources

Change-Id: I818ff20b8eee8a172cfedfd39d67a9533e25b590
Signed-off-by: Pranav Vashi <neobuddy89@gmail.com>
This improves app launch performance by 0.13%.

http://go/art-benchmark?p=BootImageProfileId:36062

Test: presubmit
Change-Id: If438da2f0a814e951687b50e46187bbf450a7629
Change-Id: Ibd26e301366f8693a8f775f7ab55bf37159e7f09
Adjusted buffer caching logic to prevent flickering by ensuring
buffers with frame numbers above 4 are not cached.

Bug: 438100090
Test: kill activity and monkey 60 minutes no flicker.

Change-Id: I890b18c10a2f5fca454dce0bf36fa0452b4f9e00
Signed-off-by: Pranav Vashi <neobuddy89@gmail.com>
Optimization from Nothing

Change-Id: Iadde94964816fd32602debf59e1d709df3bf5961
Signed-off-by: rmp22 <195054967+rmp22@users.noreply.github.com>
In streaming video case, video decoder needs to change buffer
color space dynamically according to video stream real-time
parameters.

CRs-Fixed: 3155775
Change-Id: I21590ee41708f5cb1ed45cb5a7f5c00f1c129bb0
Signed-off-by: tejas101k <tejassingh649@rediffmail.com>
Signed-off-by: Pranav Vashi <neobuddy89@gmail.com>
During the application startup, we find that the Servicemanager thread is frequently preempted
due to its low priority, resulting in slow application startup and responsiveness.
We hope to optimize these issues by adjusting the priority to RT(98) of the SM
to avoid time-consuming binder in clients to ServiceManager, and improve application startup and response speed.

Bug: 419835707

Change-Id: I58d6baed9a01b38ec804273063ced7a24d12b842
Signed-off-by: zhanghao56 <zhanghao56@xiaomi.corp-partner.google.com>
Signed-off-by: Pranav Vashi <neobuddy89@gmail.com>
power apps that shows a lot of bitmaps needs more heap memory in order to avoid out of memory errors. The tuning is based on Qualcomm’s approach, where a script checks the display’s framebuffer width (fb_width) and assigns fixed vendor LCD density values. In their setup, a density of 640 usually maps to a 512MB heap growth limit. They combine this with custom Dalvik tuning. On newer Android versions, Qualcomm uses the default AOSP 6GB Dalvik config together with their LCD density–based tuning.

Change-Id: I5bbca25363edffbc87105d2c83295fe8aceac0f7
Signed-off-by: rmp22 <195054967+rmp22@users.noreply.github.com>
Change-Id: Icdfd3512a79d9935d3fdb611c96dc8fd91a5e1c5
Signed-off-by: DennySPB <dennyspb@gmail.com>
Signed-off-by: Pranav Vashi <neobuddy89@gmail.com>
Signed-off-by: Dmitrii <bankersenator@gmail.com>
Change-Id: I75f596ed1b333e32bd25fdbc8ff3275caa7d746c
Signed-off-by: Ghosuto <clash.raja10@gmail.com>
Signed-off-by: Pranav Vashi <neobuddy89@gmail.com>
Signed-off-by: Ghosuto <clash.raja10@gmail.com>
Signed-off-by: Pranav Vashi <neobuddy89@gmail.com>
Based on 39e8590263123212777ed9cfb8b9b72f2ca4a94c

Signed-off-by: Ghosuto <clash.raja10@gmail.com>
Signed-off-by: Pranav Vashi <neobuddy89@gmail.com>
- getLuts was introduced with aidl composerV3
- Cannot provide a stub implementation via HIDL composer

Change-Id: I694cb7c7ed090f12e75894b54cd64bede5f16c0e
Signed-off-by: Omkar Chandorkar <gotenksIN@aospa.co>
Signed-off-by: Ghosuto <clash.raja10@gmail.com>
Signed-off-by: Pranav Vashi <neobuddy89@gmail.com>
Root Cause:
The crash is coming from getHoveringWindowsLocked() in InputDispatcher.cpp:679. It assumes that if a window is
in both the old and new hover sets for a pointer, the incoming action must be AMOTION_EVENT_ACTION_HOVER_MOVE.
But the log shows an inconsistent stream (HOVER_ENTER even though the pointer is already considered hovering that window),
so the LOG(FATAL) aborts. 2026-02-13 15:35:46.030881 -0500 1820 2141 F InputDispatcher: InputDispatcher.cpp:679] Expected
ACTION_HOVER_MOVE instead of MotionEvent(deviceId=-1, eventTime=313128000000, source=TOUCHSCREEN | STYLUS, displayId=0,
action=HOVER_ENTER, actionButton=0x00000000, flags=0x00000000, metaState=0x00000000, buttonState=0x00000000, classification=NONE,
edgeFlags=0x00000000, xPrecision=1.0, yPrecision=1.0, xCursorPosition=nan, yCursorPosition=nan, pointers=[0: (779.0, 92.0)]),
policyFlags=0x6b000000 deviceId=-1 means it is not a real hardware device.

Resoultion:
If (entry.isSynthesized()) will check that this event is not from a registered real hardware device.
I added it as part of additional condition.

Bug: 489353448

Change-Id: Id3fa5a375c4f3257605b9809f2b4d97c1575e5a1
Signed-off-by: Shailendra Gupta <sgupta96@ford.com>
Change-Id: I82f555cd8731f56948473c42058652fa3931525e
Signed-off-by: Pranav Vashi <neobuddy89@gmail.com>
rmp22 and others added 8 commits September 28, 2026 18:47
client comp cache hit was clearing the expensive rendering hint mid-blur, causing the gpu boost to flap on/off frame to frame during steady state blur like qs held open. keep it asserted while a blur layer is still present.

Change-Id: I3632b4cca7ebb06d21ea1098764bef8de5e78f1c
Signed-off-by: Ghosuto <clash.raja10@gmail.com>
Signed-off-by: Pranav Vashi <neobuddy89@gmail.com>
Increase BINDER_VM_SIZE from 1MB to 4MB for fix RAW processing on Master Mode

Signed-off-by: Pranav Vashi <neobuddy89@gmail.com>
[Pulkit077]: Adapt to A14 QPR1
[SamarV-121]: Adapt to A14 QPR3

Taken from CLO (QSSI 13). Some Qualcomm devices can still
benefit from disabling backpressure propagation by setting:

debug.sf.disable_backpressure=1

Signed-off-by: Pulkit077 <pulkitagarwal2k1@gmail.com>
Signed-off-by: Pranav Vashi <neobuddy89@gmail.com>
Validate BPF map key/value sizes in getBpfMap() before creating
BpfMap objects to avoid abort() in forAll()->abortOnMismatch()
on devices with incompatible kernel BPF structures.

12-07 08:09:55.596  2405  2405 F DEBUG   : Executable: /system/bin/gpuservice
12-07 08:09:55.596  2405  2405 F DEBUG   : Cmdline: /system/bin/gpuservice
12-07 08:09:55.596  2405  2405 F DEBUG   : pid: 1231, tid: 1247, name: gpuservice  >>> /system/bin/gpuservice <<<
12-07 08:09:55.597  2405  2405 F DEBUG   : uid: 1072
12-07 08:09:55.597  2405  2405 F DEBUG   : signal 6 (SIGABRT), code -1 (SI_QUEUE), fault addr --------
12-07 08:09:55.598  2405  2405 F DEBUG   :     x0  0000000000000000  x1  00000000000004df  x2  0000000000000006  x3  0000007149209790
12-07 08:09:55.598  2405  2405 F DEBUG   :     x4  fefefefefefefeff  x5  fefefefefefefeff  x6  fefefefefefefeff  x7  00000071492099a2
12-07 08:09:55.598  2405  2405 F DEBUG   :     x8  00000000000000f0  x9  3194d4d942ab823f  x10 000000ff00000020  x11 0000000000070007
12-07 08:09:55.599  2405  2405 F DEBUG   :     x12 0000000000000018  x13 0000000000000004  x14 0000000000000001  x15 00002007429ff63c
12-07 08:09:55.599  2405  2405 F DEBUG   :     x16 00000073d9f2e970  x17 00000073d9f12780  x18 00000071473bc000  x19 00000000000004cf
12-07 08:09:55.599  2405  2405 F DEBUG   :     x20 00000000000004df  x21 00000000ffffffff  x22 0000000000000005  x23 b4000072193470d8
12-07 08:09:55.600  2405  2405 F DEBUG   :     x24 0000000000003102  x25 00000071492099b8  x26 00000071492099b9  x27 000000000000003c
12-07 08:09:55.600  2405  2405 F DEBUG   :     x28 0000000000000020  x29 0000007149209810
12-07 08:09:55.601  2405  2405 F DEBUG   :     lr  00000073d9eabe7c  sp  0000007149209790  pc  00000073d9eabea0  pst 0000000000000000
12-07 08:09:55.601  2405  2405 F DEBUG   :     esr 0000000000000000
12-07 08:09:55.601  2405  2405 F DEBUG   : 6 total frames
12-07 08:09:55.602  2405  2405 F DEBUG   : backtrace:
12-07 08:09:55.602  2405  2405 F DEBUG   :       #00 pc 0000000000077ea0  /apex/com.android.runtime/lib64/bionic/libc.so (abort+156) (BuildId: cb439518e1cfd288b54911f1b6996d75)
12-07 08:09:55.602  2405  2405 F DEBUG   :       ProjectInfinity-X#1 pc 00000000000069c0  /system/lib64/libgpuwork.so (android::bpf::BpfMapRO<android::gpuwork::GpuIdUid, android::gpuwork::UidTrackingInfo>::abortOnMismatch(bool) const+468) (BuildId: 320b366facaeb5a87fdd55a8f5628cb1)
12-07 08:09:55.603  2405  2405 F DEBUG   :       VoltageOS#2 pc 00000000000042fc  /system/lib64/libgpuwork.so (android::gpuwork::GpuWork::initialize()+416) (BuildId: 320b366facaeb5a87fdd55a8f5628cb1)
12-07 08:09:55.603  2405  2405 F DEBUG   :       ProjectInfinity-X#3 pc 0000000000071248  /system/bin/gpuservice (void* std::__1::__thread_proxy[abi:nn210000]<std::__1::tuple<std::__1::unique_ptr<std::__1::__thread_struct, std::__1::default_delete<std::__1::__thread_struct>>, android::GpuService::GpuService()::$_1>>(void*)+44) (BuildId: 44576eacfae9e714b953c1348c1f0b15)
12-07 08:09:55.604  2405  2405 F DEBUG   :       ProjectInfinity-X#4 pc 0000000000089e80  /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start(void*)+236) (BuildId: cb439518e1cfd288b54911f1b6996d75)
12-07 08:09:55.604  2405  2405 F DEBUG   :       ProjectInfinity-X#5 pc 000000000007b2e4  /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64) (BuildId: cb439518e1cfd288b54911f1b6996d75)

Signed-off-by: Dmitrii <bankersenator@gmail.com>
Signed-off-by: Pranav Vashi <neobuddy89@gmail.com>
Note that CP1A (16 QPR3) and CP21 (17 Beta 3) images no longer have the
enable_layer_command_batching aconfig flag. It looks like the flag was removed and is now
solely determined by Capability::LAYER_LIFECYCLE_BATCH_COMMAND from
AidlComposer::getLayerLifecycleBatchCommand().

CP1A surfaceflinger appears to have a logic bug that makes mEnableLayerCommandBatchingFlag always
false in AidlComposer::AidlComposer after a supposed move to caching capabilities in a data member.
mEnableLayerCommandBatchingFlag appears to be assigned from a capabilities vector that wasn't
initialized yet. There are different log messages when command batching is disabled, e.g. these are
logged when an external display is disconnected:

```
SurfaceFlinger  I  Disconnecting display 4637127956051714701(HAL ID 256)
SurfaceFlinger  E  destroyLayer failed Status(-8, EX_SERVICE_SPECIFIC): '2: '
SurfaceFlinger  E  destroyLayer(256, 504617589390614528) failed: BAD_DISPLAY (2)
SurfaceFlinger  E  destroyLayer failed Status(-8, EX_SERVICE_SPECIFIC): '2: '
SurfaceFlinger  E  destroyLayer(256, 72272025163063296) failed: BAD_DISPLAY (2)
SurfaceFlinger  E  destroyLayer failed Status(-8, EX_SERVICE_SPECIFIC): '2: '
SurfaceFlinger  E  destroyLayer(256, 648732777466454016) failed: BAD_DISPLAY (2)
SurfaceFlinger  E  destroyLayer failed Status(-8, EX_SERVICE_SPECIFIC): '2: '
SurfaceFlinger  E  destroyLayer(256, 792847965542301696) failed: BAD_DISPLAY (2)
SurfaceFlinger  E  destroyLayer failed Status(-8, EX_SERVICE_SPECIFIC): '2: '
SurfaceFlinger  E  destroyLayer(256, 288444807276707840) failed: BAD_DISPLAY (2)
hwc-display  I  [ExternalDisplay] destroyLayer : destroyLayer is done. But display is already disconnected
hwc-display  I  [ExternalDisplay] destroyLayer : destroyLayer is done. But display is already disconnected
hwc-display  I  [ExternalDisplay] destroyLayer : destroyLayer is done. But display is already disconnected
hwc-display  I  [ExternalDisplay] destroyLayer : destroyLayer is done. But display is already disconnected
hwc-display  I  [ExternalDisplay] destroyLayer : destroyLayer is done. But display is already disconnecte
```

The `destroyLayer failed Status(-8,...` lines are coming from the command batching disabled path
in AidlComposer::destroyLayer
(https://github.com/GrapheneOS/platform_frameworks_native/blob/2026032000/services/surfaceflinger/DisplayHardware/AidlComposerHal.cpp#L465)
hwc-display logging is a result of the command batching disabled calling
mAidlComposerClient->destroyLayer.

This bug appears to be fixed in CP21 surfaceflinger going by the log messages.

Signed-off-by: Pranav Vashi <neobuddy89@gmail.com>
…ct if command batching

Work around a bug in Google's Pixel HWC service (android.hardware.composer.hwc3-service.pixel) (or
SurfaceFlinger!) where ExynosExternalDisplay::closeExternalDisplay()
(https://android.googlesource.com/platform/hardware/google/graphics/common/+/refs/heads/main/libhwc2.1/libexternaldisplay/ExynosExternalDisplay.cpp#120)
cleans up fences in mLayers but never erases the mIgnoreLayers vector. Layers moved to mIgnoreLayers
(via ExynosDisplay::checkIgnoreLayers() when plane alpha becomes 0) accumulate as stale pointers
across disconnect/reconnect cycles. On reconnecting an external display, this can cause an MTE crash
via:

- DRM commit fails (ret=-16, EBUSY from drmModeAtomicCommit) in
  ExynosDisplayDrmInterface::deliverWinConfigData()
  (https://android.googlesource.com/platform/hardware/google/graphics/common/+/refs/heads/main/libhwc2.1/libdevice/ExynosDisplay.cpp#2764)
  This is not deterministic, so it won't happen on every external display connect.
- ExynosDisplay::deliverWinConfigData() error path calls ExynosDisplay::printDebugInfos()
- printDebugInfos() iterates mIgnoreLayers, calling ExynosLayer::printLayer() on stale pointers
- printLayer() calls VendorGraphicBufferMeta::init() on the freed layer's buffer handle

This leads to the Pixel HWC service crashing. The HWC's init.rc specifies that when HWC restarts, it
should also restart SurfaceFlinger. system_server sees that SurfaceFlinger dies and soft reboots the
device.

This memory bug can also be reproduced on stock OS CP21.260306.017 (Android 17 Beta 3), although
they don't have MTE enabled for the binary, so there's no crashing involved. The bug can instead be
seen from `adb shell dumpsys SurfaceFlinger` with an external display connected (this is needed for
HWC to dump layers for the external display).  Running that dumpsys command periodically after
disconnecting and reconnecting the external display multiple times will show the
`dump ignore layers` section growing for the external display between each dump.

This bug cannot be reproduced on stock CP1A.260305.018 (16 QPR3) because of a logic bug in
SurfaceFlinger which makes command batching always disabled (see previous commit for details).
Command batching is required to reproduce this stale mIgnoreLayers bug.

With enable_layer_command_batching disabled, SF's layer destruction during disconnect acts as a
second line of defense against the mIgnoreLayers bug, and this is probably unintentional(?). When
HWC2::Display::~Display() runs during SurfaceFlinger::processDisplayRemoved(), it destroys all its
HWC2::Layer objects, and AidlComposer::destroyLayer() makes direct
mAidlComposerClient->destroyLayer() AIDL calls that reach the HAL even after hotplug disconnect (the
AIDL composer client and HAL display object still exist). ExynosDisplay::destroyLayer() then removes
each layer from both mLayers and mIgnoreLayers. So even though
ExynosExternalDisplay::closeExternalDisplay() never erases mIgnoreLayers, the layers still get
cleaned up indirectly through SF's destruction path.

However, when the enable_layer_command_batching flag is enabled (the current default since bp1a,
inherited through bp4a) and the HWC service reports Capability::LAYER_LIFECYCLE_BATCH_COMMAND, this
second line of defense breaks. On Pixels, both of these are true.

- AidlComposer::destroyLayer() batches DESTROY commands in a per-display ComposerClientWriter rather
  than making direct AIDL calls.
- On disconnect,AidlComposer::onHotplugDisconnect() calls AidlComposer::removeDisplay() which erases
  the writer via mWriters.erase(display).
- HWC2::Display::~Display() runs later during SurfaceFlinger::processDisplayRemoved(), and its
  AidlComposer::destroyLayer() calls find no writer and return BAD_DISPLAY.
- The destroyLayer commands never reach the HAL, leaving layers in mIgnoreLayers as stale pointers,
  because Pixel HWC didn't seem to clean them up themselves.

It's unknown at this point if this is a bug with command batching in SurfaceFlinger (i.e., are
vendors expecting SurfaceFlinger to clear the layers for them on external display disconnect) or an
isolated issue with libexynosdisplay.so. libexynosdisplay.so seems to be buggy, because disconnect
should not leave stale layer pointers behind. But SurfaceFlinger batching also changes the effective
contract on disconnect by preventing explicit layer-destroy commands from reaching the HAL at a
point where older/non-batched behavior did allow that cleanup.

Fix this use-after-free / stale mIgnoreLayers issue by explicitly destroying all layers before the
writer is removed, ensuring ExynosDisplay::destroyLayer() in the HWC service removes each layer from
both mLayers and mIgnoreLayers. These layers are meant to be destroyed on hotplug disconnect anyway;
the destruction sequence is outlined with more detail in the code comments. The fix simply moves
destruction earlier in the disconnect sequence so the batched commands are flushed while the writer
still exists. We only use our fix for external displays on Google devices with command batching
enabled.

In SurfaceFlinger::configureLocked(), before HWComposer::onHotplug() removes the writer:

1. CompositionDisplay::clearOutputLayers() releases all OutputLayer shared_ptrs, triggering the
   destructor chain through HWC2::Layer::~Layer() -> HWC2::Layer::onOwningDisplayDestroyed() ->
   AidlComposer::destroyLayer(), which queues batched DESTROY commands in the writer
2. HWComposer::executeCommands() flushes the batched commands to the HAL, where
   ExynosDisplay::destroyLayer() removes each layer from mLayers or mIgnoreLayers and deletes it

The libexynosdisplay.so source that the HWC service uses is in a common source set
(https://android.googlesource.com/platform/hardware/google/graphics/common) compiled into all
SoC-specific builds. This source is no longer updated by Google, so the fix must be done in
SurfaceFlinger.

Note: ExynosExternalDisplay::closeExternalDisplay() sets mPlugState to false on Pixel HWC's DRM
event thread asynchronously before SF receives the hotplug callback. By the time the fix or any SF
code runs, mPlugState is already false. ExynosDisplay::destroyLayer() still performs the full
cleanup (remove from mLayers/mIgnoreLayers, delete layer) unconditionally, then checks mPlugState
and returns HWC2_ERROR_BAD_DISPLAY:

```c++
  // libhwc2.1/libdevice/ExynosDisplay.cpp
  // https://android.googlesource.com/platform/hardware/google/graphics/common/+/refs/heads/main/libhwc2.1/libdevice/ExynosDisplay.cpp#1179
  int32_t ExynosDisplay::destroyLayer(hwc2_layer_t outLayer) {
      // ...
      if (mLayers.remove(layer) < 0) {
          auto it = std::find(mIgnoreLayers.begin(), mIgnoreLayers.end(), layer);
          if (it != end) mIgnoreLayers.erase(it);
      }
      // ...
      delete layer;
      if (mPlugState == false) {
          DISPLAY_LOGI("destroyLayer is done. But display is already disconnected");
          return HWC2_ERROR_BAD_DISPLAY;  // misleading: cleanup already succeeded
      }
      return HWC2_ERROR_NONE;
  }
```

HWC2_ERROR_BAD_DISPLAY (= 2) propagates up as "HalImpl: destroyLayer failed with error: 2" (logged
by HalImpl::destroyLayer() in the HWC service) and "command '...' generated error 2" (logged by
AidlComposer::executeCommands() in SF when processing command results). Both warnings are cosmetic;
the layer has already been fully removed and freed.

An alternative approach would be to add a fallback to a direct AIDL call in
AidlComposer::destroyLayer() when the writer is missing (the else branch at
https://github.com/GrapheneOS/platform_frameworks_native/blob/2026032000/services/surfaceflinger/DisplayHardware/AidlComposerHal.cpp#L475-L477
currently returns BAD_DISPLAY). This would make the batching path resilient to writer removal
without requiring changes in SurfaceFlinger::configureLocked(). However, this would probably be too
broad for a downstream change, and AidlComposer has no display type information to gate the fallback
to external displays only (other than perhaps hardcoding display id of 256, but this ID comes from
Pixel HWC3 itself).

Test: MTE crashes no longer happen after multiple external display connect and disconnect cycles.
`adb shell dumpsys SurfaceFlinger` (with external display connected) after various disconnect and
reconnect repeats doesn't show a growing list of ignore layers for the external display.
Test: atest surfaceflinger_end2end_tests libsurfaceflinger_unittest libcompositionengine_test
Test: atest SurfaceFlinger_test

Signed-off-by: Pranav Vashi <neobuddy89@gmail.com>
Test: atest libsurfaceflinger_unittest
Signed-off-by: Pranav Vashi <neobuddy89@gmail.com>
@tejas101k
tejas101k force-pushed the 17 branch 2 times, most recently from 2d53ef5 to 9a452af Compare November 5, 2026 17:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.