Repository navigation
Description
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.
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:
- 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.
- 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.
- 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).
- 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
DirectionalLightwith shadows: 1 cascade, 2048 px, 30 m max distance, fixed PCF,cacheStaticShadowsleft at its default (true) - 1
SpotLightwith 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
SceneViewtick deltas; - p50 and p95 frame interval;
- median
FrameTimingbuild 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
FrameTimingbuild time is ~10 ms. That includesScene.renderencoding, since it runs inpaint, plus our skinning updates. Raster time is ~1–2 ms there, so the GPU work isn't reflected inFrameTiming. - 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
- 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?
- 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.
- 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
bdero commentedon Oct 10, 2026
Thanks for the detailed report. flutter_scene 0.24.3 is out with the first round of fixes.
Could you rerun your benchmark on 0.24.3 and share the numbers? Note that an fps counted from
SceneViewtick deltas on Vulkan includes frames that re-show the previous image, so it reads higher than the rate the scene actually renders at.bdero commentedon Oct 10, 2026
To your three questions.
scene.renderScalereduces the cost in proportion.maxGpuFramesInFlightat 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.pascal-ferrere commentedon Oct 10, 2026
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
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
SceneViewtick 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)
* 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:
AntiAliasingMode.msaais reported unsupported and falls back to FXAA, so "No MSAA" means almost nothing on those two rows.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.