Steps to reproduce
- Build any Flutter Windows app with Flutter 3.47.0 (or 3.47.1) in release mode. A stock
flutter create app is enough — no app code is involved.
- Run it on a machine whose GPU driver caps D3D11 at feature level 9_3, so ANGLE falls back to its
FL9_3 display.
- Confirmed machine: Intel HD Graphics "Ironlake" (
vendor=0x8086 device=0x0042), 64 MB, WDDM 1.1, no Vulkan, Windows 10.
- Look at the window.
Expected results
The app renders normally, as the identical source does when built with Flutter 3.44.2.
Actual results
The window is completely black, while the app is fully alive:
- the engine starts and Dart runs — our own logging, HTTP and WebSocket code all execute normally;
- input still works, hitting the right targets;
- nothing is logged as an error — not by the engine, not by the framework.
Forcing either renderer makes no difference: black with Impeller and black with Skia (via DartProject::set_impeller_switch). That rules out the renderer default flip in 3.47.0 (#188140) as the cause and points at the shared Windows compositor.
Regression range
- 3.44.2 — works. Field-validated: rebuilding the exact same app source with 3.44.2 and installing it on the affected end-user machine fixed it.
- 3.47.0 / 3.47.1 — black.
Two commits rewrote engine/src/flutter/shell/platform/windows/compositor_opengl.cc in that window, neither reachable from 3.44.2 (verified with git tag --contains):
| commit |
PR |
first release tag containing it |
dda3f94c84e5614353d555af5fca89809d0f9f7c — [Impeller][Windows] fix black screen on OpenGL fallback |
#187288 |
3.46.0-0.1.pre |
c7926ac4578db43158c42c8eb86be76e71913129 — [CP-beta][windows]: Uses offscreen MSAA when implicit msaa isn't available. |
#190374 (cherry-pick of #190256, c8a3c2bc3810) |
3.47.0 |
Analysis
The Windows embedder only ever creates an OpenGL ES 2.0 context. shell/platform/windows/egl/manager.cc:
const EGLint context_attributes[] = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE};
There is no ES 3 attempt with an ES 2 fallback — it is ES 2.0 on every machine.
CompositorOpenGL::Initialize negotiates a color format behind a single extension check:
if (gl_->GetDescription()->HasExtension("GL_EXT_texture_format_BGRA8888")) {
format_.sized_format = GL_BGRA8_EXT;
format_.general_format = GL_BGRA_EXT;
} else {
format_.sized_format = GL_RGBA8;
format_.general_format = GL_RGBA;
}
c7926ac then switched the backing-store texture allocation from the unsized internal format to the sized one, in all three new branches — including the no-MSAA fallback, which is the branch weak drivers land in:
- gl_->TexImage2D(GL_TEXTURE_2D, 0, format_.general_format, config.size.width,
- config.size.height, 0, format_.general_format,
+ gl_->TexImage2D(GL_TEXTURE_2D, 0, format_.sized_format, config.size.width,
+ config.size.height, 0, format_.general_format,
GL_UNSIGNED_BYTE, nullptr);
That is why both renderers go black rather than only Impeller.
Two things look wrong on an ES 2.0 context:
- Sized internal formats are not valid for
glTexImage2D in ES 2.0. ES 2.0 requires internalformat == format; sized internal formats for TexImage2D arrive with ES 3.0. GL_RGBA8 / GL_BGRA8_EXT as internalformat against GL_RGBA / GL_BGRA_EXT as format is out of spec for the context the engine actually created.
- The extension guard does not cover the token it gates.
GL_EXT_texture_format_BGRA8888 defines the unsized GL_BGRA_EXT as an accepted internalformat; the sized GL_BGRA8_EXT token comes from EXT_texture_storage. So the presence of GL_EXT_texture_format_BGRA8888 does not establish that GL_BGRA8_EXT is a legal TexImage2D internalformat.
Same file, same ES 2.0 vs ES 3.0 mismatch, no capability guard:
RenderbufferStorageMultisample (core ES 3.0, no EXT/ANGLE suffix) is called in the supports_offscreen_msaa_ branch;
GL_DEPTH24_STENCIL8 is used unconditionally — on ES 2.0 that needs GL_OES_packed_depth_stencil;
format_.sized_format (GL_RGBA8) as a renderbuffer format on ES 2.0 needs GL_OES_rgb8_rgba8.
Why the fallback branch is the one that matters here. In capabilities_gles.cc, both MSAA flags stay false on an ES 2.0 context without GL_EXT_multisampled_render_to_texture:
if (desc->HasExtension(kMultisampledRenderToTextureExt)) {
supports_implicit_msaa_ = true;
...
} else if (desc->GetGlVersion().major_version >= 3 && desc->IsES()) {
... // never taken: the context is ES 2.0
}
So the affected machine takes CreateBackingStore's plain-texture else branch — the path deliberately written for drivers that cannot do MSAA — and that branch is exactly the one that regressed from unsized to sized internalformat.
Hypothesis for why only some machines break: ANGLE's D3D11 backend at a full feature level supports a wide format set and tolerates these calls; the FL9_3 fallback has a reduced set. There the texture is never allocated, the FBO ends up incomplete, and the blitFramebuffer in CompositorOpenGL::Present copies nothing → black window over a perfectly healthy app. This last step is the part I could not verify directly (see What I could not capture).
Secondary issue: the failure is completely silent. compositor_opengl.cc calls neither glGetError nor CheckFramebufferStatus after building the backing store (git grep finds zero occurrences of either in that file). A failed allocation therefore produces a black window with no diagnostic at all, which is what made this take days to narrow down across end-user machines. A CheckFramebufferStatus + FML_LOG(ERROR) after CreateBackingStore would turn this whole class of bug into one log line.
Suggested fixes
- Pass
format_.general_format as the TexImage2D internalformat on the ES 2.0 context (restoring pre-c7926ac behavior), or request an ES 3 context in egl/manager.cc with a fallback to ES 2 and choose sized vs. unsized formats from the context version actually obtained;
- guard
GL_DEPTH24_STENCIL8, the GL_RGBA8 renderbuffer format, and RenderbufferStorageMultisample on the corresponding ES 2.0 extensions;
- independently of the fix: check and log
CheckFramebufferStatus after creating the backing store.
What I could not capture
I do not have the affected machine on hand — it belongs to an end user of our product — so I have no glGetError / CheckFramebufferStatus output or ANGLE debug log from it, and I could not test against master. What I do have is the version bisect (3.44.2 works, 3.47.0 black, same source), the machine's D3D11 feature level, and the fact that both renderers are black. I can ship a diagnostic build to that machine if a maintainer tells me exactly what to capture. The workaround build is already installed and working on that machine, so re-capturing anything from the broken build means putting a machine in daily production use back onto it — I would rather spend that disruption once, on a targeted diagnostic build, than on a screenshot.
Code sample
No app code is involved — a default flutter create Windows app reproduces it on affected hardware. The distinguishing factor is the machine, not the app.
Screenshots or Video
Screenshots / Video demonstration
About this image: it is the only one I have — a low-quality image the customer sent at the start of the support session, before we knew what we were looking at. I could not reproduce this on my own hardware; it happens only on that customer's machine, which I have no physical access to.
I cannot capture a sharper one either: the workaround (a 3.44.2 build) is already installed and working there, so re-capturing the black window would mean putting a machine in daily production use back onto the broken build. If a maintainer needs something specific from that hardware, I would rather spend that disruption on a targeted diagnostic build than on a screenshot — just tell me what to instrument.
Logs
Our runner writes a startup probe on every launch (it calls D3D11CreateDevice itself to record the achieved feature level, because the adapter name alone is ambiguous — "Intel HD Graphics" is both a 2010 part stuck at 9_3 and a 2015 part at 11_1).
Affected machine (abridged, translated from pt-BR):
GPU 0: Intel(R) HD Graphics | vendor=0x8086 device=0x0042 | VRAM 64 MB
Direct3D: D3D11 hardware OK, feature level 9_3
window created, entering message loop
…followed by normal application logs (session restored, WebSocket connected) with an entirely black window.
A working machine for contrast, same app, same build:
GPU 0: Intel(R) Iris(R) Xe Graphics | vendor=0x8086 device=0x9A49 | VRAM 128 MB
Direct3D: D3D11 hardware OK, feature level 11_1
Flutter Doctor output
Our build host is pinned back to 3.44.2 as the workaround, so this is the doctor output of the working configuration; the failing builds were produced with 3.47.0 on this same host.
[!] Flutter (Channel [user-branch], 3.44.2, on Microsoft Windows [Version 10.0.26200.9168], locale pt-BR)
! Flutter version 3.44.2 on channel [user-branch] at C:\flutter\flutter
(pinned to the 3.44.2 tag deliberately, as the workaround for this bug)
• Framework revision c9a6c48423 (2026-06-10)
• Engine revision 77e2e94772
• Dart version 3.12.2
• DevTools version 2.57.0
[√] Windows Version (Windows 11 or higher, 25H2, 2009)
[√] Android toolchain - develop for Android devices (Android SDK version 36.1.0-rc1)
[√] Chrome - develop for the web
[√] Visual Studio - develop Windows apps (Visual Studio Community 2022 17.14.13)
• Visual Studio Community 2022 version 17.14.36414.22
• Windows 10 SDK version 10.0.26100.0
[√] Connected device (3 available)
• Windows (desktop) • windows • windows-x64
[√] Network resources
Steps to reproduce
flutter createapp is enough — no app code is involved.FL9_3display.vendor=0x8086 device=0x0042), 64 MB, WDDM 1.1, no Vulkan, Windows 10.Expected results
The app renders normally, as the identical source does when built with Flutter 3.44.2.
Actual results
The window is completely black, while the app is fully alive:
Forcing either renderer makes no difference: black with Impeller and black with Skia (via
DartProject::set_impeller_switch). That rules out the renderer default flip in 3.47.0 (#188140) as the cause and points at the shared Windows compositor.Regression range
Two commits rewrote
engine/src/flutter/shell/platform/windows/compositor_opengl.ccin that window, neither reachable from3.44.2(verified withgit tag --contains):dda3f94c84e5614353d555af5fca89809d0f9f7c—[Impeller][Windows] fix black screen on OpenGL fallback3.46.0-0.1.prec7926ac4578db43158c42c8eb86be76e71913129—[CP-beta][windows]: Uses offscreen MSAA when implicit msaa isn't available.c8a3c2bc3810)3.47.0Analysis
The Windows embedder only ever creates an OpenGL ES 2.0 context.
shell/platform/windows/egl/manager.cc:There is no ES 3 attempt with an ES 2 fallback — it is ES 2.0 on every machine.
CompositorOpenGL::Initializenegotiates a color format behind a single extension check:c7926acthen switched the backing-store texture allocation from the unsized internal format to the sized one, in all three new branches — including the no-MSAA fallback, which is the branch weak drivers land in:That is why both renderers go black rather than only Impeller.
Two things look wrong on an ES 2.0 context:
glTexImage2Din ES 2.0. ES 2.0 requiresinternalformat == format; sized internal formats forTexImage2Darrive with ES 3.0.GL_RGBA8/GL_BGRA8_EXTas internalformat againstGL_RGBA/GL_BGRA_EXTas format is out of spec for the context the engine actually created.GL_EXT_texture_format_BGRA8888defines the unsizedGL_BGRA_EXTas an accepted internalformat; the sizedGL_BGRA8_EXTtoken comes fromEXT_texture_storage. So the presence ofGL_EXT_texture_format_BGRA8888does not establish thatGL_BGRA8_EXTis a legalTexImage2Dinternalformat.Same file, same ES 2.0 vs ES 3.0 mismatch, no capability guard:
RenderbufferStorageMultisample(core ES 3.0, noEXT/ANGLEsuffix) is called in thesupports_offscreen_msaa_branch;GL_DEPTH24_STENCIL8is used unconditionally — on ES 2.0 that needsGL_OES_packed_depth_stencil;format_.sized_format(GL_RGBA8) as a renderbuffer format on ES 2.0 needsGL_OES_rgb8_rgba8.Why the fallback branch is the one that matters here. In
capabilities_gles.cc, both MSAA flags stay false on an ES 2.0 context withoutGL_EXT_multisampled_render_to_texture:So the affected machine takes
CreateBackingStore's plain-textureelsebranch — the path deliberately written for drivers that cannot do MSAA — and that branch is exactly the one that regressed from unsized to sized internalformat.Hypothesis for why only some machines break: ANGLE's D3D11 backend at a full feature level supports a wide format set and tolerates these calls; the
FL9_3fallback has a reduced set. There the texture is never allocated, the FBO ends up incomplete, and theblitFramebufferinCompositorOpenGL::Presentcopies nothing → black window over a perfectly healthy app. This last step is the part I could not verify directly (see What I could not capture).Secondary issue: the failure is completely silent.
compositor_opengl.cccalls neitherglGetErrornorCheckFramebufferStatusafter building the backing store (git grepfinds zero occurrences of either in that file). A failed allocation therefore produces a black window with no diagnostic at all, which is what made this take days to narrow down across end-user machines. ACheckFramebufferStatus+FML_LOG(ERROR)afterCreateBackingStorewould turn this whole class of bug into one log line.Suggested fixes
format_.general_formatas theTexImage2Dinternalformat on the ES 2.0 context (restoring pre-c7926acbehavior), or request an ES 3 context inegl/manager.ccwith a fallback to ES 2 and choose sized vs. unsized formats from the context version actually obtained;GL_DEPTH24_STENCIL8, theGL_RGBA8renderbuffer format, andRenderbufferStorageMultisampleon the corresponding ES 2.0 extensions;CheckFramebufferStatusafter creating the backing store.What I could not capture
I do not have the affected machine on hand — it belongs to an end user of our product — so I have no
glGetError/CheckFramebufferStatusoutput or ANGLE debug log from it, and I could not test againstmaster. What I do have is the version bisect (3.44.2 works, 3.47.0 black, same source), the machine's D3D11 feature level, and the fact that both renderers are black. I can ship a diagnostic build to that machine if a maintainer tells me exactly what to capture. The workaround build is already installed and working on that machine, so re-capturing anything from the broken build means putting a machine in daily production use back onto it — I would rather spend that disruption once, on a targeted diagnostic build, than on a screenshot.Code sample
No app code is involved — a default
flutter createWindows app reproduces it on affected hardware. The distinguishing factor is the machine, not the app.Screenshots or Video
Screenshots / Video demonstration
Logs
Our runner writes a startup probe on every launch (it calls
D3D11CreateDeviceitself to record the achieved feature level, because the adapter name alone is ambiguous — "Intel HD Graphics" is both a 2010 part stuck at 9_3 and a 2015 part at 11_1).Affected machine (abridged, translated from pt-BR):
…followed by normal application logs (session restored, WebSocket connected) with an entirely black window.
A working machine for contrast, same app, same build:
Flutter Doctor output
Our build host is pinned back to 3.44.2 as the workaround, so this is the doctor output of the working configuration; the failing builds were produced with 3.47.0 on this same host.