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:
- Discovering and initializing a display connection
- Choosing framebuffer configurations
- Creating rendering contexts
- Creating and managing drawing surfaces
- 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:
- Resource lifetime control: surfaces and contexts are expensive objects, and EGL is responsible for creating, tracking, and destroying them correctly
- Platform abstraction: applications should not need a separate rendering path for every window system
- Multi-context support: toolkits and compositors often use multiple contexts across threads
- 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:
- Generic EGL state lives in common structures such as
_EGLDisplay,_EGLSurface, and_EGLContext - Platform-specific state is stored in backend-specific extensions of those structures
- 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:
EGLNativeDisplayTypeEGLNativePixmapTypeEGLNativeWindowType
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:
- https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/docs/relnotes/9.0.1.rst
- 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:
- establishing and maintaining the client connection
- retrieving the global registry
- dispatching protocol events
- 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:
_EGL_PLATFORM_X11_EGL_PLATFORM_WAYLAND_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:
- connect to the Wayland server and obtain
wl_display - create an event queue and queue-aware wrappers where needed
- fetch the registry and bind the required globals
- identify the DRM device
- load the matching driver
- create the screen and wire up extensions
- finalize swap interval and surface-related setup
The rough sequence looks like this:
wl_display_connect()wl_display_create_queue()wl_proxy_create_wrapper()wl_proxy_set_queue()wl_display_get_registry()wl_registry_add_listener()loader_get_user_preferred_fd()_eglAddDevice()drmGetNodeTypeFromFd()loader_get_driver_for_fd()dri2_load_driver()dri2_create_screen()dri2_setup_extensions()dri2_setup_screen()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:
- validate the display, surfaces, and context
- make sure the context and surfaces are compatible
- update thread-local current state
- 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:
- fetch the current context with
_eglGetCurrentContext() - validate and lock the display
- look up the target surface
- verify that the surface is valid and still bound correctly
- confirm that the surface type is swappable
- hand off the actual swap to the backend implementation
- 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:
- common EGL code does generic validation and bookkeeping
- the backend vtable selects the right swap implementation
- 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:
wl_egl_window_create()wl_egl_window_destroy()eglCreateWindowSurface()eglDestroySurface()
For rendering, QWaylandGLContext is where the core EGL interaction tends to show up:
eglMakeCurrent()eglSwapInterval()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
- EGL specification: https://registry.khronos.org/EGL/specs/
- Mesa release notes for historical Wayland EGL changes: https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/docs/relnotes/9.0.1.rst
- Mesa commit removing
wl_egl_pixmap: https://gitlab.freedesktop.org/mesa/mesa/-/commit/e20a0f14b5fdbff9afa5d0d6ee35de8728f6a200