Use case
Flutter GPU users can currently render into a gpu.Texture, convert it to a ui.Image, then draw that image with Canvas.drawImageRect.
final color = gpu.gpuContext.createTexture(
gpu.StorageMode.devicePrivate,
width,
height,
format: gpu.gpuContext.defaultColorFormat,
enableRenderTargetUsage: true,
enableShaderReadUsage: true,
);
final commandBuffer = gpu.gpuContext.createCommandBuffer();
final renderPass = commandBuffer.createRenderPass(
gpu.RenderTarget.singleColor(gpu.ColorAttachment(texture: color)),
);
// Bind a pipeline, bind resources, and draw.
commandBuffer.submit();
final image = color.asImage();
canvas.drawImageRect(image, src, dst, paint);
This works with Impeller because the image can wrap the underlying shader readable Impeller texture. The hard part is lifetime. If an app renders a new frame each build, it must avoid overwriting a texture that the framework or rasterizer may still be sampling. Apps can work around this with a small texture ring, but that forces application code to guess how many frames may be in flight. Some backends or future backend modes may require three buffers, dynamic buffer growth, or explicit acquire and release fences.
The missing API is not another way to create a texture. It is a presentation contract between Flutter GPU and the Flutter framework.
Proposal
Add a first class Flutter GPU surface that owns a pool of presentable, shader readable render targets.
final surface = gpu.gpuContext.createSurface(
width,
height,
format: gpu.gpuContext.defaultColorFormat,
);
final frame = surface.acquireNextFrame();
final commandBuffer = gpu.gpuContext.createCommandBuffer();
final renderPass = commandBuffer.createRenderPass(
gpu.RenderTarget.singleColor(
gpu.ColorAttachment(texture: frame.colorTexture),
),
);
// Bind a pipeline, bind resources, and draw.
final image = frame.present(commandBuffer);
commandBuffer.submit();
canvas.drawImageRect(image, src, dst, paint);
Expected semantics:
GpuSurface owns the backing texture pool and chooses the buffer count.
SurfaceFrame.colorTexture is writable only until present.
present ties publication to command buffer completion.
surface.currentImage returns the latest presented image, not the texture currently being written.
- The engine does not reuse a backing texture until producer GPU work and framework raster sampling are complete.
- Resizing invalidates future acquired frames while keeping old presented images alive until safe.
Texture.asImage() remains useful for static and expert cases, but animated render targets should use GpuSurface.
The surface should own only the final presentable color texture. Renderers can still use normal Flutter GPU render targets for depth, stencil, MSAA, and intermediate color attachments:
final frame = surface.acquireNextFrame();
final depthStencil = gpu.gpuContext.createTexture(
gpu.StorageMode.deviceTransient,
width,
height,
format: gpu.gpuContext.defaultDepthStencilFormat,
enableRenderTargetUsage: true,
enableShaderReadUsage: false,
);
final commandBuffer = gpu.gpuContext.createCommandBuffer();
final renderPass = commandBuffer.createRenderPass(
gpu.RenderTarget.singleColor(
gpu.ColorAttachment(texture: frame.colorTexture),
depthStencilAttachment: gpu.DepthStencilAttachment(texture: depthStencil),
),
);
For multiple color attachments, the renderer would keep using renderer owned textures for intermediate passes, then resolve or composite the final result into frame.colorTexture before presenting. A later convenience API could provide per frame transient attachments, but the core surface abstraction only needs to manage the image that will be presented to Flutter.
Rationale from related APIs:
This would let Flutter GPU keep Flutter's normal composition model while removing the fragile part from user code: guessing how many textures are needed and when a previous texture is safe to render into again.
Use case
Flutter GPU users can currently render into a
gpu.Texture, convert it to aui.Image, then draw that image withCanvas.drawImageRect.This works with Impeller because the image can wrap the underlying shader readable Impeller texture. The hard part is lifetime. If an app renders a new frame each build, it must avoid overwriting a texture that the framework or rasterizer may still be sampling. Apps can work around this with a small texture ring, but that forces application code to guess how many frames may be in flight. Some backends or future backend modes may require three buffers, dynamic buffer growth, or explicit acquire and release fences.
The missing API is not another way to create a texture. It is a presentation contract between Flutter GPU and the Flutter framework.
Proposal
Add a first class Flutter GPU surface that owns a pool of presentable, shader readable render targets.
Expected semantics:
GpuSurfaceowns the backing texture pool and chooses the buffer count.SurfaceFrame.colorTextureis writable only untilpresent.presentties publication to command buffer completion.surface.currentImagereturns the latest presented image, not the texture currently being written.Texture.asImage()remains useful for static and expert cases, but animated render targets should useGpuSurface.The surface should own only the final presentable color texture. Renderers can still use normal Flutter GPU render targets for depth, stencil, MSAA, and intermediate color attachments:
For multiple color attachments, the renderer would keep using renderer owned textures for intermediate passes, then resolve or composite the final result into
frame.colorTexturebefore presenting. A later convenience API could provide per frame transient attachments, but the core surface abstraction only needs to manage the image that will be presented to Flutter.Rationale from related APIs:
GPUCanvasContext.configure()andgetCurrentTexture()so the context vends the current renderable texture and owns canvas texture lifecycle: https://gpuweb.github.io/gpuweb/explainer/#canvas-outputSurface::get_current_texture()andSurfaceTexturefor the same acquire and present pattern: https://wgpu.rs/doc/wgpu/struct.Surface.htmlCAMetalDrawableobjects from a limited pool and register presentation with the command buffer: https://developer.apple.com/library/archive/documentation/3DDrawing/Conceptual/MTLBestPracticesGuide/Drawables.htmlvkAcquireNextImageKHR, andvkQueuePresentKHRexplicit because image ownership and reuse are backend concerns: https://docs.vulkan.org/refpages/latest/refpages/source/VkSwapchainCreateInfoKHR.htmlThis would let Flutter GPU keep Flutter's normal composition model while removing the fragile part from user code: guessing how many textures are needed and when a previous texture is safe to render into again.