Skip to content

Android: low frame rate and frequent long frames on mid/low-end devices (Vulkan), and much slower on the OpenGL ES path #484

Open

Description

@pascal-ferrere

Summary

We ported one level of our game (an isometric top-down stealth room) from Unity 6.6 (URP) to flutter_scene 0.24.1, and benchmarked both on the same phones in Firebase Test Lab.

Image Image

The Vulkan crash we hit on 0.23 is gone, but frame rate is well below the Unity build on every device, and much worse on low-end ones:

Device Unity 6.6 URP flutter_scene 0.24.1
Pixel 6a (bluejay, API 32) 60 fps ~21 fps (p95 frame ≈ 100 ms)
Galaxy S22 Ultra (b0q, API 33) 60 fps ~35–42 fps (p95 ≈ 50–67 ms)
Galaxy S22 Ultra (b0qxxx, API 36) 60 fps ~39 fps (p95 ≈ 50 ms)
Galaxy A52s (a52sxq, API 34) ~30 fps ~5 fps (p95 ≈ 300 ms)

Four things stand out:

  1. Frequent long frames on Vulkan. The median frame interval is a normal ~16.7 ms, but the 95th percentile is 50–150 ms. Frames are mostly on time, with regular large hitches.
  2. The shadow cost doesn't depend on shadow-map size. Halving the shadow resolution or shortening the cascade distance changes nothing. Only disabling every shadow-casting light helps.
  3. The OpenGL ES path is very slow. Forcing OpenGL ES on the Pixel 6a drops it from ~21 to ~5 fps. The A52s falls back from Vulkan to OpenGL ES on its own, and gets ~5 fps (~300 ms raster per frame).
  4. Even with every expensive feature off, the Pixel 6a and A52s stay around 30 fps.

Environment

  • Flutter 3.47.1 (stable), flutter_scene 0.24.1, Impeller, Flutter GPU enabled
  • Profile builds, arm64 APK, Firebase Test Lab (Game Loop test)
  • Models imported offline via the build hook (.fsceneb)

Scene

  • One room: ~525 static mesh nodes (low-poly Synty assets, PBR metallic-roughness, one atlas texture), all flagged shadowStatic
  • 2 skinned characters (~70 joints each), animated on the CPU by setting bone localTransforms
  • 1 DirectionalLight with shadows: 1 cascade, 2048 px, 30 m max distance, fixed PCF, cacheStaticShadows left at its default (true)
  • 1 SpotLight with shadows (the character's torch)
  • 9 PointLights, no shadows
  • Bloom, MSAA 4×, Reinhard tone mapping with a colour-grading LUT, SH ambient
  • Fixed top-down camera following the character; 1080×2400 class screens

How we measured

The app opens straight into the room, walks the character around a loop, and logs every 3 s:

  • fps, from the SceneView tick deltas;
  • p50 and p95 frame interval;
  • median FrameTiming build and raster times.

Each run starts with a 10 s warm-up. Each step lasts 20 s, and the full scene runs again at the end to detect thermal throttling. Values below are the median of each step's 3-second windows (first window skipped).

Run 1: feature isolation (fps)

Device Backend Full No MSAA No shadows No bloom No point lights All off Full again
Pixel 6a Vulkan 22 16 36 20 21 29 22
S22 Ultra (API 33) Vulkan 42 42 69 48 55 90 43
S22 Ultra (API 36) Vulkan 40 42 56 44 50 87 40
A52s OpenGL ES (fallback) 6 6 16 7 10 31 6

(The S22s have 120 Hz screens.) "No point lights" removes the light nodes from the scene, not just zeroing their intensity.

Run 2: shadow setups (fps)

Device Backend Full (2048 px, 30 m, both shadows) 1024 px 1024 px, 20 m No spot shadow No directional shadow 1024 px, no spot shadow No shadows
Pixel 6a Vulkan 21 22 21 23 21 23 33
Pixel 6a OpenGL ES (forced) 5 5 6 7 6 6 11
S22 Ultra (API 36) Vulkan 39 38 39 39 45 39 49
A52s OpenGL ES (fallback) 5 6 5 5 10 6 14

OpenGL ES was forced with the io.flutter.embedding.android.ImpellerBackend=opengles manifest metadata.

Other observations

  • UI thread. On the S22 the median FrameTiming build time is ~10 ms. That includes Scene.render encoding, since it runs in paint, plus our skinning updates. Raster time is ~1–2 ms there, so the GPU work isn't reflected in FrameTiming.
  • Thermal. One S22 (API 36) fell from 35 to 18 fps within a minute in one run. The Pixel 6a and A52s numbers are steady across runs.
  • Emulator. On an x86_64 Android emulator (OpenGL ES, host GPU) the same build runs at 50–57 fps with all steps working, so the scene renders correctly.

Questions

  1. Is a large fixed cost per shadow-casting light expected, independent of resolution and distance? Is there a cheaper path for a mostly static scene with one directional light?
  2. Is the OpenGL ES backend expected to be this much slower than Vulkan for flutter_scene? That matters because Impeller falls back to it automatically on devices like the A52s.
  3. Could the regular 50–150 ms frames on Vulkan (Pixel 6a, Mali-G78) come from GPU synchronisation or frame pacing in flutter_scene / Flutter GPU? Are there settings worth trying (e.g. maxGpuFramesInFlight)?

Thank you,

Activity

  1. added
    bugSomething isn't working.
    performanceFrame rate, memory, or runtime cost regressions.
    on Oct 9, 2026
  2. bdero commented on Oct 10, 2026

    @bdero
    Owner

    Thanks for the detailed report. flutter_scene 0.24.3 is out with the first round of fixes.

    • Lit materials now use a lean shader when the scene has no fog, ambient occlusion, area lights, or similar heavier features, which matches your room. This was the main cost on Mali (1.6x to 1.8x faster on a Galaxy A16).
    • The long Vulkan frames came from per-frame uploads blocking the UI thread on held frames. Those are gone (p95 230 ms to 40 ms on the A16).
    • Meshes lit by point and spot lights batch into instanced draws again.

    Could you rerun your benchmark on 0.24.3 and share the numbers? Note that an fps counted from SceneView tick deltas on Vulkan includes frames that re-show the previous image, so it reads higher than the rate the scene actually renders at.

  3. bdero commented on Oct 10, 2026

    @bdero
    Owner

    To your three questions.

    1. Yes, before 0.24.3 that was expected. The cost is per lit pixel, not per shadow-map texel. Any shadow-casting light moves every lit pixel onto a heavier shader, so resolution and distance barely change it. Your static casters are already cached, so what remains is sampling. If one shadow can go, I would drop the torch, since a moving spot re-renders every caster in its cone each frame. Lowering scene.renderScale reduces the cost in proportion.
    2. Not on the GPU side. On the A16, OpenGL ES spent about the same GPU time per frame as Vulkan. It looks slower because the GLES backend waits for the GPU on the raster thread, so frames cannot be re-shown while the GPU catches up. I have not reproduced the Pixel 6a dropping from 21 to 5, so numbers from 0.24.3 on that device would help.
    3. Partly. The UI-thread stalls were the uploads fixed in 0.24.3. The rest is pacing. With maxGpuFramesInFlight at 1 (the default), the view re-shows its last image while the GPU is busy, so tick intervals alternate between 16.7 ms and the GPU frame time. Setting it to 2 renders slightly more frames on a saturated GPU, at the cost of the UI thread blocking in submit again. I would keep 1 unless the rendered rate matters more than input latency.
  4. pascal-ferrere commented on Oct 10, 2026

    @pascal-ferrere
    Author

    Thanks, and thanks for the answers. We reran the same Test Lab benchmark on 0.24.3 (same build otherwise: Flutter 3.47.1, profile, arm64, Game Loop, same scene and step tour). We also added a Galaxy A16 (a16, API 35, Helio G99 / Mali-G57 MC2), which looks like the GPU you measured on.

    Values are the median of each step's 3-second windows, first window skipped, as before.

    Full room, 0.24.1 → 0.24.3

    Device Backend fps p95 frame (ms) UI thread (ms) Raster thread (ms)
    Pixel 6a (Mali-G78) Vulkan 22 → 59 100 → 17 2.3 → 1.3 15 → 14
    S22 Ultra, API 33 (Adreno 730) Vulkan 42 → 57 50 → 33 10.6 → 0.9 1 → 1
    S22 Ultra, API 36 Vulkan 40 → 49 58 → 40 11.6 → 1.3 2 → 2
    A52s (Adreno 642L) OpenGL ES (fallback) 6 → 12 296 → 143 8.5 → 6.8 276 → 122
    A16 (Mali-G57 MC2) OpenGL ES – → 9 – → 211 – → 15.7 – → 184

    The upload fix shows clearly: the UI thread on the S22s went from ~11 ms to ~1 ms, and the Pixel 6a's regular 100 ms frames are gone (p95 is now one vsync). The lean shader more than halves the A52s's frame time.

    Your note on tick-based fps

    Our fps and p50/p95 come from SceneView tick deltas, so on the three Vulkan devices they include re-shown frames and are an upper bound on the rate the scene actually renders at. The Pixel 6a's 59 fps with a p95 of 17 ms may partly reflect that. On the two OpenGL ES devices, if I read your answer right, there's no re-showing, so their numbers (and the raster time) should be the real per-frame cost.

    Feature isolation on 0.24.3 (fps; raster ms in brackets)

    Device Backend Full No MSAA No shadows No bloom No point lights All off Full again
    Pixel 6a Vulkan 59 (14) 60 (16) 60 (4) 59 (16) 60 (4) 60 (4) 58 (16)
    S22 Ultra (API 33) Vulkan 57 51 70 35* 32* 101 28*
    S22 Ultra (API 36) Vulkan 49 44 54 33* 31* 95 26*
    A52s OpenGL ES (fallback) 12 (122) 12 (132) 28 (45) 12 (116) 16 (86) 54 (17) 12 (131)
    A16 OpenGL ES 9 (184) 10 (176) 19 (65) 9 (155) 10 (144) 28 (31) 10 (174)

    * Both S22s throttle during the run: the same full scene goes from 57/49 fps at the start to 28/26 fps at the end, so their middle steps aren't comparable with each other. The Pixel 6a, A52s and A16 are steady from first step to last.

    Notes:

    • The low-end devices don't get Vulkan here. On the A16, Impeller picked OpenGL ES straight away (we can't see a Vulkan attempt in the log). On the A52s it logged Vulkan and fell back to OpenGL ES immediately. So for this class of phone, the OpenGL ES path is the one players will actually get. Your Galaxy A16 numbers were on Vulkan, I believe. Do you know why Impeller doesn't use Vulkan on this one? It may simply be the Test Lab unit's driver version.
    • On OpenGL ES, AntiAliasingMode.msaa is reported unsupported and falls back to FXAA, so "No MSAA" means almost nothing on those two rows.
    • Shadows are still the largest cost: turning them off is ~2.3× on the A52s (122 → 45 ms) and ~2.8× on the A16 (184 → 65 ms). On the A52s, the no-shadows room (28 fps) is now about where Unity URP runs the full room (~30 fps). Removing the 9 point lights is the next biggest saving on both.
    • On the Pixel 6a the FrameTiming raster time falls from ~15 ms to ~4 ms with either shadows or point lights removed, even though the tick rate is pinned at 60.

    About dropping the torch

    Thanks for the suggestion, but the torch can't go. This is a stealth game, and the whole game is built on light and shadow: the player's torch lights the way, guards spot the thief when he's lit, and hiding means staying in the dark. The torch and the shadows it casts are the main element of the gameplay, not decoration, so we need a moving, shadow-casting spot light to run well on these phones.

    Thanks again for the quick turnaround. 0.24.3 is a big step for us.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working.performanceFrame rate, memory, or runtime cost regressions.platform:android

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions