GPU APIs
OpenGL ES, Vulkan, EGL and the layers between your code and the graphics driver — plus why most apps never call any of them directly.
Almost no Android app calls OpenGL ES directly, and fewer call Vulkan. Yet every app's pixels go through one of them, because the framework's own renderer uses them. Knowing which layer does what tells you when a GPU problem is yours and when it is the driver's.
TL;DR#
- OpenGL ES is the older, higher-level GPU API. Most Android GPU work has historically gone through it.
- Vulkan is lower-level and more explicit. It arrived in Android with API 24Android 7.0 and gives apps control over memory, synchronisation and command submission.
- EGL is the glue between a GPU context and a Surface — it is how rendered output reaches the compositor.
- Most apps never touch these directly: HWUI and Skia do it for them, and Compose goes through the same path.
- The interesting question for an app developer is not "which API" but "am I GPU-bound, and if so why".
Mental model#
Think of a kitchen.
OpenGL ES is a set of appliances with fixed controls: you press "bake" and the oven decides how. Convenient, and good enough for most meals.
Vulkan is a bare kitchen: you choose the heat source, place the racks, and manage the timing yourself. More work, more control, and the ability to cook several dishes in parallel without the appliances fighting.
EGL is the counter where finished dishes are placed for the waiter to collect. It is not cooking; it is the handoff.
Where the analogy breaks#
A kitchen has one chef. A GPU has thousands of tiny cores and a driver that reorders and batches work. The API is not a set of instructions to a single cook; it is a contract with a compiler and a scheduler, and the same API call can cost wildly different amounts on different hardware.
The big picture#
Text description of this diagram
The frame pipeline, with the GPU API layer highlighted between the RenderThread and the BufferQueue. The RenderThread replays display lists through HWUI and Skia; Skia emits either OpenGL ES or Vulkan commands; EGL binds the rendered output to a Surface so the buffer can be queued for SurfaceFlinger. The 16.67 ms budget includes whatever the GPU spends executing those commands.
The API layer sits inside the render stage. When people say "the GPU is slow", they usually mean the driver is spending time in this box, and the fix is rarely an API change in app code.
Deep dive#
OpenGL ES: the default for a decade#
OpenGL ES is a trimmed OpenGL for embedded devices. Android has supported it from the beginning, and the framework's own rendering used it as the primary backend for most of the platform's life.
Its model is a state machine: you bind objects, set state, and issue draw calls. The driver decides how to schedule them, how to allocate memory and when to synchronise. That is convenient and portable, and it hides a lot — including performance cliffs you cannot see from the API.
Starting with API 21Android 5.0, Android exposed OpenGL ES 3.1.1
Vulkan: explicit control#
Vulkan arrived in Android with API 24Android 7.0. It removes most of the driver's freedom: you allocate memory, you record command buffers, you manage synchronisation primitives, and you decide how work is submitted.2
The payoff is predictability and parallelism. The cost is a great deal of code. For a UI app, Vulkan is almost never the right direct choice; for a game engine or a 3D renderer, it can be.
An important nuance: the presence of Vulkan on a device does not mean Android's own renderer uses it. Whether Skia uses a Vulkan backend depends on the platform build and the driver, not on what your app requests.
EGL: the bridge to a Surface#
EGL is not a rendering API. It is the interface that creates a rendering context and binds it to a surface — and on Android, a surface is a BufferQueue endpoint.
The sequence for rendering with a GPU into a window is roughly:
EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
eglInitialize(display, nullptr, nullptr);
EGLConfig config;
EGLint numConfigs;
eglChooseConfig(display, attribs, &config, 1, &numConfigs);
EGLSurface surface = eglCreateWindowSurface(display, config, nativeWindow, nullptr);
EGLContext context = eglCreateContext(display, config, EGL_NO_CONTEXT, contextAttribs);
eglMakeCurrent(display, surface, surface, context);nativeWindow is an ANativeWindow, which wraps a Surface. After eglSwapBuffers, the buffer is queued and
SurfaceFlinger can pick it up. That swap is where the producer/consumer handoff happens for GPU-rendered content.
Who actually calls these APIs#
| Layer | What it does | Typical API |
|---|---|---|
| Your Canvas / Compose code | records drawing | none — the framework records |
| HWUI + Skia | replays display lists | OpenGL ES or Vulkan, via Skia's backend |
| MediaCodec output | decodes into a Surface | none — the decoder writes buffers |
| Camera | writes into a Surface | none |
| Game engine (Unity, Unreal) | draws everything itself | OpenGL ES or Vulkan |
Custom GLSurfaceView renderer | draws itself | OpenGL ES via EGL |
The top three rows cover almost every app. That is why "which GPU API should I use" is usually the wrong question for an app developer: the answer is "none, unless you are writing a renderer".
Being GPU-bound versus CPU-bound#
The distinction changes what you fix.
- CPU-bound frame: the UI thread or RenderThread is busy too long. Cause: too much work in
onDraw, too many layout passes, too much allocation. Fix: reduce work, cache, flatten the hierarchy. - GPU-bound frame: the GPU is executing too many fragments. Cause: overdraw, large blurs, complex shaders, high resolution. Fix: reduce overdraw, simplify effects, lower resolution.
A trace tells you which. On the UI thread, look at the length of DrawFrame and the traversal slices. On the GPU side,
look at the GPU track and the RenderThread's DrawFrame. If the RenderThread's frame is long and the UI thread's is
short, you are likely GPU-bound.
Debugging tools#
Android's GPU rendering profile tool draws bars per frame, colour-coded by phase, over the app. It is coarse but immediate.3 For real analysis, Perfetto's GPU tracks and the RenderThread slices are far more precise.
The GPU pipeline in one page#
A GPU does not draw in the order your code called it. Work is batched and reordered for throughput, and the driver is the component that decides how.
For a typical 2D frame:
- Vertex processing transforms the geometry the renderer emitted into screen space.
- Rasterisation turns each triangle into fragments — one per pixel it covers.
- Fragment shading decides the colour of each fragment, applying the texture, the blend mode and any shader.
- Blending and writing combines the fragment with what is already in the framebuffer.
The cost of step 3 and 4 scales with the number of pixels touched, not the number of draw calls. That is why overdraw — drawing several opaque layers on top of each other — is the classic Android GPU cost: the GPU shades pixels that are immediately covered.
Overdraw, concretely#
A screen with a background, a card, a row and a thumbnail draws four layers of pixels. If each is opaque, three of those layers are wasted work. Android's GPU rendering profile tool renders overdraw as coloured bars over the screen: blue for one layer, green for two, pink for three, red for four or more.
The fixes are structural rather than clever:
- Let the window background be drawn once, by the theme, rather than by both the theme and the root view.
- Avoid stacking opaque backgrounds; use a single background on the outermost container.
- Clip or
drawRectrather than drawing a full-size image and masking it.
The GPU cannot eliminate the wasted work for you because it cannot know which layers will be hidden until it has
already shaded them — unless the framework tells it, which is what clipRect and quickReject are for.
How the compositor decides overlays#
SurfaceFlinger does not guess. On each frame it asks the Hardware Composer, through the HWC HAL, which layers it can handle directly. The HAL's answer depends on:
- The number of overlay planes the display controller supports.
- Whether a layer is opaque — an overlay with alpha blending may need a plane capable of blending.
- Whether layers overlap — overlapping layers generally cannot both be overlays.
- Format and transform constraints — some formats and rotations are supported only via GPU.
When the stack does not fit, SurfaceFlinger falls back to client composition: it renders the excess layers into a client target with the GPU and presents that as a single overlay. The frame is still correct; it is just more expensive.
Why the HAL matters to app developers#
An app cannot choose its composition strategy, but it can make one more likely. A full-screen opaque SurfaceView is a near-perfect overlay candidate. A window with rounded corners and a shadow, drawn over another window, is not — the system has to composite the rounded region.
This is a real reason games and video players use SurfaceView: not because it is new or special, but because it produces exactly the layer shape the display hardware wants.
How it evolved#
| API | Version | What changed |
|---|---|---|
| 1 | 1.0 |
|
| 3 | 1.5 Cupcake |
|
| 9 | 2.3 Gingerbread |
|
| 11 | 3.0 Honeycomb |
|
| 12 | 3.1 Honeycomb |
|
| 16 | 4.1 Jelly Bean |
|
| 20 | 4.4W KitKat (Wear) |
|
| 21 | 5.0 Lollipop |
|
| 24 | 7.0 Nougat |
|
| 26 | 8.0 Oreo |
|
| 28 | 9 Pie |
|
| 29 | 10 |
|
| 30 | 11 |
|
| 31 | 12 |
|
| 33 | 13 Tiramisu |
|
| 36 | 16 Baklava |
|
Under the hood#
- eglApi.cppthe EGL entry points and the Surface binding
- the Vulkan loader and the platform's Vulkan support
- EglManager.cpphow HWUI sets up EGL for the RenderThread
- Skia's GPU backends, including the GL and Vulkan paths
Curiosity corner#
- A phone can support Vulkan and still run its UI on OpenGL ES. Support is a capability, not a configuration.
- The driver is the wild card. Two devices with the same GPU can behave differently because the driver decides how to schedule and how to compile shaders.
- Shader compilation can jank. The first draw with a new shader may be much slower than later ones, which is why games warm up their pipelines.
Try it#
adb shell dumpsys SurfaceFlinger | grep -i "gles|vulkan" -A3Example output
GLES: 0
3.2
Vulkan: 1
version: 4202656adb shell getprop | grep -i "gpu|egl|opengles"Example output
[ro.hardware.egl]: [adreno]
[ro.opengles.version]: [196610]Check yourself#
Check yourself
At 120 Hz, how much time does one frame have before it misses the display's deadline?
Sources#
Sources
Every version number, API level, class name and date on this page is traced to one of these. Pages are re-verified against them; last_verified above says when.
- docsEGL and OpenGL ESsource.android.comaccessed 2026-10-27↩
- docsVulkansource.android.comaccessed 2026-10-27↩
- docsInspect GPU rendering speeddeveloper.android.comaccessed 2026-10-27↩
- docsGraphics architecturesource.android.comaccessed 2026-10-27↩
- docsRendering | App qualitydeveloper.android.comaccessed 2026-10-27↩
- docsSurface and SurfaceHoldersource.android.comaccessed 2026-10-27↩