Repository navigation
Conversation
gmackall
force-pushed
the
optimize-ahb-swapchain-no-pv
branch
2 times, most recently
from
September 1, 2026 17:31
3dd68a7 to
d1703e7
Compare
gmackall
force-pushed
the
optimize-ahb-swapchain-no-pv
branch
from
September 3, 2026 04:54
199940c to
b040ea1
Compare
10 of 11 tasks
gmackall
added a commit
to gmackall/flutter
that referenced
this pull request
Sep 11, 2026
…o issue list - Add alternative fix direction for PV-3/PV-4 using a pre-vended transaction pool to establish a one-way C++ -> Java dependency. - Add PV-11: AHB swapchain triple-buffering (kMaxPendingPresents = 3u) to prevent 30 FPS lockstep stalls under bursty rasterization. - Add PV-12: ADPF core affinity integration to guide Energy-Aware Scheduling (EAS). - Add PV-13: Decoupling AHB swapchain from Java when zero platform views are active (flutter#192083). - Add PV-14: Eliminating cross-stream tx.merge() overhead in onEndFrame().
…orm views are active When HCPP (hybrid composition++) is enabled but no platform views are present in the frame, the AHB swapchain no longer round-trips its SurfaceControl transaction through Java. The raster thread creates and applies an owned ASurfaceTransaction directly, avoiding a platform-thread hop and the resulting dependency on main-thread availability. Frames that contain platform views, that follow a frame which had platform views, or that change the frame size continue to route through Java so that platform view mutations and the Flutter content are applied in the same transaction via applyTransactionOnDraw. This is a squash-rebase of the original flutter#192083 branch onto master after flutter#192606 (createTransaction publishes directly into pendingRasterTransactions) landed; the parts of this change that overlapped with flutter#192606 are dropped.
…ction path Forwarding the vsync target as ASurfaceTransaction_setDesiredPresentTime is orthogonal to routing no-platform-view frames around Java, and it has its own correctness question: SurfaceFlinger holds a transaction back while desiredPresentTime >= expectedPresentTime, and the value that was being forwarded (frame_time + 1/refresh_rate from the vsync waiter) can be a frame late across LTPO refresh-rate switches. It is split into a follow-up change so it can be evaluated on its own.
…outer
The per-frame decision of whether the AHB swapchain applies its
SurfaceControl transaction directly from the raster thread or hands it to
Java lived on PlatformViewAndroidJNI as SetFrameUsesJavaTransactions /
FrameUsesJavaTransactions, plus an in-flight counter on the embedder.
Those facade methods never crossed JNI: they were raster-thread state that
happened to be stored on the Java bridge because the surface and the
embedder both already held it.
Introduce flutter::SurfaceTransactionRouter, a small raster-side object
that owns:
* the latched route of the frame being submitted (kDirect / kPlatform),
read by AndroidSurfaceVKImpeller's CreateTransactionCB for the root
and overlay swapchains; and
* the count of platform-routed frames that have not been committed yet,
which keeps subsequent frames on the platform route so a direct
ASurfaceTransaction_apply cannot overtake an applyTransactionOnDraw
that is still waiting for a View draw.
PlatformViewAndroid owns the router and shares it with the surface
factory (so every AndroidSurfaceVKImpeller it creates, including overlay
surfaces created through the surface pool, consults the same route) and
with the HC++ external view embedder, which sets the route around each
submission and reports platform frames to it.
The facade, its mock, the JNI impl and the toggle unit test are back to
their pre-PR shape. The embedder unit tests now assert the route the
swapchain would observe during submission instead of expecting facade
calls. The test file also stops wrapping lambdas in testing::Invoke, which
current googletest marks deprecated and the engine builds with -Werror.
The commit point is unchanged in this commit: the counter is still
decremented right after onEndFrame2() returns on the platform thread. The
next commit moves it to the transaction-committed callback.
…a commit Direct and platform-routed swapchain transactions are applied with different apply tokens, so a direct frame can be committed ahead of an older frame whose transaction is still parked in ViewRootImpl. The previous guard against that was a counter that was decremented as soon as onEndFrame2() returned plus a one-frame cooldown. Neither matches what applyTransactionOnDraw() does: it only merges the transaction into ViewRootImpl's pending transaction, which is applied with the next View draw (or by ViewRootImpl itself if that draw never happens), so the frame was being counted as committed while it was still pending. Replace the heuristic with the actual signal. PlatformViewsController2. onEndFrame() now registers a TransactionCommittedListener on the merged transaction before handing it to applyTransactionOnDraw() (merging carries listeners along, so the order matters). The listener hops to the main thread and calls FlutterJNI.onEndFrameTransactionCommitted(), which reaches SurfaceTransactionRouter::OnPlatformFrameCommitted() through PlatformViewAndroid. Frames whose transaction is dropped because there is no root SurfaceControl report themselves immediately, since nothing will ever commit them. The raster side keeps routing through the platform thread while the router still has uncommitted platform frames, and the cooldown is gone. The router's count is deliberately not reset in DestroySurfaces(): ViewRootImpl applies its pending transactions even when the window dies, so every frame that was handed over eventually reports back, and resetting could let a direct frame overtake one that is still pending.
…ginFrame2() PlatformViewsController2.platformTransaction() records View-state mutations (overlay visibility, SurfaceView clips) into a pending transaction that is only picked up by the next swapTransactions() and applied by that frame's onEndFrame(). Now that steady-state frames without platform views bypass the platform thread entirely, a mutation recorded outside of one of the embedder's platform tasks would sit there until some later frame happens to be routed through Java, which may never happen. Make the window explicit. The embedder calls the new PlatformViewAndroidJNI::onBeginFrame2() (FlutterJNI.beginFrame2() -> PlatformViewsController2.onBeginFrame()) at the start of both platform tasks, before any mutation. swapTransactions() closes the window, since anything recorded after it would only be picked up by the next swap. platformTransaction() throws an IllegalStateException in debug builds when it is called outside of that window and logs an error otherwise. All existing callers of platformTransaction() are reached from the embedder's platform tasks, so the guard does not change behavior; the surfaceCreated callback path already applies its out-of-frame clip on its own and schedules a frame (see flutter#175546).
gmackall
force-pushed
the
optimize-ahb-swapchain-no-pv
branch
from
October 2, 2026 19:26
96b1376 to
a02ea52
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
With HC++ (the SurfaceControl swapchain), every swapchain frame is presented through Java today: the raster thread synchronously creates a
SurfaceControl.Transactionvia JNI, and the platform thread merges it and hands it toAttachedSurfaceControl.applyTransactionOnDraw. That is needed when a frame changes View state (platform views shown/hidden/moved, the overlay, aFlutterViewresize), but for the common case of an app with no platform views it costs a JNI round trip on the raster thread and ties presentation to a platform-thread task plus a View draw, which shows up as jank.This PR applies swapchain transactions directly on the raster thread (
ASurfaceTransaction_apply) whenever a frame has nothing to synchronize with the View hierarchy, and keeps the existing Java path otherwise.This addresses the follow-up note left by Jonah in PR #162750 (commit
17cb12d):How it works
SurfaceTransactionRouter(new,external_view_embedder/): owned byPlatformViewAndroidand shared with the HC++ view embedder and everyAndroidSurfaceVKImpellerthe surface factory creates. It holdskDirect/kPlatform; raster thread only), andRoute selection (
AndroidExternalViewEmbedder2::SubmitFlutterView): a frame goes through the platform thread if itViewRootImpldraw), orOtherwise it takes the direct route and no platform task is posted at all. The route is latched for the whole submission (
ScopedCleanupClosurerestoreskDirect), so the overlay surfaces and the root surface of one frame always agree.Swapchain callback (
AndroidSurfaceVKImpeller::SetNativeWindow): onkDirectit returns an owned nativeSurfaceTransaction, which the AHB swapchain applies itself; onkPlatformit creates the transaction through Java as before.No reordering across routes: direct and platform-routed transactions use different apply tokens, so SurfaceFlinger does not order them relative to each other, and a direct frame could otherwise be shown before an older platform frame that is still waiting for its View draw.
PlatformViewsController2.onEndFrame()registers aTransactionCommittedListeneron the transaction beforeapplyTransactionOnDraw(merging carries listeners along); when it fires, Java callsFlutterJNI.onEndFrameTransactionCommitted()→PlatformViewAndroid::OnPlatformFrameCommitted(), which decrements the count. Frames stay on the platform route until the count drops to zero. IfonEndFrame()closes the transaction without applying it (no root surface control, e.g. after detach), it reports the frame right away so the engine is not left waiting. Unmatched commits saturate at zero.Explicit frame boundaries on the platform thread: new
onBeginFrame2()JNI call /PlatformViewsController2.onBeginFrame(). Platform-transaction mutations are only valid betweenonBeginFrame()andswapTransactions(); a mutation outside a frame would otherwise sit in the pending transaction until some later platform-routed frame, which may never come now that idle frames bypass Java. This throws in debug engine builds and logs in release.surfaceCreatedclip callback:createSurfaceClipCallbackused to record the initial crop/alpha into the pending platform transaction, outside of any frame. It now applies its own transaction withapplyTransactionOnDraw+invalidate()(orapply()if there is no root surface control), so it no longer depends on a later frame going through Java. It also bails out if the platform view was disposed before the surface was created.SurfaceTransactiongains a defaulted move constructor/assignment.Notes for reviewers
DestroySurfaces()resets the resize tracking but intentionally leaves the uncommitted-frame count alone: frames already handed to the platform thread still commit and report themselves.See demo (ignore the fps, the jank is what matters here):
Screen_recording_20260902_141338.mp4
Screen_recording_20260902_141218.mp4
Related Issues
Follow-up to #162750 and #192606
Tests
surface_transaction_router_unittests.cc: default route, route latching, uncommitted-frame counting, unmatched commits.external_view_embedder_unittests.cc:StillEndsFrameAfterLastPlatformViewGoesAway: platform-view frame → hide frame → follow-up frame held on the platform route until commit → steady-state direct frame (no platform task) → resize with zero platform views → frame held on the platform route while the previous frame's platform task is still queued → back to direct.StaysOnPlatformRouteUntilJavaReportsTheCommit: running the platform task is not enough; only the reported commit releases the platform route.TransactionScopeLatchesAcrossOverlayAndRootSubmissions: overlay and root submissions of one frame observe the same route.onBeginFrame2().PlatformViewsController2Test.java:onEndFrameReportsCommitOnlyAfterTheFrameTransactionIsCommitted(listener registered beforeapplyTransactionOnDraw, reported on the main looper only after it fires).onEndFrameReportsCommitImmediatelyWhenThereIsNoRootSurfaceControl,onEndFrameReportsCommitImmediatelyAfterDetachFromView.overlayMutationOutsideOfAFrameThrowsInDebug,overlayMutationsAreAcceptedBetweenBeginFrameAndSwapOnly.surfaceCreated_appliesClipDirectlyViaApplyTransactionOnDraw.onBeginFrame().PR code and description updated with Gemini / AI assistance.