You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Embedder API] Support Making a Render Target from Vulkan backing store #187525
changed the title [-][Embedder API] Support Vulkan backing store[/-][+][Embedder API] Support Making a Render Target from Vulkan backing store[/+]on Jun 3, 2026
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:
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.
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:
StrcmpFixed prefix match (test-only, few lines).
render_to_surface gate for GPUSurfaceVulkanImpeller, with tests that fail without it.
Lift WrapTextureSourceVK out of gpu_surface_vulkan_impeller.cc into impeller/renderer/backend/vulkan/ (refactor, net -53).
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.
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.
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.
Activity
[-][Embedder API] Support Vulkan backing store[/-][+][Embedder API] Support Making a Render Target from Vulkan backing store[/+]flutter-triage-bot commentedon Jul 30, 2026
This issue is missing a priority label. Please set a priority label when adding the
triaged-androidlabel.jwinarske commentedon Sep 15, 2026
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,CreateEmbedderRenderTargethits theUnimplementedbranch and rasterization fails every frame.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 avc4display at 1280x1440, same app and bundle, 20s runs: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
FlutterCompositorwhen 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
CapabilitiesVKto 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_enumerationis the only candidate implying no window system), andVK_KHR_swapchainon the device.VK_KHR_displayis not accepted by that check, and enabling it makes the loader take the display so the embedder's owndrmSetMasterfails withEPERM. Possibly worth its own issue.On implementation
The wrapping already exists in tree.
GPUSurfaceVulkanImpeller::AcquireFramedoes 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();FlutterVulkanBackingStorecarries the same two fields that path consumes — aVkImagehandle and aVkFormat. Factoring that sequence into a helper taking (image, format, size) returning animpeller::RenderTargetand calling it from both places looks like the shape, mirroring the Metal overload ofMakeRenderTargetFromBackingStoreImpeller. 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.
kFlutterBackingStoreTypeVulkanbacking stores #192816 as a duplicate of this issuejwinarske commentedon Sep 18, 2026
Have this working. Validated on master (
a1498f2449f) and stable 3.47.4 (9584c6713b), and on hardware — V3D 4.2 / vc4,drm-kms-vulkanembedder, 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:
GPUSurfaceVulkanImpellerhas norender_to_surfacegate.GPUSurfaceVulkanandGPUSurfaceGLImpellerboth 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.StrcmpFixedinembedder_vk_unittests.cccomparesN-1bytes, 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):
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
RenderToTargetitself (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:
StrcmpFixedprefix match (test-only, few lines).render_to_surfacegate forGPUSurfaceVulkanImpeller, with tests that fail without it.WrapTextureSourceVKout ofgpu_surface_vulkan_impeller.ccintoimpeller/renderer/backend/vulkan/(refactor, net -53).TestVulkanContextcreates its own device withVK_KHR_swapchain, requests the surface extensions, and reports both throughFlutterVulkanRendererConfig. Without this Impeller refuses the embedder context, so no Impeller Vulkan embedder test can run at all.MakeRenderTargetFromBackingStoreImpellerforkFlutterBackingStoreTypeVulkan, 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.Two things I want to flag for (5):
COLOR_ATTACHMENT_OPTIMAL, Impeller leavesGENERAL(the wrapped image is marked a swapchain image, and that picks the final layout).embedder.hdoesn'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.Filing the paint-region difference separately; it is not ours and it bites embedders regardless of this work.
drm_kms_vulkan: let --drm-compositor select the present path under Im…
4 remaining items
Do not present the root surface when an external view embedder owns it
Move the Vulkan wrapped-texture helper out of the GPU surface
Do not present the root surface when an external view embedder owns it
Move the Vulkan wrapped-texture helper out of the GPU surface
Give the Vulkan test context the extensions Impeller requires (flutte…
Fix StrcmpFixed matching prefixes in the Vulkan embedder tests (flutt…
Do not present the root surface when an external view embedder owns it (
Move the Vulkan wrapped-texture helper out of the GPU surface
Move the Vulkan wrapped-texture helper out of the GPU surface (flutte…
Give the Vulkan test context the extensions Impeller requires (flutte…
Fix StrcmpFixed matching prefixes in the Vulkan embedder tests (flutt…
Do not present the root surface when an external view embedder owns it (
Move the Vulkan wrapped-texture helper out of the GPU surface (flutte…