Skip to content

[Linux] A frame with nothing to rasterize is never presented, so the previous frame stays on screen indefinitely #191870

Closed

Description

@edde746

Steps to reproduce

  1. flutter create bug and replace lib/main.dart with the sample below.
  2. flutter run -d linux. Reproduces in debug, profile and release, on both renderers (FLUTTER_LINUX_RENDERER=opengl, the default, and =software).
  3. 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:

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:

  1. On master's Wayland path the zero-layer return sits before fl_subsurface_egl_present, fl_task_runner_stop_wait (:322) and g_idle_add(redraw_cb) (:325). Since redraw_cb is the only route to fl_view_renderer_notify_frame and FlApplication shows the window on first-frame, an app whose first frame has no layers would never show its window. Master-only; 3.44.0 has no subsurface renderer.
  2. The stale stored frame also keeps the wrong size, which the blocking wait_for_frame loops 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=3

Root-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 resources

Searched 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

  1. suryaKommana2662 commented on Aug 28, 2026

    @suryaKommana2662
    Contributor

    @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-linux for further investigation.

    [probe] phase=0 red
    [probe] phase=1 clear built=2 rasterized=2
    [probe] phase=2 clear+static-alpha1 built=3 rasterized=4
    

    Flutter Doctor output

    [✓] Flutter (Channel stable, 3.47.1, on Ubuntu 26.04 LTS
        6.6.135-09383-g1140e4f27e24, locale C.UTF-8) [78ms]
        • Flutter version 3.47.1 on channel stable at /home/suryak2662/flutter
        • Upstream repository https://github.com/flutter/flutter.git
        • Framework revision 6655482ec0 (9 days ago), 2026-08-19 10:07:23 -0700
        • Engine revision 5d53178869
        • Dart version 3.13.1
        • DevTools version 2.60.0
        • Feature flags: enable-web, enable-linux-desktop, enable-macos-desktop,
          enable-windows-desktop, enable-android, enable-ios, cli-animations,
          enable-native-assets, enable-record-use, enable-swift-package-manager,
          omit-legacy-version-file, enable-lldb-debugging, enable-uiscene-migration
    
    [✓] Android toolchain - develop for Android devices (Android SDK version 36.0.0)
        [3.7s]
        • Android SDK at /home/suryak2662/Android/Sdk
        • Emulator version 36.6.11.0 (build_id 15507667) (CL:N/A)
        • Platform android-36.1, build-tools 36.0.0
        • Java binary at: /opt/android-studio/jbr/bin/java
          This is the JDK bundled with the latest Android Studio installation on this
          machine.
          To manually set the JDK path, use: `flutter config --jdk-dir="path/to/jdk"`.
        • Java version OpenJDK Runtime Environment (build 21.0.10+-14961533-b1163.108)
        • All Android licenses accepted.
    
    [✓] Chrome - develop for the web [17ms]
        • Chrome at google-chrome
    
    [✓] Linux toolchain - develop for Linux desktop [1,012ms]
        • Ubuntu clang version 21.1.8 (6ubuntu1)
        • cmake version 4.2.3
        • ninja version 1.13.2
        • pkg-config version 2.5.1
        • OpenGL core renderer: llvmpipe (LLVM 21.1.8, 256 bits)
        • OpenGL core version: 4.5 (Core Profile) Mesa 26.0.3-1ubuntu1
        • OpenGL core shading language version: 4.50
        • OpenGL ES renderer: llvmpipe (LLVM 21.1.8, 256 bits)
        • OpenGL ES version: OpenGL ES 3.2 Mesa 26.0.3-1ubuntu1
        • OpenGL ES shading language version: OpenGL ES GLSL ES 3.20
        • GL_EXT_framebuffer_blit: yes
        • GL_EXT_texture_format_BGRA8888: yes
    
    [✓] Connected device (2 available) [124ms]
        • Linux (desktop) • linux  • linux-x64      • Ubuntu 26.04 LTS
          6.6.135-09383-g1140e4f27e24
        • Chrome (web)    • chrome • web-javascript • Google Chrome 150.0.7871.128
    
    [✓] Network resources [1,213ms]
        • All expected network resources are available.
    
    • No issues found!
  2. added
    engineflutter/engine related. See also e: labels.
    team-linuxOwned by the Linux platform team
    platform-linuxBuilding on or for Linux specifically
    has reproducible stepsThe issue has been confirmed reproducible and is ready to work on
    and removed
    in triagePresently being triaged by the triage team
    on Aug 28, 2026
  3. loic-sharma commented on Aug 28, 2026

    @loic-sharma
    Member

    @robert-ancell FYI, Windows clears the screen if there are no layers:

    // Clear the view if there are no layers to present.
    if (layers_count == 0) {
    // Normally the compositor is initialized when the first backing store is
    // created. However, on an empty frame no backing stores are created and
    // the present needs to initialize the compositor.
    if (!is_initialized_ && !Initialize()) {
    return false;
    }
    return Clear(view);
    }

    It looks like we'll need to add this logic to Linux here as well:

    if (layers_count == 0) {
    return;
    }

  4. self-assigned this
    on Sep 9, 2026
  5. added
    P2Important issues not at the top of the work list
    triaged-linuxTriaged by the Linux platform team
    on Sep 9, 2026
  6. github-actions commented on Sep 28, 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.

  7. locked as resolved and limited conversation to collaborators on Sep 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

P2Important issues not at the top of the work lista: desktopRunning on desktopengineflutter/engine related. See also e: labels.has reproducible stepsThe issue has been confirmed reproducible and is ready to work onplatform-linuxBuilding on or for Linux specificallyteam-linuxOwned by the Linux platform teamtriaged-linuxTriaged by the Linux platform team

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions