Skip to content

Android Embedder Migration to Embedder API - #24

Open
mboetger wants to merge 27 commits into
flutter:mainfrom
mboetger:rfc-410-android-embedder-c-api
Open

mboetger wants to merge 27 commits into
flutter:mainfrom
mboetger:rfc-410-android-embedder-c-api

Conversation

@mboetger

@mboetger mboetger commented Sep 22, 2026 •

Copy link
Copy Markdown

Android Embedder Migration RFC

Tracking issue: flutter/flutter#193309

Pre-launch Checklist

  • I read [RFC 000.0001: Taxonomy] and followed the file and path naming and metadata standards.
  • I read [RFC 000.0002: Process] and confirmed this proposal meets the threshold for a full RFC.
  • I read and agree to the [Code of Conduct].
  • I signed the [CLA].
  • I have linked an issue from flutter/flutter with the label design doc.
  • All existing and new tests are passing.
  • I have enabled "Allow edits from maintainers" on this PR so the bot can automatically assign an RFC number (or I will run dart run bin/assign_rfc_number.dart locally when instructed).

@mboetger mboetger changed the title Android Embedder Migration Android Embedder Migration to Embedder API Sep 24, 2026
@mboetger
mboetger requested a review from gaaclarke September 24, 2026 21:22
- [`//flutter/shell/platform/embedder:embedder_headers`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/BUILD.gn#L210) and [`//flutter/shell/platform/embedder:embedder_as_internal_library`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/BUILD.gn#L206) (with `defines = [ "FLUTTER_ENGINE_NO_PROTOTYPES" ]` enforcing dynamic `FlutterEngineProcTable` resolution)
- [`//flutter/fml`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/fml)
- [`//flutter/common`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/common)
- [`//flutter/assets`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/assets)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Just curious, why is this needed? I would think we could remove this dependency as well. I believe desktop platforms do not depend on this library. Not a big deal either ways, asking for my understanding.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

It's not actually needed - I just added it since it was considered optional - will make it more clear.

Comment thread rfc/410.0000-flutter-android-embedder-c-api-migration.md
Comment thread rfc/410.0000-flutter-android-embedder-c-api-migration.md
Comment thread rfc/410.0000-flutter-android-embedder-c-api-migration.md Outdated
- **Texture Layer Hybrid Composition (TLHC) and Virtual Display (VD) via External Textures**: Both [`configureForTextureLayerComposition`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java#L652) and [`configureForVirtualDisplay`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java#L596) allocate a `PlatformViewRenderTarget` from `TextureRegistry` (`SurfaceProducer` / `AHardwareBuffer` or `SurfaceTexture`) in Java and return its texture ID to the Dart framework `Texture` widget. At the native engine boundary, both TLHC and VD operate strictly through the C-API external texture callbacks (`FlutterEngineRegisterExternalTexture`, `FlutterEngineMarkExternalTextureFrameAvailable`, `FlutterVulkanExternalTextureFrameCallback`, `FlutterHardwareBufferExternalTextureFrameCallback`, and `FlutterOpenGLTexture`) without creating `FlutterPlatformView` layers.
- **Hybrid Composition (HC) and Hybrid Composition++ (HCPP) via `FlutterCompositor`**: The engine's internal [`EmbedderExternalViewEmbedder`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder_external_view_embedder.h) implements `flutter::ExternalViewEmbedder` behind the public [`FlutterCompositor`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder.h#L2117) interface (`create_backing_store_callback`, `collect_backing_store_callback`, and `present_layers_callback` / `present_view_callback`). `AndroidCompositor` and `AndroidPlatformViewsController` drive both HC and HCPP over `FlutterCompositor` without including internal `//flutter/flow` or `fml::RasterThreadMerger` headers:

1. **Backing Store Allocation (`create_backing_store_callback`)**: For the primary Flutter layer (`platform_views_count == 0`), `AndroidCompositor::CreateBackingStore` returns the main surface framebuffer (`GetFBO()`). When a Hybrid Composition platform view splits the scene and Flutter renders UI layers above the platform view, subsequent `CreateBackingStore` calls within the same frame allocate offscreen framebuffers (`AcquireOffscreenFBO`).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

For the primary Flutter layer (platform_views_count == 0), AndroidCompositor::CreateBackingStore returns the main surface framebuffer (GetFBO()).

Does the prototype already do this? I'd be curious how this works. This seems like a good optimization, but AFAICT FlutterBackingStoreConfig does not provide enough information in the create_backing_store_callback to determine if you can render to the main surface framebuffer directly.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I did try doing this - but ended up adding an is_overlay to the FlutterBackingStoreConfig to get things to actually work, so this is no longer true - you're right that there wasn't enough info.

I'll rewrite step 1 to propose an is_root_layer field on FlutterBackingStoreConfig (false unless the layer is the bottom one with no platform view beneath it), require avoid_backing_store_cache = true, and fall back to an offscreen store plus a blit when the field is missing. I'd imagine the iOS RFC needs the same info - wdyt @cbracken ?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Here's the similar conversation on the iOS RFC: #27 (comment)

FYI here's a concern I bubbled up on the iOS design:

One thing that's not clear to me: how will resizing work with the drawable-root approach? Today, embedders that use FlutterCompositor resize during FlutterCompositor.present_view_callback. I imagine with the drawable-root approach, the embedder will instead need to resize when the engine requests the root layer's backing store? Assuming that's correct, this might require migrating existing embedders that use FlutterCompositor.

If Android uses the main surface framebuffer as the backing store, we'll need to resize step to happen when the engine requests the backing store, instead of when the engine presents (which is what all embedders that use FlutterCompositor do today).

2. **Raster-Before-Acquire Ordering and Synchronous Platform-Thread Latch**:
In the legacy architecture, `AndroidExternalViewEmbedder` merged the raster thread onto the platform thread via `fml::RasterThreadMerger` only because it called JNI view hierarchy mutations inline during `SubmitFlutterView`. In `AndroidCompositor::PresentLayers`:
- On the raster thread, `surface_manager_->Present()` and `surface_manager_->BlitAndSwapOverlaySurface(...)` first complete `eglSwapBuffers` / `vkQueuePresentKHR` into the root `FlutterImageView` and `PlatformOverlayView` `ImageReader` `ANativeWindow` queues for Frame $N$.
- `AndroidCompositor` packages Frame $N$'s platform view geometry (`offset`, `size`), value-copied `AndroidMutatorsStack`, and overlay rectangles into a single atomic platform-thread frame transaction (`onBeginFrame` $\rightarrow$ `onDisplayPlatformView` $\rightarrow$ `onDisplayOverlaySurface` $\rightarrow$ `onEndFrame`).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

... single atomic platform-thread frame transaction...

Could you expand on this? I don't follow this bit. Is this the synchronization_fence_fd mechanism described below, or something else?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Something else, and "transaction" was the wrong word next to SurfaceControl.Transaction. synchronization_fence_fd only exists on the HCPP path (API 34+, Vulkan); HC never uses it. What I meant: after the raster thread swaps frame N into the ImageReaders, it posts one platform-thread task that runs onBeginFrame, every onDisplayPlatformView and onDisplayOverlaySurface, and onEndFrame, so no Android traversal can draw a half-applied frame; today's HCPP embedder already does this with a single PostTask (external_view_embedder_2.cc). For HC the raster thread also waits for that task, because onEndFrame calls acquireLatestImage() (PlatformViewsController.java), which would otherwise show frame N+1's pixels with frame N's view positions; the legacy SubmitFlutterView got this ordering for free by rendering and making the JNI calls on the merged platform thread. I'll rewrite the section in those terms, drop "atomic", and describe the two steps that still need a synchronous platform-thread hop mid-frame: creating an overlay surface, and the first HC frame's switch to FlutterImageView.

Comment thread rfc/410.0000-flutter-android-embedder-c-api-migration.md Outdated
Comment thread rfc/410.0000-flutter-android-embedder-c-api-migration.md Outdated
Comment thread rfc/410.0000-flutter-android-embedder-c-api-migration.md
Comment thread rfc/410.0000-flutter-android-embedder-c-api-migration.md
Comment thread rfc/410.0000-flutter-android-embedder-c-api-migration.md Outdated
Comment thread rfc/410.0000-flutter-android-embedder-c-api-migration.md Outdated
Comment thread rfc/410.0000-flutter-android-embedder-c-api-migration.md Outdated
double max_value;
int32_t traversal_parent;
FlutterTransformation hit_test_transform;
```

@loic-sharma loic-sharma Sep 30, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

These LGTM, but we should get these new members reviewed by @chunhtai and @hannah-hyj

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

We also need SemanticsRole

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Added SemanticRole and tried to align more with what Android currently uses. Dropped traversal_parent - since I guess that's just web. PTAL

Comment thread rfc/410.0000-flutter-android-embedder-c-api-migration.md
Comment thread rfc/410.0000-flutter-android-embedder-c-api-migration.md Outdated
@mboetger
mboetger requested a review from loic-sharma October 15, 2026 21:52
@mboetger mboetger added the rfc-ready RFC is ready for review. label Oct 15, 2026
@mboetger
mboetger marked this pull request as ready for review October 15, 2026 22:38
Comment thread rfc/410.0000-flutter-android-embedder-c-api-migration.md Outdated
FlutterRect clip_rect;
FlutterRoundedRect clip_rounded_rect;
FlutterTransformation transformation;
const FlutterPath* clip_path;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Does this need to be a pointer?

3. **Pre-run buffering is self-contained in `FlutterEmbedderNative`**: Queueing platform messages, initial route, viewport metrics, and external texture registrations in `FlutterEmbedderNative` between [`attachToNative()`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java#L433-L442) and [`runBundleAndSnapshotFromLibrary()`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java#L1072-L1089)—and flushing them immediately after [`FlutterEngineRunInitialized`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder.h#L3006-L3007)—requires no new [`embedder.h`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder.h) entry points or [`RuntimeController`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/runtime/runtime_controller.h#L52) lifecycle flags.
4. **`defer_surface_creation` already moves [`SetupImpellerContext()`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/common/platform_view.h#L1040) off the UI thread**: Because [`runBundleAndSnapshotFromLibrary()`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java#L1072-L1089) executes during `Activity.onCreate` / `onStart` well before WindowManager dispatches `SurfaceHolder.Callback.surfaceCreated`, skipping the implicit [`NotifyCreated()`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/common/platform_view.cc#L64-L85) call inside [`FlutterEngineRunInitialized`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder.h#L3006-L3007) already gives the raster thread the entire window between [`runBundleAndSnapshotFromLibrary()`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java#L1072-L1089) and `surfaceCreated()` to complete [`SetupImpellerContext()`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/common/platform_view.h#L1040) asynchronously.

| Dimension | Proposed: Two-Phase Init ([`FlutterEngineInitialize`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder.h#L2970-L2975) + [`FlutterEngineRunInitialized`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder.h#L3006-L3007) + `defer_surface_creation`) | Alternative: Split Shell & Isolate Launch (`FlutterEngineLaunchShell` + `FlutterEngineLaunchEngine`) |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

A potential alternative might be adding a new renderer type that is a callback that produces a renderer config.

Strawman API...
typedef enum {
  ...

  /// The renderer is chosen lazily by the embedder after the engine
  /// has been initialized. See |FlutterDeferredRendererConfig|.
  kDeferred,
} FlutterRendererType;

struct FlutterRendererConfig {
  FlutterRendererType type;
  union {
    FlutterOpenGLRendererConfig open_gl;
    FlutterSoftwareRendererConfig software;
    FlutterMetalRendererConfig metal;
    FlutterVulkanRendererConfig vulkan;
    FlutterDeferredRendererConfig deferred;
  };
};

/// Callback invoked by the engine to resolve a deferred renderer
/// configuration into a concrete one.
///
/// @param[in]  user_data   The `user_data` passed to
///                         `FlutterEngineInitialize`
///                         or `FlutterEngineRun`.
/// @param[out] config_out  Engine-owned storage. The embedder must set
///                         `config_out->type` to `kOpenGL`, `kSoftware`,
///                         `kMetal`, or `kVulkan` and populate the
///                         corresponding union member. `kDeferred` is not
///                         allowed. All memory referenced by the
///                         configuration must remain valid until
///                         `FlutterEngineDeinitilaize` or
///                         `FlutterEngineShutdown` returns.
///
/// @return     On error, `false`.
typedef bool (*FlutterRendererConfigResolveCallback)(
    void* /* user_data */,
    FlutterRendererConfig* /* config_out */);

typedef struct {
  size_t struct_size;
  /// Invoked exactly once by the engine to obtain the concrete renderer
  /// configuration. Required.
  ///
  /// The callback is invoked once on the raster thread.
  /// Platform thread work that requires a GPU context blocks until this
  /// callback returns.
  FlutterRendererConfigResolveCallback resolve_callback;
} FlutterDeferredRendererConfig;

This approach should remove the need for FlutterEngineLaunchShell, FlutterEngineNotifyCreated, FlutterEngineNotifyDestroyed. I suspect we might be able to remove defer_surface_creation by deferring PlatformView::NotifyCreated until the deferred renderer config is resolved.

Opus's feedback:

  1. External texture registration would need to be updated since that depends on the renderer type.
  2. Today, FlutterEngineInitialize validates the renderer config at startup. This would now happen in a background thread.

/// An embedder may return the view's on-screen render target (for example,
/// the next swapchain image or default framebuffer) when this is true. The
/// engine's backing store cache ignores this field, so such an embedder must
/// set `FlutterCompositor.avoid_backing_store_cache` to true.

@loic-sharma loic-sharma Oct 16, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I imagine the engine should never cache the default framebuffer / next swapchain image? Should we make a prerequisite for this being true that avoid_backing_store_cache is true? Otherwise, the engine would never be able to cache the root layer's backing store.

1. **Explicit Hybrid Composition ([`initExpensiveAndroidView`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/packages/flutter/lib/src/services/platform_views.dart#L194-L210))**: The plugin requests [`createForPlatformViewLayer`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java#L168-L177), routing directly to [`PlatformViewsController.configureForHybridComposition`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java#L581-L585).
2. **Embedded Child `SurfaceView` or `TextureView` ([`VIEW_TYPES_REQUIRE_NON_TLHC`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java#L68))**: When a plugin uses [`initAndroidView`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/packages/flutter/lib/src/services/platform_views.dart#L134-L152) ([`createForTextureLayer`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java#L194-L247)) and the embedded Android `View` hierarchy contains a `SurfaceView` or `TextureView` child whose out-of-band buffer queue bypasses `ViewParent.onDescendantInvalidated()`, [`PlatformViewsController.createForTextureLayer`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java#L194-L247) detects `!supportsTextureLayerMode` and branches by `request.displayMode`:
- **[`TEXTURE_WITH_VIRTUAL_FALLBACK`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformViewCreationRequest.java#L24) (Virtual Display / VD)**: Calls [`configureForVirtualDisplay`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java#L596-L638), hosting the view and its child `SurfaceView` inside an offscreen `VirtualDisplay` backed by a [`TextureRegistry`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/io/flutter/view/TextureRegistry.java#L19) render target. Frame synchronization is handled entirely on the GPU/raster thread as a standard C-API external texture ([`FlutterEngineRegisterExternalTexture`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder.h#L3227-L3229)).
- **[`TEXTURE_WITH_HYBRID_FALLBACK`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformViewCreationRequest.java#L28) (Hybrid Composition / HC)**: Calls [`configureForHybridComposition`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java#L581-L585) and returns [`NON_TEXTURE_FALLBACK`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformViewsChannel.java#L316) (`-2`), instructing the Dart framework to composite the view via a [`PlatformViewLayer`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/flow/layers/platform_view_layer.h#L13) ([`kFlutterLayerContentTypePlatformView`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder.h#L2175)).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This section seems unnecessarily dense. Could we simplify it?

Comment on lines +695 to +696
- `AndroidCompositor` captures frame N's platform view geometry (`offset`, `size`), value-copied `AndroidMutatorsStack`, and overlay rectangles by value and posts a single task (`PostTask`) to the platform thread that executes [`onBeginFrame`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java#L1311-L1319), every [`onDisplayPlatformView`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java#L1252-L1289), every [`onDisplayOverlaySurface`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java#L1291-L1309), and [`onEndFrame`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java#L1321-L1347) in order. Because the Android UI `Looper` runs one message at a time and `ViewRootImpl` schedules view traversals in a separate `Choreographer.CALLBACK_TRAVERSAL` callback, no Android view traversal can run mid-task and observe a partially applied frame (the same single-`PostTask` pattern used by [`AndroidExternalViewEmbedder2::SubmitFlutterView`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/external_view_embedder/external_view_embedder_2.cc#L89-L229)). This is purely a CPU-side `Looper` ordering mechanism; it does not use `synchronization_fence_fd`, which exists only on the HCPP `SurfaceControl.Transaction` path.
- When `platform_views_count > 0` and HCPP is not active (`!IsHcppEnabled()`), if `PresentLayers` was called on the raster thread (`!platform_runner->RunsTasksOnCurrentThread()`), the raster thread waits on a frame-completion latch ([`fml::AutoResetWaitableEvent`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/fml/synchronization/waitable_event.h#L28)) until the platform thread finishes [`onEndFrame()`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java#L1321-L1347) (polling a surface-attached flag that the platform thread clears before calling `FlutterEngineNotifyDestroyed` so teardown never deadlocks). Waiting on this latch is required because [`onEndFrame()`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java#L1321-L1347) calls `ImageReader.acquireLatestImage()`, which drops older queued images; without the wait, the raster thread could queue frame N+1 before the platform thread runs frame N's task, pairing frame N+1's pixels with frame N's [`FlutterMutatorView`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorView.java#L32) geometry.

@loic-sharma loic-sharma Oct 16, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This seems like a major shift in how HC platform views work.

From my understanding, the Android embedder checks at startup whether it can use HC or HCPP. If HC is picked, we merge the raster thread whenever there are platform views. Did we consider an alternative where the embedder API has an opt-in flag to merge the raster thread if there are platform views, and the Android embedder turns on this flag if it detects the current device does not support HCPP?

If we do stick with this approach, this seems like something we complete in phase 1.

Comment thread rfc/410.0000-flutter-android-embedder-c-api-migration.md
Comment thread rfc/410.0000-flutter-android-embedder-c-api-migration.md
kFlutterPlatformViewMutationTypeClipRect,
kFlutterPlatformViewMutationTypeClipRoundedRect,
kFlutterPlatformViewMutationTypeTransformation,
kFlutterPlatformViewMutationTypeClipPath,

@loic-sharma loic-sharma Oct 16, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One concern is that an embedder might break if they receive a new mutation type they don't know how to handle. We could add a kFlutterPlatformViewMutationCapabilityClipPath capability, and if the embedder didn't opt-in, fallback to a clip rect mutation. Am I being too paranoid though? I lean towards skipping this and keeping it simple.

cc @cbracken for thoughts.

kFlutterSemanticsRoleMain = 30,
kFlutterSemanticsRoleNavigation = 31,
kFlutterSemanticsRoleRegion = 32,
kFlutterSemanticsRoleSearch = 33,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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


/// File descriptor representing a sync_fence / ASyncFence.
/// When presenting on Android API 34+ via SurfaceControl.Transaction,
/// the engine passes this fence to the embedder.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I assume this isn't used with the OpenGL renderer?

Because split APK assets on Android are loaded via `AAssetManager` or memory-mapped (`mmap`) from disk, buffer lifetime must be explicitly managed:

1. When Dart requests a library, [`FlutterRequestDartDeferredLibraryCallback`](#dart-deferred-library-loading) runs asynchronously (`void` return type), so the embedder can initiate download or extraction without blocking the engine thread.
2. When loading completes, `FlutterDartDeferredLibrary.destruction_callback` provides the unmap trigger. In the proposed [`embedder.cc`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder.cc) implementation, the engine captures this callback in a [`DeferredLibraryLifetime`](#deferred-library-memory-lifetime-management) handle held by [`fml::NonOwnedMapping`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/fml/mapping.h#L115). When the Dart VM finishes loading and drops its references to both data and instruction buffers, the destructor triggers `destruction_callback(user_data)`, which permits the Android embedder to safely `munmap()` or close the underlying [`AAsset`](https://developer.android.com/ndk/reference/group/asset).

@loic-sharma loic-sharma Oct 17, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

When the Dart VM finishes loading and drops its references to both data and instruction buffers, ...

It's not clear to me this is correct. The Dart embedder API does not specify the lifetime of these buffers: https://github.com/dart-lang/sdk/blob/7b69ded4350b8f3b02883ccdb34ac4d48c43e71d/runtime/include/dart_api.h#L3607-L3618

The Flutter engine appears to attempt to keep these buffers alive while it is alive: https://github.com/flutter/flutter/blob/d60340594eb2ab394bd1198c2f950d066dd679c6/engine/src/flutter/runtime/dart_isolate.cc#L571

I suspect the embedder will need to keep the buffers in memory until the engine is shutdown.

Input events processed in [`AndroidTouchProcessor.java`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/io/flutter/embedding/android/AndroidTouchProcessor.java#L372-L445) provide axis and device metadata. The C-API embedder populates [`FlutterPointerEvent`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder.h#L1362-L1409) with:

- Device kind: [`kFlutterPointerDeviceKindTouch`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder.h#L1321), [`kFlutterPointerDeviceKindMouse`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder.h#L1320), [`kFlutterPointerDeviceKindStylus`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder.h#L1322), or [`kFlutterPointerDeviceKindTrackpad`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder.h#L1323).
- Axis values: `tilt`, `orientation`, `pressure`, and secondary scroll axes.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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


### Pointer Input Events, Viewport Metrics, and System Configuration

#### Pointer Input Events

@loic-sharma loic-sharma Oct 17, 2026 •

Copy link
Copy Markdown
Member

int entrypoint_argc;
void* user_data;
const FlutterProjectArgs* project_args;
const FlutterRendererConfig* renderer_config;

@loic-sharma loic-sharma Oct 17, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It's weird that some of the members on FlutterProjectArgs are repeated on FlutterEngineSpawnInfo. For example, the entrypoint arguments are on both structs.

Is it possible to remove the project args and renderer config? Can the new engine reuse the parent engine's configuration? This would let us avoid weird things like attempting to use different rendering configs.

#### Synchronization Fence Lifecycle and Ownership

1. **GPU Fence Export**: When Impeller submits render passes for a backing store, it exports the GPU completion semaphore or fence into a POSIX `sync_fence` file descriptor using the [`VK_KHR_external_fence_fd`](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VK_KHR_external_fence_fd.html) extension (`vkGetFenceFdKHR`).
2. **Transfer to Embedder**: The engine populates `FlutterBackingStorePresentInfo.synchronization_fence_fd` and delivers the layers to [`present_view_callback`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder.h#L2321). Ownership of the file descriptor transfers to the embedder.

@loic-sharma loic-sharma Oct 19, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

From my understanding, the reason the embedder API's Skia Vulkan renderer config didn't need this is because the engine performed the host sync before invoking the present callback:

https://github.com/flutter/flutter/blob/b304cf18b6864d141cf08e26a07898901cddb4b5/engine/src/flutter/shell/platform/embedder/embedder.h#L1061-L1063

Can/should we reuse this approach for HCPP?


If we do decide to keep this fence, I suspect we should make this an opt-in feature. Perhaps something like FlutterVulkanRendererConfig.enable_manual_present_host_sync.

size_t struct_size;
const char* entrypoint;
const char* library_uri;
const char* initial_route;

@loic-sharma loic-sharma Oct 19, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should we add the initial route to FlutterProjectArgs instead? It looks like you can use set the initial route without multi-engine: https://docs.flutter.dev/add-to-app/android/add-flutter-fragment#run-flutter-with-a-specified-initial-route

val flutterFragment = FlutterFragment.withNewEngine()
    .initialRoute("myInitialRoute/")
    .build()

EDIT: Never mind this suggestion. Android/iOS set the initial engine's initial route by sending a message immediately at startup (link). This is gross, but we can fix that later.

FlutterEngineResult FlutterEngineGetCallbackInformation(
int64_t handle,
FlutterDartCallbackInformation* info_out);
```

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I would add more information to this section. I wasn't familiar with the callback cache and how one uses it. Consider linking to this blog post: https://flutter.dev/blog/executing-dart-in-the-background-with-flutter-plugins-and-geofencing

void* user_data;
FlutterImageGeneratorGetInfoCallback get_info;
FlutterImageGeneratorGetPixelsCallback get_pixels;
VoidCallback destruction_callback;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could we document which threads each of these callbacks are invoked from?

typedef enum {
kFlutterImageGeneratorColorTypeRGBA8888,
kFlutterImageGeneratorColorTypeBGRA8888,
} FlutterImageGeneratorColorType;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could you expand how you picked the name "image generator" instead of "image decoder"? The latter seems more common.

size_t struct_size;
void* user_data;
FlutterImageGeneratorGetInfoCallback get_info;
FlutterImageGeneratorGetPixelsCallback get_pixels;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could you explain how an image with multiple fames (eg. GIFs) is decoded? Does the engine call get_pixels for each frame or something? Or does the engine create a generator per frame?

uint32_t width;
uint32_t height;
FlutterImageGeneratorColorType color_type;
FlutterImageGeneratorAlphaType alpha_type;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should we add a FlutterImageGeneratorColorSpace enum that today only has a kFlutterImageGeneratorColorSpaceSRGB value? This would make it easier for us to add wide gamut support in the future.

typedef struct {
size_t struct_size;
FlutterImageGeneratorFactoryCallback create_generator;
int32_t priority;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could you add docs on this? I assume the engine picks these in descending order, 0 is the engine's built-in priority, -1 means prefer engine built-in implementation, etc?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

rfc-ready RFC is ready for review.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants