Skip to content

[Android] Fix HCPP frame synchronization when multiple frames run before a Choreographer traversal - #194591

Merged
auto-submit[bot] merged 9 commits into
flutter:masterfrom
gmackall:hcpp-traversal-staging
Oct 21, 2026
Merged

auto-submit[bot] merged 9 commits into
flutter:masterfrom
gmackall:hcpp-traversal-staging

Conversation

@gmackall

@gmackall gmackall commented Oct 16, 2026 •

Copy link
Copy Markdown
Member

Fixes a few related synchronization issues in HCPP when the raster thread gets ahead of the platform thread, or when two platform-thread frame tasks run before Android's Choreographer.doFrame() has drawn the first one. Together, this effectively moves HCPP presentation from mailbox semantics (where a second frame arriving before doFrame() overwrote the pending frame in applyTransactionOnDraw) to FIFO, aligning with core Flutter.

The end result is that scrolling with multiple platform views looks a lot, lot smoother.

  1. Previously, PlatformViewsController2.publishTransaction() appended every raster SurfaceControl.Transaction into a single flat pendingRasterTransactions list, and swapTransactions() on the platform thread drained the entire list at once. Because SubmitFlutterView() posts its platform-thread task asynchronously after submitting the overlay and main SurfaceFrames, the raster thread can start or finish frame N+1 before frame N's platform task runs swapTransactions(). Draining the flat list caused frame N to merge frame N+1's transactions into its onEndFrame(), pairing frame N+1's Flutter surface buffers with frame N's platform view layout (and leaving frame N+1's platform task without its transactions). This adds sealFrameTransactions() and calls it on the raster thread at the end of SubmitFlutterView() so swapTransactions() pops one sealed frame batch at a time.
  2. Similarly, on the platform thread, onEndFrame() calls flutterView.invalidate() and rootSurfaceControl.applyTransactionOnDraw(tx), which defers drawing the view hierarchy and applying the transaction until the upcoming Choreographer traversal. If a second frame's platform task runs before that traversal happens, it would immediately mutate the platform view layout params and merge a second transaction into applyTransactionOnDraw, overwriting the first frame before it was ever drawn. PlatformViewsController2 now defers a second frame's view operations and transactions into a QueuedFrame while a frame is already staged, and replays/commits it once the traversal finishes (using a Choreographer.FrameCallback that posts an async message to the front of the main Handler queue, plus a flushCompletedTraversalIfNeeded() check in case MessageLoopAndroid runs inside MessageQueue.nativePollOnce() right after doFrame() before that Handler message is dispatched).
  3. When two transactions on the same SurfaceControl do get merged before a single draw (for example if 3+ frames arrive within one VSync and coalesce in QueuedFrame), the later transaction's buffer overwrites the earlier one, but Android's SurfaceComposerClient retains both transaction completion callbacks in an unordered_set and can invoke the newer callback before the older, superseded callback. AHBSwapchainImplVK now tracks a monotonic present_id so a superseded callback recycles its own dropped texture into pool_ instead of displacing currently_displayed_texture_.

mackall-work and others added 5 commits October 14, 2026 23:49
PlatformViewsController2.publishTransaction() was appending every raster
SurfaceControl.Transaction into a single flat pendingRasterTransactions
list across frames, and swapTransactions() on the platform thread
drained the entire list at once. Because SubmitFlutterView() posts its
platform-thread task asynchronously after submitting the overlay and
main SurfaceFrames, the raster thread can begin or complete frame N+1
before frame N's platform task runs swapTransactions(). Draining the
flat list caused frame N's platform task to merge frame N+1's overlay
(or overlay and main) transaction into frame N's onEndFrame(), pairing
frame N+1's Flutter surface buffer(s) with frame N's platform view
layout and leaving frame N+1's platform task short.

Add sealFrameTransactions() across PlatformViewAndroidJNI, FlutterJNI,
and PlatformViewsController2, and call it from
AndroidExternalViewEmbedder2::SubmitFlutterView() on the raster thread
after submitting the frame's surfaces and before posting the platform
task. PlatformViewsController2 queues each sealed frame batch into
pendingRasterFrames so swapTransactions() pops one frame's raster
transactions per frame.
- PlatformViewsController2Test: verify that swapTransactions() pops one
  sealed frame batch at a time, leaving an in-progress next frame's
  published overlay transaction unmerged until that frame seals and
  keeping back-to-back sealed frames separated across swaps.
- AndroidExternalViewEmbedder2: verify that SubmitFlutterView() seals
  each frame's raster transactions synchronously on the raster thread
  before the posted platform-thread task runs swapTransaction() and
  onEndFrame2().
@gmackall gmackall added the CICD Run CI/CD label Oct 16, 2026
@github-actions github-actions Bot added platform-android Android applications specifically engine flutter/engine related. See also e: labels. e: impeller Impeller rendering backend issues and features requests team-android Owned by Android platform team labels Oct 16, 2026
@gmackall
gmackall marked this pull request as ready for review October 16, 2026 21:33
@gmackall
gmackall requested a review from a team as a code owner October 16, 2026 21:33
@gmackall
gmackall requested review from jesswrd and removed request for a team October 16, 2026 21:33
@gmackall
gmackall requested a review from mboetger October 16, 2026 21:33

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces monotonic present ID tracking in AHBSwapchainImplVK to prevent superseded callbacks from displacing displayed textures, and updates PlatformViewsController2 to queue transactions per raster frame and defer frames arriving before the current Choreographer traversal completes. The review feedback focuses on reducing garbage collection churn by optimizing pendingRasterFrames to use List and Collections.emptyList() when empty, as well as simplifying the handler logic by eagerly initializing mainHandler to eliminate redundant null checks.

@mboetger mboetger left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like the new method caused google3 issues and needs a fix.

@gmackall

gmackall commented Oct 20, 2026 •

Copy link
Copy Markdown
Member Author

Looks like the new method caused google3 issues and needs a fix.

gemini suggests its an issue with how we're resolving engine artifacts for google testing and rebasing will fix 🤷 🙏
find out soon!

@gmackall gmackall added the autosubmit Merge PR when tree becomes green via auto submit App label Oct 20, 2026
@auto-submit
auto-submit Bot added this pull request to the merge queue Oct 21, 2026
Merged via the queue into flutter:master with commit 6597889 Oct 21, 2026
22 of 23 checks passed
@flutter-dashboard flutter-dashboard Bot removed the autosubmit Merge PR when tree becomes green via auto submit App label Oct 21, 2026
LongCatIsLooong pushed a commit to LongCatIsLooong/flutter that referenced this pull request Oct 22, 2026
…ore a Choreographer traversal (flutter#194591)

Fixes a few related synchronization issues in HCPP when the raster
thread gets ahead of the platform thread, or when two platform-thread
frame tasks run before Android's `Choreographer.doFrame()` has drawn the
first one. Together, this effectively moves HCPP presentation from
mailbox semantics (where a second frame arriving before `doFrame()`
overwrote the pending frame in `applyTransactionOnDraw`) to FIFO,
aligning with core Flutter.

The end result is that scrolling with multiple platform views looks a
lot, lot smoother.

1. Previously, `PlatformViewsController2.publishTransaction()` appended
every raster `SurfaceControl.Transaction` into a single flat
`pendingRasterTransactions` list, and `swapTransactions()` on the
platform thread drained the entire list at once. Because
`SubmitFlutterView()` posts its platform-thread task asynchronously
after submitting the overlay and main `SurfaceFrame`s, the raster thread
can start or finish frame `N+1` before frame `N`'s platform task runs
`swapTransactions()`. Draining the flat list caused frame `N` to merge
frame `N+1`'s transactions into its `onEndFrame()`, pairing frame
`N+1`'s Flutter surface buffers with frame `N`'s platform view layout
(and leaving frame `N+1`'s platform task without its transactions). This
adds `sealFrameTransactions()` and calls it on the raster thread at the
end of `SubmitFlutterView()` so `swapTransactions()` pops one sealed
frame batch at a time.
2. Similarly, on the platform thread, `onEndFrame()` calls
`flutterView.invalidate()` and
`rootSurfaceControl.applyTransactionOnDraw(tx)`, which defers drawing
the view hierarchy and applying the transaction until the upcoming
`Choreographer` traversal. If a second frame's platform task runs before
that traversal happens, it would immediately mutate the platform view
layout params and merge a second transaction into
`applyTransactionOnDraw`, overwriting the first frame before it was ever
drawn. `PlatformViewsController2` now defers a second frame's view
operations and transactions into a `QueuedFrame` while a frame is
already staged, and replays/commits it once the traversal finishes
(using a `Choreographer.FrameCallback` that posts an async message to
the front of the main `Handler` queue, plus a
`flushCompletedTraversalIfNeeded()` check in case `MessageLoopAndroid`
runs inside `MessageQueue.nativePollOnce()` right after `doFrame()`
before that `Handler` message is dispatched).
3. When two transactions on the same `SurfaceControl` do get merged
before a single draw (for example if 3+ frames arrive within one VSync
and coalesce in `QueuedFrame`), the later transaction's buffer
overwrites the earlier one, but Android's `SurfaceComposerClient`
retains both transaction completion callbacks in an `unordered_set` and
can invoke the newer callback before the older, superseded callback.
`AHBSwapchainImplVK` now tracks a monotonic `present_id` so a superseded
callback recycles its own dropped texture into `pool_` instead of
displacing `currently_displayed_texture_`.

---------

Co-authored-by: Gray Mackall <[email protected]>
suryaKommana2662 pushed a commit to suryaKommana2662/flutter that referenced this pull request Oct 29, 2026
…ore a Choreographer traversal (flutter#194591)

Fixes a few related synchronization issues in HCPP when the raster
thread gets ahead of the platform thread, or when two platform-thread
frame tasks run before Android's `Choreographer.doFrame()` has drawn the
first one. Together, this effectively moves HCPP presentation from
mailbox semantics (where a second frame arriving before `doFrame()`
overwrote the pending frame in `applyTransactionOnDraw`) to FIFO,
aligning with core Flutter.

The end result is that scrolling with multiple platform views looks a
lot, lot smoother.

1. Previously, `PlatformViewsController2.publishTransaction()` appended
every raster `SurfaceControl.Transaction` into a single flat
`pendingRasterTransactions` list, and `swapTransactions()` on the
platform thread drained the entire list at once. Because
`SubmitFlutterView()` posts its platform-thread task asynchronously
after submitting the overlay and main `SurfaceFrame`s, the raster thread
can start or finish frame `N+1` before frame `N`'s platform task runs
`swapTransactions()`. Draining the flat list caused frame `N` to merge
frame `N+1`'s transactions into its `onEndFrame()`, pairing frame
`N+1`'s Flutter surface buffers with frame `N`'s platform view layout
(and leaving frame `N+1`'s platform task without its transactions). This
adds `sealFrameTransactions()` and calls it on the raster thread at the
end of `SubmitFlutterView()` so `swapTransactions()` pops one sealed
frame batch at a time.
2. Similarly, on the platform thread, `onEndFrame()` calls
`flutterView.invalidate()` and
`rootSurfaceControl.applyTransactionOnDraw(tx)`, which defers drawing
the view hierarchy and applying the transaction until the upcoming
`Choreographer` traversal. If a second frame's platform task runs before
that traversal happens, it would immediately mutate the platform view
layout params and merge a second transaction into
`applyTransactionOnDraw`, overwriting the first frame before it was ever
drawn. `PlatformViewsController2` now defers a second frame's view
operations and transactions into a `QueuedFrame` while a frame is
already staged, and replays/commits it once the traversal finishes
(using a `Choreographer.FrameCallback` that posts an async message to
the front of the main `Handler` queue, plus a
`flushCompletedTraversalIfNeeded()` check in case `MessageLoopAndroid`
runs inside `MessageQueue.nativePollOnce()` right after `doFrame()`
before that `Handler` message is dispatched).
3. When two transactions on the same `SurfaceControl` do get merged
before a single draw (for example if 3+ frames arrive within one VSync
and coalesce in `QueuedFrame`), the later transaction's buffer
overwrites the earlier one, but Android's `SurfaceComposerClient`
retains both transaction completion callbacks in an `unordered_set` and
can invoke the newer callback before the older, superseded callback.
`AHBSwapchainImplVK` now tracks a monotonic `present_id` so a superseded
callback recycles its own dropped texture into `pool_` instead of
displacing `currently_displayed_texture_`.

---------

Co-authored-by: Gray Mackall <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CICD Run CI/CD e: impeller Impeller rendering backend issues and features requests engine flutter/engine related. See also e: labels. platform-android Android applications specifically team-android Owned by Android platform team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants