diff --git a/rfc/410.0000-flutter-android-embedder-c-api-migration.md b/rfc/410.0000-flutter-android-embedder-c-api-migration.md new file mode 100644 index 0000000..23137dd --- /dev/null +++ b/rfc/410.0000-flutter-android-embedder-c-api-migration.md @@ -0,0 +1,1676 @@ +--- +type: rfc +rfc: '410.0000' +title: Flutter Android Embedder C-API Migration +description: Migrates the Flutter Android embedder to the public, ABI-stable Flutter Embedder C-API (embedder.h) to eliminate all direct dependencies on engine internal headers. +status: draft +created: 2026-09-24T00:00:00Z +updated: 2026-09-24T00:00:00Z +tags: + - 410-android + - 200-engine + - embedder-api + - c-api + - impeller +authors: + - https://github.com/mboetger # Matt Boetger +--- + +# RFC 410.0000: Flutter Android Embedder C-API Migration + +## Executive Summary and Overview + +> [!IMPORTANT] +> This RFC proposes a phased, in-place migration of the Flutter Android embedder ([`engine/src/flutter/shell/platform/android/`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android)) to the public Flutter Embedder C-API ([`embedder.h`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder.h)), reducing direct internal engine `#include`s in `shell/platform/android/` from **98 to 0** while keeping `libflutter.so` as a single shared library and preserving the public Java/Kotlin API. + +Today, the Android embedder interacts directly with engine internals: it allocates task runners, instantiates [`flutter::Shell`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/common/shell.h#L113), inspects internal graphics contexts, and manages platform view compositing via private `flow` headers. This coupling causes several problems: + +- Internal engine refactors in Impeller, Flow, or the Dart runtime break the Android embedder. +- Compiling `libflutter.so` requires pulling internal engine translation units into the Android target. +- Third-party desktop and embedded platforms use [`embedder.h`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder.h), while Flutter's primary mobile embedders do not. Because the Android embedder bypasses the public C-API, missing features in [`embedder.h`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder.h) (such as dynamic thread merging, platform view mutators, and hardware buffer external textures) are not exercised by first-party code. + +This document establishes the plan to decouple the Android embedder from engine internals. + +### Strict Dependency Management + +The dependency graph of [`shell/platform/android/`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android) in [`BUILD.gn`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/BUILD.gn) will be reduced strictly to: + +|Allowed Dependencies|| +|---|---| +|[`//flutter/shell/platform/embedder:embedder_headers`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/BUILD.gn#L210)|| +|[`//flutter/shell/platform/embedder:embedder_as_internal_library`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/BUILD.gn#L206)|Enforce dynamic `FlutterEngineProcTable` resolution with`defines = [ "FLUTTER_ENGINE_NO_PROTOTYPES" ]`| +|[`//flutter/fml`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/fml)|Optional| +|[`//flutter/common`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/common)|Optional| + +All direct dependencies on internal engine subsystems ([`//flutter/shell/common`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/common), [`//flutter/runtime`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/runtime), [`//flutter/flow`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/flow), [`//flutter/skia`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/skia), [`//flutter/impeller`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/impeller), [`//flutter/lib/ui`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/lib/ui), [`//flutter/txt`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/txt), and [`//flutter/assets`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/assets)) will be eliminated. + +### Goals + +- Establish an enforced architectural boundary between Android embedder and the engine core. +- Reduce direct internal engine header inclusions in [`shell/platform/android/`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android) from 98 to 0. +- Enable independent compilation and stable ABI boundaries for Android embeddings. +- Maintain complete behavioral correctness, performance parity, and accessibility support across all supported Android API levels and rendering backends (Impeller Vulkan, Impeller OpenGL, and legacy Skia). +- Ensure the embedder API exposes the features needed to create an out-of-tree embedder that is as feature-complete as Flutter Android. + +### Non-Goals + +- Modifying the public Java/Kotlin Flutter Android API ([`io.flutter.embedding.android.*`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/io/flutter/embedding/android)). +- Repackaging or splitting `libflutter.so` into separate shared libraries (such as `libflutter_engine.so` and `libflutter_embedder.so`). Compilation remains unified within `libflutter.so` to avoid symbol export inflation, increased APK binary size, and runtime dynamic linker overhead on Android. + +## Background and Problem Statement + +The Android embedder in [`engine/src/flutter/shell/platform/android/`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android) bypasses [`embedder.h`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder.h). [`FlutterJNI`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java#L108) instantiates [`AndroidShellHolder`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/android_shell_holder.h#L38), which creates [`flutter::ThreadHost`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/common/thread_host.h#L21) and [`flutter::Shell`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/common/shell.h#L113) directly, while [`PlatformViewAndroid`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/platform_view_android.h#L43) subclasses internal [`flutter::PlatformView`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/common/platform_view.h#L51): + +```mermaid +flowchart TD + subgraph Java Layer + Activity["FlutterActivity / FlutterFragment"] + FlutterView["FlutterView"] + FlutterJNI["FlutterJNI"] + end + + subgraph Android Embedder JNI + AndroidShellHolder["AndroidShellHolder"] + PlatformViewAndroid["PlatformViewAndroid"] + end + + subgraph Flutter Engine Core + Shell["Shell"] + PlatformView["PlatformView"] + TaskRunners["TaskRunners: Platform, UI, Raster, IO"] + ThreadHost["ThreadHost"] + Engine["Engine"] + Rasterizer["Rasterizer"] + end + + Activity --> FlutterView + FlutterView --> FlutterJNI + FlutterJNI --> AndroidShellHolder + AndroidShellHolder --> PlatformViewAndroid + AndroidShellHolder --> Shell + AndroidShellHolder --> ThreadHost + AndroidShellHolder --> TaskRunners + PlatformViewAndroid -- Subclasses --> PlatformView + PlatformViewAndroid -. Delegate .-> Shell + Shell --> Engine + Shell --> Rasterizer +``` + +```mermaid +sequenceDiagram + autonumber + participant FlutterJNI as "jni : FlutterJNI" + participant Holder as "holder : AndroidShellHolder" + participant PV as "platformView : PlatformViewAndroid" + participant Shell as "shell : Shell" + participant Raster as "Raster Thread" + + FlutterJNI->>Holder: AttachJNI (construct AndroidShellHolder) + Holder->>Holder: Create ThreadHost (Platform, UI, Raster, IO) + Holder->>Shell: Shell::Create(settings, task_runners, on_create_platform_view) + Shell->>PV: on_create_platform_view (uninitialized AndroidContextDynamicImpeller) + Shell-)Raster: Post CreateImpellerContext (async on FIFO raster queue) + Shell-->>Holder: Return Shell (does not wait for SetupImpellerContext) + par Raster Thread Initializes Impeller Context + Raster->>PV: SetupImpellerContext() (probe Vulkan / fallback to GLES) + and Platform Thread Launches Dart Entrypoint + FlutterJNI->>Holder: RunBundleAndSnapshotFromLibrary (Launch) + Holder->>Shell: RunEngine(RunConfiguration) + end + FlutterJNI->>PV: SurfaceCreated(ANativeWindow) -> NotifyCreated (runs on Raster after SetupImpellerContext) +``` + +
+shell/platform/android/ includes 98 internal engine headers across 9 subsystems + +| Subsystem & Headers | Direct GN Dependencies | Key Internal Headers | Functional Purpose in Embedder | +| :--- | :--- | :--- | :--- | +| **Impeller & Graphics Context Setup**
(29 headers) | [`//flutter/impeller`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/impeller)
[`//flutter/impeller/toolkit/android`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/impeller/toolkit/android)
[`//flutter/impeller/toolkit/egl`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/impeller/toolkit/egl)
[`//flutter/impeller/toolkit/gles`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/impeller/toolkit/gles)
[`//flutter/impeller/toolkit/glvk`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/impeller/toolkit/glvk) | [`renderer/backend/vulkan/context_vk.h`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/impeller/renderer/backend/vulkan/context_vk.h)
[`surface_context_vk.h`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/impeller/renderer/backend/vulkan/surface_context_vk.h)
[`renderer/backend/gles/context_gles.h`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/impeller/renderer/backend/gles/context_gles.h)
[`toolkit/android/hardware_buffer.h`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/impeller/toolkit/android/hardware_buffer.h)
[`renderer/backend/vulkan/android/ahb_texture_source_vk.h`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/impeller/renderer/backend/vulkan/android/ahb_texture_source_vk.h) | Initializing rendering contexts on the raster thread ([`SetupImpellerContext`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/platform_view_android.h#L128)), creating Vulkan and EGL swapchains, and managing zero-copy [`AHardwareBuffer`](https://developer.android.com/ndk/reference/group/a-hardware-buffer) imports. | +| **Platform Views & Layer Compositing**
(2 headers) | [`//flutter/flow`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/flow) | [`flow/embedded_views.h`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/flow/embedded_views.h)
[`flow/surface.h`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/flow/surface.h) | Platform view compositing across Hybrid Composition++ (HCPP via [`SurfaceControl`](https://developer.android.com/reference/android/view/SurfaceControl)), Hybrid Composition (HC), Texture Layer Hybrid Composition (TLHC), and Virtual Displays (VD); managing geometric clipping and transforms via [`flutter::MutatorsStack`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/flow/embedded_views.h#L211), view slicing, surface pooling ([`surface_pool.h`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/external_view_embedder/surface_pool.h)), and multi-surface composition frame results ([`external_view_embedder_2.h`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/external_view_embedder/external_view_embedder_2.h)). | +| **External Textures & Surface Control**
(5 headers) | [`//flutter/common/graphics`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/common/graphics)
[`//flutter/display_list`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/display_list) | [`common/graphics/texture.h`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/common/graphics/texture.h)
[`common/graphics/gl_context_switch.h`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/common/graphics/gl_context_switch.h)
[`display_list/image/dl_image_skia.h`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/display_list/image/dl_image_skia.h)
[`display_list/geometry/dl_geometry_types.h`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/display_list/geometry/dl_geometry_types.h) | Texture lifecycle and frame updates for Android [`SurfaceTexture`](https://developer.android.com/reference/android/graphics/SurfaceTexture) (OpenGL ES) and [`SurfaceProducer`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/io/flutter/view/TextureRegistry.java#L21) (Vulkan); feeding decoded video and camera frames into the [`DisplayList`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/display_list/display_list.h) compositor. | +| **Engine Core, Lifecycle & Threading**
(11 headers) | [`//flutter/shell/common`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/common) | [`shell/common/shell.h`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/common/shell.h)
[`thread_host.h`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/common/thread_host.h)
[`platform_view.h`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/common/platform_view.h)
[`rasterizer.h`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/common/rasterizer.h)
[`run_configuration.h`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/common/run_configuration.h)
[`vsync_waiter.h`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/common/vsync_waiter.h)
[`snapshot_surface_producer.h`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/common/snapshot_surface_producer.h) | Direct instantiation of [`flutter::Shell::Create(...)`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/common/shell.h#L170-L176), thread host allocation (Platform, UI, Raster, IO), vsync scheduling, snapshot generation, and rasterizer coordination. | +| **UI Layer & Message Dispatch**
(5 headers) | [`//flutter/lib/ui`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/lib/ui) | [`lib/ui/window/platform_message.h`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/lib/ui/window/platform_message.h)
[`platform_message_response.h`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/lib/ui/window/platform_message_response.h)
[`lib/ui/plugins/callback_cache.h`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/lib/ui/plugins/callback_cache.h)
[`lib/ui/painting/image_generator.h`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/lib/ui/painting/image_generator.h) | Binary message encoding and decoding for platform channels, background Dart entrypoint lookup ([`PluginUtilities.getCallbackHandle`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/lib/ui/plugins.dart#L54) via [`callback_cache.h`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/lib/ui/plugins/callback_cache.h)), and native platform image decoding. | +| **Dart VM, Runtime & Service Isolate**
(2 headers) | [`//flutter/runtime`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/runtime)
[`//flutter/runtime:libdart`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/runtime/BUILD.gn) | [`runtime/dart_vm.h`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/runtime/dart_vm.h)
[`runtime/dart_service_isolate.h`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/runtime/dart_service_isolate.h) | Managing the Dart VM lifecycle, configuring concurrent message loops, isolate creation, and Dart service isolate initialization. | +| **Asset Management & Packaging**
(1 header) | [`//flutter/assets`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/assets) | [`assets/asset_resolver.h`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/assets/asset_resolver.h) | Resolving assets and kernel blobs packaged within the Android APK via [`APKAssetProvider`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/apk_asset_provider.h#L17). | +| **GPU Backends & Surface Delegates**
(5 headers) | [`//flutter/shell/gpu`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/gpu)
[`//flutter/vulkan`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/vulkan) | [`shell/gpu/gpu_surface_gl_impeller.h`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/gpu/gpu_surface_gl_impeller.h)
[`gpu_surface_vulkan_impeller.h`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/gpu/gpu_surface_vulkan_impeller.h)
[`vulkan/vulkan_native_surface_android.h`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/vulkan/vulkan_native_surface_android.h) | Implementing platform window surface delegates for OpenGL and Vulkan render pipelines. | +| **Foundational Utilities & Concurrency**
(24 headers) | [`//flutter/fml`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/fml) | [`fml/raster_thread_merger.h`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/fml/raster_thread_merger.h)
[`fml/task_runner.h`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/fml/task_runner.h)
[`fml/message_loop.h`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/fml/message_loop.h)
[`fml/concurrent_message_loop.h`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/fml/concurrent_message_loop.h)
[`fml/platform/android/jni_util.h`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/fml/platform/android/jni_util.h)
[`fml/platform/android/scoped_java_ref.h`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/fml/platform/android/scoped_java_ref.h) | Message loops, JNI handle wrappers, task scheduling, and raster thread leasing. | + +
+ +## Detailed Design + +The following sections specify the [`embedder.h`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder.h) additions and behavioral changes required to decouple the Android embedder from internal engine headers. + +### Initialization + +> [!IMPORTANT] +> +> - **Embedder API additions (`embedder.h`)**: +> - `FlutterRendererType::kDeferred` (`FlutterDeferredRendererConfig`) on [`FlutterRendererConfig`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder.h#L1055) +> - `FlutterRendererConfigResolveCallback` +> - **Behavioral changes**: +> - [`PlatformViewEmbedder`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/platform_view_embedder.h#L36), [`EmbedderSurfaceVulkanImpeller`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder_surface_vulkan_impeller.h), and [`EmbedderSurfaceGLImpeller`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder_surface_gl_impeller.h) move [`impeller::Context`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/impeller/renderer/context.h#L70) creation from their UI-thread constructors into `PlatformViewEmbedder::SetupImpellerContext()` on the raster thread. +> - `PlatformViewEmbedder::NotifyCreated()` queues surface creation on the FIFO raster task runner instead of blocking the UI thread on a latch. +> - `AndroidSurfaceControl` performs all Android Vulkan eligibility checks inside `resolve_callback` before creating a `VkDevice` to return `kVulkan` or `kOpenGL` in a single call. +> - The `VkPhysicalDeviceProperties` GPU parser and known-bad-driver check in [`DriverInfoVK::IsKnownBadDriver()`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/impeller/renderer/backend/vulkan/driver_info_vk.cc#L379-L431) move into [`//flutter/common`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/common) (so `//flutter/impeller`, `AndroidSurfaceControl`, and other embedders share a single implementation), while the required Android Vulkan capability checks in [`CapabilitiesVK`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/impeller/renderer/backend/vulkan/capabilities_vk.cc#L98-L529) move into `AndroidSurfaceControl` alongside the Android system-property and SoC checks from [`android_context_dynamic_impeller.cc`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/android_context_dynamic_impeller.cc#L23-L119). +> - During `FlutterEngineInitialize`, the Android embedder configures [`FlutterCompositor`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder.h#L2266-L2322) (`FlutterProjectArgs.compositor`) with [`avoid_backing_store_cache = true`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder.h#L2312) to turn off the engine's backing store cache ([`EmbedderRenderTargetCache`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder_render_target_cache.h)) so the Android embedder is responsible for managing surfaces. + +
+Why shell/platform/embedder/ must change, what Impeller checks move to //flutter/common and the Android embedder, and why backing store caches are disabled + +[`PlatformView::SetupImpellerContext()`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/common/platform_view.h#L1040) was added in [#167765](https://github.com/flutter/flutter/pull/167765) because Vulkan driver and context initialization can take 100+ ms on Android devices, but [#167765](https://github.com/flutter/flutter/pull/167765) only wired it up in `PlatformViewAndroid`: + +- [`EmbedderSurfaceVulkanImpeller`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder_surface_vulkan_impeller.cc#L76-L95) and [`EmbedderSurfaceGLImpeller`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder_surface_gl_impeller.cc#L98-L125) currently construct `impeller::ContextVK` and `impeller::ContextGLES` synchronously in their constructors on the UI thread during `FlutterEngineRunInitialized`. +- [`PlatformView::NotifyCreated()`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/common/platform_view.cc#L64-L85) (called by `FlutterEngineRunInitialized` right after `LaunchShell()`) posts `CreateRenderingSurface()` to the FIFO raster task runner behind `CreateImpellerContext` and blocks the UI thread on `latch.Wait()`. +- **Impeller Vulkan checks moved to `//flutter/common` and the Android embedder**: Today, [`GetActualRenderingAPIForImpeller`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/android_context_dynamic_impeller.cc#L79-L139) checks Android system properties (`IsDeviceEmulator`, `ro.com.google.clientidbase == "android-huawei"`, MediaTek API < 32, and `kBadSocs` in `ro.product.board`) and then calls `ContextVK::Create` and `DriverInfoVK::IsKnownBadDriver()`. Because `AndroidSurfaceControl` owns `VkInstance`, `VkPhysicalDevice`, and `VkDevice` creation under `embedder.h`, the following Impeller checks are made available directly to `AndroidSurfaceControl`: + 1. **Physical device and driver blocklist in [`//flutter/common`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/common) (from [`driver_info_vk.cc#L20-L431`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/impeller/renderer/backend/vulkan/driver_info_vk.cc#L20-L431))**: Inspecting `VkPhysicalDeviceProperties` (`vendorID`, `deviceID`, `driverVersion`, `apiVersion`, `deviceType`, `deviceName`) to block Adreno <= 650 (`GetAdrenoVersion`), Huawei Maleoon (`0x19E5`), Samsung Xclipse (`0x144D`) with Vulkan < 1.3, PowerVR series older than `kCXT` (`GetPowerVRVersion`), Pixel 10 (`0x71061212`) with driver < `6794074`, and SwiftShader CPU emulators (`IsEmulator`). Placing this pure `VkPhysicalDeviceProperties` parser in `//flutter/common` allows `//flutter/impeller` (`workarounds_vk.cc`), `AndroidSurfaceControl`, and other embedders on Adreno/PowerVR/Mali hardware to share the same table without duplicating checks or adding a driver-query API to `embedder.h`. + 2. **Required Vulkan extensions and physical device capabilities in `AndroidSurfaceControl` ([`capabilities_vk.cc#L98-L529`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/impeller/renderer/backend/vulkan/capabilities_vk.cc#L98-L529))**: Verifying `VK_KHR_surface` and `VK_KHR_android_surface`; verifying required Android device extensions (`VK_KHR_swapchain`, `VK_ANDROID_external_memory_android_hardware_buffer`, `VK_KHR_sampler_ycbcr_conversion`, `VK_KHR_external_memory`, `VK_EXT_queue_family_foreign`, `VK_KHR_dedicated_allocation`); checking `VK_FORMAT_R8G8B8A8_UNORM` color and `D32_SFLOAT_S8_UINT` / `D24_UNORM_S8_UINT` depth-stencil attachment support, `1x`/`4x` sample counts, graphics/compute/transfer queue families, and enabling `samplerYcbcrConversion` and `uniformAndStorageBuffer16BitAccess` when calling `vkCreateDevice`. +- **Disabling backing store caches (`avoid_backing_store_cache = true`) so the Android embedder owns surfaces**: In the legacy architecture, [`PlatformViewAndroid`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/platform_view_android.h#L43) called [`PlatformView::NotifyCreated`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/common/platform_view.cc#L64-L85) and [`PlatformView::NotifyDestroyed`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/common/platform_view.cc#L87-L89) whenever an `ANativeWindow*` attached or detached because `AndroidSurface` rendered directly into the window. Passing [`FlutterCompositor`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder.h#L2266-L2322) with `avoid_backing_store_cache = true` during `FlutterEngineInitialize` configures the engine's internal GPU surface as context-only (`render_to_surface = false`, [#193000](https://github.com/flutter/flutter/pull/193000)) and bypasses [`EmbedderRenderTargetCache`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder_render_target_cache.h). The engine requests each backing store (`FlutterBackingStoreConfig`) inside the frame that uses it ([`embedder_external_view_embedder.cc#L561-L572`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder_external_view_embedder.cc#L561-L572)) and collects it as soon as `present_view_callback` returns ([`#L627-L643`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder_external_view_embedder.cc#L627-L643)). When no window is attached or the OS invalidates the surface mid-frame, `create_backing_store_callback` returns `false` and the engine skips that layer ([`#L261-L265`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder_external_view_embedder.cc#L261-L265)). `AndroidSurfaceControl` therefore manages `ANativeWindow*` swapchains and overlay surfaces across `surfaceCreated`, `surfaceChanged`, and `surfaceDestroyed` (and calls [`FlutterEngineNotifyLowMemoryWarning`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder.h#L3553-L3554) on `onTrimMemory` and `surfaceDestroyed` to release Impeller's transient offscreen render-target and texture caches). + +
+ +```c +// FlutterRendererConfig gains a struct tag so that the callback below can +// refer to it. Adding a tag to a typedef'd struct is source and ABI compatible. +typedef struct FlutterRendererConfig FlutterRendererConfig; + +typedef enum { + // ... existing values ... + /// Optional renderer type where the embedder chooses the concrete renderer on + /// the raster thread while the engine launches. See + /// |FlutterDeferredRendererConfig|. + kDeferred, +} FlutterRendererType; + +/// Callback invoked by the engine on the raster thread to resolve a deferred +/// renderer configuration into a concrete one. +/// +/// Embedders should prefer `kVulkan` (or `kMetal` on Apple platforms) when +/// supported by the device and treat `kOpenGL` as a fallback. +/// +/// @param[in] user_data The `user_data` passed to `FlutterEngineInitialize` +/// or `FlutterEngineRun`. +/// @param[out] config_out Zero-initialized, engine-owned storage. Set `type` +/// to `kVulkan`, `kMetal`, `kOpenGL`, or `kSoftware` +/// and populate the corresponding union member +/// (`kDeferred` is not allowed). All memory referenced +/// by the configuration must remain valid until +/// `FlutterEngineDeinitialize` or +/// `FlutterEngineShutdown` returns for this engine and +/// any engine spawned from it. +/// @return `false` if no renderer can be created. +typedef bool (*FlutterRendererConfigResolveCallback)( + void* /* user_data */, + FlutterRendererConfig* /* config_out */); + +typedef struct { + /// The size of this struct. Must be sizeof(FlutterDeferredRendererConfig). + size_t struct_size; + /// Required. Invoked once on the raster task runner before the engine creates + /// its rendering context, while the engine launches. The engine creates its + /// rendering surface after this callback returns, so only + /// `FlutterEngineDeinitialize` and `FlutterEngineShutdown` wait for it. The + /// callback may block, but it must not wait on the platform thread. Engines + /// created with `FlutterEngineSpawn` reuse the resolved configuration and + /// rendering context; the callback is not invoked for them. + FlutterRendererConfigResolveCallback resolve_callback; +} FlutterDeferredRendererConfig; + +struct FlutterRendererConfig { + FlutterRendererType type; + union { + FlutterOpenGLRendererConfig open_gl; + FlutterSoftwareRendererConfig software; + FlutterMetalRendererConfig metal; + FlutterVulkanRendererConfig vulkan; + FlutterDeferredRendererConfig deferred; + }; +}; +``` + +```mermaid +sequenceDiagram + autonumber + participant UI as "UI Thread" + participant Emb as "engine : PlatformViewEmbedder" + participant Raster as "Raster Thread" + participant SC as "surfaceControl : AndroidSurfaceControl" + + UI->>Emb: FlutterEngineInitialize(FlutterRendererConfig{kDeferred}, FlutterCompositor{avoid_backing_store_cache = true}) + UI->>Emb: FlutterEngineRunInitialized() + Emb-)Raster: LaunchShell() posts CreateImpellerContext + Emb-)Raster: NotifyCreated() queues surface creation (FIFO, no latch) + Emb->>Emb: RunRootIsolate() + Emb-->>UI: Return without waiting for the raster thread + par Raster thread resolves the renderer + Raster->>SC: resolve_callback(user_data, config_out) + SC->>SC: Check SoC, system properties, VkPhysicalDeviceProperties blocklist, and extensions + alt Vulkan supported and not blocklisted + SC-->>Raster: Populate config_out with kVulkan (VkInstance, VkDevice, VkQueue) + Raster->>Raster: Build EmbedderSurfaceVulkanImpeller and ContextVK::Create + else Fallback to OpenGL ES + SC-->>Raster: Populate config_out with kOpenGL (EGLDisplay, EGLContext) + Raster->>Raster: Build EmbedderSurfaceGLImpeller and ContextGLES::Create + end + Raster->>Raster: CreateRenderingSurface (context only) and Rasterizer::Setup + Raster-)UI: ScheduleFrame() + and UI Thread Inflates Activity View Hierarchy + UI->>UI: Activity.onStart() / onResume() / WindowManager layout + end + UI->>UI: surfaceCreated(ANativeWindow*): create swapchain, allow backing stores + UI->>Emb: FlutterEngineScheduleFrame(engine) +``` + +
+Alternatives + +- Probing Vulkan on a Java background thread in `FlutterLoader.initAsync` and caching the result in `SharedPreferences` was rejected because it duplicates Vulkan initialization in Java/JNI before `FlutterEngineInitialize` and cached driver eligibility can become stale across system OTA updates. +- Letting `ContextVK::Create` reject a `kVulkan` renderer config and re-invoke `resolve_callback` was rejected in favor of moving the `DriverInfoVK` blocklist to `//flutter/common` and the `CapabilitiesVK` checks into `AndroidSurfaceControl` so `resolve_callback` resolves `kVulkan` or `kOpenGL` in a single pass. +- Adding `FlutterEngineNotifyCreated` and `FlutterEngineNotifyDestroyed` C-API functions for `ANativeWindow*` lifecycle was rejected because setting `FlutterCompositor.avoid_backing_store_cache = true` and calling `FlutterEngineNotifyLowMemoryWarning` already lets `AndroidSurfaceControl` own surface creation and teardown without new `embedder.h` entry points. + +
+ +### Platform Views + +> [!IMPORTANT] +> +> - **Embedder API additions (`embedder.h`)**: +> - `FlutterBackingStoreConfig.is_root_layer` (`bool`) +> - `FlutterCompositor.merge_raster_thread_on_platform_views` (`bool`) +> - `FlutterCompositor.coalesce_overlay_layers` (`bool`) +> - **Behavioral changes**: +> - Replaces [`AndroidExternalViewEmbedder`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/external_view_embedder/external_view_embedder.h#L29) and [`AndroidExternalViewEmbedder2`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/external_view_embedder/external_view_embedder_2.h#L34) with [`FlutterCompositor`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder.h#L2264-L2322), using `is_root_layer` to distinguish the bottom-most background layer from full-screen overlay layers above a platform view. +> - [`EmbedderExternalViewEmbedder`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder_external_view_embedder.h#L30) implements `SupportsDynamicThreadMerging()` and `PostPrerollAction()` when `merge_raster_thread_on_platform_views` is `true`, dynamically merging the raster task runner onto the platform thread during Hybrid Composition (HC) frames. +> - When `coalesce_overlay_layers` is `true` (used by HCPP), [`EmbedderExternalViewEmbedder`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder_external_view_embedder.cc#L186-L246) collapses all non-root Flutter slices above platform views into a single top-most overlay backing store, clipping out higher-Z platform views with `DlClipOp::kDifference`. + +
+Why is_root_layer, merge_raster_thread_on_platform_views, and coalesce_overlay_layers are needed + +- `is_root_layer` (root vs. overlay backing stores): During `create_backing_store_callback(config, store_out)`, `config->size` equals the full window size for both the bottom-most background layer and any full-screen overlay layer above a platform view (`[Flutter_Root, PlatformView, Flutter_Overlay]`), and `present_view_callback` runs only after all layers are rendered. `is_root_layer` lets the embedder resize and bind the bottom-most layer directly to the on-screen `ANativeWindow*` swapchain or FBO (`avoid_backing_store_cache = true`) inside `create_backing_store_callback`, while allocating off-screen overlay surfaces ([`SurfacePool`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/external_view_embedder/surface_pool.h) / [`FlutterImageView`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/io/flutter/embedding/android/FlutterImageView.java#L38)) when `is_root_layer` is `false`. +- `merge_raster_thread_on_platform_views` (dynamic thread merging for HC): In Hybrid Composition (HC), [`PlatformViewsController`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java#L951-L1084) mutates live Android `View` instances (`FlutterMutatorView`, `FlutterView.convertToImageView()`) on the platform thread during frame presentation. If the raster thread remained unmerged and blocked on a platform-thread latch during `present_view_callback`, `FlutterView.convertToImageView()` on the platform thread would simultaneously block waiting on the raster thread (`NotifySurfaceWindowChanged` / `waitUntilimpellerTasksFinished`), causing a deadlock. Setting `merge_raster_thread_on_platform_views = true` dynamically merges the raster task runner onto the platform thread during HC frames so both execute on the same thread without cross-thread blocking. +- `coalesce_overlay_layers` (single overlay surface coalescing for HCPP): In HCPP, [`PlatformViewsController2`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController2.java#L835-L843) places all platform views inside the Activity's `ViewRootImpl` surface (at Z = 0) and maintains a single persistent [`overlayerSurface`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController2.java#L838) (`SurfaceControl` at Z = 1000) above them. By default, [`EmbedderExternalViewEmbedder::LayerBuilder`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder_external_view_embedder.cc#L186-L246) allocates a separate overlay `FlutterBackingStore` (and clears it on the first slice) whenever a higher-Z platform view overlaps an earlier Flutter overlay slice (`[Flutter_Root, PV_1, Flutter_Mid, PV_2, Flutter_Top]`). Setting `coalesce_overlay_layers = true` instructs `EmbedderExternalViewEmbedder` to merge all non-root Flutter slices into one top-most overlay backing store and clip out the bounding rects of higher-Z platform views using `DlClipOp::kDifference` (matching [`AndroidExternalViewEmbedder2::SubmitFlutterView`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/external_view_embedder/external_view_embedder_2.cc#L168-L181)). + +
+ +
+Platform View Selection + +```mermaid +flowchart TD + Req["Platform View Request in PlatformViewsController"] --> IsHCPP{"enable_surface_control AND API 34+ AND Impeller Vulkan?"} + IsHCPP -- "Yes" --> HCPP["HCPP: configureForHybridComposition++ (FlutterCompositor, coalesce_overlay_layers = true, unmerged threads)"] + IsHCPP -- "No" --> CreationPath{"Plugin Creation Method"} + CreationPath -- "initExpensiveAndroidView (createForPlatformViewLayer)" --> HC["HC: configureForHybridComposition (FlutterCompositor, merge_raster_thread_on_platform_views = true)"] + CreationPath -- "initAndroidView (createForTextureLayer)" --> HasSurfaceView{"View hierarchy contains SurfaceView (VIEW_TYPES_REQUIRE_NON_TLHC)?"} + HasSurfaceView -- "No" --> TLHC["TLHC: configureForTextureLayerComposition (External Texture C-API)"] + HasSurfaceView -- "Yes, TEXTURE_WITH_VIRTUAL_FALLBACK" --> VD["VD: configureForVirtualDisplay (External Texture C-API)"] + HasSurfaceView -- "Yes, TEXTURE_WITH_HYBRID_FALLBACK" --> HC +``` + +
+ +```c +typedef struct { + /// The size of this struct. Must be sizeof(FlutterBackingStoreConfig). + size_t struct_size; + /// The size of the render target the engine expects to render into. + FlutterSize size; + /// The identifier for the view that the engine will use this backing store to + /// render into. + FlutterViewId view_id; + /// True when this backing store is for the bottom-most layer of the frame + /// for `view_id`, with no platform view composited beneath it, and false + /// otherwise. The engine requests at most one such backing store per view + /// per frame, and none when all Flutter contents are composited above + /// platform views. Read this field only if `struct_size` covers it, and + /// treat it as false otherwise. + /// + /// An embedder that returns an uncacheable on-screen render target (for + /// example, the next swapchain image or default framebuffer) when this is + /// true must set `FlutterCompositor.avoid_backing_store_cache` to `true` and + /// resize that target to `size` inside `create_backing_store_callback` + /// before returning it (or in `present_view_callback` on frames that have no + /// root backing store). + bool is_root_layer; +} FlutterBackingStoreConfig; +``` + +#### Opt-In Dynamic Raster Thread Merging and Overlay Layer Coalescing + +The Android embedder sets `merge_raster_thread_on_platform_views = true` and `coalesce_overlay_layers = false` when HCPP (`enable_surface_control`) is not active, and sets `merge_raster_thread_on_platform_views = false` and `coalesce_overlay_layers = true` when HCPP is active: + +```c +typedef struct { + // ... existing fields ... + /// When true, the engine dynamically merges the raster task runner onto the + /// platform task runner for frames that composite one or more + /// `kFlutterLayerContentTypePlatformView` layers, holding a short frame + /// lease before unmerging once platform views leave the scene. + /// + /// While the threads are merged, `create_backing_store_callback`, + /// `collect_backing_store_callback`, and `present_view_callback` (or + /// `present_layers_callback`) execute on the platform thread. + /// + /// Requires engine-managed message loop task queues for the platform and + /// render threads (`FlutterCustomTaskRunners.platform_task_runner` and + /// `render_task_runner` must be NULL so the engine binds `fml::MessageLoop` + /// to the calling platform thread and creates the raster thread). + bool merge_raster_thread_on_platform_views; + /// When true, the engine coalesces all non-root Flutter slices composited + /// above platform views into a single top-most overlay `FlutterBackingStore` + /// per view per frame, clipping out the bounding rectangles of any higher-Z + /// platform views using `DlClipOp::kDifference`. + /// + /// When false, the engine creates a separate overlay `FlutterBackingStore` + /// whenever a higher-Z platform view overlaps an earlier Flutter overlay + /// slice. + bool coalesce_overlay_layers; +} FlutterCompositor; +``` + +```mermaid +sequenceDiagram + autonumber + participant Raster as "Raster Thread" + participant Plat as "Platform Thread (Merged)" + participant Emb as "embedder : EmbedderExternalViewEmbedder" + participant SC as "surface : AndroidSurfaceControl" + participant PVC as "controller : PlatformViewsController" + + Note over Raster,PVC: 1. Unmerged Raster Thread Detects First HC Platform View (No Latch / No Deadlock) + Raster->>Emb: Preroll() + PostPrerollAction() (!IsMerged) + Emb->>Emb: CancelFrame() + MergeWithLease(10 frames) + Emb-->>Raster: kSkipAndRetryFrame (never blocks on Platform Thread) + Raster-)Plat: Retry frame on merged Platform Thread + + Note over Plat,PVC: 2. First Merged Pass: Swap to FlutterImageView Inline Without Deadlock + Plat->>Emb: Preroll() + PostPrerollAction() (IsMerged, previous_frame_view_count_ == 0) + Emb->>Emb: ExtendLeaseTo(10) + Emb-->>Plat: kResubmitFrame (skips PrepareBackingStore & Render) + Plat->>Emb: SubmitFlutterView() -> present_view_callback(platform view layers only) + Emb->>PVC: onDisplayPlatformView() -> FlutterView.convertToImageView() + PVC->>SC: swapSurface() -> NotifySurfaceWindowChanged(FlutterImageView ANativeWindow*) + Note over Plat,SC: Runs inline on merged Platform Thread (unmerged latch would deadlock here!) + PVC->>PVC: onEndFrame() keeps FlutterMutatorView View.GONE (no image queued yet) + Plat->>Plat: DrawToSurfaceUnsafe() sees kResubmitFrame -> immediate resubmit pass + + Note over Plat,PVC: 3. Resubmitted Pass: is_root_layer Sizing in create_backing_store_callback & Render + Plat->>Emb: Preroll() + PostPrerollAction() -> kSuccess + Plat->>Emb: PrepareBackingStore() before Render() + Emb->>SC: create_backing_store_callback(size, is_root_layer = true) + SC->>SC: Resize on-screen FlutterImageView swapchain to config->size & acquire root image + Emb->>SC: create_backing_store_callback(size, is_root_layer = false) + SC->>PVC: Create/resize off-screen overlay surface (SurfacePool) to config->size + Emb->>Emb: Render() root layer and overlay layer into backing stores + Emb->>PVC: present_view_callback(root layer, platform view, overlay layer) + onEndFrame() + PVC->>PVC: Acquire ImageReader buffer & set FlutterMutatorView View.VISIBLE + + Note over Raster,PVC: 4. Subsequent Frames After Last HC View Removed + Plat->>PVC: onEndFrame() -> revertImageView() (returns to FlutterSurfaceView) + loop 10 frames without platform views + Plat->>Plat: Rasterizer::DoDraw() decrements lease until unmerged + end +``` + +When HCPP (`enable_surface_control` on API 34+ with Impeller Vulkan) is active, `merge_raster_thread_on_platform_views` is `false`, `coalesce_overlay_layers` is `true`, and `AndroidSurfaceControl` exports an acquire fence on its `VkQueue` inside `present_view_callback`: + +```mermaid +sequenceDiagram + autonumber + participant Impeller as "impeller : ContextVK" + participant Engine as "engine : PlatformViewEmbedder" + participant Embedder as "surfaceControl : AndroidSurfaceControl" + participant SC as "transaction : SurfaceControl.Transaction" + participant SF as "compositor : SurfaceFlinger" + + Impeller->>Impeller: Record and submit render pass to VkQueue (FlushCommandBuffers) + Impeller->>Engine: Finish backing store render + Engine->>Embedder: present_view_callback with ordered FlutterLayers + Embedder->>Embedder: Submit barrier on VkQueue and export VkSemaphore via vkGetSemaphoreFdKHR + Embedder->>SC: setBuffer with surfaceControl, hardwareBuffer, fenceFd + Embedder->>SC: sealFrameTransactions and swapTransactions + SC->>SF: Commit transaction with acquire fence + SF->>SF: GPU waits on acquire fence before compositing + SF-->>Embedder: Release fence signaled when buffer reading completes + Embedder->>Embedder: Return buffer to AndroidSurfaceControl for recycling +``` + +
+Alternatives + +- Running Hybrid Composition (HC) with unmerged threads and blocking the raster thread on a platform-thread latch during `present_view_callback` was rejected because `FlutterView.convertToImageView()` on the first HC frame calls `NotifySurfaceWindowChanged`, which deadlocks if the raster thread is blocked waiting on the platform thread. +- Adding a `synchronization_fence_fd` field to `FlutterBackingStorePresentInfo` for HCPP was rejected because `AndroidSurfaceControl` owns the `VkDevice` and `VkQueue` and can submit and export its own `VkSemaphore` via `vkGetSemaphoreFdKHR` inside `present_view_callback`. + +
+ +### Platform View Mutator Stack Serialization + +> [!IMPORTANT] +> +> - **Embedder API additions (`embedder.h`)**: +> - `FlutterPath` (`FlutterPathFillType`, `FlutterPathVerb`) +> - `kFlutterPlatformViewMutationTypeClipPath` on `FlutterPlatformViewMutationType` (`FlutterPlatformViewMutation.clip_path`) +> - `FlutterCompositor.platform_view_mutation_capabilities` (`FlutterPlatformViewMutationCapability`) +> - **Behavioral changes**: +> - Arbitrary path clips serialize into an ABI-stable structure-of-arrays `FlutterPath` layout instead of exposing `SkPath` or `//flutter/flow` headers. +> - On Android API 34+ (`android.graphics.Path.conicTo`), the Android compositor sets `kFlutterPlatformViewMutationCapabilityPathConics` to receive exact conic path verbs (`kFlutterPathVerbConic`). +> - `AndroidMutatorsMapper` translates `FlutterPlatformView.mutations` into Java [`FlutterMutatorView`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorView.java#L32) and [`SurfaceControl.Transaction`](https://developer.android.com/reference/android/view/SurfaceControl.Transaction) transforms. + +
+Why FlutterPlatformViewMutation needs FlutterPath and capability negotiation + +[`EmbedderLayers::PushPlatformViewLayer`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder_layers.cc#L102-L103) currently drops `MutatorType::kClipPath` (`"Unsupported mutation"`), whereas Android's [`PlatformViewAndroidJNIImpl`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/platform_view_android_jni_impl.cc#L1834-L1855) and HCPP apply arbitrary path clips from [`flutter::MutatorsStack`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/flow/embedded_views.h#L211). Serializing `DlPath` into a structure-of-arrays `FlutterPath` (`verbs`, `points`, `conic_weights`, `bounds`) avoids exposing internal C++ path types across `embedder.h`, and `kFlutterPlatformViewMutationCapabilityPathConics` lets Android API 34+ (`Path.conicTo`) consume native conic verbs while older API levels receive quadratic Beziers converted by the engine. +
+ +```c +typedef enum { + kFlutterPlatformViewMutationTypeOpacity, + kFlutterPlatformViewMutationTypeClipRect, + kFlutterPlatformViewMutationTypeClipRoundedRect, + kFlutterPlatformViewMutationTypeTransformation, + kFlutterPlatformViewMutationTypeClipPath, +} FlutterPlatformViewMutationType; + +typedef struct { + FlutterPlatformViewMutationType type; + union { + double opacity; + FlutterRect clip_rect; + FlutterRoundedRect clip_rounded_rect; + FlutterTransformation transformation; + FlutterPath clip_path; + }; +} FlutterPlatformViewMutation; + +typedef enum { + kFlutterPathFillTypeNonZero, + kFlutterPathFillTypeEvenOdd, +} FlutterPathFillType; + +typedef enum { + kFlutterPathVerbMove, + kFlutterPathVerbLine, + kFlutterPathVerbQuad, + kFlutterPathVerbConic, + kFlutterPathVerbCubic, + kFlutterPathVerbClose, +} FlutterPathVerb; + +typedef struct { + /// The size of this struct. Must be sizeof(FlutterPath). + size_t struct_size; + /// The fill rule for this path. + FlutterPathFillType fill_type; + /// The number of entries in `verbs`. + size_t verbs_count; + /// The path verbs, in drawing order. Every contour begins with + /// `kFlutterPathVerbMove`. Valid only for the duration of the frame + /// presentation callback. + const FlutterPathVerb* verbs; + /// The number of entries in `points`. Equals the sum of the points used by + /// each verb: move 1, line 1, quad 2, conic 2, cubic 3, close 0. + size_t points_count; + /// The points, consumed in verb order. Valid only for the duration of the + /// frame presentation callback. + const FlutterPoint* points; + /// The number of entries in `conic_weights`. Equals the number of + /// `kFlutterPathVerbConic` verbs. + size_t conic_weights_count; + /// The conic weights, consumed in verb order. Valid only for the duration of + /// the frame presentation callback. + const double* conic_weights; + /// Conservative control-point bounds containing all points of the path, in + /// the same coordinate space. An embedder that cannot clip to a path clips + /// to this rectangle. + FlutterRect bounds; +} FlutterPath; + +/// Optional platform view mutation features that a compositor can consume. +/// +/// The engine only emits a mutation type or path verb that is gated by one of +/// these bits when the compositor sets that bit in +/// `FlutterCompositor.platform_view_mutation_capabilities`. +typedef enum { + /// Only the default mutation types and path verbs are emitted. + kFlutterPlatformViewMutationCapabilityDefault = 0, + /// Clip paths may contain `kFlutterPathVerbConic` verbs. Without this bit, + /// the engine converts conics to quadratic Bezier curves before handing + /// paths to the embedder. + kFlutterPlatformViewMutationCapabilityPathConics = 1 << 0, +} FlutterPlatformViewMutationCapability; + +typedef struct { + // ... existing fields ... + /// Bitmask of `FlutterPlatformViewMutationCapability` values supported by + /// this compositor. + uint64_t platform_view_mutation_capabilities; +} FlutterCompositor; + +typedef struct { + size_t struct_size; + FlutterPlatformViewIdentifier identifier; + size_t mutations_count; + /// Applied in order. The first entry is a transformation mutation when a + /// device pixel ratio or root surface transformation is in effect, so that + /// subsequent mutations are already in the correct coordinate space. + const FlutterPlatformViewMutation** mutations; +} FlutterPlatformView; +``` + +
+Alternatives + +- Approximating arbitrary path clips with `bounds` (`kFlutterPlatformViewMutationTypeClipRect`) was rejected because legacy Android HC and HCPP already clip platform views to arbitrary paths. +- Using an array of tagged union command structs (`FlutterPathCommand`) instead of structure-of-arrays (`FlutterPath`) was rejected because a union per verb pads every `moveTo` and `lineTo` to the size of a cubic segment and prevents bulk-copying verb and point buffers. +- Unconditionally converting conics to quadratic Beziers or unconditionally emitting conics to all embedders was rejected in favor of `kFlutterPlatformViewMutationCapabilityPathConics` so Android API 34+ uses native `Path.conicTo` while older Android versions and other embedders receive engine-approximated quadratics. + +
+ +### External Textures + +> [!IMPORTANT] +> +> - **Embedder API additions (`embedder.h`)**: +> - `FlutterVulkanYcbcrConversionInfo` (`FlutterVulkanComponentMapping`, `FlutterVulkanComponentSwizzle`) +> - `FlutterVulkanExternalTexture.ycbcr_conversion_info` (extending [`FlutterVulkanExternalTexture` from #188855](https://github.com/flutter/flutter/pull/188855)) +> - `FlutterOpenGLExternalTexture` (with `has_uv_transform` and column-major `float uv_transform[16]`) +> - `FlutterOpenGLRendererConfig.external_texture_frame_callback` (`FlutterOpenGLTextureFrameCallback`, deprecating `gl_external_texture_frame_callback`) +> - **Behavioral changes**: +> - Replaces the internal `flutter::Texture` subclasses ([`ImageExternalTexture`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/image_external_texture.h#L36) and [`SurfaceTextureExternalTexture`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/surface_texture_external_texture.h#L33)) across [`SurfaceProducer`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/io/flutter/view/TextureRegistry.java#L21) (`AHardwareBuffer`) and [`SurfaceTexture`](https://developer.android.com/reference/android/graphics/SurfaceTexture) (`GL_TEXTURE_EXTERNAL_OES`). +> - The Vulkan renderer samples multi-planar and vendor-specific external YCbCr camera and video frames (`VkSamplerYcbcrConversion`) without exposing Android `AHardwareBuffer` types in `embedder.h`. +> - The OpenGL ES renderer applies `SurfaceTexture.getTransformMatrix()` UV transforms while keeping [`FlutterOpenGLTexture`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder.h#L519-L540) unchanged for backing store render targets. + +
+Why FlutterVulkanYcbcrConversionInfo and FlutterOpenGLExternalTexture are needed + +- **Vulkan YCbCr conversion**: Android hardware video decoders and camera pipelines produce multi-planar or vendor-proprietary YCbCr `AHardwareBuffer` frames (`VK_FORMAT_UNDEFINED` with a non-zero `VkAndroidHardwareBufferFormatPropertiesANDROID.externalFormat`). Sampling those `VkImage` handles in Impeller Vulkan requires a `VkSamplerYcbcrConversion` configured with the buffer's chroma offsets, range, model, and `externalFormat`, which [`FlutterVulkanExternalTexture`](https://github.com/flutter/flutter/pull/188855) previously lacked. +- **OpenGL ES UV transformation**: On OpenGL ES, [`SurfaceTexture.getTransformMatrix()`](https://developer.android.com/reference/android/graphics/SurfaceTexture#getTransformMatrix(float%5B%5D)) supplies a column-major $4 \times 4$ `float[16]` UV crop and rotation matrix per frame, which [`FlutterOpenGLTexture`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder.h#L519-L540) (a frozen struct without `struct_size` shared with `FlutterOpenGLBackingStore.texture`) cannot carry without breaking ABI compatibility. + +
+ +```mermaid +flowchart LR + subgraph Producers["Android TextureRegistry Producers"] + SP["SurfaceProducer (ImageReader AHardwareBuffer, API 29+)"] + ST["SurfaceTexture (GL_TEXTURE_EXTERNAL_OES + getTransformMatrix)"] + end + + subgraph Embedder["Android Embedder (Owns VkDevice & EGLContext)"] + VK_AHB["Import AHardwareBuffer -> VkImage + query VkAndroidHardwareBufferFormatPropertiesANDROID"] + VK_BLIT["SurfaceTextureExternalTextureVKImpeller: GL-to-AHardwareBuffer blit (bakes UV matrix)"] + GL_AHB["Import AHardwareBuffer -> EGLImageKHR (identity uv_transform)"] + GL_ST["updateTexImage() + read getTransformMatrix()"] + end + + subgraph CAPI["embedder.h External Texture Callbacks"] + VK_OUT["FlutterVulkanExternalTexture (+ optional FlutterVulkanYcbcrConversionInfo)"] + GL_OUT["FlutterOpenGLExternalTexture (+ float uv_transform[16])"] + end + + SP -- "Impeller Vulkan" --> VK_AHB --> VK_OUT + ST -- "Impeller Vulkan" --> VK_BLIT --> VK_OUT + SP -- "Impeller OpenGL ES" --> GL_AHB --> GL_OUT + ST -- "Impeller OpenGL ES" --> GL_ST --> GL_OUT +``` + +```c +/// VkComponentSwizzle values for FlutterVulkanComponentMapping. +typedef enum { + kFlutterVulkanComponentSwizzleIdentity = 0, + kFlutterVulkanComponentSwizzleZero = 1, + kFlutterVulkanComponentSwizzleOne = 2, + kFlutterVulkanComponentSwizzleR = 3, + kFlutterVulkanComponentSwizzleG = 4, + kFlutterVulkanComponentSwizzleB = 5, + kFlutterVulkanComponentSwizzleA = 6, +} FlutterVulkanComponentSwizzle; + +/// Equivalent to VkComponentMapping for YCbCr conversion. +typedef struct { + FlutterVulkanComponentSwizzle r; + FlutterVulkanComponentSwizzle g; + FlutterVulkanComponentSwizzle b; + FlutterVulkanComponentSwizzle a; +} FlutterVulkanComponentMapping; + +/// Parameters for creating a VkSamplerYcbcrConversion when sampling an +/// external-format or YCbCr-encoded VkImage (mirrors +/// VkSamplerYcbcrConversionCreateInfo and VkExternalFormatANDROID). +typedef struct { + /// The size of this struct. Must be sizeof(FlutterVulkanYcbcrConversionInfo). + size_t struct_size; + /// The VkFormat of the image, or VK_FORMAT_UNDEFINED (0) when using + /// `external_format`. + uint32_t format; + /// VkSamplerYcbcrModelConversion value. + uint32_t ycbcr_model; + /// VkSamplerYcbcrRange value. + uint32_t ycbcr_range; + /// Component swizzle mapping. + FlutterVulkanComponentMapping components; + /// VkChromaLocation for X chroma samples. + uint32_t x_chroma_offset; + /// VkChromaLocation for Y chroma samples. + uint32_t y_chroma_offset; + /// VkFilter for chroma reconstruction. + uint32_t chroma_filter; + /// Whether explicit chroma reconstruction is forced (VkBool32). + bool force_explicit_reconstruction; + /// VkFormatFeatureFlags for the external format. + uint32_t format_features; + /// Platform-defined external format identifier (for example from + /// VkAndroidHardwareBufferFormatPropertiesANDROID.externalFormat), or 0 + /// if `format` is a known Vulkan format. + uint64_t external_format; +} FlutterVulkanYcbcrConversionInfo; + +typedef struct { + // ... existing members from flutter/flutter#188855 (struct_size, image, + // format, user_data, destruction_callback, width, height) ... + + /// Optional YCbCr sampler conversion info for multi-planar or + /// external-format images. NULL for standard RGBA sampling. + const FlutterVulkanYcbcrConversionInfo* ycbcr_conversion_info; +} FlutterVulkanExternalTexture; + +typedef struct { + /// The size of this struct. Must be sizeof(FlutterOpenGLExternalTexture). + size_t struct_size; + /// Target texture of the active texture unit (for example GL_TEXTURE_2D or + /// GL_TEXTURE_EXTERNAL_OES). + uint32_t target; + /// The name of the texture. + uint32_t name; + /// The texture format (for example GL_RGBA8). Use GL_RGBA8 when `target` + /// is GL_TEXTURE_EXTERNAL_OES. + uint32_t format; + /// Embedder-owned context pointer (for example, the per-frame texture or + /// EGLImage handle) passed to `destruction_callback`. + void* user_data; + /// Callback invoked with `user_data` (on an engine-managed thread) when the + /// engine is done sampling this texture frame. + VoidCallback destruction_callback; + /// Width of the texture in pixels. + size_t width; + /// Height of the texture in pixels. + size_t height; + /// Whether `uv_transform` is set. When false (including zero-initialized + /// structs), the texture is sampled with the identity UV transformation. + bool has_uv_transform; + /// A 4x4 column-major OpenGL ES transformation matrix applied to + /// homogeneous normalized texture coordinates `(s, t, 0, 1)^T` before + /// sampling (matching the `float[16]` layout written by Android's + /// `SurfaceTexture.getTransformMatrix()`) when `has_uv_transform` is true. + float uv_transform[16]; +} FlutterOpenGLExternalTexture; + +typedef bool (*FlutterOpenGLTextureFrameCallback)( + void* /* user data */, + int64_t /* texture identifier */, + size_t /* width */, + size_t /* height */, + FlutterOpenGLExternalTexture* /* texture out */); + +typedef struct { + // ... existing fields ... + FlutterOpenGLTextureFrameCallback external_texture_frame_callback; +} FlutterOpenGLRendererConfig; +``` + +
+Alternatives + +- Adding Android-specific `AHardwareBuffer*` callbacks to `embedder.h` was rejected to keep `embedder.h` platform-agnostic by extending `FlutterVulkanExternalTexture` with `FlutterVulkanYcbcrConversionInfo` instead. +- Appending `uv_transform` to `FlutterOpenGLTexture` was rejected because `FlutterOpenGLTexture` has no `struct_size` field and is embedded by value inside `FlutterOpenGLBackingStore`, so growing it would break ABI compatibility. +- Using `FlutterTransformation` ($3 \times 3$ `double` matrix) for `uv_transform` was rejected in favor of column-major `float uv_transform[16]` with `bool has_uv_transform` so the embedder can pass `SurfaceTexture.getTransformMatrix(float[16])` directly to Impeller's `DlMatrix` without element swizzling or `float`-to-`double` conversion. + +
+ +### Accessibility and Semantics Parity + +> [!IMPORTANT] +> +> - **Embedder API additions (`embedder.h`)**: +> - `FlutterSemanticsRole` +> - Appends nine node fields (`max_value_length`, `current_value_length`, `link_url`, `locale`, `min_value`, `max_value`, `hit_test_transform`, `role`, and `hit_test_child_count`) in place to [`FlutterSemanticsNode2`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder.h#L1677-L1783) +> - **Behavioral changes**: +> - `hit_test_child_count` separates the length of `children_in_hit_test_order` from `child_count` (`children_in_traversal_order`, which can differ when the framework grafts nodes such as `OverlayPortal`). +> - `AndroidSemanticsMapper` consumes `FlutterSemanticsUpdate2` via `FlutterProjectArgs.update_semantics_callback2` to pack the direct `ByteBuffer` and string pool arrays expected by Java [`AccessibilityBridge`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/io/flutter/view/AccessibilityBridge.java#L80). +> - Accessibility actions route back through `FlutterEngineSendSemanticsAction`. + +
+Why FlutterSemanticsNode2 is extended in place + +- **Missing node attributes**: Java [`AccessibilityBridge`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/io/flutter/view/AccessibilityBridge.java#L2614-L2720) reads `maxValueLength`, `currentValueLength`, `linkUrl`, `locale`, `minValue`, `maxValue`, `hitTestTransform`, and `role` from [`flutter::SemanticsNode`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/lib/ui/semantics/semantics_node.h#L101), which were omitted from [`FlutterSemanticsNode2`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder.h#L1677-L1783). +- **Separate child array counts (`hit_test_child_count`)**: [`EmbedderSemanticsUpdate2`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder_semantics_update.cc#L96-L108) previously used `child_count` (`children_in_traversal_order.size()`) as the length of both child arrays, even though `children_in_hit_test_order` can have a different length when the framework grafts nodes such as `OverlayPortal`. + +
+ +```c +typedef enum { + kFlutterSemanticsRoleNone = 0, + kFlutterSemanticsRoleTab = 1, + kFlutterSemanticsRoleTabBar = 2, + kFlutterSemanticsRoleTabPanel = 3, + kFlutterSemanticsRoleDialog = 4, + kFlutterSemanticsRoleAlertDialog = 5, + kFlutterSemanticsRoleTable = 6, + kFlutterSemanticsRoleCell = 7, + kFlutterSemanticsRoleRow = 8, + kFlutterSemanticsRoleColumnHeader = 9, + kFlutterSemanticsRoleDragHandle = 10, + kFlutterSemanticsRoleSpinButton = 11, + kFlutterSemanticsRoleComboBox = 12, + kFlutterSemanticsRoleMenuBar = 13, + kFlutterSemanticsRoleMenu = 14, + kFlutterSemanticsRoleMenuItem = 15, + kFlutterSemanticsRoleMenuItemCheckbox = 16, + kFlutterSemanticsRoleMenuItemRadio = 17, + kFlutterSemanticsRoleList = 18, + kFlutterSemanticsRoleListItem = 19, + kFlutterSemanticsRoleForm = 20, + kFlutterSemanticsRoleTooltip = 21, + kFlutterSemanticsRoleLoadingSpinner = 22, + kFlutterSemanticsRoleProgressBar = 23, + kFlutterSemanticsRoleHotKey = 24, + kFlutterSemanticsRoleRadioGroup = 25, + kFlutterSemanticsRoleStatus = 26, + kFlutterSemanticsRoleAlert = 27, + kFlutterSemanticsRoleComplementary = 28, + kFlutterSemanticsRoleContentInfo = 29, + kFlutterSemanticsRoleMain = 30, + kFlutterSemanticsRoleNavigation = 31, + kFlutterSemanticsRoleRegion = 32, + kFlutterSemanticsRoleSlider = 33, + kFlutterSemanticsRoleListBox = 34, + kFlutterSemanticsRoleOption = 35, +} FlutterSemanticsRole; + +typedef struct { + // ... existing fields ... + + /// The maximum character length of an editable text field, or -1 if none. + int32_t max_value_length; + /// The current character length of an editable text field, or -1 if none. + int32_t current_value_length; + /// The target URL when the node represents a link, or NULL if none. + const char* link_url; + /// The BCP-47 locale tag for the node's text, or NULL if inherited. + const char* locale; + /// The minimum possible value for a range or slider node, or NULL if none. + const char* min_value; + /// The maximum possible value for a range or slider node, or NULL if none. + const char* max_value; + /// The transform from this node's coordinate system to its parent's + /// coordinate system for hit testing. + FlutterTransformation hit_test_transform; + /// The semantic role of this node. + FlutterSemanticsRole role; + /// The number of entries in `children_in_hit_test_order` (which may differ + /// from `child_count`, the length of `children_in_traversal_order`). + size_t hit_test_child_count; +} FlutterSemanticsNode2; +``` + +
+Alternatives + +- Introducing a new `FlutterSemanticsNode3` struct and `FlutterSemanticsUpdate3` callback was rejected because `FlutterSemanticsNode2` begins with `size_t struct_size` and is passed as an array of pointers (`const FlutterSemanticsNode2** nodes`) in `FlutterSemanticsUpdate2`, allowing new fields to be appended in place without breaking ABI compatibility. + +
+ +### Dart Deferred Library Loading + +> [!IMPORTANT] +> +> - **Embedder API additions (`embedder.h`)**: +> - `FlutterRequestDartDeferredLibraryCallback` (`FlutterProjectArgs.request_dart_deferred_library_callback`) +> - `FlutterDartDeferredLibrary` (with `destruction_callback`) +> - `FlutterEngineLoadDartDeferredLibrary` +> - `FlutterEngineNotifyDartDeferredLibraryLoadError` (`FlutterDartDeferredLibraryLoadError`) +> - **Behavioral changes**: +> - The engine invokes `request_dart_deferred_library_callback` on the platform task runner when Dart requests a `loading_unit_id`. +> - The engine wraps the embedder's `snapshot_data` and `snapshot_instructions` buffers in a shared-ownership `fml::NonOwnedMapping`. +> - The engine fires `destruction_callback` once the Dart VM releases both buffers. + +
+Why deferred library callbacks and destruction_callback are needed + +When Dart executes `loadLibrary()` on a deferred import, the engine calls `PlatformView::RequestDartDeferredLibrary(loading_unit_id)`, which [`PlatformViewEmbedder`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/platform_view_embedder.h#L36) did not expose in `embedder.h`. Once Play Feature Delivery installs the split APK and [`FlutterJNI.loadDartDeferredLibrary`](https://github.com/flutter/flutter/blob/4762dec4fc9072dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/platform_view_android_jni_impl.cc#L1395-L1486) resolves the `_kDartIsolateSnapshotData` and `_kDartIsolateSnapshotInstructions` symbols, the Dart VM registers those pages directly in the isolate group heap without copying them, so `FlutterDartDeferredLibrary.destruction_callback` notifies the embedder when the engine releases both mappings. +
+ +```c +/// Callback invoked by the engine when Dart requests loading of a deferred +/// library / loading unit. +/// +/// This callback runs on the platform task runner thread. +/// +/// @param[in] loading_unit_id The ID of the loading unit to load. +/// @param[in] user_data The user data supplied to `FlutterEngineRun` or +/// `FlutterEngineInitialize`. +typedef void (*FlutterRequestDartDeferredLibraryCallback)( + intptr_t loading_unit_id, + void* user_data); + +/// Information passed to `FlutterEngineLoadDartDeferredLibrary`. +typedef struct { + /// The size of this struct. Must be sizeof(FlutterDartDeferredLibrary). + size_t struct_size; + + /// The ID of the loading unit being loaded. + intptr_t loading_unit_id; + + /// The snapshot data buffer containing the Dart deferred library. Must not be null. + const uint8_t* snapshot_data; + + /// The size of the snapshot data buffer in bytes. + size_t snapshot_data_size; + + /// The snapshot instructions buffer containing the Dart deferred library. + const uint8_t* snapshot_instructions; + + /// The size of the snapshot instructions buffer in bytes. + size_t snapshot_instructions_size; + + /// Optional user data pointer passed to `destruction_callback`. + void* user_data; + + /// Optional callback invoked when the engine releases its references to + /// `snapshot_data` and `snapshot_instructions`. + /// + /// The Dart VM maps `snapshot_data` and `snapshot_instructions` directly into + /// the isolate group heap without copying them, so the buffers must remain + /// valid for the lifetime of the isolate group. If non-null, this callback is + /// invoked when the isolate group shuts down (or immediately if loading + /// fails). + VoidCallback destruction_callback; +} FlutterDartDeferredLibrary; + +FLUTTER_EXPORT +FlutterEngineResult FlutterEngineLoadDartDeferredLibrary( + FLUTTER_API_SYMBOL(FlutterEngine) engine, + const FlutterDartDeferredLibrary* library); + +typedef struct { + size_t struct_size; + intptr_t loading_unit_id; + const char* error_message; + bool transient; +} FlutterDartDeferredLibraryLoadError; + +FLUTTER_EXPORT +FlutterEngineResult FlutterEngineNotifyDartDeferredLibraryLoadError( + FLUTTER_API_SYMBOL(FlutterEngine) engine, + const FlutterDartDeferredLibraryLoadError* error); + +typedef struct { + // ... existing fields ... + FlutterRequestDartDeferredLibraryCallback + request_dart_deferred_library_callback; +} FlutterProjectArgs; +``` + +```mermaid +sequenceDiagram + autonumber + participant VM as "Dart VM (Root Isolate)" + participant Engine as "embedder.cc & fml::NonOwnedMapping" + participant Emb as "Android Embedder (FlutterJNI)" + participant Play as "PlayStoreDeferredComponentManager" + + VM->>Engine: loadLibrary() requests loading_unit_id + Engine-)Emb: request_dart_deferred_library_callback(loading_unit_id, user_data) + Emb->>Play: installDeferredComponent(loading_unit_id) + alt Split APK installed & symbols resolved + Play->>Emb: loadAssets() -> updateJavaAssetManager() + loadDartDeferredLibrary() + Emb->>Emb: dlopen split .so and resolve snapshot_data & snapshot_instructions + Emb->>Engine: FlutterEngineLoadDartDeferredLibrary(engine, &library) + Engine->>VM: LoadDartDeferredLibrary(loading_unit_id, data_mapping, instr_mapping) + Note over Engine,Emb: On isolate group shutdown (or immediately if load fails) + Engine->>Emb: destruction_callback(user_data) + else Download or extraction failed + Play->>Emb: deferredComponentInstallFailure() + Emb->>Engine: FlutterEngineNotifyDartDeferredLibraryLoadError(engine, &error) + Engine->>VM: LoadDartDeferredLibraryError(loading_unit_id, error_message, transient) + end +``` + +
+Alternatives + +- Freeing or `dlclose`-ing the split shared library immediately after `FlutterEngineLoadDartDeferredLibrary` returns was rejected because the Dart VM maps `snapshot_data` and `snapshot_instructions` directly into the isolate group heap without copying them, so the mapped memory must remain valid until the isolate group shuts down and invokes `destruction_callback`. +- Having the engine open split APK shared libraries directly by file path was rejected because Play Feature Delivery split APK installation, path resolution, and `dlopen` symbol lookup are Android-specific concerns managed by `PlayStoreDeferredComponentManager` and `FlutterJNI`. + +
+ +### Custom Asset and Kernel Resolution + +> [!IMPORTANT] +> +> - **Embedder API additions (`embedder.h`)**: +> - `FlutterAssetMapping` +> - `FlutterCustomAssetResolver` (`FlutterAssetResolverGetAsMappingCallback`) +> - `FlutterProjectArgs.custom_asset_resolvers` (`custom_asset_resolvers_count`) +> - **Behavioral changes**: +> - The engine's internal `CustomAssetResolver` wrapper returns `true` from `IsValidAfterAssetManagerChange()` so custom resolvers stay attached across hot reload and hot restart. +> - [`APKAssetProvider`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/apk_asset_provider.h#L26) supplies a `FlutterCustomAssetResolver` that maps assets directly from the NDK [`AAssetManager`](https://developer.android.com/ndk/reference/group/asset) in `get_as_mapping_callback`. +> - `APKAssetProvider` atomically swaps its `AAssetManager*` pointer inside `user_data` when `FlutterJNI.updateJavaAssetManager` runs after a split APK install. + +
+Why FlutterCustomAssetResolver is needed + +`FlutterProjectArgs.assets_path` requires an uncompressed directory on disk (`DirectoryAssetBundle`), whereas Android packages Flutter assets inside APK and AAB archives accessed via the NDK `AAssetManager`. Adding `FlutterCustomAssetResolver` to `embedder.h` lets [`APKAssetProvider`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/apk_asset_provider.h#L26) resolve assets through a C callback without subclassing `flutter::AssetResolver` or extracting APK assets to disk on first launch. +
+ +```c +typedef struct { + size_t struct_size; + const uint8_t* mapping; + size_t mapping_size; + void* user_data; + VoidCallback destruction_callback; +} FlutterAssetMapping; + +typedef bool (*FlutterAssetResolverGetAsMappingCallback)( + const char* asset_name, + void* user_data, + FlutterAssetMapping* mapping_out); + +typedef struct { + size_t struct_size; + void* user_data; + FlutterAssetResolverGetAsMappingCallback get_as_mapping_callback; + VoidCallback destruction_callback; +} FlutterCustomAssetResolver; + +typedef struct { + // ... existing fields ... + size_t custom_asset_resolvers_count; + const FlutterCustomAssetResolver** custom_asset_resolvers; +} FlutterProjectArgs; +``` + +
+Alternatives + +- Keeping `//flutter/assets` as an allowed C++ dependency of the Android embedder was rejected because subclassing `flutter::AssetResolver` links the Android embedder directly to internal engine C++ classes and breaks C-API isolation. +- Extracting APK assets to the device filesystem on first launch and passing `FlutterProjectArgs.assets_path` was rejected due to first-launch disk I/O latency and duplicate storage usage. +- Adding runtime `FlutterEngineRegisterAssetResolver` or `FlutterEngineUpdateAssetResolverByType` functions was rejected because `CustomAssetResolver` stays valid across asset manager changes (`IsValidAfterAssetManagerChange() == true`) and `APKAssetProvider` can atomically swap its `AAssetManager*` pointer inside `user_data` when split APKs are installed. + +
+ +### Multi-Engine Spawning and Add-to-App + +> [!IMPORTANT] +> +> - **Embedder API additions (`embedder.h`)**: +> - `FlutterEngineSpawnInfo` +> - `FlutterEngineSpawn` +> - **Behavioral changes**: +> - `AndroidEngineGroup` replaces [`AndroidShellHolder::Spawn`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/android_shell_holder.cc#L222-L285) by calling `FlutterEngineSpawn`. +> - Spawned child `FlutterEngine` instances inherit the parent engine's Dart VM isolate group, `io_manager`, resolved [`impeller::Context`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/impeller/renderer/context.h#L70), task runners, asset resolvers, and `FlutterProjectArgs` callbacks (invoked with the child engine's `user_data`). + +
+Why FlutterEngineSpawn is exposed in embedder.h while FlutterEngineGroup remains the public Java/Objective-C API + +[`FlutterEngineGroup`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngineGroup.java#L41) in Java (and `FlutterEngineGroup` in Objective-C on iOS) is a thin collection (`activeEngines`) that initializes the first `FlutterEngine` normally and then calls `activeEngines.get(0).spawn(...)` -> [`Shell::Spawn`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/common/shell.h#L204-L210) so secondary engines share the parent engine's Dart VM isolate group heap, `io_manager`, and GPU context (~180 KB per spawned engine vs. ~15–20 MB for an independent `FlutterEngineInitialize` call). Because the C++ engine core has no `EngineGroup` class and shares resources across `Shell::Spawn` via `std::shared_ptr`, exposing `FlutterEngineSpawn` in `embedder.h` maps 1:1 to `Shell::Spawn` while keeping `FlutterEngineGroup` as the public developer-facing API. +
+ +```c +typedef struct { + size_t struct_size; + const char* entrypoint; + const char* library_uri; + const char* initial_route; + const char* const* entrypoint_argv; + int entrypoint_argc; + void* user_data; + const FlutterCompositor* compositor; +} FlutterEngineSpawnInfo; + +FLUTTER_EXPORT +FlutterEngineResult FlutterEngineSpawn( + FLUTTER_API_SYMBOL(FlutterEngine) parent_engine, + const FlutterEngineSpawnInfo* config, + FLUTTER_API_SYMBOL(FlutterEngine)* spawned_engine_out); +``` + +
+Alternatives + +- Introducing a `FlutterEngineGroup` handle and lifecycle API in `embedder.h` was rejected because the C++ engine has no `EngineGroup` class (`Shell::Spawn` clones directly from an existing `Shell` via `std::shared_ptr`), so a C-API group object would duplicate the collection wrapper already in `FlutterEngineGroup.java`. +- Accepting a separate `FlutterRendererConfig` in `FlutterEngineSpawnInfo` was rejected because spawned engines share the parent engine's `impeller::Context` and `io_manager`, so a per-child renderer configuration would either be ignored or create a conflicting GPU context. + +
+ +### Image Decoders / Font Prefetching / VM Service URI + +> [!IMPORTANT] +> +> - **Embedder API additions (`embedder.h`)**: +> - `FlutterProjectArgs.image_decoders` (`FlutterImageDecoderRegistration`, `FlutterImageDecoder`, `FlutterImageDecoderInfo`, `FlutterImageDecoderColorSpace`) +> - `FlutterEnginePrefetchDefaultFontManager` +> - `FlutterProjectArgs.vm_service_uri_callback` (`FlutterVMServiceUriInfo`) +> - **Behavioral changes**: +> - The Android embedder registers its single-frame fallback [`ImageDecoder`](https://developer.android.com/reference/android/graphics/ImageDecoder) (for HEIF and platform formats at priority `-1`) without subclassing `flutter::ImageGenerator`. +> - The engine keeps the encoded byte buffer alive until `FlutterImageDecoder.destruction_callback` runs. +> - `FlutterEnginePrefetchDefaultFontManager` warms up the process-wide default font manager on a background thread during `FlutterLoader.initAsync` before any engine exists. +> - `vm_service_uri_callback` delivers the Dart VM Service URI to `FlutterJNI.getVMServiceUri()`. +> - The non-Dart `{name, class_name, library_path}` JSON cache table in [`DartCallbackCache`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/lib/ui/plugins/callback_cache.h#L24) moves from [`//flutter/lib/ui/plugins/callback_cache.{h,cc}`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/lib/ui/plugins/callback_cache.h) into [`//flutter/fml`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/fml) (keeping only `LookupDartClosure` in `//flutter/lib/ui`). + +
+Why these three C-APIs and the DartCallbackCache move are needed + +- [`AndroidShellHolder`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/android_shell_holder.cc#L166-L171) registers [`AndroidImageGenerator`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/android_image_generator.h#L18) via `Shell::RegisterImageDecoder` so Android's NDK `AImageDecoder` can decode single-frame HEIF and platform image formats not handled by built-in engine codecs (while multi-frame formats such as GIF, animated WebP, and APNG are decoded by the engine's built-in priority-`0` codecs). +- [`FlutterMain::PrefetchDefaultFontManager`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/flutter_main.cc#L211-L214) calls `txt::GetDefaultFontManager()` (`//flutter/txt`) during `FlutterLoader.initAsync` before any `FlutterEngine` instance exists. +- [`PlatformViewAndroidJNIImpl`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/platform_view_android_jni_impl.cc#L186-L195) includes `//flutter/runtime/dart_service_isolate.h` directly to register a callback for `FlutterJNI.getVMServiceUri()`. +- [`FlutterMain::Init`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/flutter_main.cc#L143-L157) and [`PlatformViewAndroid::LookupCallbackInformation`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/platform_view_android.cc#L483-L486) include `//flutter/lib/ui/plugins/callback_cache.h` only to access the `{name, class_name, library_path}` JSON file map used for [background Dart execution from plugins](https://flutter.dev/blog/executing-dart-in-the-background-with-flutter-plugins-and-geofencing), which has no dependency on `dart:ui` or `dart_api.h` (as noted by the existing `TODO` in [`flutter_main.cc#L143`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/flutter_main.cc#L143)). + +
+ +```c +typedef enum { + kFlutterImageDecoderColorTypeRGBA8888, + kFlutterImageDecoderColorTypeBGRA8888, +} FlutterImageDecoderColorType; + +typedef enum { + kFlutterImageDecoderAlphaTypeOpaque, + kFlutterImageDecoderAlphaTypePremul, + kFlutterImageDecoderAlphaTypeUnpremul, +} FlutterImageDecoderAlphaType; + +typedef enum { + kFlutterImageDecoderColorSpaceSRGB = 0, +} FlutterImageDecoderColorSpace; + +typedef struct { + size_t struct_size; + uint32_t width; + uint32_t height; + FlutterImageDecoderColorType color_type; + FlutterImageDecoderAlphaType alpha_type; + FlutterImageDecoderColorSpace color_space; +} FlutterImageDecoderInfo; + +/// Invoked on the UI thread (during ImageDescriptor creation) and on an engine +/// concurrent worker thread (during decoding). +typedef bool (*FlutterImageDecoderGetInfoCallback)( + void* user_data, + FlutterImageDecoderInfo* info_out); + +/// Invoked on an engine concurrent worker thread; never invoked on the UI thread. +typedef bool (*FlutterImageDecoderGetPixelsCallback)( + void* user_data, + const FlutterImageDecoderInfo* info, + void* pixels, + size_t row_bytes); + +/// Single-frame fallback image decoder (multi-frame formats such as GIF, +/// animated WebP, and APNG are handled by the engine's built-in priority-0 +/// codecs). +typedef struct { + size_t struct_size; + void* user_data; + FlutterImageDecoderGetInfoCallback get_info; + FlutterImageDecoderGetPixelsCallback get_pixels; + /// May be invoked on any thread (UI, IO, or concurrent worker) when the last + /// reference to the decoder is released. + VoidCallback destruction_callback; +} FlutterImageDecoder; + +/// Invoked on the UI thread to sniff the encoded header and construct a +/// decoder. `data` remains valid until `FlutterImageDecoder.destruction_callback` runs. +typedef bool (*FlutterImageDecoderFactoryCallback)( + void* user_data, + const uint8_t* data, + size_t data_size, + FlutterImageDecoder* decoder_out); + +typedef struct { + size_t struct_size; + FlutterImageDecoderFactoryCallback create_decoder; + /// Factories are tried in descending `priority` order (ties broken by + /// registration order). Built-in engine codecs register at `0`, so + /// `priority > 0` overrides built-in codecs and `priority < 0` (such as `-1` + /// on Android) is a fallback. + int32_t priority; + void* user_data; + /// Invoked on the UI thread during engine teardown. + VoidCallback destruction_callback; +} FlutterImageDecoderRegistration; + +typedef struct { + size_t struct_size; + const char* uri; +} FlutterVMServiceUriInfo; + +typedef void (*FlutterVMServiceUriCallback)( + const FlutterVMServiceUriInfo* info, + void* user_data); + +FLUTTER_EXPORT +FlutterEngineResult FlutterEnginePrefetchDefaultFontManager(void); + +typedef struct { + // ... existing fields ... + size_t image_decoders_count; + const FlutterImageDecoderRegistration** image_decoders; + FlutterVMServiceUriCallback vm_service_uri_callback; +} FlutterProjectArgs; +``` + +
+Alternatives + +- Adding multi-frame animation callbacks (`get_frame_count`, `get_frame_info`) to `FlutterImageDecoder` was rejected because the engine's built-in priority-`0` codecs already decode GIF, animated WebP, and APNG, while Android's fallback `AImageDecoder` (`priority = -1`) only decodes single-frame platform formats such as HEIF. +- Adding `FlutterEngineInitializeCallbackCache` and `FlutterEngineGetCallbackInformation` to `embedder.h` for `FlutterCallbackInformation` was rejected because the `{name, class_name, library_path}` JSON file cache has no interaction with `Shell` or the Dart VM and can live inside the Android embedder or `//flutter/fml`. + +
+ +### Platform Channel Messaging, Background Handlers, and Task Queues + +> [!IMPORTANT] +> +> - **Embedder API additions (`embedder.h`)**: +> - `FlutterProjectArgs.enable_direct_platform_message_dispatch` (`bool`, defaulting to `false`) +> - **Behavioral changes**: +> - When `enable_direct_platform_message_dispatch` is `true`, [`EmbedderPlatformMessageHandler`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/platform_view_embedder.cc#L22-L36) invokes `platform_message_callback` directly on the calling isolate's thread. +> - `EmbedderPlatformMessageHandler::DoesHandlePlatformMessageOnPlatformThread()` returns `false` when `enable_direct_platform_message_dispatch` is `true`. +> - Root-isolate messages avoid an extra main-`Looper` post. +> - Background-isolate messages route directly to [`makeBackgroundTaskQueue()`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/dart/DartMessenger.java#L184-L190) executors without bouncing through the Android main thread. + +
+Why enable_direct_platform_message_dispatch is needed + +By default, [`EmbedderPlatformMessageHandler`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/platform_view_embedder.cc#L22-L36) posts every Dart-to-platform message onto the platform thread before invoking `platform_message_callback`. On Android, where the UI and platform threads are merged (`MergedPlatformUIThread::kEnabled`) and [`DartMessenger.dispatchMessageToQueue`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/dart/DartMessenger.java#L279-L315) already routes each channel to either the main `Looper` or a background `TaskQueue` executor, that unconditional platform-thread post adds a redundant `Looper` hop for root-isolate messages and forces background-isolate messages destined for a background `TaskQueue` to wait on the Android UI thread first. +
+ +```mermaid +flowchart LR + subgraph Engine["Engine Isolate Threads"] + Root["Root Isolate (UI Thread / Shell::OnEngineHandlePlatformMessage)"] + BgIso["Background Isolate (UIDartState::HandlePlatformMessage)"] + end + + subgraph Handler["EmbedderPlatformMessageHandler (enable_direct_platform_message_dispatch = true)"] + Startup{"During FlutterEngineRunInitialized (route_messages_through_platform_thread_)?"} + Direct["Invoke platform_message_callback synchronously on calling thread"] + end + + subgraph Messenger["Java DartMessenger.dispatchMessageToQueue"] + QueueCheck{"Handler registered with makeBackgroundTaskQueue()?"} + BgExec["Background Executor (SerialTaskQueue / ConcurrentTaskQueue)"] + MainLooper["PlatformTaskQueue (Single post to Android Main Looper)"] + end + + Root --> Startup + Startup -- "Yes (startup turn)" --> MainLooper + Startup -- "No" --> Direct + BgIso --> Direct + Direct --> QueueCheck + QueueCheck -- "Yes (background TaskQueue)" --> BgExec + QueueCheck -- "No (default queue)" --> MainLooper + BgExec -. "FlutterEngineSendPlatformMessageResponse (thread-safe)" .-> Engine + MainLooper -. "FlutterEngineSendPlatformMessageResponse" .-> Engine +``` + +```c +typedef struct { + // ... existing fields ... + /// If true, the engine invokes `platform_message_callback` directly on the + /// thread that sent the message (typically the UI thread) instead of posting + /// a task to the platform thread. Defaults to false, which preserves the + /// existing platform thread delivery. + bool enable_direct_platform_message_dispatch; +} FlutterProjectArgs; +``` + +
+Alternatives + +- Changing `EmbedderPlatformMessageHandler` to dispatch on the calling thread for all embedders by default was rejected because existing desktop and custom embedders assume `platform_message_callback` is always invoked on the platform thread. +- Adding a `does_handle_platform_messages_on_platform_thread` boolean that defaults to `true` was rejected because zero-initialized `FlutterProjectArgs` structs set boolean fields to `false`, which would silently change the default behavior for existing callers. + +
+ +### Viewport Metrics, Pointer Events, and Non-Linear Font Scaling + +> [!IMPORTANT] +> +> - **Embedder API additions (`embedder.h`)**: +> - `FlutterEdgeInsets` +> - `FlutterDisplayCornerRadii` +> - `FlutterDisplayFeature` (`FlutterDisplayFeatureType`, `FlutterDisplayFeatureState`) +> - Appends `physical_padding`, `physical_system_gesture_insets`, `physical_touch_slop`, `display_corner_radii`, and `display_features` (`display_features_count`) in place to [`FlutterWindowMetricsEvent`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder.h#L1062-L1110) +> - Appends `distance`, `distance_max`, `size`, `radius_major`, `radius_minor`, `radius_min`, `radius_max`, `orientation`, `tilt`, `platform_data`, and `embedder_id` in place to [`FlutterPointerEvent`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder.h#L1400-L1447) +> - `FlutterScaleFontSizeInfo` +> - `FlutterScaleFontSizeCallback` (`FlutterProjectArgs.scale_font_size_callback`) +> - **Behavioral changes**: +> - [`FlutterJNI.nativeSetViewportMetrics`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/platform_view_android_jni_impl.cc#L334-L433) populates the extended [`FlutterWindowMetricsEvent`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder.h#L1062-L1110) struct. +> - [`AndroidTouchProcessor`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/io/flutter/embedding/android/AndroidTouchProcessor.java#L358-L479) populates the extended [`FlutterPointerEvent`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder.h#L1400-L1447) struct. +> - `embedder.cc` reads the new fields using `SAFE_ACCESS(event, member, default_value)`. +> - `PlatformViewEmbedder::GetScaledFontSize` delegates to `FlutterProjectArgs.scale_font_size_callback` with `(font_size, text_scale_factor)` when set, preserving Android 14+ (API 34+) non-linear font scaling via [`FlutterJNI.getScaledFontSize`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java#L1529-L1544). +> - Passing `text_scale_factor` directly in `FlutterScaleFontSizeInfo` decouples font scaling from [`SettingsChannel.ConfigurationQueue`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/SettingsChannel.java#L118-L215) and the `"configurationId"` field on the `flutter/settings` platform channel. + +
+Why FlutterWindowMetricsEvent, FlutterPointerEvent, and scale_font_size_callback are needed + +- [`PlatformViewAndroid::SetViewportMetrics`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/platform_view_android.cc#L337-L341) constructs an internal [`flutter::ViewportMetrics`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/lib/ui/window/viewport_metrics.h#L16) struct containing system bar padding, gesture insets, touch slop, display corner radii (API 31+), and foldable display features (`WindowInfoTracker`), which [`FlutterWindowMetricsEvent`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder.h#L1062-L1110) previously lacked. +- [`AndroidTouchProcessor.java`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/io/flutter/embedding/android/AndroidTouchProcessor.java#L358-L479) populates stylus tilt/orientation, touch contact radii, `platform_data` (multi-pointer batching flags), and `embedder_id` ([`MotionEventTracker`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java#L730-L800) lookup ID for platform view touch forwarding), which were missing from [`FlutterPointerEvent`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder.h#L1400-L1447). +- On Android 14+ (API 34+), [`PlatformViewAndroid::GetScaledFontSize`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/platform_view_android.cc#L516-L522) overrides `PlatformView::GetScaledFontSize` to evaluate `TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, font_size, displayMetrics) / displayMetrics.density` via [`FlutterJNI.getScaledFontSize`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java#L1529-L1544). Because that calculation depends only on `font_size` and `text_scale_factor` ([`SettingsChannel.java#L149-L153`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/SettingsChannel.java#L149-L153)), passing `(font_size, text_scale_factor)` directly in `FlutterScaleFontSizeInfo` wires `PlatformViewEmbedder::GetScaledFontSize` to `FlutterJNI.getScaledFontSize` without subclassing `PlatformView` or coupling `embedder.h` to `"configurationId"` on the `flutter/settings` platform channel. + +
+ +#### `FlutterWindowMetricsEvent` In-Place Extensions + +```c +/// Distances from each edge of a rectangle, in physical pixels. +/// Frozen value struct (like `FlutterRect`) embedded by value. +typedef struct { + double left; + double top; + double right; + double bottom; +} FlutterEdgeInsets; + +/// Radii of the four display corners, in physical pixels. +typedef struct { + /// The size of this struct. Must be sizeof(FlutterDisplayCornerRadii). + size_t struct_size; + double top_left; + double top_right; + double bottom_right; + double bottom_left; +} FlutterDisplayCornerRadii; + +typedef enum { + kFlutterDisplayFeatureTypeUnknown = 0, + kFlutterDisplayFeatureTypeFold = 1, + kFlutterDisplayFeatureTypeHinge = 2, + kFlutterDisplayFeatureTypeCutout = 3, +} FlutterDisplayFeatureType; + +typedef enum { + kFlutterDisplayFeatureStateUnknown = 0, + kFlutterDisplayFeatureStatePostureFlat = 1, + kFlutterDisplayFeatureStatePostureHalfOpened = 2, +} FlutterDisplayFeatureState; + +typedef struct { + size_t struct_size; + FlutterRect bounds; + FlutterDisplayFeatureType type; + FlutterDisplayFeatureState state; +} FlutterDisplayFeature; + +typedef struct { + // ... existing fields ... + + /// Physical window padding (for example, status bar, navigation bar, cutouts). + FlutterEdgeInsets physical_padding; + + /// System gesture insets (for example, Android 10+ edge back gestures). + FlutterEdgeInsets physical_system_gesture_insets; + + /// Physical touch slop threshold in physical pixels for scroll + /// disambiguation. A non-positive value (<= 0.0, including zero-initialized + /// structs) indicates that the platform does not specify a custom touch slop + /// and the framework default should be used. + double physical_touch_slop; + + /// Display corner radii in physical pixels, or NULL when unknown or + /// unsupported (for example on Android < API 31 or desktop platforms). + const FlutterDisplayCornerRadii* display_corner_radii; + + /// Number of display features in the `display_features` array. + size_t display_features_count; + + /// Array of pointers to physical display features (foldable hinges, cutouts). + const FlutterDisplayFeature** display_features; +} FlutterWindowMetricsEvent; +``` + +#### `FlutterPointerEvent` In-Place Extensions + +```c +typedef struct { + // ... existing fields ... + double distance; + double distance_max; + double size; + double radius_major; + double radius_minor; + double radius_min; + double radius_max; + double orientation; + double tilt; + int64_t platform_data; + int64_t embedder_id; +} FlutterPointerEvent; +``` + +#### Android 14+ Non-Linear Font Scaling + +```c +typedef struct { + size_t struct_size; + double font_size; + double text_scale_factor; +} FlutterScaleFontSizeInfo; + +/// Runs on the UI thread. Returns false on failure. +typedef bool (*FlutterScaleFontSizeCallback)( + const FlutterScaleFontSizeInfo* info, + double* scaled_font_size_out, + void* user_data); + +typedef struct { + // ... existing fields ... + /// Optional; if NULL, the engine falls back to linear scaling. + FlutterScaleFontSizeCallback scale_font_size_callback; +} FlutterProjectArgs; +``` + +
+Alternatives + +- Passing `const FlutterDisplayFeature* display_features` as a contiguous value array instead of `const FlutterDisplayFeature**` was rejected because indexing a contiguous array of structs uses the compile-time `sizeof(FlutterDisplayFeature)`, which would break ABI compatibility if fields are appended to `FlutterDisplayFeature` in the future. +- Serializing Android 14+ non-linear font-scale lookup tables over the `flutter/settings` platform channel (`fontScaleCurves`) and interpolating in Dart was rejected in favor of `FlutterProjectArgs.scale_font_size_callback`, which delegates directly to `FlutterJNI.getScaledFontSize` (`TypedValue.applyDimension`). +- Passing an `int64_t configuration_id` in `FlutterScaleFontSizeInfo` and requiring a preceding `flutter/settings` message was rejected because `TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, font_size, displayMetrics) / displayMetrics.density` depends only on `(font_size, text_scale_factor)` and `embedder.h` callbacks should not depend on `flutter/settings` platform channel state (`SettingsChannel.ConfigurationQueue`). + +
+ +## Target Architecture and Design Principles + +> [!IMPORTANT] +> The target architecture replaces [`AndroidShellHolder`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/android_shell_holder.h#L38), [`PlatformViewAndroid`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/platform_view_android.h#L43), and [`PlatformViewAndroidJNIImpl`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/platform_view_android_jni_impl.cc) with `FlutterEmbedderNative` and single-responsibility C-API components in `:flutter_embedder_native_src` compiled with `FLUTTER_ENGINE_NO_PROTOTYPES`. Outbound C++-to-Java calls route through injectable `JniRouter` / `JniDelegate` / `JvmInvoker` interfaces so `flutter_embedder_native_unittests` run on the host without a live JVM or Android device. + +```mermaid +flowchart TD + subgraph Java Layer + Activity["FlutterActivity / FlutterFragment"] + FlutterView["FlutterView"] + FlutterJNI["FlutterJNI"] + end + + subgraph Android Embedder C++: flutter_embedder_native_src + FlutterEmbedderNative["FlutterEmbedderNative: C-API Client & JNI Coordinator"] + JniRouter["JniRouter / JniDelegate / JvmInvoker (Host-Testable JNI)"] + SurfaceControl["AndroidSurfaceControl (Keyed by FlutterViewId)"] + PlatformViews["AndroidPlatformViewsController & AndroidMutatorsMapper"] + Textures["AndroidHardwareBuffer & AndroidVulkanExternalTexture"] + Mappers["AndroidSemanticsMapper & AndroidWindowMetricsMapper"] + VsyncAssets["AndroidVsyncWaiter, AndroidVMInit, AndroidEngineGroup, APKAssetProvider"] + end + + subgraph Public C-API Boundary + EmbedderH["embedder.h: FlutterEngineProcTable (FLUTTER_ENGINE_NO_PROTOTYPES)"] + end + + subgraph Flutter Engine Core + FlutterEngineOpaque["FlutterEngine Opaque Handle (PlatformViewEmbedder)"] + Shell["Shell"] + ImpellerFlow["Impeller / Flow Compositor"] + DartVM["Dart VM"] + end + + Activity --> FlutterView --> FlutterJNI --> FlutterEmbedderNative + FlutterEmbedderNative --> JniRouter + FlutterEmbedderNative --> SurfaceControl + FlutterEmbedderNative --> PlatformViews + FlutterEmbedderNative --> Textures + FlutterEmbedderNative --> Mappers + FlutterEmbedderNative --> VsyncAssets + FlutterEmbedderNative --> EmbedderH --> FlutterEngineOpaque --> Shell + Shell --> ImpellerFlow + Shell --> DartVM +``` + +```mermaid +sequenceDiagram + autonumber + participant FlutterJNI as "jni : FlutterJNI" + participant FlutterEmbedderNative as "embedder : FlutterEmbedderNative" + participant EmbedderAPI as "procTable : FlutterEngineProcTable" + participant Core as "engine : PlatformViewEmbedder" + participant Raster as "Raster Thread" + + FlutterJNI->>FlutterEmbedderNative: attachToNative (create FlutterEmbedderNative) + FlutterJNI->>FlutterEmbedderNative: Pre-run calls (queued) + FlutterJNI->>FlutterEmbedderNative: runBundleAndSnapshotFromLibrary (entrypoint and assets) + FlutterEmbedderNative->>EmbedderAPI: FlutterEngineInitialize(kDeferred, project_args) + EmbedderAPI-->>FlutterEmbedderNative: FlutterEngine Opaque Handle + FlutterEmbedderNative->>EmbedderAPI: FlutterEngineRunInitialized(engine_handle) + EmbedderAPI->>Core: LaunchShell(), NotifyCreated() (async), RunRootIsolate() + Core-)Raster: Post CreateImpellerContext & NotifyCreated (FIFO raster queue) + EmbedderAPI-->>FlutterEmbedderNative: Return without waiting for Raster Thread + par Raster Thread Resolves Renderer via C-API Callback + Raster->>Core: PlatformViewEmbedder::SetupImpellerContext() + Core->>FlutterEmbedderNative: resolve_callback(user_data, config_out) + FlutterEmbedderNative-->>Core: kVulkan (or kOpenGL fallback) + Raster->>Core: CreateRenderingSurface (context-only) & Rasterizer::Setup + and Platform Thread Flushes Queued Calls + FlutterEmbedderNative->>EmbedderAPI: Flush queued pre-run calls + end +``` + +### Mapping Embedder C-APIs to Target Architecture Components + +| Target Component | Eliminated Internal Engine Dependencies | Embedder C-API Additions Used | Architectural Role | +| :--- | :--- | :--- | :--- | +| **`FlutterEmbedderNative`, `AndroidVMInit`, and `AndroidEngineGroup`** | [`//flutter/shell/common`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/common), [`//flutter/runtime`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/runtime), [`//flutter/lib/ui`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/lib/ui), [`//flutter/txt`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/txt) | - [`FlutterEngineSpawn`](#multi-engine-spawning-and-add-to-app) (`FlutterEngineSpawnInfo`)
- [`FlutterProjectArgs.enable_direct_platform_message_dispatch`](#platform-channel-messaging-background-handlers-and-task-queues)
- [`FlutterProjectArgs.request_dart_deferred_library_callback`](#dart-deferred-library-loading), `FlutterEngineLoadDartDeferredLibrary`, `FlutterEngineNotifyDartDeferredLibraryLoadError`
- [`FlutterProjectArgs.image_decoders`](#image-decoders--font-prefetching--vm-service-uri) (`FlutterImageDecoderRegistration`), `FlutterEnginePrefetchDefaultFontManager`, `FlutterProjectArgs.vm_service_uri_callback` | Owns the opaque [`FlutterEngine`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder.h#L389) handle and `FlutterEngineProcTable`; replaces `AndroidShellHolder` and `PlatformViewAndroid`. | +| **`AndroidSurfaceControl`** | [`//flutter/shell/common`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/common), [`//flutter/impeller`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/impeller), [`//flutter/vulkan`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/vulkan) | - [`kDeferred`](#initialization) (`FlutterDeferredRendererConfig`, `FlutterRendererConfigResolveCallback`) | Keys native surfaces (`ANativeWindow*`, `ASurfaceControl*`) by `FlutterViewId` and resolves the renderer asynchronously during startup. | +| **`AndroidPlatformViewsController` and `AndroidMutatorsMapper`** | [`//flutter/flow`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/flow), [`//flutter/fml`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/fml) | - [`FlutterCompositor.merge_raster_thread_on_platform_views`](#platform-views), `FlutterCompositor.coalesce_overlay_layers`, and `FlutterBackingStoreConfig.is_root_layer`
- [`FlutterCompositor.platform_view_mutation_capabilities`](#platform-view-mutator-stack-serialization), `FlutterPlatformView.mutations`, `FlutterPlatformViewMutation`, `FlutterPath` | Unifies HC, HCPP (`SurfaceControl`), TLHC, and VD behind `FlutterCompositor` and deletes `external_view_embedder/`. | +| **`AndroidHardwareBuffer` and `AndroidVulkanExternalTexture`** | [`//flutter/common/graphics`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/common/graphics) | - [`FlutterVulkanExternalTexture`](#external-textures) (`ycbcr_conversion_info`), `FlutterVulkanYcbcrConversionInfo`, `FlutterVulkanTextureFrameCallback`
- [`FlutterOpenGLExternalTexture`](#external-textures) (`has_uv_transform`, `uv_transform`), `FlutterOpenGLTextureFrameCallback` | Imports zero-copy `AHardwareBuffer` (`VkImage` / `EGLImageKHR`) and `SurfaceTexture` UV transforms without subclassing `flutter::Texture`. | +| **`AndroidSemanticsMapper` and `AndroidWindowMetricsMapper`** | [`//flutter/flow`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/flow), [`//flutter/lib/ui/window/viewport_metrics.h`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/lib/ui/window/viewport_metrics.h) | - [`FlutterSemanticsNode2`](#accessibility-and-semantics-parity) (`role`, `hit_test_child_count`, `FlutterSemanticsRole`)
- [`FlutterWindowMetricsEvent`](#viewport-metrics-pointer-events-and-non-linear-font-scaling), `FlutterPointerEvent`, `FlutterProjectArgs.scale_font_size_callback` | Serializes semantics trees for Java `AccessibilityBridge`, packs insets/cutouts/foldable features and pointer data, and delegates non-linear font scaling. | +| **[`APKAssetProvider`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/apk_asset_provider.h#L26)** | [`//flutter/assets/asset_resolver.h`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/assets/asset_resolver.h) | - [`FlutterProjectArgs.custom_asset_resolvers`](#custom-asset-and-kernel-resolution), `FlutterCustomAssetResolver`, `FlutterAssetMapping` | Maps APK and split-AAB assets via NDK `AAssetManager` without subclassing `flutter::AssetResolver`. | + +## Phased Implementation Strategy + +Phase 1 (`shell/platform/embedder/`) and Phase 2 (`shell/platform/android/` modularization and GN partitioning) execute in parallel before converging at Phase 3: + +```mermaid +flowchart LR + Phase1["Phase 1:
Embedder C-API
Extensions"] + Phase2["Phase 2:
Android Shell
Modularization"] + Phase3["Phase 3:
C-API Cutover
Behind Feature Flag"] + Phase4["Phase 4:
Build Decoupling
& CI Ratchet"] + + Phase1 --> Phase3 + Phase2 --> Phase3 + Phase3 --> Phase4 +``` + +### Phase 1: Embedder C-API Extensions + +Implements all C-API extensions and `shell/platform/embedder/` engine changes, validated by [`embedder_unittests`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/BUILD.gn#L372-L390) with zero modifications to `shell/platform/android/`: + +- `kDeferred` renderer resolution (`PlatformViewEmbedder::SetupImpellerContext()`) and non-blocking surface initialization (`PlatformViewEmbedder::NotifyCreated()` -> `Shell::OnPlatformViewCreatedAsync()`). +- Opt-in dynamic raster thread merging (`FlutterCompositor.merge_raster_thread_on_platform_views`) and overlay layer coalescing (`FlutterCompositor.coalesce_overlay_layers`) in `EmbedderExternalViewEmbedder`, plus Impeller Vulkan backing store presentation ([#187525](https://github.com/flutter/flutter/issues/187525)). +- Platform view mutator serialization (`FlutterPlatformViewMutation`), external texture callbacks (`FlutterVulkanTextureFrameCallback`, `FlutterOpenGLTextureFrameCallback`), `FlutterSemanticsNode2`, `FlutterWindowMetricsEvent`, and `FlutterPointerEvent` in-place extensions, `scale_font_size_callback`, deferred library callbacks, `FlutterCustomAssetResolver`, `FlutterEngineSpawn`, direct UI-thread platform message dispatch, and process-scoped utilities. + +### Phase 2: Android Shell Modularization and GN Target Partitioning + +Phase 2 decomposes [`AndroidShellHolder`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/android_shell_holder.h#L38) and [`PlatformViewAndroid`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/platform_view_android.h#L43) into focused C-API components (`FlutterEmbedderNative`, `JniRouter`, `AndroidSurfaceControl`, `AndroidPlatformViewsController`, `AndroidMutatorsMapper`, `AndroidHardwareBuffer`, `AndroidVulkanExternalTexture`, `AndroidSemanticsMapper`, `AndroidWindowMetricsMapper`, `AndroidVsyncWaiter`, `AndroidVMInit`, `AndroidEngineGroup`, and [`APKAssetProvider`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/apk_asset_provider.h#L26)) validated on the host by `flutter_embedder_native_unittests`. [`shell/platform/android/BUILD.gn`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/BUILD.gn) isolates all new C-API components inside `source_set("flutter_embedder_native_src")` with `check_includes = true` and `defines = [ "FLUTTER_ENGINE_NO_PROTOTYPES" ]`, blocking private engine headers at compile time: + +```gn +# 1. Modular C-API Source Set (Compile-Time Firewall) +source_set("flutter_embedder_native_src") { + visibility = [ ":*" ] + check_includes = true + defines = [ "FLUTTER_ENGINE_NO_PROTOTYPES" ] + + sources = [ + "android_engine_group.cc", + "android_engine_group.h", + "android_hardware_buffer.cc", + "android_hardware_buffer.h", + "android_mutators_mapper.cc", + "android_mutators_mapper.h", + "android_platform_views_controller.cc", + "android_platform_views_controller.h", + "android_semantics_mapper.cc", + "android_semantics_mapper.h", + "android_surface_control.cc", + "android_surface_control.h", + "android_vm_init.cc", + "android_vm_init.h", + "android_vsync_waiter.cc", + "android_vsync_waiter.h", + "android_vulkan_external_texture.cc", + "android_vulkan_external_texture.h", + "android_window_metrics_mapper.cc", + "android_window_metrics_mapper.h", + "apk_asset_provider.cc", + "apk_asset_provider.h", + "flutter_embedder_native.cc", + "flutter_embedder_native.h", + "jni_delegate.h", + "jni_router.cc", + "jni_router.h", + "jvm_invoker.cc", + "jvm_invoker.h", + ] + + deps = [ + "//flutter/common", + "//flutter/fml", + "//flutter/shell/platform/embedder:embedder_headers", + ] +} + +# 2. Outer Shell Source Set (Reduced to 3 files in Phase 4) +source_set("flutter_shell_native_src") { + visibility = [ ":*" ] + sources = [ + "flutter_main.cc", + "flutter_main.h", + "library_loader.cc", + ] + public_deps = [ + ":flutter_embedder_native_src", + "//flutter/fml", + "//flutter/shell/platform/embedder:embedder_as_internal_library", + ] +} +``` + +```mermaid +flowchart TD + subgraph BuildTarget["Build Target: //flutter/shell/platform/android:flutter_shell_native"] + SharedLib["shared_library flutter_shell_native (libflutter.so)"] + ShellSrc["source_set flutter_shell_native_src (flutter_main.cc, library_loader.cc)"] + end + + subgraph SourceSets["Partitioned Source Sets in BUILD.gn"] + CAPISourceSet["source_set flutter_embedder_native_src
check_includes = true, FLUTTER_ENGINE_NO_PROTOTYPES
----------------------------------------
flutter_embedder_native.*, jni_router.*, jni_delegate.h, jvm_invoker.*
android_surface_control.*, android_platform_views_controller.*, android_mutators_mapper.*
android_hardware_buffer.*, android_vulkan_external_texture.*
android_semantics_mapper.*, android_window_metrics_mapper.*
android_vsync_waiter.*, android_vm_init.*, android_engine_group.*, apk_asset_provider.*"] + LegacySourceSet["Transitional Phase 2-3 Legacy Files (Deleted in Phase 4)
----------------------------------------
android_shell_holder.*, platform_view_android.*, platform_view_android_jni_impl.*
external_view_embedder/"] + end + + subgraph AllowedCAPIDeps["Allowed Dependencies"] + EmbedderHeaders["//flutter/shell/platform/embedder:embedder_headers & embedder_as_internal_library"] + FML["//flutter/fml & //flutter/common"] + end + + subgraph ForbiddenCAPIDeps["Forbidden Internal Dependencies (Blocked by GN check_includes)"] + Internal["//flutter/flow, //flutter/runtime, //flutter/shell/common,
//flutter/skia, //flutter/impeller, //flutter/lib/ui, //flutter/txt, //flutter/assets"] + end + + SharedLib --> ShellSrc + ShellSrc --> CAPISourceSet + ShellSrc -.->|Phases 2-3 Only| LegacySourceSet + CAPISourceSet --> AllowedCAPIDeps + CAPISourceSet -.->|COMPILE ERROR if included| ForbiddenCAPIDeps + LegacySourceSet --> ForbiddenCAPIDeps +``` + +### Phase 3: C-API Cutover Behind Feature Flag + +In Phase 3, the cutover from [`AndroidShellHolder`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/android_shell_holder.h#L38) to `FlutterEmbedderNative` is gated behind the `--enable-embedder-api` shell flag (`io.flutter.embedding.android.EnableEmbedderAPI` manifest metadata) and flipped to `true` by default once CI and DeviceLab suites pass. When enabled, `FlutterEmbedderNative` initializes [`FlutterEngine`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder.h#L389) via [`FlutterEngineInitialize`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder.h#L2970) and [`FlutterEngineRunInitialized`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder.h#L3006) and drives all runtime events exclusively through [`FlutterEngineProcTable`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder.h#L3651-L3827): + +- **Event and channel dispatch**: Routes touch events (`FlutterEngineSendPointerEvent`), window metrics (`FlutterEngineSendWindowMetricsEvent`), platform messages (`FlutterEngineSendPlatformMessage`), and accessibility actions (`FlutterEngineSendSemanticsAction`) through `FlutterEngineProcTable`. +- **Textures and multi-surface composition**: Registers external textures (`FlutterEngineRegisterExternalTexture`) and presents multi-surface frames through [`FlutterCompositor`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder.h#L2264-L2322) (`present_view_callback`), while [`PlatformViewEmbedder`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/platform_view_embedder.h) and `EmbedderExternalViewEmbedder` fulfill the internal [`Shell`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/common/shell.h#L113) contracts. + +### Phase 4: Legacy Class Purge and Build Decoupling + +Once `FlutterEmbedderNative` is validated as the default across all CI and DeviceLab tests, the `--enable-embedder-api` flag is removed and [`shell/platform/android/`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android) and [`BUILD.gn`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/BUILD.gn) are permanently decoupled: + +- **Delete legacy classes**: Remove `--enable-embedder-api`, [`AndroidShellHolder`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/android_shell_holder.h#L38), [`PlatformViewAndroid`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/platform_view_android.h#L43), [`PlatformViewAndroidJNIImpl`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/platform_view_android_jni_impl.cc), [`VsyncWaiterAndroid`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/vsync_waiter_android.h#L19), [`PlatformMessageHandlerAndroid`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/platform_message_handler_android.h#L18), [`SurfaceTextureExternalTexture`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/surface_texture_external_texture.h#L33), [`ImageExternalTexture`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/image_external_texture.h#L36), and the [`external_view_embedder/`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/external_view_embedder) directory, reducing [`:flutter_shell_native_src`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/BUILD.gn#L85) to [`flutter_main.cc`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/flutter_main.cc), [`flutter_main.h`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/flutter_main.h#L17), and [`library_loader.cc`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/library_loader.cc#L12) (which calls `FlutterEmbedderNative::RegisterJni(env)` directly). +- **Prune internal GN dependencies and lock the CI ratchet**: Remove [`//flutter/runtime`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/runtime), [`//flutter/flow`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/flow), [`//flutter/skia`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/skia), [`//flutter/impeller`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/impeller), [`//flutter/lib/ui`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/lib/ui), [`//flutter/txt`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/txt), [`//flutter/assets`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/assets), and [`//flutter/shell/common`](https://github.com/flutter/flutter/tree/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/common) from `BUILD.gn`, restricting dependencies to `embedder_headers`, `embedder_as_internal_library`, `//flutter/fml`, and `//flutter/common`, and locking `allowed_internal_headers.yaml` to `total_allowed_internal_headers: 0`. + +### The Dependency Ratchet Mechanism + +> [!IMPORTANT] +> To guarantee monotonic removal of the 98 internal headers across Phases 2–4 without regressions or `// nogncheck` bypasses, CI enforces a three-layer ratchet driven by `check_android_embedder_deps.py` and `shell/platform/android/allowed_internal_headers.yaml`. + +```mermaid +flowchart TD + subgraph Layer1["Layer 1: GN Target Partitioning (Compile-Time Firewall)"] + NewCAPI["flutter_embedder_native_src (check_includes = true, FLUTTER_ENGINE_NO_PROTOTYPES)"] + StrictDeps["Only embedder_headers + fml + common allowed"] + NewCAPI --> StrictDeps + end + + subgraph Layer2["Layer 2: CI Presubmit Linter (check_android_embedder_deps.py)"] + PR["PR touching shell/platform/android/"] + YAML["allowed_internal_headers.yaml (Starts at 98 headers)"] + Check1{"Unlisted file, new internal #include,
// nogncheck, or widened visibility?"} + Check2{"Internal #include removed without
decrementing YAML baseline?"} + Reject1["Fail Presubmit"] + Reject2["Fail Presubmit: Ratchet down YAML"] + Accept["Pass Presubmit"] + + PR --> Check1 + YAML --> Check1 + Check1 -- Yes --> Reject1 + Check1 -- No --> Check2 + Check2 -- Yes --> Reject2 + Check2 -- No --> Accept + end + + subgraph Layer3["Layer 3: Phase 4 Terminal Lock"] + Purge["Delete legacy classes & prune internal BUILD.gn deps"] + Zero["Lock allowed_internal_headers.yaml to total_allowed_internal_headers: 0"] + Purge --> Zero + end + + Layer1 --> Layer2 --> Layer3 +``` + +```yaml +# shell/platform/android/allowed_internal_headers.yaml (ratcheted from 98 to 0 in Phase 4) +version: 1 +total_allowed_internal_headers: 98 +subsystems: + impeller: + count: 29 + allowed_headers: + - "flutter/impeller/renderer/backend/vulkan/context_vk.h" + - "flutter/impeller/renderer/backend/gles/context_gles.h" + - "flutter/impeller/toolkit/android/hardware_buffer.h" + flow: + count: 2 + allowed_headers: + - "flutter/flow/embedded_views.h" + - "flutter/flow/surface.h" + engine_core: + count: 11 + allowed_headers: + - "flutter/shell/common/shell.h" + - "flutter/shell/common/thread_host.h" + - "flutter/shell/common/platform_view.h" +``` + +## Verification and Testing Plan + +### Engine Unit Tests + +- Host-executable C++ unit tests (`flutter_embedder_native_unittests`) test `FlutterEmbedderNative` and each modular component using mock [`FlutterEngineProcTable`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder.h#L3651-L3827) entries and injectable `JniDelegate` / `JvmInvoker` providers without an Android JVM or device. +- [`embedder_unittests`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/BUILD.gn#L372-L390) cover `kDeferred` renderer resolution, non-blocking `FlutterEngineRunInitialized` while `resolve_callback` runs on the raster thread, opt-in dynamic raster thread merging (`merge_raster_thread_on_platform_views`), [`FlutterPlatformViewMutation`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/embedder/embedder.h#L2083-L2092) serialization, and per-`FlutterViewId` frame state capture. + +### Android Integration and DeviceLab Tests + +- Run [`flutter_shell_native_unittests`](https://github.com/flutter/flutter/blob/4762dec4fc9072cd9269814ef36364f35a9a66dc/engine/src/flutter/shell/platform/android/BUILD.gn#L42-L67), platform view composition tests across all four Android composition modes (HCPP, HC, TLHC, VD), activity stop/start and configuration-change loops (`surfaceDestroyed()` / `surfaceCreated()`), JNI platform channels, background isolate callbacks, and deferred library loading. +- Verify with golden tests that clip paths, rounded rectangles, and opacity mutators render identically across all four composition modes (HCPP, HC, TLHC, VD), and validate external texture playback across camera and video player plugins. +- Monitor the existing DeviceLab benchmarks across the Phase 3 cutover to verify zero regressions in frame pacing, rasterization latency, HCPP/HC scrolling and fence timing, [`FlutterCustomAssetResolver`](#custom-asset-and-kernel-resolution) startup latency, cold-start time-to-first-frame (`CreateImpellerContext`), and post-`surfaceDestroyed()` GPU memory reclamation (`FlutterEngineNotifyLowMemoryWarning`).