Skip to content

[Embedder API] Support Making a Render Target from Vulkan backing store #187525

Open
0 of 2 issues completed

Description

@mboetger

Problem

The embedder API does not support rendering using Vulkan when Impeller is enabled:

case kFlutterBackingStoreTypeVulkan: {
if (enable_impeller) {
FML_LOG(ERROR) << "Unimplemented";
break;

Background

At startup, an embedder provides a FlutterCompositor to allow the engine to render content.

To present a frame, the engine will:

  1. Invoke FlutterCompositor.create_backing_store_callback zero or more times to get render targets (aka "backing stores") from the embedder that the engine can render into.
  2. Invoke FlutterCompositor.present_view_callback to notify the embedder that the backing stores can be displayed on the screen.

The embedder API supports rendering using Vulkan and Skia, but not Vulkan and Impeller.

Activity

  1. changed the title [-][Embedder API] Support Vulkan backing store[/-] [+][Embedder API] Support Making a Render Target from Vulkan backing store[/+] on Jun 3, 2026
  2. added
    platform-androidAndroid applications specifically
    engineflutter/engine related. See also e: labels.
    e: embedderUsers of the Embedder API
    team-androidOwned by Android platform team
    on Jun 3, 2026
  3. flutter-triage-bot commented on Jul 30, 2026

    @flutter-triage-bot

    This issue is missing a priority label. Please set a priority label when adding the triaged-android label.

  4. added
    P2Important issues not at the top of the work list
    triaged-androidTriaged by Android platform team
    on Aug 4, 2026
  5. jwinarske commented on Sep 15, 2026

    @jwinarske
    Contributor

    Another affected embedder, with measurements — ivi-homescreen, a KMS-direct Linux shell (no Wayland/X; it holds DRM master and scans out dma-bufs itself). It uses the compositor API for layers so Flutter content and platform views can land on separate hardware planes, which is the reason not to use the root surface.

    Symptom is as described: with --enable-impeller, CreateEmbedderRenderTarget hits the Unimplemented branch and rasterization fails every frame.

    [ERROR:embedder.cc(1519)] Unimplemented
    [ERROR:embedder.cc(1532)] Could not create a surface from an embedder provided render target.
    

    Worth noting how quiet that is from the embedder's side: those come from the engine's logger, so an embedder with its own logging shows nothing wrong, keeps running, holds DRM master, and presents zero frames. A blank display with a clean exit.

    What it costs

    Measured on V3D 4.2 (v3dv) with a vc4 display at 1280x1440, same app and bundle, 20s runs:

    renderer path fps RSS threads
    Skia compositor, Vulkan backing stores 42.0 99.4 MB 15
    Impeller root surface (workaround) 59.7 93.2 MB 19

    43% more frames for 9% more CPU, and slightly lower RSS. So this is not a theoretical gap — Impeller is the faster renderer on this hardware and the compositor path is what blocks it.

    Workaround, and what it gives up

    Offer no FlutterCompositor when Impeller is active and present through the root surface (get_next_image / present_image). That works, but the root-surface path carries no layer stack, so platform-view layers cannot reach a hardware plane — which is the whole reason the compositor API was being used. Tracked downstream at toyota-connected/ivi-homescreen#582.

    Three instance/device extensions also have to be declared for CapabilitiesVK to accept the embedder-provided instance, none of which a KMS-direct embedder uses: VK_KHR_surface, one entry from the WSI list (VK_KHR_portability_enumeration is the only candidate implying no window system), and VK_KHR_swapchain on the device. VK_KHR_display is not accepted by that check, and enabling it makes the loader take the display so the embedder's own drmSetMaster fails with EPERM. Possibly worth its own issue.

    On implementation

    The wrapping already exists in tree. GPUSurfaceVulkanImpeller::AcquireFrame does exactly this for the embedder's root surface image:

    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. Factoring that sequence into a helper taking (image, format, size) returning an impeller::RenderTarget and calling it from both places looks like the shape, mirroring the Metal overload of MakeRenderTargetFromBackingStoreImpeller. No new Impeller capability required; the difference between the paths is where the image comes from, not what is done with it.

    Noting #144613 (same area, engine team's position there was that a contributed implementation with tests would likely be accepted) and #188855, which is doing exactly that for Vulkan external textures with both Skia and Impeller paths.

    Closing #192816 as a duplicate of this.

  6. jwinarske commented on Sep 18, 2026

    @jwinarske
    Contributor

    Have this working. Validated on master (a1498f2449f) and stable 3.47.4 (9584c6713b), and on hardware — V3D 4.2 / vc4, drm-kms-vulkan embedder, 1280x1440@60, Impeller rendering into embedder-supplied Vulkan backing stores and reaching a KMS plane.

    Two bugs turned up on the way, both independent of the feature:

    • GPUSurfaceVulkanImpeller has no render_to_surface gate. GPUSurfaceVulkan and GPUSurfaceGLImpeller both have one. With a compositor the engine presents an unrendered root image after every composited frame; on an embedder that recycles scanout buffers that overwrites the frame just shown. Symptom is one good frame, then black, while the flip rate still reads 60fps.
    • StrcmpFixed in embedder_vk_unittests.cc compares N-1 bytes, so "vkQueueSubmit" also matches "vkQueueSubmit2". Harmless until something resolves both — Impeller does — then the test swaps in a function with the wrong signature and it segfaults.

    Cost, measured against the same engine and app on that target. All three are vsync-capped at 60fps, so the number that separates them is per-frame headroom (shell idle time waiting on the flip):

    engine work/frame
    Skia + compositor ~4.1 ms
    Impeller + root surface ~7.0 ms
    Impeller + backing stores ~7.8 ms

    So backing stores cost ~0.8 ms over the root surface, of a 16.67 ms budget. MSAA off saves ~0.5 ms of that; the rest is RenderToTarget itself (5.25 ms), and the display-list wrapping the compositor path adds is 30 us — not worth chasing.

    Planning six PRs rather than one, smallest first. 1-4 are independent; 5 needs 3 and 4, 6 needs everything:

    1. StrcmpFixed prefix match (test-only, few lines).
    2. render_to_surface gate for GPUSurfaceVulkanImpeller, with tests that fail without it.
    3. Lift WrapTextureSourceVK out of gpu_surface_vulkan_impeller.cc into impeller/renderer/backend/vulkan/ (refactor, net -53).
    4. Test fixture: TestVulkanContext creates its own device with VK_KHR_swapchain, requests the surface extensions, and reports both through FlutterVulkanRendererConfig. Without this Impeller refuses the embedder context, so no Impeller Vulkan embedder test can run at all.
    5. The feature: MakeRenderTargetFromBackingStoreImpeller for kFlutterBackingStoreTypeVulkan, plus the MSAA transients cache it needs. Building transients per render target allocates tens of MB per frame on an embedder that asks for a fresh backing store each frame — that measured 23.5fps before caching, 58.8 after.
    6. Impeller variants of the shared Vulkan tests, with goldens.

    Two things I want to flag for (5):

    • The backing store comes back in a different layout depending on renderer — Skia leaves COLOR_ATTACHMENT_OPTIMAL, Impeller leaves GENERAL (the wrapped image is marked a swapchain image, and that picks the final layout). embedder.h doesn't say, so every embedder gets to find out the hard way. I'll document it there; say if you'd rather the engine hand back a defined layout instead.
    • Goldens for (6) were generated against our SwiftShader build and the comparison is exact. If CI's differs by a level or two they'll flake. I'd rather give them a tolerance than pin a number that isn't reproducible.

    Filing the paint-region difference separately; it is not ours and it bites embedders regardless of this work.

  7. 4 remaining items

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Important issues not at the top of the work liste: embedderUsers of the Embedder APIengineflutter/engine related. See also e: labels.platform-androidAndroid applications specificallyteam-androidOwned by Android platform teamtriaged-androidTriaged by Android platform team

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions