Skip to main content
  • Linux
  • Mesa
  • Wayland

Mesa EGL Notes

Published 7 July 2024

Mesa sits in the middle of most Linux graphics stacks. On a modern desktop it is usually the layer that turns EGL calls into driver operations, negotiates with DRM/KMS, and connects rendering APIs to the native window system.

This note is not meant to be a full Mesa overview. I only focus on the part that matters when reading the Wayland EGL path: the native handle types, the dri2 backend model, initialization, and the eglMakeCurrent / eglSwapBuffers path that frameworks such as QtWayland eventually use.

Background

OpenGL

OpenGL is a graphics API and also a specification. The specification defines the observable behavior of the API, while the actual implementation is left to the graphics stack vendor. Mesa is one such implementation on Linux and related platforms.

Traditional OpenGL targets desktops and workstations. It exposes a fairly large feature set and is usually what people think of when they talk about a full graphics stack for desktop applications, CAD tools, scientific visualization, and games.

OpenGL ES

OpenGL ES is the embedded profile of OpenGL. It keeps the overall rendering model, but trims and reshapes parts of the API so it fits constrained devices and simpler hardware better.

In practice, OpenGL ES matters well beyond phones. It is common in compositors, toolkits, browsers, and cross-platform UI frameworks because it gives you a smaller API surface with good portability.

EGL

EGL is the glue between a rendering API and the native platform. It is responsible for:

  1. Discovering and initializing a display connection
  2. Choosing framebuffer configurations
  3. Creating rendering contexts
  4. Creating and managing drawing surfaces
  5. Binding the current context and presenting rendered buffers

Without EGL, OpenGL ES would not have a standard way to talk to X11, Wayland, GBM, Android, or other native systems.

Why this layer needs to be efficient

The EGL path is on the critical path of every rendered frame. Even though the actual GPU work happens deeper in the stack, EGL still decides how surfaces are created, how contexts are bound, and when buffers are handed off to the window system.

That makes the following properties important:

  1. Resource lifetime control: surfaces and contexts are expensive objects, and EGL is responsible for creating, tracking, and destroying them correctly
  2. Platform abstraction: applications should not need a separate rendering path for every window system
  3. Multi-context support: toolkits and compositors often use multiple contexts across threads
  4. Predictable presentation: buffer swaps, damage tracking, and synchronization must be consistent

DRM and DRI

DRM (Direct Rendering Manager) is the kernel-side interface used to talk to GPUs on Linux. It handles low-level device access, memory management, synchronization primitives, and display-related plumbing.

DRI (Direct Rendering Infrastructure) is the user-space side that sits above DRM. In Mesa, DRI drivers are the pieces that know how to talk to a specific GPU driver stack while exposing higher-level graphics APIs.

If you want a rough mental model: DRM is the kernel-facing contract, DRI is the Mesa-facing implementation layer.

How Mesa models EGL backends

Mesa’s EGL implementation is written in C, so backend polymorphism is expressed with structs and function tables rather than C++ classes.

At a high level:

  1. Generic EGL state lives in common structures such as _EGLDisplay, _EGLSurface, and _EGLContext
  2. Platform-specific state is stored in backend-specific extensions of those structures
  3. Operations that vary by platform are dispatched through a vtable

One representative example is dri2_egl_display_vtbl:

struct dri2_egl_display_vtbl {
   /* mandatory on Wayland, unused otherwise */
   int (*authenticate)(_EGLDisplay *disp, uint32_t id);

   /* mandatory */
   _EGLSurface* (*create_window_surface)(_EGLDisplay *disp, _EGLConfig *config,
                                         void *native_window,
                                         const EGLint *attrib_list);

   /* optional */
   _EGLSurface* (*create_pixmap_surface)(_EGLDisplay *disp, _EGLConfig *config,
                                         void *native_pixmap,
                                         const EGLint *attrib_list);
   ......
   /* mandatory */
   EGLBoolean (*swap_buffers)(_EGLDisplay *disp, _EGLSurface *surf);

   ......
};

This is a clean C-style abstraction. The higher-level EGL code does not need to know whether the underlying platform is X11, Wayland, or GBM. It only needs a display object with the right set of callbacks attached.

For Wayland software rendering, Mesa wires up a specific instance like this:

static const struct dri2_egl_display_vtbl dri2_wl_swrast_display_vtbl = {
   .authenticate = NULL,
   .create_window_surface = dri2_wl_create_window_surface,
   .create_pixmap_surface = dri2_wl_create_pixmap_surface,
   .destroy_surface = dri2_wl_destroy_surface,
   .create_image = dri2_create_image_khr,
   .swap_buffers = dri2_wl_swrast_swap_buffers,
   .get_msc_rate = dri2_wayland_get_msc_rate,
   .get_dri_drawable = dri2_surface_get_dri_drawable,
};

And the actual swap implementation is just another backend hook:

static EGLBoolean
dri2_wl_swrast_swap_buffers(_EGLDisplay *disp, _EGLSurface *draw)
{
   struct dri2_egl_display *dri2_dpy = dri2_egl_display(disp);
   struct dri2_egl_surface *dri2_surf = dri2_egl_surface(draw);

   if (!dri2_surf->wl_win)
      return _eglError(EGL_BAD_NATIVE_WINDOW, "dri2_swap_buffers");

   dri2_dpy->core->swapBuffers(dri2_surf->dri_drawable);
   return EGL_TRUE;
}

One small detail I like in this code path is that Mesa assigns the vtable late during initialization:

/* Fill vtbl last to prevent accidentally calling virtual function during
 * initialization.
 */
dri2_dpy->vtbl = &dri2_wl_display_vtbl;

That is exactly the kind of defensive initialization pattern you want in a C codebase that relies heavily on callback dispatch.

Native platform handles in eglplatform.h

include/EGL/eglplatform.h is the bridge between generic EGL types and native platform handles. Three typedefs matter the most:

  1. EGLNativeDisplayType
  2. EGLNativePixmapType
  3. EGLNativeWindowType

These types are platform-dependent. The generic API stays the same, but the concrete native object behind the handle changes.

The generic definition looks like this:

/* EGL 1.2 types, renamed for consistency in EGL 1.3 */
typedef EGLNativeDisplayType NativeDisplayType;
typedef EGLNativePixmapType  NativePixmapType;
typedef EGLNativeWindowType  NativeWindowType;

X11

On X11, the native handles map directly to Xlib types:

#elif defined(__unix__) || defined(USE_X11)

/* X11 (tentative)  */
#include <X11/Xlib.h>
#include <X11/Xutil.h>

typedef Display *EGLNativeDisplayType;
typedef Pixmap   EGLNativePixmapType;
typedef Window   EGLNativeWindowType;

Wayland

On Wayland, the handles are Wayland protocol objects:

#elif defined(WL_EGL_PLATFORM)

typedef struct wl_display     *EGLNativeDisplayType;
typedef struct wl_egl_pixmap  *EGLNativePixmapType;    // deprecated
typedef struct wl_egl_window  *EGLNativeWindowType;

wl_egl_pixmap is effectively dead. Mesa removed support for it a long time ago because the design was vague and not used in practice. The release notes and commit history are still worth reading if you want the historical context:

  1. https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/docs/relnotes/9.0.1.rst
  2. https://gitlab.freedesktop.org/mesa/mesa/-/commit/e20a0f14b5fdbff9afa5d0d6ee35de8728f6a200

GBM

For render paths that use GBM directly:

#elif defined(__GBM__)

typedef struct gbm_device  *EGLNativeDisplayType;
typedef struct gbm_bo      *EGLNativePixmapType;
typedef void               *EGLNativeWindowType;

Wayland objects that matter

wl_display

wl_display represents the client connection to the Wayland server. It is the root object for protocol communication.

In practical terms it is responsible for:

  1. establishing and maintaining the client connection
  2. retrieving the global registry
  3. dispatching protocol events
  4. coordinating message queues

If you are tracing EGL initialization on Wayland, wl_display is the first native object that matters.

wl_egl_window

wl_egl_window is the adapter object that lets EGL treat a Wayland surface as a native window:

struct wl_egl_window {
    const intptr_t version;

    int width;
    int height;
    int dx;
    int dy;

    int attached_width;
    int attached_height;

    void *driver_private;
    void (*resize_callback)(struct wl_egl_window *, void *);
    void (*destroy_window_callback)(void *);

    struct wl_surface *surface;
};

Toolkits usually create this object before calling eglCreateWindowSurface. In other words, it is the object that makes a Wayland wl_surface usable as an EGL window surface target.

wl_drm

wl_drm is a Mesa-defined Wayland protocol extension. Historically it existed because not every compositor supported the newer dma-buf path.

That history still shows up in real codebases. Chromium, for example, carried wl_drm support in its Ozone/Wayland stack for compatibility reasons:

https://chromium.googlesource.com/chromium/src.git/+/cd3bebdd6b95d6be0393df4017cf7d4c2f251e2b

Initialization path on Wayland

The broad call chain during initialization looks like this:

@startuml

qtwayland -> EGL: 1. eglInitialize()
EGL -> EGL: dri2_initialize()
EGL -> EGL: dri2_initialize_wayland()
EGL -> EGL: dri2_initialize_wayland_drm()
@enduml

1. eglInitialize()

eglInitialize() is the public API entry point. It validates the display handle, routes the call to the internal EGL driver layer, and starts backend-specific initialization.

2. dri2_initialize()

This is the main entry point for Mesa’s egl_dri2 backend. It converts the generic _EGLDisplay into a dri2_egl_display, then selects a platform-specific initializer based on the detected platform.

Common platform values here include:

  1. _EGL_PLATFORM_X11
  2. _EGL_PLATFORM_WAYLAND
  3. _EGL_PLATFORM_ANDROID

From that point on, the flow diverges into functions such as dri2_initialize_x11() or dri2_initialize_wayland().

3. dri2_initialize_wayland()

This layer decides whether Mesa should use the hardware path or the software rasterizer path.

One important knob here is LIBGL_ALWAYS_SOFTWARE. If it is set, Mesa goes down the software-rendering path. Otherwise it continues into the DRM-backed Wayland initialization path.

4. dri2_initialize_wayland_drm()

This is where the interesting platform setup happens. The exact details evolve over time, but the overall job is stable:

  1. connect to the Wayland server and obtain wl_display
  2. create an event queue and queue-aware wrappers where needed
  3. fetch the registry and bind the required globals
  4. identify the DRM device
  5. load the matching driver
  6. create the screen and wire up extensions
  7. finalize swap interval and surface-related setup

The rough sequence looks like this:

  1. wl_display_connect()
  2. wl_display_create_queue()
  3. wl_proxy_create_wrapper()
  4. wl_proxy_set_queue()
  5. wl_display_get_registry()
  6. wl_registry_add_listener()
  7. loader_get_user_preferred_fd()
  8. _eglAddDevice()
  9. drmGetNodeTypeFromFd()
  10. loader_get_driver_for_fd()
  11. dri2_load_driver()
  12. dri2_create_screen()
  13. dri2_setup_extensions()
  14. dri2_setup_screen()
  15. dri2_wl_setup_swap_interval()

The important part is not memorizing the full list. The important part is understanding that Mesa first resolves the native platform connection, then the DRM device, then the matching DRI driver, and only after that finishes the EGL-facing screen and surface setup.

eglMakeCurrent

eglMakeCurrent() binds a context and draw/read surfaces to the calling thread. In day-to-day rendering this is the point where EGL turns a set of handles into the thread-local state the driver will use for subsequent GL commands.

The broad responsibilities are:

  1. validate the display, surfaces, and context
  2. make sure the context and surfaces are compatible
  3. update thread-local current state
  4. call into the backend to perform driver-specific binding work

This call is less visible than eglSwapBuffers(), but it is just as important. If initialization builds the objects and swap presents the frame, eglMakeCurrent() is the step that actually makes rendering legal on the current thread.

eglSwapBuffers

eglSwapBuffers() is the frame boundary most people care about. At a high level it presents the rendered image to the native window system.

In Mesa’s generic EGL path, the work is roughly:

  1. fetch the current context with _eglGetCurrentContext()
  2. validate and lock the display
  3. look up the target surface
  4. verify that the surface is valid and still bound correctly
  5. confirm that the surface type is swappable
  6. hand off the actual swap to the backend implementation
  7. clear or update damage-tracking state

The egl_dri2 side eventually reaches code like this:

static EGLBoolean
dri2_swap_buffers(_EGLDisplay *disp, _EGLSurface *surf)
{
   struct dri2_egl_display *dri2_dpy = dri2_egl_display(disp);
   __DRIdrawable *dri_drawable = dri2_dpy->vtbl->get_dri_drawable(surf);
   _EGLContext *ctx = _eglGetCurrentContext();
   EGLBoolean ret;

   if (ctx && surf)
      dri2_surf_update_fence_fd(ctx, disp, surf);
   ret = dri2_dpy->vtbl->swap_buffers(disp, surf);

   /* SwapBuffers marks the end of the frame; reset the damage region for
    * use again next time.
    */
   if (ret && dri2_dpy->buffer_damage &&
       dri2_dpy->buffer_damage->set_damage_region)
      dri2_dpy->buffer_damage->set_damage_region(dri_drawable, 0, NULL);

   return ret;
}

That is a good example of Mesa’s layering:

  1. common EGL code does generic validation and bookkeeping
  2. the backend vtable selects the right swap implementation
  3. driver-specific paths handle the platform details

The post-swap reset logic is also worth noticing. It makes the swap call a natural place to finalize one frame and prepare state for the next one.

Mesa also resets some state related to partial update tracking:

/* EGL_KHR_partial_update
 * Frame boundary successfully reached,
 * reset damage region and reset BufferAgeRead
 */
if (ret) {
   surf->SetDamageRegionCalled = EGL_FALSE;
   surf->BufferAgeRead = EGL_FALSE;
}

And the simplified call chain is:

@startuml
!theme cerulean-outline

Client -> eglapi: eglSwapBuffers
eglapi -> egl_dri2: dri2_swap_buffers
egl_dri2 -> platform_wayland: swap_buffers

@enduml

Where QtWayland enters the stack

If you are debugging a Qt application on Wayland, it helps to map the toolkit objects to the EGL objects they eventually create.

The initialization entry point is in:

src/hardwareintegration/client/wayland-egl/QWaylandEglClientBufferIntegration::initialize()

QtWayland creates wl_display, turns it into an EGLDisplay, and calls eglInitialize().

On the window side, QWaylandEglWindow is the abstraction that owns the native Wayland/EGL window pairing. That usually means it is directly involved in:

  1. wl_egl_window_create()
  2. wl_egl_window_destroy()
  3. eglCreateWindowSurface()
  4. eglDestroySurface()

For rendering, QWaylandGLContext is where the core EGL interaction tends to show up:

  1. eglMakeCurrent()
  2. eglSwapInterval()
  3. eglSwapBuffers()

That is the practical bridge from toolkit code to Mesa’s EGL path.

A useful debug backtrace

When chasing a swap-related issue, a backtrace like the following is a good sanity check because it shows the whole vertical slice from application rendering code down into EGL:

#0  0x00007fffe2fb2d30 in eglSwapBuffers () from /lib/x86_64-linux-gnu/libEGL.so.1
#1  0x00007ffff003b217 in QtWaylandClient::QWaylandGLContext::swapBuffers (this=0x5555557f9620, surface=0x5555555a1c70)
    at /home/user/ocean/qtwayland/src/hardwareintegration/client/wayland-egl/qwaylandglcontext.cpp:518
#2  0x00007ffff069ca3e in QSGRenderThread::syncAndRender (this=0x5555557a5630, grabImage=0x0)
    at /home/user/ocean/qtdeclarative/src/quick/scenegraph/qsgthreadedrenderloop.cpp:870
#3  0x00007ffff069d6d7 in QSGRenderThread::run (this=0x5555557a5630) at /home/user/ocean/qtdeclarative/src/quick/scenegraph/qsgthreadedrenderloop.cpp:1043
#4  0x00007ffff6e35271 in QThreadPrivate::start(void*) () from /lib/x86_64-linux-gnu/libQt5Core.so.5
#5  0x00007ffff6758ea7 in start_thread (arg=<optimized out>) at pthread_create.c:477
#6  0x00007ffff69ecdef in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:95

This kind of trace immediately tells you whether you are looking at a toolkit problem, an EGL binding problem, or a lower driver/backend issue.

References

  1. EGL specification: https://registry.khronos.org/EGL/specs/
  2. Mesa release notes for historical Wayland EGL changes: https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/docs/relnotes/9.0.1.rst
  3. Mesa commit removing wl_egl_pixmap: https://gitlab.freedesktop.org/mesa/mesa/-/commit/e20a0f14b5fdbff9afa5d0d6ee35de8728f6a200