Sitelet https://github.com/flutter/flutter/issues/192147
Skip to content

[Impeller][Android] GLES backend re-issues all GL state per draw (no state cache): raster thread CPU-bound, 2–3× slower than Skia GL on Adreno 610 #192147

Description

@MostafaAlyy

Summary

On Impeller's OpenGL ES backend, RenderPassGLES::EncodeCommandsInReactor re-issues the complete GL state for every draw command — blend, stencil, depth, scissor, vertex attributes, program, uniforms and texture parameters — with no redundancy check apart from the viewport, cull mode and winding order. On a Qualcomm Adreno 610 (Snapdragon 662/665, one of the most common SoCs in budget phones and tablets, and denylisted for Vulkan so it always runs GLES) that makes a flat list of Material cards CPU-bound on the raster thread at 2–3× the frame time of Skia GL on the same GPU and driver. A single render pass with no blur, no shadows and no platform views spends 6.5 ms per frame inside EncodeCommandsInReactor.

This is the encode-cost half of what #187009 and #169423 report as symptoms; the blur/pass-count half is already covered by #169423 / #162312. Filing separately because it is a distinct mechanism with a contained fix.

Steps to reproduce

  1. git clone https://github.com/MostafaAlyy/impeller_gles_repro && cd impeller_gles_repro
  2. flutter build apk --release --dart-define=SCENE=plain and run it on an Adreno 610 device (the list flings itself; frame timings are printed to logcat as REPRO scene=… total_p50=… raster_p50=…).
  3. Add <meta-data android:name="io.flutter.embedding.android.EnableImpeller" android:value="false" /> to the manifest, rebuild, run again.
  4. Compare. DEV=<serial> MODE=release RENDERERS="impeller skia" DRIVERS=0 ./measure.sh 20 "plain blur all" does 2–4 automatically and writes results.tsv.

Expected results

A ListView of rounded cards with text and a progress bar renders at roughly the same cost on Impeller GLES as on Skia GL — both are talking to the same Adreno (TM) 610, OpenGL ES 3.2 V@0615.100 driver.

Actual results

Release mode, vivo V2424 (Adreno 610, Android 16, 120 Hz), p50 frame / p50 raster / share of frames over 16.7 ms:

Scene Impeller GLES Skia GL
plain (rounded Container + border + 2 × Text + LinearProgressIndicator + gradient thumb) 12.0 ms / 8.5 ms / 10 % 6.7 ms / 3.4 ms / 0 %
blur (+ BackdropFilter on every 4th card) 46.8 ms / 28.8 ms / 99 % 12.9 ms / 8.7 ms / 34 %
all (shadows + gradient + opacity + shader mask + blur) 71.7 ms / 41.7 ms / 100 % 15.8 ms / 10.3 ms / 24 %

Where the plain frame goes (Dart VM timeline, Embedder stream, profile mode, timeline_capture.py in the repo):

Impeller GLES Skia GL
GPURasterizer::Draw per frame 10.4 ms p50 3.4 ms p50
Render passes per frame (RenderPassGLES::EncodeCommandsInReactor count) 1.00 —
RenderPassGLES::EncodeCommandsInReactor 6.5 ms —
SurfaceFrame::Encode 8.5 ms 1.6 ms (GrDirectContext::flushAndSubmit)
Raster thread CPU (top -H, 1.raster) ~45 % of a core ~18 %

One pass, no GPU wait to speak of, 6.5 ms of CPU issuing GL calls. (Blur is a different story — 9.35 passes per frame and ~18 ms blocked in swap — and belongs to #169423.)

Analysis

impeller/renderer/backend/gles/render_pass_gles.cc (EncodeCommandsInReactor, identical on stable 3.47.2 / engine a804b26 and on master 644a7c8): inside the per-command loop, the only state that is compared against the previous command is the viewport (EncodeViewport), the cull mode and the winding order. Everything else is issued unconditionally for every command:

  • ConfigureBlending: Enable/Disable(GL_BLEND) + BlendFuncSeparate + BlendEquationSeparate + ColorMask
  • ConfigureStencil: Enable/Disable(GL_STENCIL_TEST) + StencilOpSeparate + StencilFuncSeparate + StencilMaskSeparate per face
  • depth: Enable/Disable(GL_DEPTH_TEST) + DepthFunc + DepthMask
  • Scissor / Enable(GL_SCISSOR_TEST)
  • BufferBindingsGLES::BindVertexAttributes: per attribute EnableVertexAttribArray + VertexAttribPointer + VertexAttribDivisor (no VAO on ES)
  • PipelineGLES::BindProgram: unconditional UseProgram
  • y-flip Uniform1fv
  • BindUniformBufferV3: BindBufferRange per uniform block
  • BindTextures: per texture ActiveTexture + BindTexture + SamplerGLES::ConfigureBoundTexture → 4–6 TexParameteri (MIN/MAG_FILTER, WRAP_S/T, MAX_LEVEL, …; no sampler objects even on ES 3.0) + Uniform1i

That is on the order of 25–35 GL entry points per draw. Consecutive draws in a list of cards share the pipeline, blend state, depth/stencil state and often the texture, so most of those calls are redundant; Skia's GrGLGpu keeps a shadow of the GL state and skips them, which is the whole gap on this scene. On Qualcomm's GLES driver those redundant calls are expensive enough to dominate the frame on a Snapdragon 662-class CPU.

Two side observations from the same session:

  • impeller_debug is enabled for profile engines (impeller/tools/args.gni: flutter_runtime_mode == "debug" || flutter_runtime_mode == "profile"), so profile builds additionally pay PushDebugGroup/PopDebugGroup per command. plain measures 20.5 ms in profile vs 12.0 ms in release on this device. Anyone following the "measure in profile mode" guidance for Impeller GLES will see inflated numbers.
  • Same repro on the Android 16 x86_64 emulator with a desktop host GPU: plain ties (5.1 vs 5.0 ms) — the encode cost only bites on mobile drivers.

Proposed fix

Keep a per-pass shadow of the bound GL state in EncodeCommandsInReactor (or a small GLStateCache on the reactor) and skip: UseProgram when the program is unchanged; blend/stencil/depth calls when the pipeline's descriptors are equal to the previous command's; vertex attribute setup when the pipeline and vertex buffer are unchanged; BindBufferRange when the same range is already bound. On ES 3.0 use sampler objects (GenSamplers/BindSampler) keyed by SamplerDescriptor instead of re-issuing TexParameteri on every texture bind. ReactorGLES already owns the context and the handle lifetimes, so the cache has a natural home and a natural reset point (per pass / per React). Existing unit tests use the mock proc table (buffer_bindings_gles_unittests.cc), so the skipped calls can be asserted directly.

I'm happy to send this PR. We have two Adreno 610 devices (vivo V2424, Galaxy Tab A7 SM-T505N) on the desk to measure before/after in release.

Code sample

https://github.com/MostafaAlyy/impeller_gles_repro — lib/main.dart is the whole app; SCENE=plain is the case above.

Performance profiling on master channel

  • The issue still persists on the master channel

Measured on stable 3.47.2. Not re-measured on master, but the code path is byte-for-byte the same: render_pass_gles.cc on master 644a7c8067 (2026-09-02) has the identical per-command loop with only viewport/cull/winding de-duplicated.

Timeline Traces

Per-scene summaries (event, count per frame, ms per frame) are in the repo: timeline-impeller-plain.txt, timeline-skia-plain.txt, timeline-impeller-blur.txt, timeline-impeller-all.txt; raster-thread CPU samples in cpu-*.txt. Full matrix (profile and release, both devices, all scenes) in RESULTS-all.tsv. Raw timeline JSON available on request.

Video demonstration

Not needed — the numbers come from FrameTiming, and the same difference is visible in production: same APK, only the renderer toggled, home-screen fling on a Galaxy Tab A7 (Adreno 610, Android 12) p50 43–107 ms on Impeller GLES vs 8–10 ms on Skia GL; Firebase Analytics across our Android fleet, video-screen p50 frame, SM-T505N 52–69 ms → 23–28 ms and CPH2591 51 → 27 ms after switching to Skia. Full field tables in #187009 (comment).

What target platforms are you seeing this bug on?

Android

OS/Browser name and version | Device information

  • vivo V2424 — Adreno 610, Android 16, OpenGL ES 3.2 V@0615.100 (GIT@b82916be04, I507ba793b7, 1754480107) (Date:08/06/25), 120 Hz
  • Samsung Galaxy Tab A7 SM-T505N — Adreno 610, Android 12, 60 Hz

Does the problem occur on emulator/simulator as well as on physical devices?

Physical devices only. On the Android 16 x86_64 emulator with a host Intel Arc GPU the plain scene ties with Skia (5.1 vs 5.0 ms p50); the cost is specific to mobile GLES drivers.

Is the problem only reproducible with Impeller?

Yes

Logs

logcat — renderer selection and frame timings (release, Impeller, SCENE=plain)
I flutter : [IMPORTANT:flutter/shell/platform/android/android_context_vk_impeller.cc(62)] Using the Impeller rendering backend (Vulkan).
I flutter : [IMPORTANT:flutter/shell/platform/android/android_context_gl_impeller.cc(104)] Using the Impeller rendering backend (OpenGLES).
I flutter : REPRO scene=plain driver=0 frames=163 total_p50=12.2 total_p95=30.4 raster_p50=9.3 raster_p95=20.0 build_p50=1.2 jank16=0.13
I flutter : REPRO scene=plain driver=0 frames=161 total_p50=11.8 total_p95=20.9 raster_p50=8.6 raster_p95=14.4 build_p50=1.1 jank16=0.07
I flutter : REPRO scene=plain driver=0 frames=161 total_p50=12.0 total_p95=19.3 raster_p50=8.5 raster_p95=16.0 build_p50=1.2 jank16=0.12
I flutter : REPRO scene=plain driver=0 frames=179 total_p50=11.7 total_p95=16.5 raster_p50=8.6 raster_p95=13.1 build_p50=1.5 jank16=0.04
timeline-impeller-plain.txt (profile mode, 6 s window)
== impeller-plain: 332 frames, GPURasterizer::Draw p50 10.4 ms, max 29.5 ms
event                                                    per-frame   avg ms  ms/frame
GPURasterizer::Draw                                           1.00   11.288     11.29
Rasterizer::DoDraw                                            1.00   11.265     11.27
Rasterizer::DrawToSurfaces                                    1.00   11.242     11.24
SurfaceFrame::Submit                                          1.00   10.914     10.91
SurfaceFrame::Encode                                          1.00    8.493      8.49
ReactorGLES::React                                            1.00    6.597      6.60
ReactOnce                                                     1.00    6.590      6.59
FlushOps                                                      1.00    6.566      6.57
ReactorGLES::Operation                                        1.00    6.528      6.53
RenderPassGLES::EncodeCommandsInReactor                       1.00    6.518      6.52
timeline-skia-plain.txt (profile mode, 6 s window)
== skia-plain: 321 frames, GPURasterizer::Draw p50 3.4 ms, max 14.8 ms
event                                                    per-frame   avg ms  ms/frame
GPURasterizer::Draw                                           1.00    3.884      3.88
Rasterizer::DoDraw                                            1.00    3.824      3.82
Rasterizer::DrawToSurfaces                                    1.00    3.806      3.81
SurfaceFrame::Submit                                          1.00    2.668      2.67
SurfaceFrame::Encode                                          1.00    1.585      1.59
GrDirectContext::flushAndSubmit                               1.00    1.578      1.58
AndroidContextGL::SwapBuffers                                 1.00    1.070      1.07

Flutter Doctor output

Doctor output
[✓] Flutter (Channel stable, 3.47.2, on Fedora Linux 44 (Workstation Edition) 7.1.10-200.fc44.x86_64, locale en_US.UTF-8)
    • Flutter version 3.47.2 on channel stable at /home/mostafaali/development/flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision d3b14c8769 (6 days ago), 2026-08-26 16:07:51 -0700
    • Engine revision a804b26164
    • Dart version 3.13.2
    • DevTools version 2.60.0
[!] Android toolchain - develop for Android devices (Android SDK version 37.0.0-rc1)
    • Android SDK at /home/mostafaali/Android/Sdk
    • Emulator version 37.1.11.0 (build_id 15917651) (CL:N/A)
    • Platform android-37.0, build-tools 37.0.0-rc1
    ! Multiple adb binaries found. This can cause conflicts and device detection issues:
        - /home/mostafaali/Android/Sdk/platform-tools/adb
        - /usr/bin/adb
    ✗ Android license status unknown.
      Run `flutter doctor --android-licenses` to accept the SDK licenses.
[✓] Chrome - develop for the web
    • Chrome at google-chrome
! Doctor found issues in 1 category.

Activity

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 listc: performanceRelates to speed or footprint issues (see "perf:" labels)e: impellerImpeller rendering backend issues and features requestsengineflutter/engine related. See also e: labels.has reproducible stepsThe issue has been confirmed reproducible and is ready to work onplatform-androidAndroid applications specificallyteam-engineOwned by Engine teamtriaged-engineTriaged by Engine team

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions