Repository navigation
Description
Use case
Linux embedders that own KMS directly: no Wayland or X compositor, the embedder holds DRM master, renders into dma-bufs and scans them out itself. Automotive IVI, kiosk, signage.
Such an embedder uses the compositor API rather than the root surface for one reason: layers. present_layers hands it each layer separately, so Flutter UI and platform-view content can go to separate hardware planes, composited by the display controller instead of the GPU. That buys:
- a camera or video feed reaching the panel without a GPU round trip, as YUV, on
an overlay plane; - protected/secure planes for content that must not transit the GPU;
- fill rate saved on every frame the display controller composites for free.
Vulkan rather than GL because that is where the drivers are going, because explicit sync lets the display engine wait on a dma-buf fence (IN_FENCE_FD) instead of blocking the raster thread, and because Vulkan-native platform-view producers can share images without a GL interop copy.
Today that combination does not work. kFlutterBackingStoreTypeVulkan has no Impeller path, so these embedders are Skia-only. With Skia being retired, the class has no forward path: the renderer it must move to cannot use the presentation API it depends on.
Current behavior
shell/platform/embedder/embedder.cc, CreateEmbedderRenderTarget:
case kFlutterBackingStoreTypeVulkan: {
if (enable_impeller) {
FML_LOG(ERROR) << "Unimplemented";
break;
} else {
auto skia_surface = MakeSkSurfaceFromBackingStore(
context, config, &backing_store.vulkan);
render_target = MakeRenderTargetFromSkSurface(
backing_store, std::move(skia_surface), collect_callback.Release());
break;
}
}Metal calls MakeRenderTargetFromBackingStoreImpeller. OpenGL calls it for
kFlutterOpenGLTargetTypeFramebuffer. Vulkan has neither.
With --enable-impeller, every frame fails and nothing is presented:
[ERROR:embedder.cc(1519)] Unimplemented
[ERROR:embedder.cc(1532)] Could not create a surface from an embedder provided render target.
Quiet from the embedder's side: those come from the engine's logger, so an embedder with its own logging shows nothing wrong, keeps running, and leaves a blank display.
Related: #151670 covers the GL surface target type, a different case in the same switch.
Proposal
Implement MakeRenderTargetFromBackingStoreImpeller for FlutterVulkanBackingStore, matching the existing GL and Metal overloads, and call it from the kFlutterBackingStoreTypeVulkan case.
The wrapping already exists in the tree. GPUSurfaceVulkanImpeller::AcquireFrame does exactly this for the embedder's root surface image (shell/gpu/gpu_surface_vulkan_impeller.cc):
impeller::vk::Image vk_image = impeller::vk::Image(
reinterpret_cast<VkImage>(flutter_image.image));
impeller::TextureDescriptor desc; // kDevicePrivate, kRenderTarget
...createImageViewUnique(view_info);
auto wrapped = std::make_shared<WrappedTextureSourceVK>(
vk_image, std::move(image_view), desc);
auto surface = impeller::SurfaceVK::WrapSwapchainImage(transients_, wrapped, ...);
impeller::RenderTarget render_target = surface->GetRenderTarget();FlutterVulkanBackingStore carries the same two fields that path consumes: a VkImage handle and a VkFormat. Suggested shape is to factor that sequence out of AcquireFrame into a helper taking (image, format, size) returning an impeller::RenderTarget, then call it from both places. No new Impeller capability required — the difference between the paths is where the image comes from, not what is done with it.
Secondary: WSI requirement on an embedder-provided instance
CapabilitiesVK::GetEnabledInstanceExtensions requires VK_KHR_surface plus one platform WSI extension, and the device check requires VK_KHR_swapchain, even when the context is built around an embedder-provided image and never creates a VkSurfaceKHR or a swapchain.
A KMS-direct embedder has no window system to declare, and declares one anyway:
VK_KHR_portability_enumeration is the only entry on that list implying no window system, so that is what gets used — plainly not what it means. VK_KHR_display, the one extension that would suit a KMS embedder, is not on the list, and enabling it makes the loader take the display so the embedder's own drmSetMaster fails with EPERM.
Suggestion: skip the swapchain-related capability checks when the context will not own a swapchain. Can be filed separately if preferred.
Workaround, and what it costs
Offer no FlutterCompositor when Impeller is active and present through the root surface (get_next_image / present_image). Measured on V3D 4.2 (v3dv) with a vc4 display at 1280x1440:
| renderer | path | fps | RSS |
|---|---|---|---|
| Skia | compositor, Vulkan backing stores | 42.0 | 99.4 MB |
| Impeller | root surface | 59.7 | 93.2 MB |
Impeller is faster and slightly smaller, so the motivation is not theoretical. But the root-surface path has no layer stack, so platform-view layers cannot reach a hardware plane — the reason the compositor path exists. It also requires declaring the three WSI extensions described above, none of which the embedder uses.
Downstream reference
toyota-connected/ivi-homescreen#582 — same analysis, the workaround as landed, and the measurements.
Flutter version
Engine 77e2e94772b6eb43759e34ed1ad7da4674e19cab.
Activity
jwinarske commentedon Sep 15, 2026
Duplicate of #187525, which is the same gap (
CreateEmbedderRenderTargethas no Impeller path forkFlutterBackingStoreTypeVulkan) and already triaged P2.Moved the downstream evidence there: measurements, the root-surface workaround and what it gives up, and a note that the wrapping already exists in
GPUSurfaceVulkanImpeller::AcquireFrame. Tracking downstream at toyota-connected/ivi-homescreen#582.github-actions commentedon Oct 1, 2026
This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of
flutter doctor -vand a minimal reproduction of the issue.