Repository navigation
Description
Steps to reproduce
flutter create bugand replacelib/main.dartwith the sample below.flutter run -d linux. Reproduces in debug, profile and release, on both renderers (FLUTTER_LINUX_RENDERER=opengl, the default, and=software).- Watch the window for ~20 seconds without touching it. It shows an opaque red fill, then a fully transparent scene for 9 seconds, then the same transparent scene plus one static 1x1 pixel at alpha 1/255.
Expected results
When the scene stops painting anything, the window stops showing red. Whatever is behind the Flutter content becomes visible — the view's background colour, or another surface for an app using an ARGB visual.
Actual results
The window keeps showing red for the whole transparent phase, and would keep showing it forever: once the app is quiescent, nothing ever forces another frame. The transparent frame is built and rasterized — the engine reports it through addTimingsCallback — but never reaches the screen.
One static 1x1 ColoredBox(Color(0x01000000)) — alpha 1/255, no timer, no repaints — makes the identical scene commit immediately. That is the third phase, and it pins the trigger to Color.isTransparent().
Root-window sample, once per second, Flutter 3.44.0, opengl:
| phase | scene | layers | on screen |
|---|---|---|---|
| 0 | Color(0xFFFF0000) |
>= 1 | red |
| 1 | Color(0x00000000), 9 s |
0 | red — stale, 8 consecutive samples |
| 2 | same + static Color(0x01000000) 1x1 |
1 | background, immediately |
Identical on software. Controls, so these numbers mean something: a plain GTK3 app painting red in the same environment reads #FF0000 100%, and an all-opaque colour cycling every 2 s tracks perfectly on 3.44.0 and master, so frames that have layers present fine.
How apps hit this in practice
Two common cases produce a scene with nothing to rasterize without the author ever intending "an empty frame":
Native content behind a transparent Flutter surface. Video on a wl_subsurface, a GL plane, a map view: the Flutter scene over that region paints nothing by design, so the frame that should reveal the content never commits and the last opaque Flutter frame keeps covering it.
Fading a full-screen overlay out. RenderAnimatedOpacity.paint returns without painting its child once _alpha == 0, so the terminal frame of a fade to zero contains no draw ops and is dropped. The fade visibly stops one step short of invisible and stays there: measured with a 5 s fade of a red box in the same environment, the screen tracked down through the intermediate values and then held #010000 indefinitely, while the app logged opacity=0.000.
Real hardware, first-hand. I originally hit this on a Steam Deck — Desktop Mode, Wayland, SteamOS 3.x — in plezy, which renders video through libmpv on a wl_subsurface stacked below the Flutter surface, with an RGBA visual and fl_view_set_background_color(view, transparent). Both cases above apply at once, and the symptom was that the player controls never fully faded out: a faint residue of the chrome stayed over the video. Keeping one perpetually-repainting 1x1 widget in the tree was the only thing that made the final state commit. That device observation is a report, not a measurement — no per-frame instrumentation, and I have not run the sample above on it.
It does exercise the same code as the numbers above, because in 3.44.0 Wayland and X11 share the affected compositor: fl_view.cc:490-495 selects FlCompositorOpenGL for both and differs only in whether the frame is shared via EGLImage or copied through the CPU, and the zero-layer early return precedes any use of that distinction.
Suspected cause
Not maintainer-confirmed; my reading of the source plus the measurements above.
A scene whose draw ops are all fully transparent produces zero layers. DlOpSpy::setColor clears will_draw_ for a transparent colour, so HasEngineRenderedContents() is false, no render target is allocated, PushLayers pushes nothing, and the present callback is invoked with layers_count == 0.
Every Linux renderer treats that as "nothing to do" and early-returns without clearing what it stored for the previous frame, so the stored framebuffer/surface still holds the last frame that had content and the following GTK draw re-uploads it:
- 3.44.0:
fl_compositor_opengl.cc:212andfl_compositor_software.cc:38, both returning beforefl_task_runner_stop_waitat:359/:66.fl_view.cc:293still schedulesredraw_cbunconditionally, which is why the window is still shown and simply redraws stale content. - master:
fl_opengl_frame.cc:64-66,fl_view_renderer_software.cc:174,fl_view_renderer_subsurface.cc:278-281, plus the layer combiners atfl_compositor_opengl.cc:84-86andfl_compositor_software.cc:27-29.
The fix shape already exists in the same files. A single layer of size 0x0 — the other spelling of "this frame shows nothing" — is handled correctly and tears the stored frame down: fl_opengl_frame.cc:71-79 (g_clear_object(&self->framebuffer)) and fl_view_renderer_software.cc:178-182. So the two cases take opposite paths, one a teardown and one a no-op.
Two further consequences I read in the code but did not measure:
- On master's Wayland path the zero-layer return sits before
fl_subsurface_egl_present,fl_task_runner_stop_wait(:322) andg_idle_add(redraw_cb)(:325). Sinceredraw_cbis the only route tofl_view_renderer_notify_frameandFlApplicationshows the window onfirst-frame, an app whose first frame has no layers would never show its window. Master-only; 3.44.0 has no subsurface renderer. - The stale stored frame also keeps the wrong size, which the blocking
wait_for_frameloops compare against the window size (100 ms bound,fl_compositor.h:16), so resizing while the scene has nothing to rasterize should stall the GTK thread ~100 ms per step. My attempt to measure this was inconclusive — the app segfaulted mid-resize under Xvfb/llvmpipe and I could not attribute the crash.
Scope of my master evidence. The transparent-fill case reproduces on master (3.48.0-0.3.pre, engine 58cc1b845d) and the opaque control passes there, but master additionally showed frames containing partial alpha not updating on screen in my environment — during the 5 s fade it stayed saturated #FF0000 instead of tracking down through the intermediate values as 3.44.0 does, even though the animation logged opacity=0.000. I could not attribute that second anomaly, so I exclude master from the measured claims above; the code citations stand on their own and master is worth re-checking on real hardware.
Code sample
lib/main.dart
import 'dart:async';
import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart';
// Raw widgets only, so the only draw ops in the scene are the ones written here.
//
// phase 0 0-3s opaque red
// phase 1 3-12s fully transparent -> engine emits layers_count == 0
// phase 2 12-18s transparent + ONE STATIC 1x1 box at alpha 1/255, no timer
const List<String> kPhases = <String>['red', 'clear', 'clear+static-alpha1'];
const List<int> kHoldSeconds = <int>[3, 9, 6];
void main() => runApp(const Probe());
class Probe extends StatefulWidget {
const Probe({super.key});
@override
State<Probe> createState() => _ProbeState();
}
class _ProbeState extends State<Probe> {
int _phase = 0;
int _built = 0;
int _rasterized = 0;
@override
void initState() {
super.initState();
SchedulerBinding.instance.addTimingsCallback((List<FrameTiming> t) => _rasterized += t.length);
_schedule();
print('[probe] phase=0 red');
}
void _schedule() {
if (_phase == kPhases.length - 1) {
return;
}
Timer(Duration(seconds: kHoldSeconds[_phase]), () {
setState(() => _phase++);
SchedulerBinding.instance.addPostFrameCallback((_) {
print('[probe] phase=$_phase ${kPhases[_phase]} built=$_built rasterized=$_rasterized');
});
_schedule();
});
}
@override
Widget build(BuildContext context) {
_built++;
return Directionality(
textDirection: TextDirection.ltr,
child: switch (_phase) {
0 => const ColoredBox(color: Color(0xFFFF0000)),
1 => const ColoredBox(color: Color(0x00000000)),
_ => const Stack(
children: <Widget>[
Positioned.fill(child: ColoredBox(color: Color(0x00000000))),
// No timer, no repaints: one static almost-invisible pixel.
Positioned(
top: 0,
left: 0,
child: SizedBox(
width: 1,
height: 1,
child: ColoredBox(color: Color(0x01000000)),
),
),
],
),
},
);
}
}Logs
Logs
The transparent frame is built and rasterized — rasterized advances — and no warning or error is emitted while it is dropped.
[probe] phase=0 red
[probe] phase=1 clear built=2 rasterized=2
[probe] phase=2 clear+static-alpha1 built=3 rasterized=3Root-window sample, once per second, times relative to app launch:
app+ 1.0s .. app+ 3.0s #FF0000 100% phase 0, opaque red
app+ 4.0s .. app+11.0s #FF0000 100% phase 1, transparent -> STALE, 8 samples
app+12.0s .. app+19.0s #000000 100% phase 2, static alpha-1/255 pixel -> presented
Flutter Doctor output
Doctor output
Measured in a container, so the toolchain entries are minimal. The original real-hardware observation was on a Steam Deck, SteamOS 3.x Desktop Mode under Wayland, with a 3.44.0-based app.
[!] Flutter (Channel [user-branch], 3.44.0, on Ubuntu 24.04.4 LTS 6.12.76-linuxkit, locale en_US)
• Framework revision 559ffa3f75 (3 months ago), 2026-05-15 14:13:13 -0700
• Engine revision 4c525dac5e
• Dart version 3.12.0
• DevTools version 2.57.0
[✗] Android toolchain - develop for Android devices
[✗] Chrome - develop for the web
[✓] Linux toolchain - develop for Linux desktop
• Ubuntu clang version 18.1.3 (1ubuntu1)
• cmake version 3.28.3
• ninja version 1.11.1
• pkg-config version 1.8.1
• GL_EXT_framebuffer_blit: no
• GL_EXT_texture_format_BGRA8888: no
[✓] Connected device (1 available)
• Linux (desktop) • linux • linux-arm64 • Ubuntu 24.04.4 LTS 6.12.76-linuxkit
[✓] Network resourcesSearched for duplicates
#191245 and #183703 cover Linux frame pacing and the GTK3 frame-clock 60 Hz cap — a different mechanism; this is a frame that is never presented at all. #190075 and #191865 are macOS members of the same bug class, where a frame with nothing to rasterize takes a degenerate embedder path. Searches for GTK frame clock idle, linux transparent empty frame, fade never completes linux, layers_count == 0 returned nothing matching.
Activity
suryaKommana2662 commentedon Aug 28, 2026
@edde746 Thanks for the report. I was able to reproduce the issue locally on Linux using the provided sample. The application progressed through all three phases, and the logs show that the transparent frame was built and rasterized. This confirms the reported behavior. routing to the
team-linuxfor further investigation.Flutter Doctor output
loic-sharma commentedon Aug 28, 2026
@robert-ancell FYI, Windows clears the screen if there are no layers:
flutter/engine/src/flutter/shell/platform/windows/compositor_opengl.cc
Lines 188 to 198 in 308f8fd
It looks like we'll need to add this logic to Linux here as well:
flutter/engine/src/flutter/shell/platform/linux/fl_opengl_frame.cc
Lines 65 to 67 in 308f8fd
[Linux] Clear the frame when there is nothing to render
[Linux] Clear the frame when there is nothing to render
[Linux] Clear the frame when there is nothing to render (flutter#192461)
github-actions commentedon Sep 28, 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.