Zygote
Graphicsnerd

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.

12 minAPI 1–37 · 17interactivequizverified 2026-10-27

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#

Loading diagram…
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#

LayerWhat it doesTypical API
Your Canvas / Compose coderecords drawingnone — the framework records
HWUI + Skiareplays display listsOpenGL ES or Vulkan, via Skia's backend
MediaCodec outputdecodes into a Surfacenone — the decoder writes buffers
Camerawrites into a Surfacenone
Game engine (Unity, Unreal)draws everything itselfOpenGL ES or Vulkan
Custom GLSurfaceView rendererdraws itselfOpenGL 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:

  1. Vertex processing transforms the geometry the renderer emitted into screen space.
  2. Rasterisation turns each triangle into fragments — one per pixel it covers.
  3. Fragment shading decides the colour of each fragment, applying the texture, the blend mode and any shader.
  4. 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 drawRect rather 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 1–API 20OpenGL ES 1.x and 2.0the only GPU pathAPI 1
API 21 →OpenGL ES 3.1compute and newer featuresAPI 21
API 24 →Vulkanexplicit GPU control for enginesAPI 24
Interface changes16 releases
APIVersionWhat changed
11.0
  • Pull-down notification shade
  • Home screen with widgets
31.5 Cupcake
  • Home screen widgets and folders
92.3 Gingerbread
  • New UI with dark theme and green accents
113.0 Honeycomb
  • The Holo design language arrives
123.1 Honeycomb
  • Resizable widgets
164.1 Jelly Bean
  • Project Butter: vsync-driven, smooth UI
  • Expandable notifications
204.4W KitKat (Wear)
  • Voice-driven card UI
215.0 Lollipop
  • Material Design
  • Notifications on the lock screen
247.0 Nougat
  • Quick Settings tiles
  • Direct Reply in notifications
268.0 Oreo
  • Notification channels
289 Pie
  • Dark theme groundwork
2910
  • System-wide dark theme
3011
  • Conversation notifications and bubbles
  • Media controls in Quick Settings
3112
  • Material You dynamic colour
3313 Tiramisu
  • Themed app icons
  • Runtime notification permission
3616 Baklava
  • Material 3 Expressive
  • Live Updates notifications

Under the hood#

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#

GPU and API versions
adb shell dumpsys SurfaceFlinger | grep -i "gles|vulkan" -A3
Example output
GLES: 0
3.2
Vulkan: 1
version: 4202656
GPU driver properties
adb shell getprop | grep -i "gpu|egl|opengles"
Example output
[ro.hardware.egl]: [adreno]
[ro.opengles.version]: [196610]

Check yourself#

Check yourself

1 / 12

At 120 Hz, how much time does one frame have before it misses the display's deadline?

0 answered · 0 correct

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.

  1. docsEGL and OpenGL ESsource.android.comaccessed 2026-10-27↩
  2. docsVulkansource.android.comaccessed 2026-10-27↩
  3. docsInspect GPU rendering speeddeveloper.android.comaccessed 2026-10-27↩
  4. docsGraphics architecturesource.android.comaccessed 2026-10-27↩
  5. docsRendering | App qualitydeveloper.android.comaccessed 2026-10-27↩
  6. docsSurface and SurfaceHoldersource.android.comaccessed 2026-10-27↩