Skip to content

[Impeller] GLES: copying a wrapped texture to a buffer reads the default framebuffer #194150

Closed

Description

@dzolotov

Steps to reproduce

ConfigureFBO in engine/src/flutter/impeller/renderer/backend/gles/blit_command_gles.cc decides how a texture is bound for a blit:

if (TextureGLES::Cast(*texture).IsWrapped()) {
  // The texture is attached to the default FBO, so there's no need to
  // create/configure one.
  gl.BindFramebuffer(fbo_type, 0);
  return 0;
}

IsWrapped() is true for two different kinds of texture. TextureGLES::WrapFBO wraps a framebuffer: the onscreen surface, where binding framebuffer 0 is what the comment describes, or a caller's own FBO, where it binds the wrong one. TextureGLES::WrapTexture wraps an existing GL texture name, as ImpellerTextureCreateWithOpenGLTextureHandleNew in the interop toolkit does; such a texture has no framebuffer at all, and GetFBO() returns std::nullopt. The blit binds framebuffer 0 for both.

This can't be reproduced from Dart today: Flutter GPU doesn't take a GL texture it didn't create, so a wrapped texture name never reaches it. On main the path is open through the interop toolkit (ImpellerTextureCreateWithOpenGLTextureHandleNew) and skwasm (images_impeller.cc), and from Dart it opens with texture import (#194142), which is where I ran into it. So the repro is at the HAL level:

  1. Wrap a GL texture name with known contents with TextureGLES::WrapTexture (same share group).
  2. Encode a texture-to-buffer copy from it into a host-visible buffer and submit.
  3. Read the buffer.

The code sample below does this at the HAL level. With the mock GL proc table in impeller_unittests the same thing shows as glBindFramebuffer(GL_FRAMEBUFFER, 0) followed by glReadPixels, with no glFramebufferTexture2D for the texture name.

Expected results

The buffer holds the wrapped texture's pixels: the texture name is attached to a temporary framebuffer and read from there, as for any texture the engine made. A wrapped framebuffer other than 0 is read from that framebuffer.

Actual results

The buffer holds the pixels of whatever is bound as framebuffer 0, and the copy reports success, with no error or validation message. A texture-to-texture blit with a wrapped texture name as source or destination reads or writes framebuffer 0 the same way. RenderPassGLES has the same confusion: with a wrapped texture name as its color attachment it binds no framebuffer, turns off the y flip and draws into whatever is bound.

Suggested fix

Tell the two apart with a TextureGLES::IsWrappedFBO(). A wrapped framebuffer binds the FBO it wraps, and the caller's FBO is never deleted; a wrapped texture name is read through a temporary FBO.

I'd keep it read-only. Uploads and SetContents already refuse wrapped textures, so a blit into a wrapped texture name, or a render pass that uses one as its color attachment, should be refused too, with a validation error, instead of writing into the wrong framebuffer. Writing into textures the caller owns is what #192059 is about, and this issue doesn't try to cover it. I found the bug while reading back imported GL textures for #194142.

Code sample

HAL-level reproduction
GLuint name = /* a GL_TEXTURE_2D with known contents, same share group */;
TextureDescriptor desc;
desc.format = PixelFormat::kR8G8B8A8UNormInt;
desc.size = {16, 16};
desc.usage = TextureUsage::kShaderRead;
auto texture = TextureGLES::WrapTexture(
    reactor, desc, reactor->CreateHandle(HandleType::kTexture, name));

auto buffer = context->GetResourceAllocator()->CreateBuffer(
    {StorageMode::kHostVisible, 16 * 16 * 4});
auto cmd = context->CreateCommandBuffer();
auto blit = cmd->CreateBlitPass();
blit->AddCopy(texture, buffer);
blit->EncodeCommands();
context->GetCommandQueue()->Submit({cmd});
// buffer now holds the default framebuffer's pixels, not `name`'s.

Flutter Doctor output

Found in an engine built from source: macOS arm64, host_debug_unopt_arm64, engine at f1ac3fe4b34; the code is the same on current main.

Doctor output
[✓] Flutter (Channel stable, 3.47.0, on macOS 27.2 26B5091g darwin-arm64)
    • Framework revision 4cf2416426, 2026-08-11
    • Engine revision 5f77625673
    • Dart version 3.13.0
[✓] Xcode - develop for iOS and macOS (Xcode 27.0)

Activity

  1. added
    in triagePresently being triaged by the triage team
    engineflutter/engine related. See also e: labels.
    e: impellerImpeller rendering backend issues and features requests
    and removed
    in triagePresently being triaged by the triage team
    on Oct 12, 2026
  2. puneetkukreja98 commented on Oct 12, 2026

    @puneetkukreja98
    Contributor

    @dzolotov Thanks for reporting this, routing this to team-engine for further investigation.

  3. gaaclarke commented on Oct 12, 2026

    @gaaclarke
    Member

    This can't be reproduced from Dart today: Flutter GPU doesn't take a GL texture it didn't create, so a wrapped texture name never reaches it. On main the path is open through the interop toolkit (ImpellerTextureCreateWithOpenGLTextureHandleNew) and skwasm (images_impeller.cc), and from Dart it opens with texture import (#194142), which is where I ran into it. So the repro is at the HAL level:

    My understanding is that this issue is hypothetical. We currently don't accept issues against hypothetical usages. If you can come up with a reproduction, let us know.

  4. github-actions commented on Oct 26, 2026

    @github-actions

    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 -v and a minimal reproduction of the issue.

  5. locked as resolved and limited conversation to collaborators on Oct 26, 2026
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

    e: impellerImpeller rendering backend issues and features requestsengineflutter/engine related. See also e: labels.team-engineOwned by Engine team

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions