Repository navigation
Description
Steps to reproduce
- Build any Flutter Windows app with Flutter 3.47.0 (or 3.47.1) in release mode. A stock
flutter createapp 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_3display.- Confirmed machine: Intel HD Graphics "Ironlake" (
vendor=0x8086 device=0x0042), 64 MB, WDDM 1.1, no Vulkan, Windows 10.
- Confirmed machine: Intel HD Graphics "Ironlake" (
- 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
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. - The extension guard does not cover the token it gates.
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 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_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; - guard
GL_DEPTH24_STENCIL8, theGL_RGBA8renderbuffer format, andRenderbufferStorageMultisampleon the corresponding ES 2.0 extensions; - independently of the fix: check and log
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 / 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
Activity
RafaelTadeuSilva commentedon Aug 28, 2026
I had the same issue with 2 different apps, (I think any app will have this issue).
flutter < 3.44.9 works
flutter >3.47.0 don't work
setting project.set_impeller_switch(flutter::ImpellerSwitch::Disabled); in windows\runner\main.cpp did not work as well.
puneetkukreja98 commentedon Aug 31, 2026
@gaabrielricci Thanks for reporting the issue! I tried reproducing this on the latest stable channel, but it didn't reproduce on my machine. Could you please test this again on Flutter 3.47.2?
Flutter doctor -v:
System details:
gaabrielricci commentedon Aug 31, 2026
@puneetkukreja98 Thanks for taking a look. A no-repro on that machine is the expected result — this cannot reproduce on modern hardware, and 3.47.2 contains no fix for it. So let me give you something that is verifiable without the hardware.
1. 3.47.2 is byte-identical to 3.47.0 in the relevant files
engine/src/flutter/shell/platform/windows/compositor_opengl.ccandengine/src/flutter/shell/platform/windows/egl/manager.ccare unchanged across3.47.0,3.47.1,3.47.2and currentmaster(md5c8fb711005634f9eed40901afaef419eand49ae9832ce2a7882365465757b52bcef). Retesting on 3.47.2 cannot change the outcome — I'm happy to run it on the affected machine anyway if you want the data point on record.2. Why it does not reproduce on your machine
The trigger is the GPU, not the OS build. On any adapter where ANGLE gets D3D11 feature level 10_0 or higher, the D3D11 backend accepts the sized internal formats even though the GL context is ES 2.0, and everything renders fine. The failure needs a driver capped at D3D11 feature level 9_3 (WDDM 1.1-era parts). The confirmed machine here is
vendor=0x8086 device=0x0042— Intel HD Graphics "Ironlake" (2010), 64 MB, WDDM 1.1, no Vulkan. This is the same population of devices as #169178 / #170298 ("ANGLE only implements GLES 2.0 on some Windows 10+ machines").3. The defect is visible in the source, independently of a repro
The Windows embedder always creates an ES 2.0 context —
egl/manager.cc:207:compositor_opengl.cc(lines 67, 115 and 139 in 3.47.2) then does:with
sized_formatin{GL_BGRA8_EXT, GL_RGBA8}andgeneral_formatin{GL_BGRA_EXT, GL_RGBA}. OpenGL ES 2.0 requiresinternalformat == formatforglTexImage2D; sized internal formats forTexImage2Donly arrive with ES 3.0. The call is therefore invalid for the context that is actually created — permissive drivers tolerate it, feature-level-9_3 ones do not.Exact regression: #190256 (
c8a3c2b, 2026-07-31) flipped that argument fromgeneral_formattosized_formatin every branch, including the non-Impeller one. In 3.44.2 the same line read:which is ES 2.0-legal. That is the whole regression window: 3.44.2 good, 3.47.0 black.
Same file, related:
HasExtension("GL_EXT_texture_format_BGRA8888"), but that extension definesGL_BGRA_EXT(unsized). It does not define the sizedGL_BGRA8_EXTtoken it is being used to gate.GL_RGBA8as a renderbuffer internalformat in ES 2.0 requiresOES_rgb8_rgba8;GL_DEPTH24_STENCIL8requiresOES_packed_depth_stencil. Neither is checked.glGetError/glCheckFramebufferStatusafter the backing store is built, so the failure is silent: the texture is never allocated, the framebuffer is incomplete, the blit copies nothing → black window while the engine starts normally, Dart runs, and nothing is logged. That is exactly what both reports in this issue look like.4. This is not a renderer choice
@RafaelTadeuSilva reports the same window (< 3.44.9 fine, >= 3.47.0 black) and that
project.set_impeller_switch(flutter::ImpellerSwitch::Disabled)does not help. That is consistent with the diagnosis: the non-Impeller branch (line 139) uses the samesized_formatinternalformat, so falling back to Skia does not avoid it.5. Reproducing without the hardware
EGL_PLATFORM_ANGLE_MAX_VERSION_MAJOR_ANGLE= 9,EGL_PLATFORM_ANGLE_MAX_VERSION_MINOR_ANGLE= 3, alongsideEGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE). Patchingegl/manager.ccto request that locally should reproduce the black window on any machine.compositor_opengl_unittests.ccalready mocks the proc table — an assertion thatTexImage2Dis called withinternalformat == formatwould have caught this at review time.6. What I can provide
I have a customer machine that reproduces this 100% (I currently ship a build pinned to 3.44.2 as the workaround, which fixed it in the field). If you point me at a patch or a branch, I will build it and confirm on that machine, and I can attach the DXGI adapter list plus the D3D11 feature level captured at startup. I'm also glad to send the PR myself — restore the unsized
internalformaton ES 2.0 contexts (or request ES 3.0 with an ES 2.0 fallback), guard the sized/renderbuffer formats behind their extensions, and logCheckFramebufferStatusafter backing store creation.puneetkukreja98 commentedon Sep 1, 2026
@gaabrielricci Thanks for the response, could you please share the hardware details of your customer's machine where the issue is reproducing?
Routing this to team-windows for further investigation.
7 remaining items
knopp commentedon Sep 10, 2026
cc @gaaclarke
gaaclarke commentedon Sep 10, 2026
@gaabrielricci can you try on the
mainchannel? It sounds like #191516 may have fixed your issue.krll-kov commentedon Sep 13, 2026
krll-kov commentedon Sep 16, 2026
@gaaclarke I checked
mainand #191516 does not fix it, at least not at D3D feature level 10_0, which is what the affected machine reportsI don't have the hardware, so I capped the feature level instead: Graphics Tools ->
dxcpl-> add the exe to the scope list -> Feature level limit. Windows 11 ARM64 in Parallels (Parallels Display Adapter, natively 11_1). Stockflutter createapp, release build, Skia viaImpellerSwitch::Disabled.x64:
arm64,
mainat e619960 (contains #191516):So 10_0 is a regression from 3.44.2 and is still black on
mainwith both renderers. But 9_3 was already broken in 3.44.2This is a feature level cap on a virtual GPU, not the real Ironlake machine, but it reproduces the same symptom: black window, app alive, nothing logged. I'm building the engine locally with logging around context creation and the backing store to find which call fails at 10_0, and will report back.
Correction to my earlier table: please disregard the 9_3 row of the arm64 table. The feature level cap failed to apply to those arm64 builds, so "main Skia renders" at 9_3 was really main running at full feature level. Rechecked with a control (official 3.47.4 is black at 10_0 under the same cap): at 9_3 every build shows a white window, including 3.44.2 and the fix, which matches ANGLE not supporting below 10_0. The 10_0 results stand
gaaclarke commentedon Sep 16, 2026
Okay, thanks everyone. For what it's worth, ANGLE does not support < 10_0. So getting < 10_0 rendering is not an objective. We should be running on 10_0 however.
I think we have enough theories and evidence to make this actionable. It sounds like the fix may be pretty straightforward, something like switching the general format to the sized format? I'll put it on our queue to fix but might take a while to get to. Considering there are a handful of people here that have the setup and the fix should be simple, it would be helpful if someone could try it out. I can help getting a PR tested and through review.
Ultimately what we need is a testing rig that artificially sets the feature level to 10_0 so that we can make sure this doesn't get broken in the future. I've filed an additional issue for that: #192899
krll-kov commentedon Sep 16, 2026
@gaaclarke Thanks, i have the 10_0 repro running locally and an engine build in progress. I'll try passing the unsized format to
glTexImage2Don an ES 2 context, the way 3.44.2 did, and open a PR with a unit test if it renders at 10_0.krll-kov commentedon Sep 17, 2026
Cause. At feature level 10_0 ANGLE only creates an OpenGL ES 2.0 context, so #191516 falls back to ES 2.
CompositorOpenGL::CreateBackingStoreallocates the backing store texture with the sized format asinternalformat:ES 2.0 requires
internalformatto matchformat, so the call fails withGL_INVALID_VALUE, the texture is never allocated, the framebuffer isGL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT(0x8CD6) and the blit inPresentfails withGL_INVALID_OPERATION. Nothing reaches the window and nothing is logged. 3.44.2 passed the unsized format there, and on an ES 3 context the sized format is valid, which is why only old hardware is affected.Fix. Pick the internal format once from the context version: unsized on ES 2, sized on ES 3. Applies to all three
TexImage2Dcalls (Impeller with and without MSAA, Skia). Unit tests cover ES 2 with Skia, ES 2 with Impeller and ES 3; the two ES 2 tests fail without the fix.Verified at 10_0 (dxcpl feature level limit, checked against a control: the official 3.47.4 engine is black under the same limit):
main(arm64) with the fix: renders, and the logs show an ES 2 context with a complete framebuffer and no GL errors.Separate Impeller issue found while testing, I'll file it on its own: with the compositor fixed, Impeller at 10_0 erases everything drawn before a
BackdropFilter. Without offscreen MSAA,InlinePassContext::GetRenderPasskeeps the same color texture (LoadAction::kLoad) andCanvas::FlipBackdropthen draws that texture into itself withBlendMode::kSrc. D3D11 cannot sample the texture it renders to, so the backdrop comes back as zeros. Skia is not affected.For #192899: a dxcpl scope list entry with Feature level limit 10_0 reproduces all of this on any GPU. Only entries saved by dxcpl itself take effect; editing the registry by hand does not.
krll-kov commentedon Sep 17, 2026
PR: #192917
[Windows] Use an unsized texture format for backing stores on OpenGL …
github-actions commentedon Oct 2, 2026
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 -vand a minimal reproduction of the issue.