Repository navigation
Flutter GPU: external render targets for OpenXR — prototype and request for upstream guidance #192059
Description
Hi Flutter team, I would like your guidance on contributing a Flutter GPU/engine change that lets Flutter render into GPU images owned by another system. I have a working Meta Quest 3 prototype in the forks lsited bellow. Sorry for the sizeable patch and long post!
TL;DR: I added a generic external render-target API to a Flutter engine fork, allowing Flutter Scene to render directly into OpenXR’s left- and right-eye images. This removes the intermediate Flutter Canvas presentation and native eye copy, while keeping Flutter UI panels separate. My simplified demo went from roughly 52–54 FPS to around 70 FPS, although this was not a controlled Canvas-only benchmark. I’m asking both the Flutter and Flutter Scene maintainers how to revise these forks into suitable upstream contributions.
- Flutter engine changes: Generic external GPU render surfaces — 25 files · +2.4K / −6 lines
- Flutter Scene integration: OpenXR support and Quest example gallery
- Background and discussion: Flutter Scene issue #365
Why I started this
I’m developing Visual Space, a Flutter application with a spatial UI and a 3D editor. We use Flutter Scene for ordinary 3D rendering, but initially built a separate native renderer for immersive Quest support, with Flutter supplying the interactive UI panels.
Before this work, we had already built our own custom native 3D renderer for Quest, with Flutter supplying the UI panels. It works, but leaves us maintaining two rendering engines: Flutter Scene for web and the ordinary application, and our own native renderer for VR. Every material, lighting effect, shadow improvement, and new scene feature then needs work in both places. We want to replace that duplicated native renderer with Flutter Scene, while keeping OpenXR responsible for tracking and headset presentation.
Getting Flutter Scene running in VR was the next step. Our first prototype rendered its left- and right-eye views through Flutter Canvas into a large Android surface, then copied those images into OpenXR’s eye textures. This let us reuse Flutter Scene, but added composition, buffering, and copying work to every stereo frame.
To remove that extra path, Flutter Scene needs permission to draw directly into the GPU eye images that OpenXR owns. That is the missing capability addressed by the Flutter engine patch. I added an API that lets Flutter GPU temporarily borrow those images as rendering destinations, then built a custom local Flutter engine to try it. This lets us keep Flutter Scene as the renderer without routing the stereo world through Canvas.
What the Flutter patch adds
The engine contribution is generic external GPU output, not Quest-specific session or controller code. OpenXR is the first consumer I have working.
The main pieces are:
- A new destination for Flutter GPU rendering.
GpuExternalSurfaceand its frame type use the existingGpuSurface/GpuSurfaceFramepattern. Native code supplies a target; Dart acquires it, draws into it, and presents or discards the frame. Flutter borrows the target rather than taking ownership of its GPU storage. - Native registration and frame delivery. Embedder C APIs register surfaces, supply frames, and unregister them. An Android bridge through
FlutterRendererandFlutterJNIexposes the same capability to an Android host. - Shared GLES resources. The Android bridge allows the host and Flutter to access the same GPU textures. It creates the framebuffer used for drawing inside Flutter’s own graphics context, because framebuffer objects cannot simply be shared between contexts the way textures can.
- Explicit completion and cleanup. Completion is tied to the commands that write the final image. Sending commands alone does not mean OpenXR can safely reuse it. Android uses GPU fences to detect when drawing has finished where supported, with a conservative blocking fallback.
The borrowed target is currently a single-sample final color destination. Flutter Scene still owns intermediate buffers for depth, shadows, antialiasing, HDR, and effects.
In the Flutter Scene fork, Scene.renderViewsToTargets connects the renderer to these targets. The OpenXR companion package handles the headset session, tracking, controllers, and eye-image coordination, outside the engine patch.
Who drives the frames?
The native OpenXR host waits for the headset’s next frame using xrWaitFrame, acquires both eye images, and requests stereo rendering through the Android/Dart bridge. Flutter Scene updates the scene once and renders both eyes. The host waits for both GPU-completion callbacks before returning the images to OpenXR for display.
The stereo world does not wait for a Flutter widget paint. UI panels update separately. This uses a custom Android activity/plugin alongside Flutter’s existing Android embedding; I have not replaced Flutter’s overall frame scheduler.
Is this separation appropriate, or would a dedicated OpenXR embedder with broader control over Flutter’s scheduling be a better approach?
What works, and what still needs work
I have built and tried this on Meta Quest 3 using Android/OpenGL ES. The gallery includes real Flutter Scene examples, interactive panels, and performance controls. Vulkan and Metal are not implemented for this path, and each eye is rendered separately.
The FPS improvement above followed earlier shadow tuning, and the direct path changed scheduling as well as presentation. Heavy scenes and effects still reduce performance. A fast OpenXR submission loop also does not necessarily mean equally fast fresh scene content.
Visual Space sometimes has up to three layers of Flutter panels. We already suspend texture updates for panels in “sleep mode” to preserve performance. Separating stereo rendering leaves more room and budget for that UI atlas, although it does not solve independent panel updates by itself.
The engine patch is not ready to merge unchanged. An initial review flagged concerns around access from the correct engine threads, cleanup and shared resources when multiple Flutter engines are involved, and handling the extra GPU attachments used for depth and antialiasing. These need investigation, fixes, and focused engine tests. The current engine contribution does not yet include those tests; successful builds and a working headset demo are not a substitute for them.
The Flutter fork README and Flutter Scene setup guide cover building the custom engine and running the demo. Profiling additions and fork documentation could be separated from the core API patch.
Guidance I’m asking for
We are waiting for guidance from both repositories so we can adjust the engine API and Flutter Scene integration together, then submit focused contributions for review. In particular:
- Does a generic external destination extending
GpuSurfacelook like an acceptable direction for Flutter GPU? - Where should the Android resource-sharing and frame-completion integration live, and is the current host approach appropriate?
- Would you consider an initial GLES implementation, with other backends explicitly unsupported, or should the first proposal cover a wider backend contract?
- What lifecycle guarantees, tests, and API changes would you need before reviewing this for inclusion? Who would be the right maintainers to involve?
I understand this capability was not planned, and I’m happy to split the patch into smaller changes. I have substantial programming experience, but I’m not a graphics-engine specialist. Help with the design, review, testing, and contribution process would be very welcome; I’m currently handling this work on my own alongside the application.
Both forks are already part of our development stack, and we are committed to this work. Next comes integrating the direct renderer into Visual Space itself. Until these changes can land upstream, we will maintain and rebase the forks, but I hope that does not have to become a permanent second project.
Thank you for reading, and for any guidance or help getting this into a form the Flutter and Flutter Scene teams could accept.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fields🤔 Needs Triage
Activity
mbcorona commentedon Aug 31, 2026
Hi @adrian-moisa, thanks for the detailed writeup and the working Quest 3 prototype. Routing to the Flutter GPU team for evaluation.
adrian-moisa commentedon Sep 1, 2026
Good news, was able to get the modified flutter_scene_vr / flutter_vr forks/engines and run them in my own app in the Quest. Both web and VR now have the same rendering style. Prime objective accomplished. Old custom cpp engine is deleted (wasn't planning to keep it around). Now the next challenge: merging the forks.
Side note: I've imported from the flutter_scene example app, all the graphics settings controls. Now I have far better control over what gets rendered in the Quest and I can start tweaking stuff to fine tune it for decent/good performance for my usecase. I still have lots of tuning to do till I get back to a respectable 70 fps. But it does seem like a reachable goal. I think it's completely possible to get 70 fps even when rendering complex flutter panels at the same time.
dzolotov commentedon Sep 11, 2026
This looks like the right thing to add, and keeping it generic (external GPU output, not Quest-specific code) is the important part. I ran into the same wall from another direction, with flutter3d, an independent 3D engine on Flutter GPU.
Right now every frame the engine draws goes into a
flutter_gputexture, turns into aui.Imageand gets composited through the widget tree. On the web it's a platform view with a canvas in it. Rendering into a target the host owns cuts that step out, which helps anything that draws 3D continuously, not just VR.The jump you measured (52-54 fps to around 70 on Quest 3) matches what I'd expect from dropping a full-screen copy and one more composited layer per frame. At headset resolution and 72-90 Hz that copy is pure memory bandwidth, and bandwidth is what mobile GPUs run out of first. A phone game that fills the screen pays the same cost 60 times a second, so the win isn't limited to VR. I haven't profiled that hop in my own engine, so I won't put a number on it, but it's on the list of things to cut and this is the only way to cut it.
For XR it's the blocker.
flutter3d_stereoalready renders a stereo pair side by side and reads head rotation from the phone sensor. That's enough for a Cardboard holder, and it's as far as you can get without runtime-owned images, a predicted pose and the runtime's own frame schedule.One ask: keep the contract backend-neutral even if GLES ships first. Borrowing the target instead of owning its storage, with explicit completion semantics, sounds right. If this moves forward, I'm glad to test it against a second engine.
dzolotov commentedon Oct 10, 2026
I've been working on the other half of this from the flutter3d side, and I'd like to keep the two from colliding.
What I have is a way to import external images so Flutter GPU can read them: sampling, compute storage and blit copies of an MTLTexture or IOSurface, a VkImage on Impeller's own device or an AHardwareBuffer, and a GL texture name. It goes through one hook on
impeller::Context(ImportTexture, with ContextMTL/VK/GLES behind it) andGpuContext.importTexturein Dart. Imported textures can't be render pass attachments, on purpose. Rendering into externally owned targets, the swapchain and acquire/present part, is what this issue and @adrian-moisa'sGpuExternalSurfacecover, and I'm staying out of it.It's on a local branch with tests on macOS (Metal, Vulkan through SwiftShader with validation layers, GLES through ANGLE). It also turned up a bug in
BlitCopyTextureToBufferCommandGLES: every wrapped texture is treated as the default framebuffer, so reading back a wrapped GL texture reads the screen. I'll file that one separately.The design goes in its own issue: #194142. Two points there touch this one:
ExternalSurfaceRegistryregisters natively and hands Dart an id. If maintainers want that shape for imports too, I'd rather share one registry than build a second.GpuExternalSurface. Adrian, does that fit what you have, or would it get in your way?I won't open PRs until the shape is agreed.