[v4,03/23] libcamera: software_isp: egl: Add a eGL base helper class
diff mbox series

Message ID 20251120233347.5046-4-bryan.odonoghue@linaro.org
State Superseded
Headers show
Series
  • Add GLES 2.0 GPUISP to libcamera
Related show

Commit Message

Bryan O'Donoghue Nov. 20, 2025, 11:33 p.m. UTC
Introduce an eGL base helper class which provides an eGL context based on a
passed width and height.

The initGLContext function could be overloaded to provide an interface to a
real display.

A set of helper functions is provided to compile and link GLSL shaders.
linkShaderProgram currently compiles vertex/fragment pairs but could be
overloaded or passed a parameter to link a compute shader instead.

Breaking the eGL interface away from debayering - allows to use the eGL
context inside of a dma-buf heap cleanly, reuse that context inside of a
debayer layer and conceivably reuse the context in a multi-stage shader
pass.

Small note the image_attrs[] array doesn't pass checkstyle.py however the
elements of the array are in pairs.

Acked-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
Signed-off-by: Bryan O'Donoghue <bryan.odonoghue@linaro.org>
---
 include/libcamera/internal/egl.h | 412 +++++++++++++++++++++++++++++
 src/libcamera/egl.cpp            | 436 +++++++++++++++++++++++++++++++
 src/libcamera/meson.build        |  23 ++
 3 files changed, 871 insertions(+)
 create mode 100644 include/libcamera/internal/egl.h
 create mode 100644 src/libcamera/egl.cpp

Comments

Milan Zamazal Nov. 25, 2025, 9:12 p.m. UTC | #1
Hi Bryan,

Bryan O'Donoghue <bryan.odonoghue@linaro.org> writes:

> Introduce an eGL base helper class which provides an eGL context based on a
> passed width and height.
>
> The initGLContext function could be overloaded to provide an interface to a
> real display.
>
> A set of helper functions is provided to compile and link GLSL shaders.
> linkShaderProgram currently compiles vertex/fragment pairs but could be
> overloaded or passed a parameter to link a compute shader instead.
>
> Breaking the eGL interface away from debayering - allows to use the eGL
> context inside of a dma-buf heap cleanly, reuse that context inside of a
> debayer layer and conceivably reuse the context in a multi-stage shader
> pass.
>
> Small note the image_attrs[] array doesn't pass checkstyle.py however the
> elements of the array are in pairs.
>
> Acked-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
> Signed-off-by: Bryan O'Donoghue <bryan.odonoghue@linaro.org>
> ---
>  include/libcamera/internal/egl.h | 412 +++++++++++++++++++++++++++++
>  src/libcamera/egl.cpp            | 436 +++++++++++++++++++++++++++++++
>  src/libcamera/meson.build        |  23 ++
>  3 files changed, 871 insertions(+)
>  create mode 100644 include/libcamera/internal/egl.h
>  create mode 100644 src/libcamera/egl.cpp
>
> diff --git a/include/libcamera/internal/egl.h b/include/libcamera/internal/egl.h
> new file mode 100644
> index 000000000..e61a394af
> --- /dev/null
> +++ b/include/libcamera/internal/egl.h
> @@ -0,0 +1,412 @@
> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> +/*
> + * Copyright (C) 2024, Linaro Ltd.
> + *
> + * Authors:
> + * Bryan O'Donoghue <bryan.odonoghue@linaro.org>
> + *
> + * egl_context.cpp - Helper class for managing eGL interactions.
> + */
> +
> +#pragma once
> +
> +#include <sys/types.h>
> +#include <unistd.h>
> +
> +#include <libcamera/base/log.h>
> +
> +#include "libcamera/base/utils.h"
> +#include "libcamera/internal/gbm.h"
> +
> +#define EGL_EGLEXT_PROTOTYPES
> +#include <EGL/egl.h>
> +#include <EGL/eglext.h>
> +#define GL_GLEXT_PROTOTYPES
> +#include <GLES2/gl2.h>
> +#include <GLES2/gl2ext.h>
> +
> +namespace libcamera {
> +
> +LOG_DECLARE_CATEGORY(eGL)
> +
> +/**
> + * \class eGLImage
> + * \brief Helper class for managing EGL image resources
> + *
> + * The eGLImage class encapsulates OpenGL ES texture and framebuffer objects
> + * along with their associated EGL image. It aggregates handles, descriptors,
> + * and routines for managing textures that can be associated with shader
> + * uniform IDs.
> + *
> + * This class is particularly useful for managing DMA-BUF backed textures
> + * in zero-copy rendering pipelines, where textures are bound to specific
> + * texture units and can be used as both input textures and render targets.
> + */
> +class eGLImage
> +{
> +public:
> +	/**
> +	 * \brief Construct an eGLImage with explicit stride
> +	 * \param[in] width Image width in pixels
> +	 * \param[in] height Image height in pixels
> +	 * \param[in] bpp Bytes per pixel
> +	 * \param[in] stride Row stride in bytes
> +	 * \param[in] texture_unit OpenGL texture unit (e.g., GL_TEXTURE0)
> +	 * \param[in] texture_unit_uniform_id Shader uniform ID for this texture unit
> +	 *
> +	 * Creates an eGLImage with the specified dimensions and stride. The stride
> +	 * may differ from width * bpp due to alignment.
> +	 */
> +	eGLImage(uint32_t width, uint32_t height, uint32_t bpp, uint32_t stride, GLenum texture_unit, uint32_t texture_unit_uniform_id)
> +	{
> +		init(width, height, bpp, stride, texture_unit, texture_unit_uniform_id);
> +	}
> +
> +	/**
> +	 * \brief Construct an eGLImage with automatic stride calculation
> +	 * \param[in] width Image width in pixels
> +	 * \param[in] height Image height in pixels
> +	 * \param[in] bpp Bytes per pixel
> +	 * \param[in] texture_unit OpenGL texture unit (e.g., GL_TEXTURE0)
> +	 * \param[in] texture_unit_uniform_id Shader uniform ID for this texture unit
> +	 *
> +	 * Creates an eGLImage with automatic stride calculation. The stride is
> +	 * aligned to 256 bytes because 256 byte alignment is a common baseline alignment for GPUs.
> +	 */
> +	eGLImage(uint32_t width, uint32_t height, uint32_t bpp, GLenum texture_unit, uint32_t texture_unit_uniform_id)
> +	{
> +		uint32_t stride = libcamera::utils::alignUp(width * bpp, 256);
> +
> +		init(width, height, bpp, stride, texture_unit, texture_unit_uniform_id);
> +	}
> +
> +	/**
> +	 * \brief Destroy the eGLImage
> +	 *
> +	 * Cleans up OpenGL resources by deleting the framebuffer object and
> +	 * texture.
> +	 */
> +	~eGLImage()
> +	{
> +		glDeleteFramebuffers(1, &fbo_);
> +		glDeleteTextures(1, &texture_);
> +	}
> +
> +	uint32_t width_;			/**< Image width in pixels */
> +	uint32_t height_;			/**< Image height in pixels */
> +	uint32_t stride_;			/**< Row stride in bytes */
> +	uint32_t offset_;			/**< Buffer offset (reserved for future use) */
> +	uint32_t framesize_;			/**< Total frame size in bytes (stride * height) */
> +	uint32_t bpp_;				/**< Bytes per pixel */
> +	uint32_t texture_unit_uniform_id_;	/**< Shader uniform id for texture unit */
> +	GLenum texture_unit_;			/**< Texture unit associated with this image eg (GL_TEXTURE0) */
> +	GLuint texture_;			/**< OpenGL texture object ID */
> +	GLuint fbo_;				/**< OpenGL frame buffer object ID */
> +	EGLImageKHR image_;			/**< EGL Image handle */
> +
> +private:
> +	LIBCAMERA_DISABLE_COPY_AND_MOVE(eGLImage)
> +
> +	/**
> +	 * \brief Initialise eGLImage state
> +	 * \param[in] width Image width in pixels
> +	 * \param[in] height Image height in pixels
> +	 * \param[in] bpp Bytes per pixel
> +	 * \param[in] stride Row stride in bytes
> +	 * \param[in] texture_unit OpenGL texture unit
> +	 * \param[in] texture_unit_uniform_id Shader uniform ID
> +	 *
> +	 * Common initialisation routine called by both constructors. Sets up
> +	 * member variables and generates OpenGL texture and framebuffer objects.
> +	 */
> +	void init(uint32_t width, uint32_t height, uint32_t bpp, uint32_t stride, GLenum texture_unit, uint32_t texture_unit_uniform_id)
> +	{
> +		image_ = EGL_NO_IMAGE_KHR;
> +		width_ = width;
> +		height_ = height;
> +		bpp_ = bpp;
> +		stride_ = stride;
> +		framesize_ = stride_ * height_;
> +		texture_unit_ = texture_unit;
> +		texture_unit_uniform_id_ = texture_unit_uniform_id;
> +
> +		glGenTextures(1, &texture_);
> +		glGenFramebuffers(1, &fbo_);
> +	}
> +};
> +
> +/**
> + * \class eGL
> + * \brief Helper class for managing OpenGL ES operations
> + *
> + * It provides:
> + *
> + * - EGL context setup and management
> + * - Extension function pointer retrieval
> + * - Shader compilation and program linking
> + * - DMA-BUF texture creation and management
> + * - Synchronisation primitives
> + *
> + * This class is designed to work with zero-copy buffers via DMA-BUF file
> + * descriptors.
> + */
> +class eGL
> +{
> +public:
> +	/**
> +	 * \brief Construct an EGL helper
> +	 *
> +	 * Creates an eGL instance with uninitialised context. Call initEGLContext()
> +	 * to set up the EGL display, context, and load extension functions.
> +	 */
> +	eGL();
> +
> +	/**
> +	 * \brief Destroy the EGL helper
> +	 *
> +	 * Destroys the EGL context and surface if they were successfully created.
> +	 */
> +	~eGL();
> +
> +	/**
> +	 * \brief Initialise the EGL context
> +	 * \param[in] gbmContext Pointer to initialised GBM context
> +	 *
> +	 * Sets up the EGL display from the GBM device, creates an OpenGL ES 2.0
> +	 * context, and retrieves function pointers for required extensions
> +	 * including:
> +	 * - eglCreateImageKHR / eglDestroyImageKHR
> +	 * - glEGLImageTargetTexture2DOES
> +	 * - eglCreateSyncKHR / eglDestroySyncKHR / eglClientWaitSyncKHR
> +	 *
> +	 * \return 0 on success, or -ENODEV on failure
> +	 */
> +	int initEGLContext(GBM *gbmContext);
> +
> +	/**
> +	 * \brief Clean up EGL resources
> +	 *
> +	 * Destroys the EGL sync object. Must be called from the same thread
> +	 * that created the EGL context.
> +	 */
> +	void cleanUp();
> +
> +	/**
> +	 * \brief Create an input DMA-BUF backed texture
> +	 * \param[in,out] eglImage EGL image to associate with the DMA-BUF
> +	 * \param[in] fd DMA-BUF file descriptor
> +	 *
> +	 * Creates an EGL image from a DMA-BUF file descriptor and binds it to
> +	 * a 2D texture for use as an input texture in shaders. The texture is
> +	 * configured with nearest filtering and clamp-to-edge wrapping.
> +	 *
> +	 * \return 0 on success, or -ENODEV on failure
> +	 */
> +	int createInputDMABufTexture2D(eGLImage *eglImage, int fd);
> +
> +	/**
> +	 * \brief Create an output DMA-BUF backed texture
> +	 * \param[in,out] eglImage EGL image to associate with the DMA-BUF
> +	 * \param[in] fd DMA-BUF file descriptor
> +	 *
> +	 * Creates an EGL image from a DMA-BUF file descriptor and binds it to
> +	 * a 2D texture, then attaches it to a framebuffer object for use as a
> +	 * render target. This enables zero-copy rendering directly to the
> +	 * DMA-BUF.
> +	 *
> +	 * \return 0 on success, or -ENODEV on failure
> +	 */
> +	int createOutputDMABufTexture2D(eGLImage *eglImage, int fd);
> +
> +	/**
> +	 * \brief Destroy a DMA-BUF texture's EGL image
> +	 * \param[in,out] eglImage EGL image to destroy
> +	 *
> +	 * Destroys the EGL image associated with a DMA-BUF texture. The OpenGL
> +	 * texture and framebuffer objects are destroyed separately in the
> +	 * eGLImage destructor.
> +	 */
> +	void destroyDMABufTexture(eGLImage *eglImage);
> +
> +	/**
> +	 * \brief Create a 2D texture from a memory buffer
> +	 * \param[in,out] eglImage EGL image to associate with the texture
> +	 * \param[in] format OpenGL internal format (e.g., GL_RGB, GL_RGBA)
> +	 * \param[in] width Texture width in pixels
> +	 * \param[in] height Texture height in pixels
> +	 * \param[in] data Pointer to pixel data, or nullptr for uninitialised texture
> +	 *
> +	 * Creates a 2D texture from a CPU-accessible memory buffer. The texture
> +	 * is configured with nearest filtering and clamp-to-edge wrapping. This
> +	 * is useful for uploading static data like lookup tables or uniform color
> +	 * matrices to the GPU.
> +	 */
> +	void createTexture2D(eGLImage *eglImage, GLint format, uint32_t width, uint32_t height, void *data);
> +
> +	/**
> +	 * \brief Create a 1D texture from a memory buffer
> +	 * \param[in,out] eglImage EGL image to associate with the texture
> +	 * \param[in] format OpenGL internal format
> +	 * \param[in] width Texture width in pixels
> +	 * \param[in] data Pointer to pixel data
> +	 *
> +	 * Creates a 1D texture (implemented as a 2D texture with height=1) from
> +	 * a CPU-accessible memory buffer. Useful for lookup tables in shaders.
> +	 */
> +	void createTexture1D(eGLImage *eglImage, GLint format, uint32_t width, void *data);
> +
> +	/**
> +	 * \brief Add a preprocessor definition to shader environment
> +	 * \param[in,out] shaderEnv Vector of shader environment strings
> +	 * \param[in] str Preprocessor definition string (e.g., "#define APPLY_RGB_PARAMETERS")
> +	 *
> +	 * Appends a preprocessor definition to the shader environment vector.
> +	 * These definitions are prepended to shader source code during compilation.
> +	 */
> +	void pushEnv(std::vector<std::string> &shaderEnv, const char *str);
> +
> +	/**
> +	 * \brief Make the EGL context current for the calling thread
> +	 *
> +	 * Binds the EGL context to the current thread, allowing OpenGL ES
> +	 * operations to be performed. Must be called from the thread that
> +	 * will perform rendering operations.
> +	 */
> +	void makeCurrent();
> +
> +	/**
> +	 * \brief Compile a vertex shader
> +	 * \param[out] shaderId OpenGL shader object ID
> +	 * \param[in] shaderData Pointer to shader source code
> +	 * \param[in] shaderDataLen Length of shader source in bytes
> +	 * \param[in] shaderEnv Span of preprocessor definitions to prepend
> +	 *
> +	 * Compiles a vertex shader from source code with optional preprocessor
> +	 * definitions. On compilation failure, logs the shader info log.
> +	 *
> +	 * \return 0 on success, or -EINVAL on compilation failure
> +	 */
> +	int compileVertexShader(GLuint &shaderId, unsigned char *shaderData,
> +				unsigned int shaderDataLen,
> +				Span<const std::string> shaderEnv);
> +
> +	/**
> +	 * \brief Compile a fragment shader
> +	 * \param[out] shaderId OpenGL shader object ID
> +	 * \param[in] shaderData Pointer to shader source code
> +	 * \param[in] shaderDataLen Length of shader source in bytes
> +	 * \param[in] shaderEnv Span of preprocessor definitions to prepend
> +	 *
> +	 * Compiles a fragment shader from source code with optional preprocessor
> +	 * definitions. On compilation failure, logs the shader info log.
> +	 *
> +	 * \return 0 on success, or -EINVAL on compilation failure
> +	 */
> +	int compileFragmentShader(GLuint &shaderId, unsigned char *shaderData,
> +				  unsigned int shaderDataLen,
> +				  Span<const std::string> shaderEnv);
> +
> +	/**
> +	 * \brief Link a shader program
> +	 * \param[out] programId OpenGL program object ID
> +	 * \param[in] fragmentshaderId Compiled fragment shader ID
> +	 * \param[in] vertexshaderId Compiled vertex shader ID
> +	 *
> +	 * Links vertex and fragment shaders into an executable shader program.
> +	 * On link failure, logs the program info log and deletes the program.
> +	 *
> +	 * \return 0 on success, or -ENODEV on link failure
> +	 */
> +	int linkProgram(GLuint &programId, GLuint fragmentshaderId, GLuint vertexshaderId);
> +
> +	/**
> +	 * \brief Dump shader source code to the log
> +	 * \param[in] shaderId OpenGL shader object ID
> +	 *
> +	 * Retrieves and logs the complete source code of a compiled shader.
> +	 * Useful for debugging shader compilation issues.
> +	 */
> +	void dumpShaderSource(GLuint shaderId);
> +
> +	/**
> +	 * \brief Activate a shader program for rendering
> +	 * \param[in] programId OpenGL program object ID
> +	 *
> +	 * Sets the specified program as the current rendering program. All
> +	 * subsequent draw calls will use this program's shaders.
> +	 */
> +	void useProgram(GLuint programId);
> +
> +	/**
> +	 * \brief Delete a shader program
> +	 * \param[in] programId OpenGL program object ID
> +	 *
> +	 * Deletes a shader program and frees associated resources. The program
> +	 * must not be currently in use.
> +	 */
> +	void deleteProgram(GLuint programId);
> +
> +	/**
> +	 * \brief Synchronise rendering output
> +	 *
> +	 * Flushes OpenGL commands and waits for rendering to complete using an
> +	 * EGL fence sync object. This ensures all rendering operations have
> +	 * finished before the CPU accesses the output buffers.
> +	 *
> +	 * \return 0 on success
> +	 */
> +	int syncOutput();
> +
> +private:
> +	LIBCAMERA_DISABLE_COPY_AND_MOVE(eGL)
> +
> +	int fd_;		/**< File descriptor \todo remove this */
> +	pid_t tid_;		/**< Thread ID of the thread associated with this EGL context */
> +
> +	EGLDisplay display_;	/**< EGL display handle */
> +	EGLContext context_;	/**< EGL context handle */
> +	EGLSurface surface_;	/**< EGL sufrace handle */
> +	EGLSyncKHR sync_;	/**< EGL sync object for output sychonisation */
> +
> +	/**
> +	 * \brief Compile a shader of specified type
> +	 * \param[in] shaderType GL_VERTEX_SHADER or GL_FRAGMENT_SHADER
> +	 * \param[out] shaderId OpenGL shader object ID
> +	 * \param[in] shaderData Pointer to shader source code
> +	 * \param[in] shaderDataLen Length of shader source in bytes
> +	 * \param[in] shaderEnv Span of preprocessor definitions to prepend
> +	 *
> +	 * Internal helper function for shader compilation. Prepends environment
> +	 * definitions to the shader source and compiles the shader.
> +	 *
> +	 * \return 0 on success, or -EINVAL on compilation failure
> +	 */
> +	int compileShader(int shaderType, GLuint &shaderId, unsigned char *shaderData,
> +			  unsigned int shaderDataLen,
> +			  Span<const std::string> shaderEnv);
> +
> +	/**
> +	 * \brief Create a DMA-BUF backed 2D texture
> +	 * \param[in,out] eglImage EGL image to associate with the DMA-BUF
> +	 * \param[in] fd DMA-BUF file descriptor
> +	 * \param[in] output If true, create framebuffer for render target
> +	 *
> +	 * Internal implementation for creating DMA-BUF textures. Creates an EGL
> +	 * image from the DMA-BUF and binds it to a 2D texture. If output is true,
> +	 * also creates and attaches a framebuffer object.
> +	 *
> +	 * \return 0 on success, or -ENODEV on failure
> +	 */
> +	int createDMABufTexture2D(eGLImage *eglImage, int fd, bool output);
> +
> +	PFNEGLEXPORTDMABUFIMAGEMESAPROC eglExportDMABUFImageMESA;
> +	PFNGLEGLIMAGETARGETTEXTURE2DOESPROC glEGLImageTargetTexture2DOES;
> +
> +	PFNEGLCREATEIMAGEKHRPROC eglCreateImageKHR;
> +	PFNEGLDESTROYIMAGEKHRPROC eglDestroyImageKHR;
> +
> +	PFNEGLCREATESYNCKHRPROC eglCreateSyncKHR;
> +	PFNEGLDESTROYSYNCKHRPROC eglDestroySyncKHR;
> +	PFNEGLCLIENTWAITSYNCKHRPROC eglClientWaitSyncKHR;
> +};
> +} //namespace libcamera
> diff --git a/src/libcamera/egl.cpp b/src/libcamera/egl.cpp
> new file mode 100644
> index 000000000..52d6a6249
> --- /dev/null
> +++ b/src/libcamera/egl.cpp
> @@ -0,0 +1,436 @@
> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> +/*
> + * Copyright (C) 2024, Linaro Ltd.
> + *
> + * Authors:
> + * Bryan O'Donoghue <bryan.odonoghue@linaro.org>
> + *
> + * egl.cpp - Helper class for managing eGL interactions.
> + */
> +
> +#include "libcamera/base/thread.h"
> +#include "libcamera/internal/egl.h"
> +
> +#include <fcntl.h>
> +#include <sys/ioctl.h>
> +#include <sys/mman.h>
> +#include <unistd.h>
> +
> +#include <libdrm/drm_fourcc.h>
> +#include <linux/dma-buf.h>
> +#include <linux/dma-heap.h>
> +
> +namespace libcamera {
> +
> +LOG_DEFINE_CATEGORY(eGL)
> +
> +eGL::eGL()
> +{
> +	context_ = EGL_NO_CONTEXT;
> +	surface_ = EGL_NO_SURFACE;
> +	display_ = EGL_NO_DISPLAY;
> +}
> +
> +eGL::~eGL()
> +{
> +	if (context_ != EGL_NO_CONTEXT)
> +		eglDestroyContext(display_, context_);
> +
> +	if (surface_ != EGL_NO_SURFACE)
> +		eglDestroySurface(display_, surface_);
> +
> +}
> +
> +int eGL::syncOutput(void)
> +{
> +	ASSERT(tid_ == Thread::currentId());
> +
> +	glFlush();
> +	eglClientWaitSyncKHR(display_, sync_, 0, EGL_FOREVER_KHR);
> +
> +	return 0;
> +}
> +
> +// Create linear image attached to previous BO object
> +int eGL::createDMABufTexture2D(eGLImage *eglImage, int fd, bool output)
> +{
> +	int ret = 0;
> +
> +	ASSERT(tid_ == Thread::currentId());
> +
> +	// clang-format off
> +	EGLint image_attrs[] = {
> +		EGL_WIDTH, (EGLint)eglImage->width_,
> +		EGL_HEIGHT, (EGLint)eglImage->height_,
> +		EGL_LINUX_DRM_FOURCC_EXT, DRM_FORMAT_ARGB8888,
> +		EGL_DMA_BUF_PLANE0_FD_EXT, fd,
> +		EGL_DMA_BUF_PLANE0_OFFSET_EXT, 0,
> +		EGL_DMA_BUF_PLANE0_PITCH_EXT, (EGLint)eglImage->stride_,
> +		EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT, 0,
> +		EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT, 0,
> +		EGL_NONE,
> +	};
> +	// clang-format on
> +
> +	eglImage->image_ = eglCreateImageKHR(display_, EGL_NO_CONTEXT,
> +					     EGL_LINUX_DMA_BUF_EXT,
> +					     NULL, image_attrs);
> +
> +	if (eglImage->image_ == EGL_NO_IMAGE_KHR) {
> +		LOG(eGL, Error) << "eglCreateImageKHR fail";
> +		ret = -ENODEV;
> +		goto done;
> +	}
> +
> +	// Bind texture unit and texture
> +	glActiveTexture(eglImage->texture_unit_);
> +	glBindTexture(GL_TEXTURE_2D, eglImage->texture_);
> +
> +	// Generate texture with filter semantics
> +	glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, eglImage->image_);
> +
> +	// Nearest filtering
> +	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
> +	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
> +
> +	// Wrap to edge to avoid edge artifacts
> +	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
> +	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
> +
> +	if (output) {
> +		// Generate a framebuffer from our texture direct to dma-buf handle buffer
> +		glBindFramebuffer(GL_FRAMEBUFFER, eglImage->fbo_);
> +		glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, eglImage->texture_, 0);
> +
> +		GLenum err = glCheckFramebufferStatus(GL_FRAMEBUFFER);
> +		if (err!= GL_FRAMEBUFFER_COMPLETE) {
> +			LOG(eGL, Error) << "glFrameBufferTexture2D error " << err;
> +			eglDestroyImageKHR(display_, eglImage->image_);
> +			ret = -ENODEV;
> +			goto done;
> +		}
> +	}
> +done:
> +	return ret;
> +}
> +
> +int eGL::createInputDMABufTexture2D(eGLImage *eglImage, int fd)
> +{
> +	ASSERT(tid_ == Thread::currentId());
> +
> +	return createDMABufTexture2D(eglImage, fd, false);
> +}
> +int eGL::createOutputDMABufTexture2D(eGLImage *eglImage, int fd)
> +{
> +	ASSERT(tid_ == Thread::currentId());
> +
> +	return createDMABufTexture2D(eglImage, fd, true);
> +}
> +
> +void eGL::destroyDMABufTexture(eGLImage *eglImage)
> +{
> +	eglDestroyImage(display_, eglImage->image_);
> +}
> +
> +// Generate a 2D texture from an input buffer directly
> +void eGL::createTexture2D(eGLImage *eglImage, GLint format, uint32_t width, uint32_t height, void *data)
> +{
> +	ASSERT(tid_ == Thread::currentId());
> +
> +	glActiveTexture(eglImage->texture_unit_);
> +	glBindTexture(GL_TEXTURE_2D, eglImage->texture_);
> +
> +	// Generate texture, bind, associate image to texture, configure, unbind
> +	glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);
> +
> +	// Nearest filtering
> +	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
> +	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
> +
> +	// Wrap to edge to avoid edge artifacts
> +	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
> +	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
> +}
> +
> +int eGL::initEGLContext(GBM *gbmContext)
> +{
> +	EGLint configAttribs[] = {
> +		EGL_RED_SIZE, 8,
> +		EGL_GREEN_SIZE, 8,
> +		EGL_BLUE_SIZE, 8,
> +		EGL_ALPHA_SIZE, 8,
> +		EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
> +		EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
> +		EGL_NONE
> +	};
> +
> +	EGLint contextAttribs[] = {
> +		EGL_CONTEXT_MAJOR_VERSION, 2,
> +		EGL_NONE
> +	};
> +
> +	EGLint numConfigs;
> +	EGLConfig config;
> +	EGLint major;
> +	EGLint minor;
> +
> +	if (!eglBindAPI(EGL_OPENGL_ES_API)) {
> +		LOG(eGL, Error) << "API bind fail";
> +		goto fail;
> +	}
> +
> +	display_ = eglGetDisplay(gbmContext->getDevice());
> +	if (display_ == EGL_NO_DISPLAY) {
> +		LOG(eGL, Error) << "Unable to get EGL display";
> +		goto fail;
> +	}
> +
> +	if (eglInitialize(display_, &major, &minor) != EGL_TRUE) {
> +		LOG(eGL, Error) << "eglInitialize fail";
> +		goto fail;
> +	}
> +
> +	LOG(eGL, Info) << "EGL: version " << major << "." << minor;
> +	LOG(eGL, Info) << "EGL: EGL_VERSION: " << eglQueryString(display_, EGL_VERSION);
> +	LOG(eGL, Info) << "EGL: EGL_VENDOR: " << eglQueryString(display_, EGL_VENDOR);
> +	LOG(eGL, Info) << "EGL: EGL_CLIENT_APIS: " << eglQueryString(display_, EGL_CLIENT_APIS);
> +	LOG(eGL, Info) << "EGL: EGL_EXTENSIONS: " << eglQueryString(display_, EGL_EXTENSIONS);
> +
> +	eglCreateImageKHR = (PFNEGLCREATEIMAGEKHRPROC)eglGetProcAddress("eglCreateImageKHR");
> +	if (!eglCreateImageKHR) {
> +		LOG(eGL, Error) << "eglCreateImageKHR not found";
> +		goto fail;
> +	}
> +
> +	eglDestroyImageKHR = (PFNEGLDESTROYIMAGEKHRPROC)eglGetProcAddress("eglDestroyImageKHR");
> +	if (!eglDestroyImageKHR) {
> +		LOG(eGL, Error) << "eglDestroyImageKHR not found";
> +		goto fail;
> +	}
> +
> +	glEGLImageTargetTexture2DOES = (PFNGLEGLIMAGETARGETTEXTURE2DOESPROC)eglGetProcAddress("glEGLImageTargetTexture2DOES");
> +	if (!glEGLImageTargetTexture2DOES) {
> +		LOG(eGL, Error) << "glEGLImageTargetTexture2DOES not found";
> +		goto fail;
> +	}
> +
> +	eglCreateSyncKHR = (PFNEGLCREATESYNCKHRPROC)eglGetProcAddress("eglCreateSyncKHR");
> +	if (!eglCreateSyncKHR) {
> +		LOG(eGL, Error) << "eglCreateSyncKHR not found";
> +		goto fail;
> +	}
> +
> +	eglDestroySyncKHR = (PFNEGLDESTROYSYNCKHRPROC)eglGetProcAddress("eglDestroySyncKHR");
> +	if (!eglDestroySyncKHR) {
> +		LOG(eGL, Error) << "eglDestroySyncKHR not found";
> +		goto fail;
> +	}
> +
> +	eglClientWaitSyncKHR = (PFNEGLCLIENTWAITSYNCKHRPROC)eglGetProcAddress("eglClientWaitSyncKHR");
> +	if (!eglClientWaitSyncKHR) {
> +		LOG(eGL, Error) << "eglClientWaitSyncKHR not found";
> +		goto fail;
> +	}
> +
> +	if (eglChooseConfig(display_, configAttribs, &config, 1, &numConfigs) != EGL_TRUE) {
> +		LOG(eGL, Error) << "eglChooseConfig fail";
> +		goto fail;
> +	}
> +
> +	context_ = eglCreateContext(display_, config, EGL_NO_CONTEXT, contextAttribs);
> +	if (context_ == EGL_NO_CONTEXT) {
> +		LOG(eGL, Error) << "eglContext returned EGL_NO_CONTEXT";
> +		goto fail;
> +	}
> +
> +	tid_ = Thread::currentId();
> +
> +	makeCurrent();
> +
> +	sync_ = eglCreateSyncKHR(display_, EGL_SYNC_FENCE_KHR, NULL);

Could you please elaborate a bit on the purpose and handling of `sync_'?
I'm pretty ignorant about GL but I was exploring a problem with reported
per-frame times and I also observed some tearing with v4e and I've hit
this.

According to
https://registry.khronos.org/EGL/sdk/docs/man/html/eglCreateSync.xhtml
(it's sans-KHR but I suppose it's basically the same?):

  When a fence sync object is created, eglCreateSync also inserts a
  fence command into the command stream of the bound client API's
  current context (i.e., the context returned by eglGetCurrentContext),
  and associates it with the newly created sync object.

  The only condition supported for fence sync objects is
  EGL_SYNC_PRIOR_COMMANDS_COMPLETE, which is satisfied by completion of
  the fence command corresponding to the sync object, and all preceding
  commands in the associated client API context's command stream. The
  sync object will not be signaled until all effects from these commands
  on the client API's internal and framebuffer state are fully
  realized. No other state is affected by execution of the fence
  command.

As I understand it, calling eglCreateSyncKHR inserts a fence at a given
GPU processing place and then eglClientWaitSyncKHR waits for completion
of everything before the fence.

But here it seems that `sync_' is assigned only during configuration so
I don't get its purpose and the purpose of eglClientWaitSyncKHR call in
DebayerEGL::debayerGPU per each frame.  When I insert calls to
eglCreateSyncKHR + eglClientWaitSyncKHR + eglDestroySyncKHR all together
after glDrawArrays in DebayerEGL::debayerGPU, both my time measurement
problem and the image tearing disappear.  Which may possibly be just a
workaround for a different synchronization problem but then I still
don't understand what `sync_' is about.

> +	if (sync_ == EGL_NO_SYNC_KHR) {
> +		LOG(eGL, Error) << "eglCreateSyncKHR fail";
> +		goto fail;
> +	}
> +
> +	return 0;
> +fail:
> +
> +	return -ENODEV;
> +}
> +
> +void eGL::cleanUp(void)
> +{
> +	ASSERT(tid_ == Thread::currentId());
> +
> +	if (sync_) {
> +		makeCurrent();
> +		eglDestroySyncKHR(display_, sync_);
> +	}
> +
> +}
> +
> +void eGL::makeCurrent(void)
> +{
> +	ASSERT(tid_ == Thread::currentId());
> +
> +	if (eglMakeCurrent(display_, EGL_NO_SURFACE, EGL_NO_SURFACE, context_) != EGL_TRUE) {
> +		LOG(eGL, Error) << "eglMakeCurrent fail";
> +	}
> +}
> +
> +void eGL::useProgram(GLuint programId)
> +{
> +	ASSERT(tid_ == Thread::currentId());
> +
> +	glUseProgram(programId);
> +}
> +
> +void eGL::deleteProgram(GLuint programId)
> +{
> +	ASSERT(tid_ == Thread::currentId());
> +
> +	glDeleteProgram(programId);
> +}
> +
> +void eGL::pushEnv(std::vector<std::string> &shaderEnv, const char *str)
> +{
> +	std::string addStr = str;
> +
> +	addStr.push_back('\n');
> +	shaderEnv.push_back(std::move(addStr));
> +}
> +
> +int eGL::compileVertexShader(GLuint &shaderId, unsigned char *shaderData,
> +			     unsigned int shaderDataLen,
> +			     Span<const std::string> shaderEnv)
> +{
> +	return compileShader(GL_VERTEX_SHADER, shaderId, shaderData, shaderDataLen, shaderEnv);
> +}
> +
> +int eGL::compileFragmentShader(GLuint &shaderId, unsigned char *shaderData,
> +			       unsigned int shaderDataLen,
> +			       Span<const std::string> shaderEnv)
> +{
> +	return compileShader(GL_FRAGMENT_SHADER, shaderId, shaderData, shaderDataLen, shaderEnv);
> +}
> +
> +int eGL::compileShader(int shaderType, GLuint &shaderId, unsigned char *shaderData,
> +		       unsigned int shaderDataLen,
> +		       Span<const std::string> shaderEnv)
> +{
> +	GLint success;
> +	size_t i;
> +
> +	ASSERT(tid_ == Thread::currentId());
> +
> +	auto count = 1 + shaderEnv.size();
> +	auto shaderSourceData = std::make_unique<const GLchar*[]>(count);
> +	auto shaderDataLengths = std::make_unique<GLint[]>(count);
> +
> +	// Prefix defines before main body of shader
> +	for (i = 0; i < shaderEnv.size(); i++) {
> +		shaderSourceData[i] = shaderEnv[i].c_str();
> +		shaderDataLengths[i] = shaderEnv[i].length();
> +	}
> +
> +	// Now the main body of the shader program
> +	shaderSourceData[i] = reinterpret_cast<const GLchar*>(shaderData);
> +	shaderDataLengths[i] = shaderDataLen;
> +
> +	// And create the shader
> +	shaderId = glCreateShader(shaderType);
> +	glShaderSource(shaderId, count, shaderSourceData.get(), shaderDataLengths.get());
> +	glCompileShader(shaderId);
> +
> +	// Check status
> +	glGetShaderiv(shaderId, GL_COMPILE_STATUS, &success);
> +	if (success == GL_FALSE) {
> +		GLint sizeLog = 0;
> +
> +		glGetShaderiv(shaderId, GL_INFO_LOG_LENGTH, &sizeLog);
> +		auto infoLog = std::make_unique<GLchar[]>(sizeLog);
> +
> +		glGetShaderInfoLog(shaderId, sizeLog, &sizeLog, infoLog.get());
> +		LOG(eGL, Error) << infoLog.get();
> +	}
> +
> +	return (success == GL_TRUE) ? 0 : -EINVAL;
> +}
> +
> +void eGL::dumpShaderSource(GLuint shaderId)
> +{
> +	GLint shaderLength = 0;
> +
> +	ASSERT(tid_ == Thread::currentId());
> +
> +	glGetShaderiv(shaderId, GL_SHADER_SOURCE_LENGTH, &shaderLength);
> +
> +	LOG(eGL, Debug) << "Shader length is " << shaderLength;
> +
> +	if (shaderLength > 0) {
> +		auto shaderSource = std::make_unique<GLchar []>(shaderLength);
> +
> +		glGetShaderSource(shaderId, shaderLength, &shaderLength, shaderSource.get());
> +		if (shaderLength) {
> +			LOG(eGL, Debug) << "Shader source = " << shaderSource.get();
> +		}
> +	}
> +}
> +
> +int eGL::linkProgram(GLuint &programId, GLuint vertexshaderId, GLuint fragmentshaderId)
> +{
> +	GLint success;
> +	GLenum err;
> +
> +	ASSERT(tid_ == Thread::currentId());
> +
> +	programId = glCreateProgram();
> +	if (!programId)
> +		goto fail;
> +
> +	glAttachShader(programId, vertexshaderId);
> +	if ((err = glGetError()) != GL_NO_ERROR) {
> +		LOG(eGL, Error) << "Attach compute vertex shader fail";
> +		goto fail;
> +	}
> +
> +	glAttachShader(programId, fragmentshaderId);
> +	if ((err = glGetError()) != GL_NO_ERROR) {
> +		LOG(eGL, Error) << "Attach compute vertex shader fail";
> +		goto fail;
> +	}
> +
> +	glLinkProgram(programId);
> +	if ((err = glGetError()) != GL_NO_ERROR) {
> +		LOG(eGL, Error) << "Link program fail";
> +		goto fail;
> +	}
> +
> +	glDetachShader(programId, fragmentshaderId);
> +	glDetachShader(programId, vertexshaderId);
> +
> +	// Check status
> +	glGetProgramiv(programId, GL_LINK_STATUS, &success);
> +	if (success == GL_FALSE) {
> +		GLint sizeLog = 0;
> +		GLchar *infoLog;
> +
> +		glGetProgramiv(programId, GL_INFO_LOG_LENGTH, &sizeLog);
> +		infoLog = new GLchar[sizeLog];
> +
> +		glGetProgramInfoLog(programId, sizeLog, &sizeLog, infoLog);
> +		LOG(eGL, Error) << infoLog;
> +
> +		delete[] infoLog;
> +		goto fail;
> +	}
> +
> +	return 0;
> +fail:
> +	if (programId)
> +		glDeleteProgram(programId);
> +
> +	return -ENODEV;
> +}
> +} // namespace libcamera
> diff --git a/src/libcamera/meson.build b/src/libcamera/meson.build
> index fe60c8752..b7be38a8f 100644
> --- a/src/libcamera/meson.build
> +++ b/src/libcamera/meson.build
> @@ -79,6 +79,27 @@ if libgbm.found() and gbm_works
>      ])
>  endif
>  
> +libegl = cc.find_library('EGL', required : false)
> +libglesv2 = cc.find_library('GLESv2', required : false)
> +mesa_works = cc.check_header('EGL/egl.h', required: false)
> +
> +if libegl.found() and mesa_works
> +    config_h.set('HAVE_LIBEGL', 1)
> +endif
> +
> +if libglesv2.found() and mesa_works
> +    config_h.set('HAVE_GLESV2', 1)
> +endif
> +
> +if mesa_works and gbm_works
> +    libcamera_internal_sources += files([
> +        'egl.cpp',
> +    ])
> +    gles_headless_enabled = true
> +else
> +    gles_headless_enabled = false
> +endif
> +
>  subdir('base')
>  subdir('converter')
>  subdir('ipa')
> @@ -186,7 +207,9 @@ libcamera_deps += [
>      libcamera_base_private,
>      libcrypto,
>      libdl,
> +    libegl,
>      libgbm,
> +    libglesv2,
>      liblttng,
>      libudev,
>      libyaml,
Milan Zamazal Nov. 26, 2025, 12:49 p.m. UTC | #2
Milan Zamazal <mzamazal@redhat.com> writes:

> Hi Bryan,
>
> Bryan O'Donoghue <bryan.odonoghue@linaro.org> writes:
>
>> Introduce an eGL base helper class which provides an eGL context based on a
>> passed width and height.
>>
>> The initGLContext function could be overloaded to provide an interface to a
>> real display.
>>
>> A set of helper functions is provided to compile and link GLSL shaders.
>> linkShaderProgram currently compiles vertex/fragment pairs but could be
>> overloaded or passed a parameter to link a compute shader instead.
>>
>> Breaking the eGL interface away from debayering - allows to use the eGL
>> context inside of a dma-buf heap cleanly, reuse that context inside of a
>> debayer layer and conceivably reuse the context in a multi-stage shader
>> pass.
>>
>> Small note the image_attrs[] array doesn't pass checkstyle.py however the
>> elements of the array are in pairs.
>>
>> Acked-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
>> Signed-off-by: Bryan O'Donoghue <bryan.odonoghue@linaro.org>
>> ---
>>  include/libcamera/internal/egl.h | 412 +++++++++++++++++++++++++++++
>>  src/libcamera/egl.cpp            | 436 +++++++++++++++++++++++++++++++
>>  src/libcamera/meson.build        |  23 ++
>>  3 files changed, 871 insertions(+)
>>  create mode 100644 include/libcamera/internal/egl.h
>>  create mode 100644 src/libcamera/egl.cpp
>>
>> diff --git a/include/libcamera/internal/egl.h b/include/libcamera/internal/egl.h
>> new file mode 100644
>> index 000000000..e61a394af
>> --- /dev/null
>> +++ b/include/libcamera/internal/egl.h
>> @@ -0,0 +1,412 @@
>> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
>> +/*
>> + * Copyright (C) 2024, Linaro Ltd.
>> + *
>> + * Authors:
>> + * Bryan O'Donoghue <bryan.odonoghue@linaro.org>
>> + *
>> + * egl_context.cpp - Helper class for managing eGL interactions.
>> + */
>> +
>> +#pragma once
>> +
>> +#include <sys/types.h>
>> +#include <unistd.h>
>> +
>> +#include <libcamera/base/log.h>
>> +
>> +#include "libcamera/base/utils.h"
>> +#include "libcamera/internal/gbm.h"
>> +
>> +#define EGL_EGLEXT_PROTOTYPES
>> +#include <EGL/egl.h>
>> +#include <EGL/eglext.h>
>> +#define GL_GLEXT_PROTOTYPES
>> +#include <GLES2/gl2.h>
>> +#include <GLES2/gl2ext.h>
>> +
>> +namespace libcamera {
>> +
>> +LOG_DECLARE_CATEGORY(eGL)
>> +
>> +/**
>> + * \class eGLImage
>> + * \brief Helper class for managing EGL image resources
>> + *
>> + * The eGLImage class encapsulates OpenGL ES texture and framebuffer objects
>> + * along with their associated EGL image. It aggregates handles, descriptors,
>> + * and routines for managing textures that can be associated with shader
>> + * uniform IDs.
>> + *
>> + * This class is particularly useful for managing DMA-BUF backed textures
>> + * in zero-copy rendering pipelines, where textures are bound to specific
>> + * texture units and can be used as both input textures and render targets.
>> + */
>> +class eGLImage
>> +{
>> +public:
>> +	/**
>> +	 * \brief Construct an eGLImage with explicit stride
>> +	 * \param[in] width Image width in pixels
>> +	 * \param[in] height Image height in pixels
>> +	 * \param[in] bpp Bytes per pixel
>> +	 * \param[in] stride Row stride in bytes
>> +	 * \param[in] texture_unit OpenGL texture unit (e.g., GL_TEXTURE0)
>> +	 * \param[in] texture_unit_uniform_id Shader uniform ID for this texture unit
>> +	 *
>> +	 * Creates an eGLImage with the specified dimensions and stride. The stride
>> +	 * may differ from width * bpp due to alignment.
>> +	 */
>> + eGLImage(uint32_t width, uint32_t height, uint32_t bpp, uint32_t stride, GLenum texture_unit, uint32_t
>> texture_unit_uniform_id)
>> +	{
>> +		init(width, height, bpp, stride, texture_unit, texture_unit_uniform_id);
>> +	}
>> +
>> +	/**
>> +	 * \brief Construct an eGLImage with automatic stride calculation
>> +	 * \param[in] width Image width in pixels
>> +	 * \param[in] height Image height in pixels
>> +	 * \param[in] bpp Bytes per pixel
>> +	 * \param[in] texture_unit OpenGL texture unit (e.g., GL_TEXTURE0)
>> +	 * \param[in] texture_unit_uniform_id Shader uniform ID for this texture unit
>> +	 *
>> +	 * Creates an eGLImage with automatic stride calculation. The stride is
>> +	 * aligned to 256 bytes because 256 byte alignment is a common baseline alignment for GPUs.
>> +	 */
>> + eGLImage(uint32_t width, uint32_t height, uint32_t bpp, GLenum texture_unit, uint32_t
>> texture_unit_uniform_id)
>> +	{
>> +		uint32_t stride = libcamera::utils::alignUp(width * bpp, 256);
>> +
>> +		init(width, height, bpp, stride, texture_unit, texture_unit_uniform_id);
>> +	}
>> +
>> +	/**
>> +	 * \brief Destroy the eGLImage
>> +	 *
>> +	 * Cleans up OpenGL resources by deleting the framebuffer object and
>> +	 * texture.
>> +	 */
>> +	~eGLImage()
>> +	{
>> +		glDeleteFramebuffers(1, &fbo_);
>> +		glDeleteTextures(1, &texture_);
>> +	}
>> +
>> +	uint32_t width_;			/**< Image width in pixels */
>> +	uint32_t height_;			/**< Image height in pixels */
>> +	uint32_t stride_;			/**< Row stride in bytes */
>> +	uint32_t offset_;			/**< Buffer offset (reserved for future use) */
>> +	uint32_t framesize_;			/**< Total frame size in bytes (stride * height) */
>> +	uint32_t bpp_;				/**< Bytes per pixel */
>> +	uint32_t texture_unit_uniform_id_;	/**< Shader uniform id for texture unit */
>> + GLenum texture_unit_; /**< Texture unit associated with this image eg (GL_TEXTURE0) */
>> +	GLuint texture_;			/**< OpenGL texture object ID */
>> +	GLuint fbo_;				/**< OpenGL frame buffer object ID */
>> +	EGLImageKHR image_;			/**< EGL Image handle */
>> +
>> +private:
>> +	LIBCAMERA_DISABLE_COPY_AND_MOVE(eGLImage)
>> +
>> +	/**
>> +	 * \brief Initialise eGLImage state
>> +	 * \param[in] width Image width in pixels
>> +	 * \param[in] height Image height in pixels
>> +	 * \param[in] bpp Bytes per pixel
>> +	 * \param[in] stride Row stride in bytes
>> +	 * \param[in] texture_unit OpenGL texture unit
>> +	 * \param[in] texture_unit_uniform_id Shader uniform ID
>> +	 *
>> +	 * Common initialisation routine called by both constructors. Sets up
>> +	 * member variables and generates OpenGL texture and framebuffer objects.
>> +	 */
>> + void init(uint32_t width, uint32_t height, uint32_t bpp, uint32_t stride, GLenum texture_unit,
>> uint32_t texture_unit_uniform_id)
>> +	{
>> +		image_ = EGL_NO_IMAGE_KHR;
>> +		width_ = width;
>> +		height_ = height;
>> +		bpp_ = bpp;
>> +		stride_ = stride;
>> +		framesize_ = stride_ * height_;
>> +		texture_unit_ = texture_unit;
>> +		texture_unit_uniform_id_ = texture_unit_uniform_id;
>> +
>> +		glGenTextures(1, &texture_);
>> +		glGenFramebuffers(1, &fbo_);
>> +	}
>> +};
>> +
>> +/**
>> + * \class eGL
>> + * \brief Helper class for managing OpenGL ES operations
>> + *
>> + * It provides:
>> + *
>> + * - EGL context setup and management
>> + * - Extension function pointer retrieval
>> + * - Shader compilation and program linking
>> + * - DMA-BUF texture creation and management
>> + * - Synchronisation primitives
>> + *
>> + * This class is designed to work with zero-copy buffers via DMA-BUF file
>> + * descriptors.
>> + */
>> +class eGL
>> +{
>> +public:
>> +	/**
>> +	 * \brief Construct an EGL helper
>> +	 *
>> +	 * Creates an eGL instance with uninitialised context. Call initEGLContext()
>> +	 * to set up the EGL display, context, and load extension functions.
>> +	 */
>> +	eGL();
>> +
>> +	/**
>> +	 * \brief Destroy the EGL helper
>> +	 *
>> +	 * Destroys the EGL context and surface if they were successfully created.
>> +	 */
>> +	~eGL();
>> +
>> +	/**
>> +	 * \brief Initialise the EGL context
>> +	 * \param[in] gbmContext Pointer to initialised GBM context
>> +	 *
>> +	 * Sets up the EGL display from the GBM device, creates an OpenGL ES 2.0
>> +	 * context, and retrieves function pointers for required extensions
>> +	 * including:
>> +	 * - eglCreateImageKHR / eglDestroyImageKHR
>> +	 * - glEGLImageTargetTexture2DOES
>> +	 * - eglCreateSyncKHR / eglDestroySyncKHR / eglClientWaitSyncKHR
>> +	 *
>> +	 * \return 0 on success, or -ENODEV on failure
>> +	 */
>> +	int initEGLContext(GBM *gbmContext);
>> +
>> +	/**
>> +	 * \brief Clean up EGL resources
>> +	 *
>> +	 * Destroys the EGL sync object. Must be called from the same thread
>> +	 * that created the EGL context.
>> +	 */
>> +	void cleanUp();
>> +
>> +	/**
>> +	 * \brief Create an input DMA-BUF backed texture
>> +	 * \param[in,out] eglImage EGL image to associate with the DMA-BUF
>> +	 * \param[in] fd DMA-BUF file descriptor
>> +	 *
>> +	 * Creates an EGL image from a DMA-BUF file descriptor and binds it to
>> +	 * a 2D texture for use as an input texture in shaders. The texture is
>> +	 * configured with nearest filtering and clamp-to-edge wrapping.
>> +	 *
>> +	 * \return 0 on success, or -ENODEV on failure
>> +	 */
>> +	int createInputDMABufTexture2D(eGLImage *eglImage, int fd);
>> +
>> +	/**
>> +	 * \brief Create an output DMA-BUF backed texture
>> +	 * \param[in,out] eglImage EGL image to associate with the DMA-BUF
>> +	 * \param[in] fd DMA-BUF file descriptor
>> +	 *
>> +	 * Creates an EGL image from a DMA-BUF file descriptor and binds it to
>> +	 * a 2D texture, then attaches it to a framebuffer object for use as a
>> +	 * render target. This enables zero-copy rendering directly to the
>> +	 * DMA-BUF.
>> +	 *
>> +	 * \return 0 on success, or -ENODEV on failure
>> +	 */
>> +	int createOutputDMABufTexture2D(eGLImage *eglImage, int fd);
>> +
>> +	/**
>> +	 * \brief Destroy a DMA-BUF texture's EGL image
>> +	 * \param[in,out] eglImage EGL image to destroy
>> +	 *
>> +	 * Destroys the EGL image associated with a DMA-BUF texture. The OpenGL
>> +	 * texture and framebuffer objects are destroyed separately in the
>> +	 * eGLImage destructor.
>> +	 */
>> +	void destroyDMABufTexture(eGLImage *eglImage);
>> +
>> +	/**
>> +	 * \brief Create a 2D texture from a memory buffer
>> +	 * \param[in,out] eglImage EGL image to associate with the texture
>> +	 * \param[in] format OpenGL internal format (e.g., GL_RGB, GL_RGBA)
>> +	 * \param[in] width Texture width in pixels
>> +	 * \param[in] height Texture height in pixels
>> +	 * \param[in] data Pointer to pixel data, or nullptr for uninitialised texture
>> +	 *
>> +	 * Creates a 2D texture from a CPU-accessible memory buffer. The texture
>> +	 * is configured with nearest filtering and clamp-to-edge wrapping. This
>> +	 * is useful for uploading static data like lookup tables or uniform color
>> +	 * matrices to the GPU.
>> +	 */
>> + void createTexture2D(eGLImage *eglImage, GLint format, uint32_t width, uint32_t height, void *data);
>> +
>> +	/**
>> +	 * \brief Create a 1D texture from a memory buffer
>> +	 * \param[in,out] eglImage EGL image to associate with the texture
>> +	 * \param[in] format OpenGL internal format
>> +	 * \param[in] width Texture width in pixels
>> +	 * \param[in] data Pointer to pixel data
>> +	 *
>> +	 * Creates a 1D texture (implemented as a 2D texture with height=1) from
>> +	 * a CPU-accessible memory buffer. Useful for lookup tables in shaders.
>> +	 */
>> +	void createTexture1D(eGLImage *eglImage, GLint format, uint32_t width, void *data);
>> +
>> +	/**
>> +	 * \brief Add a preprocessor definition to shader environment
>> +	 * \param[in,out] shaderEnv Vector of shader environment strings
>> +	 * \param[in] str Preprocessor definition string (e.g., "#define APPLY_RGB_PARAMETERS")
>> +	 *
>> +	 * Appends a preprocessor definition to the shader environment vector.
>> +	 * These definitions are prepended to shader source code during compilation.
>> +	 */
>> +	void pushEnv(std::vector<std::string> &shaderEnv, const char *str);
>> +
>> +	/**
>> +	 * \brief Make the EGL context current for the calling thread
>> +	 *
>> +	 * Binds the EGL context to the current thread, allowing OpenGL ES
>> +	 * operations to be performed. Must be called from the thread that
>> +	 * will perform rendering operations.
>> +	 */
>> +	void makeCurrent();
>> +
>> +	/**
>> +	 * \brief Compile a vertex shader
>> +	 * \param[out] shaderId OpenGL shader object ID
>> +	 * \param[in] shaderData Pointer to shader source code
>> +	 * \param[in] shaderDataLen Length of shader source in bytes
>> +	 * \param[in] shaderEnv Span of preprocessor definitions to prepend
>> +	 *
>> +	 * Compiles a vertex shader from source code with optional preprocessor
>> +	 * definitions. On compilation failure, logs the shader info log.
>> +	 *
>> +	 * \return 0 on success, or -EINVAL on compilation failure
>> +	 */
>> +	int compileVertexShader(GLuint &shaderId, unsigned char *shaderData,
>> +				unsigned int shaderDataLen,
>> +				Span<const std::string> shaderEnv);
>> +
>> +	/**
>> +	 * \brief Compile a fragment shader
>> +	 * \param[out] shaderId OpenGL shader object ID
>> +	 * \param[in] shaderData Pointer to shader source code
>> +	 * \param[in] shaderDataLen Length of shader source in bytes
>> +	 * \param[in] shaderEnv Span of preprocessor definitions to prepend
>> +	 *
>> +	 * Compiles a fragment shader from source code with optional preprocessor
>> +	 * definitions. On compilation failure, logs the shader info log.
>> +	 *
>> +	 * \return 0 on success, or -EINVAL on compilation failure
>> +	 */
>> +	int compileFragmentShader(GLuint &shaderId, unsigned char *shaderData,
>> +				  unsigned int shaderDataLen,
>> +				  Span<const std::string> shaderEnv);
>> +
>> +	/**
>> +	 * \brief Link a shader program
>> +	 * \param[out] programId OpenGL program object ID
>> +	 * \param[in] fragmentshaderId Compiled fragment shader ID
>> +	 * \param[in] vertexshaderId Compiled vertex shader ID
>> +	 *
>> +	 * Links vertex and fragment shaders into an executable shader program.
>> +	 * On link failure, logs the program info log and deletes the program.
>> +	 *
>> +	 * \return 0 on success, or -ENODEV on link failure
>> +	 */
>> +	int linkProgram(GLuint &programId, GLuint fragmentshaderId, GLuint vertexshaderId);
>> +
>> +	/**
>> +	 * \brief Dump shader source code to the log
>> +	 * \param[in] shaderId OpenGL shader object ID
>> +	 *
>> +	 * Retrieves and logs the complete source code of a compiled shader.
>> +	 * Useful for debugging shader compilation issues.
>> +	 */
>> +	void dumpShaderSource(GLuint shaderId);
>> +
>> +	/**
>> +	 * \brief Activate a shader program for rendering
>> +	 * \param[in] programId OpenGL program object ID
>> +	 *
>> +	 * Sets the specified program as the current rendering program. All
>> +	 * subsequent draw calls will use this program's shaders.
>> +	 */
>> +	void useProgram(GLuint programId);
>> +
>> +	/**
>> +	 * \brief Delete a shader program
>> +	 * \param[in] programId OpenGL program object ID
>> +	 *
>> +	 * Deletes a shader program and frees associated resources. The program
>> +	 * must not be currently in use.
>> +	 */
>> +	void deleteProgram(GLuint programId);
>> +
>> +	/**
>> +	 * \brief Synchronise rendering output
>> +	 *
>> +	 * Flushes OpenGL commands and waits for rendering to complete using an
>> +	 * EGL fence sync object. This ensures all rendering operations have
>> +	 * finished before the CPU accesses the output buffers.
>> +	 *
>> +	 * \return 0 on success
>> +	 */
>> +	int syncOutput();
>> +
>> +private:
>> +	LIBCAMERA_DISABLE_COPY_AND_MOVE(eGL)
>> +
>> +	int fd_;		/**< File descriptor \todo remove this */
>> +	pid_t tid_;		/**< Thread ID of the thread associated with this EGL context */
>> +
>> +	EGLDisplay display_;	/**< EGL display handle */
>> +	EGLContext context_;	/**< EGL context handle */
>> +	EGLSurface surface_;	/**< EGL sufrace handle */
>> +	EGLSyncKHR sync_;	/**< EGL sync object for output sychonisation */
>> +
>> +	/**
>> +	 * \brief Compile a shader of specified type
>> +	 * \param[in] shaderType GL_VERTEX_SHADER or GL_FRAGMENT_SHADER
>> +	 * \param[out] shaderId OpenGL shader object ID
>> +	 * \param[in] shaderData Pointer to shader source code
>> +	 * \param[in] shaderDataLen Length of shader source in bytes
>> +	 * \param[in] shaderEnv Span of preprocessor definitions to prepend
>> +	 *
>> +	 * Internal helper function for shader compilation. Prepends environment
>> +	 * definitions to the shader source and compiles the shader.
>> +	 *
>> +	 * \return 0 on success, or -EINVAL on compilation failure
>> +	 */
>> +	int compileShader(int shaderType, GLuint &shaderId, unsigned char *shaderData,
>> +			  unsigned int shaderDataLen,
>> +			  Span<const std::string> shaderEnv);
>> +
>> +	/**
>> +	 * \brief Create a DMA-BUF backed 2D texture
>> +	 * \param[in,out] eglImage EGL image to associate with the DMA-BUF
>> +	 * \param[in] fd DMA-BUF file descriptor
>> +	 * \param[in] output If true, create framebuffer for render target
>> +	 *
>> +	 * Internal implementation for creating DMA-BUF textures. Creates an EGL
>> +	 * image from the DMA-BUF and binds it to a 2D texture. If output is true,
>> +	 * also creates and attaches a framebuffer object.
>> +	 *
>> +	 * \return 0 on success, or -ENODEV on failure
>> +	 */
>> +	int createDMABufTexture2D(eGLImage *eglImage, int fd, bool output);
>> +
>> +	PFNEGLEXPORTDMABUFIMAGEMESAPROC eglExportDMABUFImageMESA;
>> +	PFNGLEGLIMAGETARGETTEXTURE2DOESPROC glEGLImageTargetTexture2DOES;
>> +
>> +	PFNEGLCREATEIMAGEKHRPROC eglCreateImageKHR;
>> +	PFNEGLDESTROYIMAGEKHRPROC eglDestroyImageKHR;
>> +
>> +	PFNEGLCREATESYNCKHRPROC eglCreateSyncKHR;
>> +	PFNEGLDESTROYSYNCKHRPROC eglDestroySyncKHR;
>> +	PFNEGLCLIENTWAITSYNCKHRPROC eglClientWaitSyncKHR;
>> +};
>> +} //namespace libcamera
>> diff --git a/src/libcamera/egl.cpp b/src/libcamera/egl.cpp
>> new file mode 100644
>> index 000000000..52d6a6249
>> --- /dev/null
>> +++ b/src/libcamera/egl.cpp
>> @@ -0,0 +1,436 @@
>> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
>> +/*
>> + * Copyright (C) 2024, Linaro Ltd.
>> + *
>> + * Authors:
>> + * Bryan O'Donoghue <bryan.odonoghue@linaro.org>
>> + *
>> + * egl.cpp - Helper class for managing eGL interactions.
>> + */
>> +
>> +#include "libcamera/base/thread.h"
>> +#include "libcamera/internal/egl.h"
>> +
>> +#include <fcntl.h>
>> +#include <sys/ioctl.h>
>> +#include <sys/mman.h>
>> +#include <unistd.h>
>> +
>> +#include <libdrm/drm_fourcc.h>
>> +#include <linux/dma-buf.h>
>> +#include <linux/dma-heap.h>
>> +
>> +namespace libcamera {
>> +
>> +LOG_DEFINE_CATEGORY(eGL)
>> +
>> +eGL::eGL()
>> +{
>> +	context_ = EGL_NO_CONTEXT;
>> +	surface_ = EGL_NO_SURFACE;
>> +	display_ = EGL_NO_DISPLAY;
>> +}
>> +
>> +eGL::~eGL()
>> +{
>> +	if (context_ != EGL_NO_CONTEXT)
>> +		eglDestroyContext(display_, context_);
>> +
>> +	if (surface_ != EGL_NO_SURFACE)
>> +		eglDestroySurface(display_, surface_);
>> +
>> +}
>> +
>> +int eGL::syncOutput(void)
>> +{
>> +	ASSERT(tid_ == Thread::currentId());
>> +
>> +	glFlush();
>> +	eglClientWaitSyncKHR(display_, sync_, 0, EGL_FOREVER_KHR);
>> +
>> +	return 0;
>> +}
>> +
>> +// Create linear image attached to previous BO object
>> +int eGL::createDMABufTexture2D(eGLImage *eglImage, int fd, bool output)
>> +{
>> +	int ret = 0;
>> +
>> +	ASSERT(tid_ == Thread::currentId());
>> +
>> +	// clang-format off
>> +	EGLint image_attrs[] = {
>> +		EGL_WIDTH, (EGLint)eglImage->width_,
>> +		EGL_HEIGHT, (EGLint)eglImage->height_,
>> +		EGL_LINUX_DRM_FOURCC_EXT, DRM_FORMAT_ARGB8888,
>> +		EGL_DMA_BUF_PLANE0_FD_EXT, fd,
>> +		EGL_DMA_BUF_PLANE0_OFFSET_EXT, 0,
>> +		EGL_DMA_BUF_PLANE0_PITCH_EXT, (EGLint)eglImage->stride_,
>> +		EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT, 0,
>> +		EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT, 0,
>> +		EGL_NONE,
>> +	};
>> +	// clang-format on
>> +
>> +	eglImage->image_ = eglCreateImageKHR(display_, EGL_NO_CONTEXT,
>> +					     EGL_LINUX_DMA_BUF_EXT,
>> +					     NULL, image_attrs);
>> +
>> +	if (eglImage->image_ == EGL_NO_IMAGE_KHR) {
>> +		LOG(eGL, Error) << "eglCreateImageKHR fail";
>> +		ret = -ENODEV;
>> +		goto done;
>> +	}
>> +
>> +	// Bind texture unit and texture
>> +	glActiveTexture(eglImage->texture_unit_);
>> +	glBindTexture(GL_TEXTURE_2D, eglImage->texture_);
>> +
>> +	// Generate texture with filter semantics
>> +	glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, eglImage->image_);
>> +
>> +	// Nearest filtering
>> +	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
>> +	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
>> +
>> +	// Wrap to edge to avoid edge artifacts
>> +	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
>> +	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
>> +
>> +	if (output) {
>> +		// Generate a framebuffer from our texture direct to dma-buf handle buffer
>> +		glBindFramebuffer(GL_FRAMEBUFFER, eglImage->fbo_);
>> + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, eglImage->texture_, 0);
>> +
>> +		GLenum err = glCheckFramebufferStatus(GL_FRAMEBUFFER);
>> +		if (err!= GL_FRAMEBUFFER_COMPLETE) {
>> +			LOG(eGL, Error) << "glFrameBufferTexture2D error " << err;
>> +			eglDestroyImageKHR(display_, eglImage->image_);
>> +			ret = -ENODEV;
>> +			goto done;
>> +		}
>> +	}
>> +done:
>> +	return ret;
>> +}
>> +
>> +int eGL::createInputDMABufTexture2D(eGLImage *eglImage, int fd)
>> +{
>> +	ASSERT(tid_ == Thread::currentId());
>> +
>> +	return createDMABufTexture2D(eglImage, fd, false);
>> +}
>> +int eGL::createOutputDMABufTexture2D(eGLImage *eglImage, int fd)
>> +{
>> +	ASSERT(tid_ == Thread::currentId());
>> +
>> +	return createDMABufTexture2D(eglImage, fd, true);
>> +}
>> +
>> +void eGL::destroyDMABufTexture(eGLImage *eglImage)
>> +{
>> +	eglDestroyImage(display_, eglImage->image_);
>> +}
>> +
>> +// Generate a 2D texture from an input buffer directly
>> +void eGL::createTexture2D(eGLImage *eglImage, GLint format, uint32_t width, uint32_t height, void
>> *data)
>> +{
>> +	ASSERT(tid_ == Thread::currentId());
>> +
>> +	glActiveTexture(eglImage->texture_unit_);
>> +	glBindTexture(GL_TEXTURE_2D, eglImage->texture_);
>> +
>> +	// Generate texture, bind, associate image to texture, configure, unbind
>> +	glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);
>> +
>> +	// Nearest filtering
>> +	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
>> +	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
>> +
>> +	// Wrap to edge to avoid edge artifacts
>> +	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
>> +	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
>> +}
>> +
>> +int eGL::initEGLContext(GBM *gbmContext)
>> +{
>> +	EGLint configAttribs[] = {
>> +		EGL_RED_SIZE, 8,
>> +		EGL_GREEN_SIZE, 8,
>> +		EGL_BLUE_SIZE, 8,
>> +		EGL_ALPHA_SIZE, 8,
>> +		EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
>> +		EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
>> +		EGL_NONE
>> +	};
>> +
>> +	EGLint contextAttribs[] = {
>> +		EGL_CONTEXT_MAJOR_VERSION, 2,
>> +		EGL_NONE
>> +	};
>> +
>> +	EGLint numConfigs;
>> +	EGLConfig config;
>> +	EGLint major;
>> +	EGLint minor;
>> +
>> +	if (!eglBindAPI(EGL_OPENGL_ES_API)) {
>> +		LOG(eGL, Error) << "API bind fail";
>> +		goto fail;
>> +	}
>> +
>> +	display_ = eglGetDisplay(gbmContext->getDevice());
>> +	if (display_ == EGL_NO_DISPLAY) {
>> +		LOG(eGL, Error) << "Unable to get EGL display";
>> +		goto fail;
>> +	}
>> +
>> +	if (eglInitialize(display_, &major, &minor) != EGL_TRUE) {
>> +		LOG(eGL, Error) << "eglInitialize fail";
>> +		goto fail;
>> +	}
>> +
>> +	LOG(eGL, Info) << "EGL: version " << major << "." << minor;
>> +	LOG(eGL, Info) << "EGL: EGL_VERSION: " << eglQueryString(display_, EGL_VERSION);
>> +	LOG(eGL, Info) << "EGL: EGL_VENDOR: " << eglQueryString(display_, EGL_VENDOR);
>> +	LOG(eGL, Info) << "EGL: EGL_CLIENT_APIS: " << eglQueryString(display_, EGL_CLIENT_APIS);
>> +	LOG(eGL, Info) << "EGL: EGL_EXTENSIONS: " << eglQueryString(display_, EGL_EXTENSIONS);
>> +
>> +	eglCreateImageKHR = (PFNEGLCREATEIMAGEKHRPROC)eglGetProcAddress("eglCreateImageKHR");
>> +	if (!eglCreateImageKHR) {
>> +		LOG(eGL, Error) << "eglCreateImageKHR not found";
>> +		goto fail;
>> +	}
>> +
>> +	eglDestroyImageKHR = (PFNEGLDESTROYIMAGEKHRPROC)eglGetProcAddress("eglDestroyImageKHR");
>> +	if (!eglDestroyImageKHR) {
>> +		LOG(eGL, Error) << "eglDestroyImageKHR not found";
>> +		goto fail;
>> +	}
>> +
>> + glEGLImageTargetTexture2DOES =
>> (PFNGLEGLIMAGETARGETTEXTURE2DOESPROC)eglGetProcAddress("glEGLImageTargetTexture2DOES");
>> +	if (!glEGLImageTargetTexture2DOES) {
>> +		LOG(eGL, Error) << "glEGLImageTargetTexture2DOES not found";
>> +		goto fail;
>> +	}
>> +
>> +	eglCreateSyncKHR = (PFNEGLCREATESYNCKHRPROC)eglGetProcAddress("eglCreateSyncKHR");
>> +	if (!eglCreateSyncKHR) {
>> +		LOG(eGL, Error) << "eglCreateSyncKHR not found";
>> +		goto fail;
>> +	}
>> +
>> +	eglDestroySyncKHR = (PFNEGLDESTROYSYNCKHRPROC)eglGetProcAddress("eglDestroySyncKHR");
>> +	if (!eglDestroySyncKHR) {
>> +		LOG(eGL, Error) << "eglDestroySyncKHR not found";
>> +		goto fail;
>> +	}
>> +
>> +	eglClientWaitSyncKHR = (PFNEGLCLIENTWAITSYNCKHRPROC)eglGetProcAddress("eglClientWaitSyncKHR");
>> +	if (!eglClientWaitSyncKHR) {
>> +		LOG(eGL, Error) << "eglClientWaitSyncKHR not found";
>> +		goto fail;
>> +	}
>> +
>> +	if (eglChooseConfig(display_, configAttribs, &config, 1, &numConfigs) != EGL_TRUE) {
>> +		LOG(eGL, Error) << "eglChooseConfig fail";
>> +		goto fail;
>> +	}
>> +
>> +	context_ = eglCreateContext(display_, config, EGL_NO_CONTEXT, contextAttribs);
>> +	if (context_ == EGL_NO_CONTEXT) {
>> +		LOG(eGL, Error) << "eglContext returned EGL_NO_CONTEXT";
>> +		goto fail;
>> +	}
>> +
>> +	tid_ = Thread::currentId();
>> +
>> +	makeCurrent();
>> +
>> +	sync_ = eglCreateSyncKHR(display_, EGL_SYNC_FENCE_KHR, NULL);
>
> Could you please elaborate a bit on the purpose and handling of `sync_'?
> I'm pretty ignorant about GL but I was exploring a problem with reported
> per-frame times and I also observed some tearing with v4e and I've hit
> this.
>
> According to
> https://registry.khronos.org/EGL/sdk/docs/man/html/eglCreateSync.xhtml
> (it's sans-KHR but I suppose it's basically the same?):
>
>   When a fence sync object is created, eglCreateSync also inserts a
>   fence command into the command stream of the bound client API's
>   current context (i.e., the context returned by eglGetCurrentContext),
>   and associates it with the newly created sync object.
>
>   The only condition supported for fence sync objects is
>   EGL_SYNC_PRIOR_COMMANDS_COMPLETE, which is satisfied by completion of
>   the fence command corresponding to the sync object, and all preceding
>   commands in the associated client API context's command stream. The
>   sync object will not be signaled until all effects from these commands
>   on the client API's internal and framebuffer state are fully
>   realized. No other state is affected by execution of the fence
>   command.
>
> As I understand it, calling eglCreateSyncKHR inserts a fence at a given
> GPU processing place and then eglClientWaitSyncKHR waits for completion
> of everything before the fence.
>
> But here it seems that `sync_' is assigned only during configuration so
> I don't get its purpose and the purpose of eglClientWaitSyncKHR call in
> DebayerEGL::debayerGPU per each frame.  When I insert calls to
> eglCreateSyncKHR + eglClientWaitSyncKHR + eglDestroySyncKHR all together
> after glDrawArrays in DebayerEGL::debayerGPU, both my time measurement
> problem and the image tearing disappear.  Which may possibly be just a
> workaround for a different synchronization problem but then I still
> don't understand what `sync_' is about.

As discussed at the meeting and suggested by Robert, I tested replacing
sync_ use with glFinish call.  It works for me.  The corresponding patch
is available here:
https://gitlab.freedesktop.org/camera/libcamera-softisp/-/commits/pdm-gpu-sync?ref_type=heads

There are three additional patches on top of it, fixing various formal
issues.  If possible, please use them too.

>> +	if (sync_ == EGL_NO_SYNC_KHR) {
>> +		LOG(eGL, Error) << "eglCreateSyncKHR fail";
>> +		goto fail;
>> +	}
>> +
>> +	return 0;
>> +fail:
>> +
>> +	return -ENODEV;
>> +}
>> +
>> +void eGL::cleanUp(void)
>> +{
>> +	ASSERT(tid_ == Thread::currentId());
>> +
>> +	if (sync_) {
>> +		makeCurrent();
>> +		eglDestroySyncKHR(display_, sync_);
>> +	}
>> +
>> +}
>> +
>> +void eGL::makeCurrent(void)
>> +{
>> +	ASSERT(tid_ == Thread::currentId());
>> +
>> +	if (eglMakeCurrent(display_, EGL_NO_SURFACE, EGL_NO_SURFACE, context_) != EGL_TRUE) {
>> +		LOG(eGL, Error) << "eglMakeCurrent fail";
>> +	}
>> +}
>> +
>> +void eGL::useProgram(GLuint programId)
>> +{
>> +	ASSERT(tid_ == Thread::currentId());
>> +
>> +	glUseProgram(programId);
>> +}
>> +
>> +void eGL::deleteProgram(GLuint programId)
>> +{
>> +	ASSERT(tid_ == Thread::currentId());
>> +
>> +	glDeleteProgram(programId);
>> +}
>> +
>> +void eGL::pushEnv(std::vector<std::string> &shaderEnv, const char *str)
>> +{
>> +	std::string addStr = str;
>> +
>> +	addStr.push_back('\n');
>> +	shaderEnv.push_back(std::move(addStr));
>> +}
>> +
>> +int eGL::compileVertexShader(GLuint &shaderId, unsigned char *shaderData,
>> +			     unsigned int shaderDataLen,
>> +			     Span<const std::string> shaderEnv)
>> +{
>> +	return compileShader(GL_VERTEX_SHADER, shaderId, shaderData, shaderDataLen, shaderEnv);
>> +}
>> +
>> +int eGL::compileFragmentShader(GLuint &shaderId, unsigned char *shaderData,
>> +			       unsigned int shaderDataLen,
>> +			       Span<const std::string> shaderEnv)
>> +{
>> +	return compileShader(GL_FRAGMENT_SHADER, shaderId, shaderData, shaderDataLen, shaderEnv);
>> +}
>> +
>> +int eGL::compileShader(int shaderType, GLuint &shaderId, unsigned char *shaderData,
>> +		       unsigned int shaderDataLen,
>> +		       Span<const std::string> shaderEnv)
>> +{
>> +	GLint success;
>> +	size_t i;
>> +
>> +	ASSERT(tid_ == Thread::currentId());
>> +
>> +	auto count = 1 + shaderEnv.size();
>> +	auto shaderSourceData = std::make_unique<const GLchar*[]>(count);
>> +	auto shaderDataLengths = std::make_unique<GLint[]>(count);
>> +
>> +	// Prefix defines before main body of shader
>> +	for (i = 0; i < shaderEnv.size(); i++) {
>> +		shaderSourceData[i] = shaderEnv[i].c_str();
>> +		shaderDataLengths[i] = shaderEnv[i].length();
>> +	}
>> +
>> +	// Now the main body of the shader program
>> +	shaderSourceData[i] = reinterpret_cast<const GLchar*>(shaderData);
>> +	shaderDataLengths[i] = shaderDataLen;
>> +
>> +	// And create the shader
>> +	shaderId = glCreateShader(shaderType);
>> +	glShaderSource(shaderId, count, shaderSourceData.get(), shaderDataLengths.get());
>> +	glCompileShader(shaderId);
>> +
>> +	// Check status
>> +	glGetShaderiv(shaderId, GL_COMPILE_STATUS, &success);
>> +	if (success == GL_FALSE) {
>> +		GLint sizeLog = 0;
>> +
>> +		glGetShaderiv(shaderId, GL_INFO_LOG_LENGTH, &sizeLog);
>> +		auto infoLog = std::make_unique<GLchar[]>(sizeLog);
>> +
>> +		glGetShaderInfoLog(shaderId, sizeLog, &sizeLog, infoLog.get());
>> +		LOG(eGL, Error) << infoLog.get();
>> +	}
>> +
>> +	return (success == GL_TRUE) ? 0 : -EINVAL;
>> +}
>> +
>> +void eGL::dumpShaderSource(GLuint shaderId)
>> +{
>> +	GLint shaderLength = 0;
>> +
>> +	ASSERT(tid_ == Thread::currentId());
>> +
>> +	glGetShaderiv(shaderId, GL_SHADER_SOURCE_LENGTH, &shaderLength);
>> +
>> +	LOG(eGL, Debug) << "Shader length is " << shaderLength;
>> +
>> +	if (shaderLength > 0) {
>> +		auto shaderSource = std::make_unique<GLchar []>(shaderLength);
>> +
>> +		glGetShaderSource(shaderId, shaderLength, &shaderLength, shaderSource.get());
>> +		if (shaderLength) {
>> +			LOG(eGL, Debug) << "Shader source = " << shaderSource.get();
>> +		}
>> +	}
>> +}
>> +
>> +int eGL::linkProgram(GLuint &programId, GLuint vertexshaderId, GLuint fragmentshaderId)
>> +{
>> +	GLint success;
>> +	GLenum err;
>> +
>> +	ASSERT(tid_ == Thread::currentId());
>> +
>> +	programId = glCreateProgram();
>> +	if (!programId)
>> +		goto fail;
>> +
>> +	glAttachShader(programId, vertexshaderId);
>> +	if ((err = glGetError()) != GL_NO_ERROR) {
>> +		LOG(eGL, Error) << "Attach compute vertex shader fail";
>> +		goto fail;
>> +	}
>> +
>> +	glAttachShader(programId, fragmentshaderId);
>> +	if ((err = glGetError()) != GL_NO_ERROR) {
>> +		LOG(eGL, Error) << "Attach compute vertex shader fail";
>> +		goto fail;
>> +	}
>> +
>> +	glLinkProgram(programId);
>> +	if ((err = glGetError()) != GL_NO_ERROR) {
>> +		LOG(eGL, Error) << "Link program fail";
>> +		goto fail;
>> +	}
>> +
>> +	glDetachShader(programId, fragmentshaderId);
>> +	glDetachShader(programId, vertexshaderId);
>> +
>> +	// Check status
>> +	glGetProgramiv(programId, GL_LINK_STATUS, &success);
>> +	if (success == GL_FALSE) {
>> +		GLint sizeLog = 0;
>> +		GLchar *infoLog;
>> +
>> +		glGetProgramiv(programId, GL_INFO_LOG_LENGTH, &sizeLog);
>> +		infoLog = new GLchar[sizeLog];
>> +
>> +		glGetProgramInfoLog(programId, sizeLog, &sizeLog, infoLog);
>> +		LOG(eGL, Error) << infoLog;
>> +
>> +		delete[] infoLog;
>> +		goto fail;
>> +	}
>> +
>> +	return 0;
>> +fail:
>> +	if (programId)
>> +		glDeleteProgram(programId);
>> +
>> +	return -ENODEV;
>> +}
>> +} // namespace libcamera
>> diff --git a/src/libcamera/meson.build b/src/libcamera/meson.build
>> index fe60c8752..b7be38a8f 100644
>> --- a/src/libcamera/meson.build
>> +++ b/src/libcamera/meson.build
>> @@ -79,6 +79,27 @@ if libgbm.found() and gbm_works
>>      ])
>>  endif
>>  
>> +libegl = cc.find_library('EGL', required : false)
>> +libglesv2 = cc.find_library('GLESv2', required : false)
>> +mesa_works = cc.check_header('EGL/egl.h', required: false)
>> +
>> +if libegl.found() and mesa_works
>> +    config_h.set('HAVE_LIBEGL', 1)
>> +endif
>> +
>> +if libglesv2.found() and mesa_works
>> +    config_h.set('HAVE_GLESV2', 1)
>> +endif
>> +
>> +if mesa_works and gbm_works
>> +    libcamera_internal_sources += files([
>> +        'egl.cpp',
>> +    ])
>> +    gles_headless_enabled = true
>> +else
>> +    gles_headless_enabled = false
>> +endif
>> +
>>  subdir('base')
>>  subdir('converter')
>>  subdir('ipa')
>> @@ -186,7 +207,9 @@ libcamera_deps += [
>>      libcamera_base_private,
>>      libcrypto,
>>      libdl,
>> +    libegl,
>>      libgbm,
>> +    libglesv2,
>>      liblttng,
>>      libudev,
>>      libyaml,
Bryan O'Donoghue Nov. 27, 2025, 1:46 a.m. UTC | #3
On 26/11/2025 12:49, Milan Zamazal wrote:
> Milan Zamazal <mzamazal@redhat.com> writes:
> 
>> Hi Bryan,
>>
>> Bryan O'Donoghue <bryan.odonoghue@linaro.org> writes:
>>
>>> Introduce an eGL base helper class which provides an eGL context based on a
>>> passed width and height.
>>>
>>> The initGLContext function could be overloaded to provide an interface to a
>>> real display.
>>>
>>> A set of helper functions is provided to compile and link GLSL shaders.
>>> linkShaderProgram currently compiles vertex/fragment pairs but could be
>>> overloaded or passed a parameter to link a compute shader instead.
>>>
>>> Breaking the eGL interface away from debayering - allows to use the eGL
>>> context inside of a dma-buf heap cleanly, reuse that context inside of a
>>> debayer layer and conceivably reuse the context in a multi-stage shader
>>> pass.
>>>
>>> Small note the image_attrs[] array doesn't pass checkstyle.py however the
>>> elements of the array are in pairs.
>>>
>>> Acked-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
>>> Signed-off-by: Bryan O'Donoghue <bryan.odonoghue@linaro.org>
>>> ---
>>>   include/libcamera/internal/egl.h | 412 +++++++++++++++++++++++++++++
>>>   src/libcamera/egl.cpp            | 436 +++++++++++++++++++++++++++++++
>>>   src/libcamera/meson.build        |  23 ++
>>>   3 files changed, 871 insertions(+)
>>>   create mode 100644 include/libcamera/internal/egl.h
>>>   create mode 100644 src/libcamera/egl.cpp
>>>
>>> diff --git a/include/libcamera/internal/egl.h b/include/libcamera/internal/egl.h
>>> new file mode 100644
>>> index 000000000..e61a394af
>>> --- /dev/null
>>> +++ b/include/libcamera/internal/egl.h
>>> @@ -0,0 +1,412 @@
>>> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
>>> +/*
>>> + * Copyright (C) 2024, Linaro Ltd.
>>> + *
>>> + * Authors:
>>> + * Bryan O'Donoghue <bryan.odonoghue@linaro.org>
>>> + *
>>> + * egl_context.cpp - Helper class for managing eGL interactions.
>>> + */
>>> +
>>> +#pragma once
>>> +
>>> +#include <sys/types.h>
>>> +#include <unistd.h>
>>> +
>>> +#include <libcamera/base/log.h>
>>> +
>>> +#include "libcamera/base/utils.h"
>>> +#include "libcamera/internal/gbm.h"
>>> +
>>> +#define EGL_EGLEXT_PROTOTYPES
>>> +#include <EGL/egl.h>
>>> +#include <EGL/eglext.h>
>>> +#define GL_GLEXT_PROTOTYPES
>>> +#include <GLES2/gl2.h>
>>> +#include <GLES2/gl2ext.h>
>>> +
>>> +namespace libcamera {
>>> +
>>> +LOG_DECLARE_CATEGORY(eGL)
>>> +
>>> +/**
>>> + * \class eGLImage
>>> + * \brief Helper class for managing EGL image resources
>>> + *
>>> + * The eGLImage class encapsulates OpenGL ES texture and framebuffer objects
>>> + * along with their associated EGL image. It aggregates handles, descriptors,
>>> + * and routines for managing textures that can be associated with shader
>>> + * uniform IDs.
>>> + *
>>> + * This class is particularly useful for managing DMA-BUF backed textures
>>> + * in zero-copy rendering pipelines, where textures are bound to specific
>>> + * texture units and can be used as both input textures and render targets.
>>> + */
>>> +class eGLImage
>>> +{
>>> +public:
>>> +	/**
>>> +	 * \brief Construct an eGLImage with explicit stride
>>> +	 * \param[in] width Image width in pixels
>>> +	 * \param[in] height Image height in pixels
>>> +	 * \param[in] bpp Bytes per pixel
>>> +	 * \param[in] stride Row stride in bytes
>>> +	 * \param[in] texture_unit OpenGL texture unit (e.g., GL_TEXTURE0)
>>> +	 * \param[in] texture_unit_uniform_id Shader uniform ID for this texture unit
>>> +	 *
>>> +	 * Creates an eGLImage with the specified dimensions and stride. The stride
>>> +	 * may differ from width * bpp due to alignment.
>>> +	 */
>>> + eGLImage(uint32_t width, uint32_t height, uint32_t bpp, uint32_t stride, GLenum texture_unit, uint32_t
>>> texture_unit_uniform_id)
>>> +	{
>>> +		init(width, height, bpp, stride, texture_unit, texture_unit_uniform_id);
>>> +	}
>>> +
>>> +	/**
>>> +	 * \brief Construct an eGLImage with automatic stride calculation
>>> +	 * \param[in] width Image width in pixels
>>> +	 * \param[in] height Image height in pixels
>>> +	 * \param[in] bpp Bytes per pixel
>>> +	 * \param[in] texture_unit OpenGL texture unit (e.g., GL_TEXTURE0)
>>> +	 * \param[in] texture_unit_uniform_id Shader uniform ID for this texture unit
>>> +	 *
>>> +	 * Creates an eGLImage with automatic stride calculation. The stride is
>>> +	 * aligned to 256 bytes because 256 byte alignment is a common baseline alignment for GPUs.
>>> +	 */
>>> + eGLImage(uint32_t width, uint32_t height, uint32_t bpp, GLenum texture_unit, uint32_t
>>> texture_unit_uniform_id)
>>> +	{
>>> +		uint32_t stride = libcamera::utils::alignUp(width * bpp, 256);
>>> +
>>> +		init(width, height, bpp, stride, texture_unit, texture_unit_uniform_id);
>>> +	}
>>> +
>>> +	/**
>>> +	 * \brief Destroy the eGLImage
>>> +	 *
>>> +	 * Cleans up OpenGL resources by deleting the framebuffer object and
>>> +	 * texture.
>>> +	 */
>>> +	~eGLImage()
>>> +	{
>>> +		glDeleteFramebuffers(1, &fbo_);
>>> +		glDeleteTextures(1, &texture_);
>>> +	}
>>> +
>>> +	uint32_t width_;			/**< Image width in pixels */
>>> +	uint32_t height_;			/**< Image height in pixels */
>>> +	uint32_t stride_;			/**< Row stride in bytes */
>>> +	uint32_t offset_;			/**< Buffer offset (reserved for future use) */
>>> +	uint32_t framesize_;			/**< Total frame size in bytes (stride * height) */
>>> +	uint32_t bpp_;				/**< Bytes per pixel */
>>> +	uint32_t texture_unit_uniform_id_;	/**< Shader uniform id for texture unit */
>>> + GLenum texture_unit_; /**< Texture unit associated with this image eg (GL_TEXTURE0) */
>>> +	GLuint texture_;			/**< OpenGL texture object ID */
>>> +	GLuint fbo_;				/**< OpenGL frame buffer object ID */
>>> +	EGLImageKHR image_;			/**< EGL Image handle */
>>> +
>>> +private:
>>> +	LIBCAMERA_DISABLE_COPY_AND_MOVE(eGLImage)
>>> +
>>> +	/**
>>> +	 * \brief Initialise eGLImage state
>>> +	 * \param[in] width Image width in pixels
>>> +	 * \param[in] height Image height in pixels
>>> +	 * \param[in] bpp Bytes per pixel
>>> +	 * \param[in] stride Row stride in bytes
>>> +	 * \param[in] texture_unit OpenGL texture unit
>>> +	 * \param[in] texture_unit_uniform_id Shader uniform ID
>>> +	 *
>>> +	 * Common initialisation routine called by both constructors. Sets up
>>> +	 * member variables and generates OpenGL texture and framebuffer objects.
>>> +	 */
>>> + void init(uint32_t width, uint32_t height, uint32_t bpp, uint32_t stride, GLenum texture_unit,
>>> uint32_t texture_unit_uniform_id)
>>> +	{
>>> +		image_ = EGL_NO_IMAGE_KHR;
>>> +		width_ = width;
>>> +		height_ = height;
>>> +		bpp_ = bpp;
>>> +		stride_ = stride;
>>> +		framesize_ = stride_ * height_;
>>> +		texture_unit_ = texture_unit;
>>> +		texture_unit_uniform_id_ = texture_unit_uniform_id;
>>> +
>>> +		glGenTextures(1, &texture_);
>>> +		glGenFramebuffers(1, &fbo_);
>>> +	}
>>> +};
>>> +
>>> +/**
>>> + * \class eGL
>>> + * \brief Helper class for managing OpenGL ES operations
>>> + *
>>> + * It provides:
>>> + *
>>> + * - EGL context setup and management
>>> + * - Extension function pointer retrieval
>>> + * - Shader compilation and program linking
>>> + * - DMA-BUF texture creation and management
>>> + * - Synchronisation primitives
>>> + *
>>> + * This class is designed to work with zero-copy buffers via DMA-BUF file
>>> + * descriptors.
>>> + */
>>> +class eGL
>>> +{
>>> +public:
>>> +	/**
>>> +	 * \brief Construct an EGL helper
>>> +	 *
>>> +	 * Creates an eGL instance with uninitialised context. Call initEGLContext()
>>> +	 * to set up the EGL display, context, and load extension functions.
>>> +	 */
>>> +	eGL();
>>> +
>>> +	/**
>>> +	 * \brief Destroy the EGL helper
>>> +	 *
>>> +	 * Destroys the EGL context and surface if they were successfully created.
>>> +	 */
>>> +	~eGL();
>>> +
>>> +	/**
>>> +	 * \brief Initialise the EGL context
>>> +	 * \param[in] gbmContext Pointer to initialised GBM context
>>> +	 *
>>> +	 * Sets up the EGL display from the GBM device, creates an OpenGL ES 2.0
>>> +	 * context, and retrieves function pointers for required extensions
>>> +	 * including:
>>> +	 * - eglCreateImageKHR / eglDestroyImageKHR
>>> +	 * - glEGLImageTargetTexture2DOES
>>> +	 * - eglCreateSyncKHR / eglDestroySyncKHR / eglClientWaitSyncKHR
>>> +	 *
>>> +	 * \return 0 on success, or -ENODEV on failure
>>> +	 */
>>> +	int initEGLContext(GBM *gbmContext);
>>> +
>>> +	/**
>>> +	 * \brief Clean up EGL resources
>>> +	 *
>>> +	 * Destroys the EGL sync object. Must be called from the same thread
>>> +	 * that created the EGL context.
>>> +	 */
>>> +	void cleanUp();
>>> +
>>> +	/**
>>> +	 * \brief Create an input DMA-BUF backed texture
>>> +	 * \param[in,out] eglImage EGL image to associate with the DMA-BUF
>>> +	 * \param[in] fd DMA-BUF file descriptor
>>> +	 *
>>> +	 * Creates an EGL image from a DMA-BUF file descriptor and binds it to
>>> +	 * a 2D texture for use as an input texture in shaders. The texture is
>>> +	 * configured with nearest filtering and clamp-to-edge wrapping.
>>> +	 *
>>> +	 * \return 0 on success, or -ENODEV on failure
>>> +	 */
>>> +	int createInputDMABufTexture2D(eGLImage *eglImage, int fd);
>>> +
>>> +	/**
>>> +	 * \brief Create an output DMA-BUF backed texture
>>> +	 * \param[in,out] eglImage EGL image to associate with the DMA-BUF
>>> +	 * \param[in] fd DMA-BUF file descriptor
>>> +	 *
>>> +	 * Creates an EGL image from a DMA-BUF file descriptor and binds it to
>>> +	 * a 2D texture, then attaches it to a framebuffer object for use as a
>>> +	 * render target. This enables zero-copy rendering directly to the
>>> +	 * DMA-BUF.
>>> +	 *
>>> +	 * \return 0 on success, or -ENODEV on failure
>>> +	 */
>>> +	int createOutputDMABufTexture2D(eGLImage *eglImage, int fd);
>>> +
>>> +	/**
>>> +	 * \brief Destroy a DMA-BUF texture's EGL image
>>> +	 * \param[in,out] eglImage EGL image to destroy
>>> +	 *
>>> +	 * Destroys the EGL image associated with a DMA-BUF texture. The OpenGL
>>> +	 * texture and framebuffer objects are destroyed separately in the
>>> +	 * eGLImage destructor.
>>> +	 */
>>> +	void destroyDMABufTexture(eGLImage *eglImage);
>>> +
>>> +	/**
>>> +	 * \brief Create a 2D texture from a memory buffer
>>> +	 * \param[in,out] eglImage EGL image to associate with the texture
>>> +	 * \param[in] format OpenGL internal format (e.g., GL_RGB, GL_RGBA)
>>> +	 * \param[in] width Texture width in pixels
>>> +	 * \param[in] height Texture height in pixels
>>> +	 * \param[in] data Pointer to pixel data, or nullptr for uninitialised texture
>>> +	 *
>>> +	 * Creates a 2D texture from a CPU-accessible memory buffer. The texture
>>> +	 * is configured with nearest filtering and clamp-to-edge wrapping. This
>>> +	 * is useful for uploading static data like lookup tables or uniform color
>>> +	 * matrices to the GPU.
>>> +	 */
>>> + void createTexture2D(eGLImage *eglImage, GLint format, uint32_t width, uint32_t height, void *data);
>>> +
>>> +	/**
>>> +	 * \brief Create a 1D texture from a memory buffer
>>> +	 * \param[in,out] eglImage EGL image to associate with the texture
>>> +	 * \param[in] format OpenGL internal format
>>> +	 * \param[in] width Texture width in pixels
>>> +	 * \param[in] data Pointer to pixel data
>>> +	 *
>>> +	 * Creates a 1D texture (implemented as a 2D texture with height=1) from
>>> +	 * a CPU-accessible memory buffer. Useful for lookup tables in shaders.
>>> +	 */
>>> +	void createTexture1D(eGLImage *eglImage, GLint format, uint32_t width, void *data);
>>> +
>>> +	/**
>>> +	 * \brief Add a preprocessor definition to shader environment
>>> +	 * \param[in,out] shaderEnv Vector of shader environment strings
>>> +	 * \param[in] str Preprocessor definition string (e.g., "#define APPLY_RGB_PARAMETERS")
>>> +	 *
>>> +	 * Appends a preprocessor definition to the shader environment vector.
>>> +	 * These definitions are prepended to shader source code during compilation.
>>> +	 */
>>> +	void pushEnv(std::vector<std::string> &shaderEnv, const char *str);
>>> +
>>> +	/**
>>> +	 * \brief Make the EGL context current for the calling thread
>>> +	 *
>>> +	 * Binds the EGL context to the current thread, allowing OpenGL ES
>>> +	 * operations to be performed. Must be called from the thread that
>>> +	 * will perform rendering operations.
>>> +	 */
>>> +	void makeCurrent();
>>> +
>>> +	/**
>>> +	 * \brief Compile a vertex shader
>>> +	 * \param[out] shaderId OpenGL shader object ID
>>> +	 * \param[in] shaderData Pointer to shader source code
>>> +	 * \param[in] shaderDataLen Length of shader source in bytes
>>> +	 * \param[in] shaderEnv Span of preprocessor definitions to prepend
>>> +	 *
>>> +	 * Compiles a vertex shader from source code with optional preprocessor
>>> +	 * definitions. On compilation failure, logs the shader info log.
>>> +	 *
>>> +	 * \return 0 on success, or -EINVAL on compilation failure
>>> +	 */
>>> +	int compileVertexShader(GLuint &shaderId, unsigned char *shaderData,
>>> +				unsigned int shaderDataLen,
>>> +				Span<const std::string> shaderEnv);
>>> +
>>> +	/**
>>> +	 * \brief Compile a fragment shader
>>> +	 * \param[out] shaderId OpenGL shader object ID
>>> +	 * \param[in] shaderData Pointer to shader source code
>>> +	 * \param[in] shaderDataLen Length of shader source in bytes
>>> +	 * \param[in] shaderEnv Span of preprocessor definitions to prepend
>>> +	 *
>>> +	 * Compiles a fragment shader from source code with optional preprocessor
>>> +	 * definitions. On compilation failure, logs the shader info log.
>>> +	 *
>>> +	 * \return 0 on success, or -EINVAL on compilation failure
>>> +	 */
>>> +	int compileFragmentShader(GLuint &shaderId, unsigned char *shaderData,
>>> +				  unsigned int shaderDataLen,
>>> +				  Span<const std::string> shaderEnv);
>>> +
>>> +	/**
>>> +	 * \brief Link a shader program
>>> +	 * \param[out] programId OpenGL program object ID
>>> +	 * \param[in] fragmentshaderId Compiled fragment shader ID
>>> +	 * \param[in] vertexshaderId Compiled vertex shader ID
>>> +	 *
>>> +	 * Links vertex and fragment shaders into an executable shader program.
>>> +	 * On link failure, logs the program info log and deletes the program.
>>> +	 *
>>> +	 * \return 0 on success, or -ENODEV on link failure
>>> +	 */
>>> +	int linkProgram(GLuint &programId, GLuint fragmentshaderId, GLuint vertexshaderId);
>>> +
>>> +	/**
>>> +	 * \brief Dump shader source code to the log
>>> +	 * \param[in] shaderId OpenGL shader object ID
>>> +	 *
>>> +	 * Retrieves and logs the complete source code of a compiled shader.
>>> +	 * Useful for debugging shader compilation issues.
>>> +	 */
>>> +	void dumpShaderSource(GLuint shaderId);
>>> +
>>> +	/**
>>> +	 * \brief Activate a shader program for rendering
>>> +	 * \param[in] programId OpenGL program object ID
>>> +	 *
>>> +	 * Sets the specified program as the current rendering program. All
>>> +	 * subsequent draw calls will use this program's shaders.
>>> +	 */
>>> +	void useProgram(GLuint programId);
>>> +
>>> +	/**
>>> +	 * \brief Delete a shader program
>>> +	 * \param[in] programId OpenGL program object ID
>>> +	 *
>>> +	 * Deletes a shader program and frees associated resources. The program
>>> +	 * must not be currently in use.
>>> +	 */
>>> +	void deleteProgram(GLuint programId);
>>> +
>>> +	/**
>>> +	 * \brief Synchronise rendering output
>>> +	 *
>>> +	 * Flushes OpenGL commands and waits for rendering to complete using an
>>> +	 * EGL fence sync object. This ensures all rendering operations have
>>> +	 * finished before the CPU accesses the output buffers.
>>> +	 *
>>> +	 * \return 0 on success
>>> +	 */
>>> +	int syncOutput();
>>> +
>>> +private:
>>> +	LIBCAMERA_DISABLE_COPY_AND_MOVE(eGL)
>>> +
>>> +	int fd_;		/**< File descriptor \todo remove this */
>>> +	pid_t tid_;		/**< Thread ID of the thread associated with this EGL context */
>>> +
>>> +	EGLDisplay display_;	/**< EGL display handle */
>>> +	EGLContext context_;	/**< EGL context handle */
>>> +	EGLSurface surface_;	/**< EGL sufrace handle */
>>> +	EGLSyncKHR sync_;	/**< EGL sync object for output sychonisation */
>>> +
>>> +	/**
>>> +	 * \brief Compile a shader of specified type
>>> +	 * \param[in] shaderType GL_VERTEX_SHADER or GL_FRAGMENT_SHADER
>>> +	 * \param[out] shaderId OpenGL shader object ID
>>> +	 * \param[in] shaderData Pointer to shader source code
>>> +	 * \param[in] shaderDataLen Length of shader source in bytes
>>> +	 * \param[in] shaderEnv Span of preprocessor definitions to prepend
>>> +	 *
>>> +	 * Internal helper function for shader compilation. Prepends environment
>>> +	 * definitions to the shader source and compiles the shader.
>>> +	 *
>>> +	 * \return 0 on success, or -EINVAL on compilation failure
>>> +	 */
>>> +	int compileShader(int shaderType, GLuint &shaderId, unsigned char *shaderData,
>>> +			  unsigned int shaderDataLen,
>>> +			  Span<const std::string> shaderEnv);
>>> +
>>> +	/**
>>> +	 * \brief Create a DMA-BUF backed 2D texture
>>> +	 * \param[in,out] eglImage EGL image to associate with the DMA-BUF
>>> +	 * \param[in] fd DMA-BUF file descriptor
>>> +	 * \param[in] output If true, create framebuffer for render target
>>> +	 *
>>> +	 * Internal implementation for creating DMA-BUF textures. Creates an EGL
>>> +	 * image from the DMA-BUF and binds it to a 2D texture. If output is true,
>>> +	 * also creates and attaches a framebuffer object.
>>> +	 *
>>> +	 * \return 0 on success, or -ENODEV on failure
>>> +	 */
>>> +	int createDMABufTexture2D(eGLImage *eglImage, int fd, bool output);
>>> +
>>> +	PFNEGLEXPORTDMABUFIMAGEMESAPROC eglExportDMABUFImageMESA;
>>> +	PFNGLEGLIMAGETARGETTEXTURE2DOESPROC glEGLImageTargetTexture2DOES;
>>> +
>>> +	PFNEGLCREATEIMAGEKHRPROC eglCreateImageKHR;
>>> +	PFNEGLDESTROYIMAGEKHRPROC eglDestroyImageKHR;
>>> +
>>> +	PFNEGLCREATESYNCKHRPROC eglCreateSyncKHR;
>>> +	PFNEGLDESTROYSYNCKHRPROC eglDestroySyncKHR;
>>> +	PFNEGLCLIENTWAITSYNCKHRPROC eglClientWaitSyncKHR;
>>> +};
>>> +} //namespace libcamera
>>> diff --git a/src/libcamera/egl.cpp b/src/libcamera/egl.cpp
>>> new file mode 100644
>>> index 000000000..52d6a6249
>>> --- /dev/null
>>> +++ b/src/libcamera/egl.cpp
>>> @@ -0,0 +1,436 @@
>>> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
>>> +/*
>>> + * Copyright (C) 2024, Linaro Ltd.
>>> + *
>>> + * Authors:
>>> + * Bryan O'Donoghue <bryan.odonoghue@linaro.org>
>>> + *
>>> + * egl.cpp - Helper class for managing eGL interactions.
>>> + */
>>> +
>>> +#include "libcamera/base/thread.h"
>>> +#include "libcamera/internal/egl.h"
>>> +
>>> +#include <fcntl.h>
>>> +#include <sys/ioctl.h>
>>> +#include <sys/mman.h>
>>> +#include <unistd.h>
>>> +
>>> +#include <libdrm/drm_fourcc.h>
>>> +#include <linux/dma-buf.h>
>>> +#include <linux/dma-heap.h>
>>> +
>>> +namespace libcamera {
>>> +
>>> +LOG_DEFINE_CATEGORY(eGL)
>>> +
>>> +eGL::eGL()
>>> +{
>>> +	context_ = EGL_NO_CONTEXT;
>>> +	surface_ = EGL_NO_SURFACE;
>>> +	display_ = EGL_NO_DISPLAY;
>>> +}
>>> +
>>> +eGL::~eGL()
>>> +{
>>> +	if (context_ != EGL_NO_CONTEXT)
>>> +		eglDestroyContext(display_, context_);
>>> +
>>> +	if (surface_ != EGL_NO_SURFACE)
>>> +		eglDestroySurface(display_, surface_);
>>> +
>>> +}
>>> +
>>> +int eGL::syncOutput(void)
>>> +{
>>> +	ASSERT(tid_ == Thread::currentId());
>>> +
>>> +	glFlush();
>>> +	eglClientWaitSyncKHR(display_, sync_, 0, EGL_FOREVER_KHR);
>>> +
>>> +	return 0;
>>> +}
>>> +
>>> +// Create linear image attached to previous BO object
>>> +int eGL::createDMABufTexture2D(eGLImage *eglImage, int fd, bool output)
>>> +{
>>> +	int ret = 0;
>>> +
>>> +	ASSERT(tid_ == Thread::currentId());
>>> +
>>> +	// clang-format off
>>> +	EGLint image_attrs[] = {
>>> +		EGL_WIDTH, (EGLint)eglImage->width_,
>>> +		EGL_HEIGHT, (EGLint)eglImage->height_,
>>> +		EGL_LINUX_DRM_FOURCC_EXT, DRM_FORMAT_ARGB8888,
>>> +		EGL_DMA_BUF_PLANE0_FD_EXT, fd,
>>> +		EGL_DMA_BUF_PLANE0_OFFSET_EXT, 0,
>>> +		EGL_DMA_BUF_PLANE0_PITCH_EXT, (EGLint)eglImage->stride_,
>>> +		EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT, 0,
>>> +		EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT, 0,
>>> +		EGL_NONE,
>>> +	};
>>> +	// clang-format on
>>> +
>>> +	eglImage->image_ = eglCreateImageKHR(display_, EGL_NO_CONTEXT,
>>> +					     EGL_LINUX_DMA_BUF_EXT,
>>> +					     NULL, image_attrs);
>>> +
>>> +	if (eglImage->image_ == EGL_NO_IMAGE_KHR) {
>>> +		LOG(eGL, Error) << "eglCreateImageKHR fail";
>>> +		ret = -ENODEV;
>>> +		goto done;
>>> +	}
>>> +
>>> +	// Bind texture unit and texture
>>> +	glActiveTexture(eglImage->texture_unit_);
>>> +	glBindTexture(GL_TEXTURE_2D, eglImage->texture_);
>>> +
>>> +	// Generate texture with filter semantics
>>> +	glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, eglImage->image_);
>>> +
>>> +	// Nearest filtering
>>> +	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
>>> +	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
>>> +
>>> +	// Wrap to edge to avoid edge artifacts
>>> +	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
>>> +	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
>>> +
>>> +	if (output) {
>>> +		// Generate a framebuffer from our texture direct to dma-buf handle buffer
>>> +		glBindFramebuffer(GL_FRAMEBUFFER, eglImage->fbo_);
>>> + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, eglImage->texture_, 0);
>>> +
>>> +		GLenum err = glCheckFramebufferStatus(GL_FRAMEBUFFER);
>>> +		if (err!= GL_FRAMEBUFFER_COMPLETE) {
>>> +			LOG(eGL, Error) << "glFrameBufferTexture2D error " << err;
>>> +			eglDestroyImageKHR(display_, eglImage->image_);
>>> +			ret = -ENODEV;
>>> +			goto done;
>>> +		}
>>> +	}
>>> +done:
>>> +	return ret;
>>> +}
>>> +
>>> +int eGL::createInputDMABufTexture2D(eGLImage *eglImage, int fd)
>>> +{
>>> +	ASSERT(tid_ == Thread::currentId());
>>> +
>>> +	return createDMABufTexture2D(eglImage, fd, false);
>>> +}
>>> +int eGL::createOutputDMABufTexture2D(eGLImage *eglImage, int fd)
>>> +{
>>> +	ASSERT(tid_ == Thread::currentId());
>>> +
>>> +	return createDMABufTexture2D(eglImage, fd, true);
>>> +}
>>> +
>>> +void eGL::destroyDMABufTexture(eGLImage *eglImage)
>>> +{
>>> +	eglDestroyImage(display_, eglImage->image_);
>>> +}
>>> +
>>> +// Generate a 2D texture from an input buffer directly
>>> +void eGL::createTexture2D(eGLImage *eglImage, GLint format, uint32_t width, uint32_t height, void
>>> *data)
>>> +{
>>> +	ASSERT(tid_ == Thread::currentId());
>>> +
>>> +	glActiveTexture(eglImage->texture_unit_);
>>> +	glBindTexture(GL_TEXTURE_2D, eglImage->texture_);
>>> +
>>> +	// Generate texture, bind, associate image to texture, configure, unbind
>>> +	glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);
>>> +
>>> +	// Nearest filtering
>>> +	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
>>> +	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
>>> +
>>> +	// Wrap to edge to avoid edge artifacts
>>> +	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
>>> +	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
>>> +}
>>> +
>>> +int eGL::initEGLContext(GBM *gbmContext)
>>> +{
>>> +	EGLint configAttribs[] = {
>>> +		EGL_RED_SIZE, 8,
>>> +		EGL_GREEN_SIZE, 8,
>>> +		EGL_BLUE_SIZE, 8,
>>> +		EGL_ALPHA_SIZE, 8,
>>> +		EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
>>> +		EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
>>> +		EGL_NONE
>>> +	};
>>> +
>>> +	EGLint contextAttribs[] = {
>>> +		EGL_CONTEXT_MAJOR_VERSION, 2,
>>> +		EGL_NONE
>>> +	};
>>> +
>>> +	EGLint numConfigs;
>>> +	EGLConfig config;
>>> +	EGLint major;
>>> +	EGLint minor;
>>> +
>>> +	if (!eglBindAPI(EGL_OPENGL_ES_API)) {
>>> +		LOG(eGL, Error) << "API bind fail";
>>> +		goto fail;
>>> +	}
>>> +
>>> +	display_ = eglGetDisplay(gbmContext->getDevice());
>>> +	if (display_ == EGL_NO_DISPLAY) {
>>> +		LOG(eGL, Error) << "Unable to get EGL display";
>>> +		goto fail;
>>> +	}
>>> +
>>> +	if (eglInitialize(display_, &major, &minor) != EGL_TRUE) {
>>> +		LOG(eGL, Error) << "eglInitialize fail";
>>> +		goto fail;
>>> +	}
>>> +
>>> +	LOG(eGL, Info) << "EGL: version " << major << "." << minor;
>>> +	LOG(eGL, Info) << "EGL: EGL_VERSION: " << eglQueryString(display_, EGL_VERSION);
>>> +	LOG(eGL, Info) << "EGL: EGL_VENDOR: " << eglQueryString(display_, EGL_VENDOR);
>>> +	LOG(eGL, Info) << "EGL: EGL_CLIENT_APIS: " << eglQueryString(display_, EGL_CLIENT_APIS);
>>> +	LOG(eGL, Info) << "EGL: EGL_EXTENSIONS: " << eglQueryString(display_, EGL_EXTENSIONS);
>>> +
>>> +	eglCreateImageKHR = (PFNEGLCREATEIMAGEKHRPROC)eglGetProcAddress("eglCreateImageKHR");
>>> +	if (!eglCreateImageKHR) {
>>> +		LOG(eGL, Error) << "eglCreateImageKHR not found";
>>> +		goto fail;
>>> +	}
>>> +
>>> +	eglDestroyImageKHR = (PFNEGLDESTROYIMAGEKHRPROC)eglGetProcAddress("eglDestroyImageKHR");
>>> +	if (!eglDestroyImageKHR) {
>>> +		LOG(eGL, Error) << "eglDestroyImageKHR not found";
>>> +		goto fail;
>>> +	}
>>> +
>>> + glEGLImageTargetTexture2DOES =
>>> (PFNGLEGLIMAGETARGETTEXTURE2DOESPROC)eglGetProcAddress("glEGLImageTargetTexture2DOES");
>>> +	if (!glEGLImageTargetTexture2DOES) {
>>> +		LOG(eGL, Error) << "glEGLImageTargetTexture2DOES not found";
>>> +		goto fail;
>>> +	}
>>> +
>>> +	eglCreateSyncKHR = (PFNEGLCREATESYNCKHRPROC)eglGetProcAddress("eglCreateSyncKHR");
>>> +	if (!eglCreateSyncKHR) {
>>> +		LOG(eGL, Error) << "eglCreateSyncKHR not found";
>>> +		goto fail;
>>> +	}
>>> +
>>> +	eglDestroySyncKHR = (PFNEGLDESTROYSYNCKHRPROC)eglGetProcAddress("eglDestroySyncKHR");
>>> +	if (!eglDestroySyncKHR) {
>>> +		LOG(eGL, Error) << "eglDestroySyncKHR not found";
>>> +		goto fail;
>>> +	}
>>> +
>>> +	eglClientWaitSyncKHR = (PFNEGLCLIENTWAITSYNCKHRPROC)eglGetProcAddress("eglClientWaitSyncKHR");
>>> +	if (!eglClientWaitSyncKHR) {
>>> +		LOG(eGL, Error) << "eglClientWaitSyncKHR not found";
>>> +		goto fail;
>>> +	}
>>> +
>>> +	if (eglChooseConfig(display_, configAttribs, &config, 1, &numConfigs) != EGL_TRUE) {
>>> +		LOG(eGL, Error) << "eglChooseConfig fail";
>>> +		goto fail;
>>> +	}
>>> +
>>> +	context_ = eglCreateContext(display_, config, EGL_NO_CONTEXT, contextAttribs);
>>> +	if (context_ == EGL_NO_CONTEXT) {
>>> +		LOG(eGL, Error) << "eglContext returned EGL_NO_CONTEXT";
>>> +		goto fail;
>>> +	}
>>> +
>>> +	tid_ = Thread::currentId();
>>> +
>>> +	makeCurrent();
>>> +
>>> +	sync_ = eglCreateSyncKHR(display_, EGL_SYNC_FENCE_KHR, NULL);
>>
>> Could you please elaborate a bit on the purpose and handling of `sync_'?
>> I'm pretty ignorant about GL but I was exploring a problem with reported
>> per-frame times and I also observed some tearing with v4e and I've hit
>> this.
>>
>> According to
>> https://registry.khronos.org/EGL/sdk/docs/man/html/eglCreateSync.xhtml
>> (it's sans-KHR but I suppose it's basically the same?):
>>
>>    When a fence sync object is created, eglCreateSync also inserts a
>>    fence command into the command stream of the bound client API's
>>    current context (i.e., the context returned by eglGetCurrentContext),
>>    and associates it with the newly created sync object.
>>
>>    The only condition supported for fence sync objects is
>>    EGL_SYNC_PRIOR_COMMANDS_COMPLETE, which is satisfied by completion of
>>    the fence command corresponding to the sync object, and all preceding
>>    commands in the associated client API context's command stream. The
>>    sync object will not be signaled until all effects from these commands
>>    on the client API's internal and framebuffer state are fully
>>    realized. No other state is affected by execution of the fence
>>    command.
>>
>> As I understand it, calling eglCreateSyncKHR inserts a fence at a given
>> GPU processing place and then eglClientWaitSyncKHR waits for completion
>> of everything before the fence.
>>
>> But here it seems that `sync_' is assigned only during configuration so
>> I don't get its purpose and the purpose of eglClientWaitSyncKHR call in
>> DebayerEGL::debayerGPU per each frame.  When I insert calls to
>> eglCreateSyncKHR + eglClientWaitSyncKHR + eglDestroySyncKHR all together
>> after glDrawArrays in DebayerEGL::debayerGPU, both my time measurement
>> problem and the image tearing disappear.  Which may possibly be just a
>> workaround for a different synchronization problem but then I still
>> don't understand what `sync_' is about.
> 
> As discussed at the meeting and suggested by Robert, I tested replacing
> sync_ use with glFinish call.  It works for me.  The corresponding patch
> is available here:
> https://gitlab.freedesktop.org/camera/libcamera-softisp/-/commits/pdm-gpu-sync?ref_type=heads
> 
> There are three additional patches on top of it, fixing various formal
> issues.  If possible, please use them too.
Done

Patch
diff mbox series

diff --git a/include/libcamera/internal/egl.h b/include/libcamera/internal/egl.h
new file mode 100644
index 000000000..e61a394af
--- /dev/null
+++ b/include/libcamera/internal/egl.h
@@ -0,0 +1,412 @@ 
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Linaro Ltd.
+ *
+ * Authors:
+ * Bryan O'Donoghue <bryan.odonoghue@linaro.org>
+ *
+ * egl_context.cpp - Helper class for managing eGL interactions.
+ */
+
+#pragma once
+
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <libcamera/base/log.h>
+
+#include "libcamera/base/utils.h"
+#include "libcamera/internal/gbm.h"
+
+#define EGL_EGLEXT_PROTOTYPES
+#include <EGL/egl.h>
+#include <EGL/eglext.h>
+#define GL_GLEXT_PROTOTYPES
+#include <GLES2/gl2.h>
+#include <GLES2/gl2ext.h>
+
+namespace libcamera {
+
+LOG_DECLARE_CATEGORY(eGL)
+
+/**
+ * \class eGLImage
+ * \brief Helper class for managing EGL image resources
+ *
+ * The eGLImage class encapsulates OpenGL ES texture and framebuffer objects
+ * along with their associated EGL image. It aggregates handles, descriptors,
+ * and routines for managing textures that can be associated with shader
+ * uniform IDs.
+ *
+ * This class is particularly useful for managing DMA-BUF backed textures
+ * in zero-copy rendering pipelines, where textures are bound to specific
+ * texture units and can be used as both input textures and render targets.
+ */
+class eGLImage
+{
+public:
+	/**
+	 * \brief Construct an eGLImage with explicit stride
+	 * \param[in] width Image width in pixels
+	 * \param[in] height Image height in pixels
+	 * \param[in] bpp Bytes per pixel
+	 * \param[in] stride Row stride in bytes
+	 * \param[in] texture_unit OpenGL texture unit (e.g., GL_TEXTURE0)
+	 * \param[in] texture_unit_uniform_id Shader uniform ID for this texture unit
+	 *
+	 * Creates an eGLImage with the specified dimensions and stride. The stride
+	 * may differ from width * bpp due to alignment.
+	 */
+	eGLImage(uint32_t width, uint32_t height, uint32_t bpp, uint32_t stride, GLenum texture_unit, uint32_t texture_unit_uniform_id)
+	{
+		init(width, height, bpp, stride, texture_unit, texture_unit_uniform_id);
+	}
+
+	/**
+	 * \brief Construct an eGLImage with automatic stride calculation
+	 * \param[in] width Image width in pixels
+	 * \param[in] height Image height in pixels
+	 * \param[in] bpp Bytes per pixel
+	 * \param[in] texture_unit OpenGL texture unit (e.g., GL_TEXTURE0)
+	 * \param[in] texture_unit_uniform_id Shader uniform ID for this texture unit
+	 *
+	 * Creates an eGLImage with automatic stride calculation. The stride is
+	 * aligned to 256 bytes because 256 byte alignment is a common baseline alignment for GPUs.
+	 */
+	eGLImage(uint32_t width, uint32_t height, uint32_t bpp, GLenum texture_unit, uint32_t texture_unit_uniform_id)
+	{
+		uint32_t stride = libcamera::utils::alignUp(width * bpp, 256);
+
+		init(width, height, bpp, stride, texture_unit, texture_unit_uniform_id);
+	}
+
+	/**
+	 * \brief Destroy the eGLImage
+	 *
+	 * Cleans up OpenGL resources by deleting the framebuffer object and
+	 * texture.
+	 */
+	~eGLImage()
+	{
+		glDeleteFramebuffers(1, &fbo_);
+		glDeleteTextures(1, &texture_);
+	}
+
+	uint32_t width_;			/**< Image width in pixels */
+	uint32_t height_;			/**< Image height in pixels */
+	uint32_t stride_;			/**< Row stride in bytes */
+	uint32_t offset_;			/**< Buffer offset (reserved for future use) */
+	uint32_t framesize_;			/**< Total frame size in bytes (stride * height) */
+	uint32_t bpp_;				/**< Bytes per pixel */
+	uint32_t texture_unit_uniform_id_;	/**< Shader uniform id for texture unit */
+	GLenum texture_unit_;			/**< Texture unit associated with this image eg (GL_TEXTURE0) */
+	GLuint texture_;			/**< OpenGL texture object ID */
+	GLuint fbo_;				/**< OpenGL frame buffer object ID */
+	EGLImageKHR image_;			/**< EGL Image handle */
+
+private:
+	LIBCAMERA_DISABLE_COPY_AND_MOVE(eGLImage)
+
+	/**
+	 * \brief Initialise eGLImage state
+	 * \param[in] width Image width in pixels
+	 * \param[in] height Image height in pixels
+	 * \param[in] bpp Bytes per pixel
+	 * \param[in] stride Row stride in bytes
+	 * \param[in] texture_unit OpenGL texture unit
+	 * \param[in] texture_unit_uniform_id Shader uniform ID
+	 *
+	 * Common initialisation routine called by both constructors. Sets up
+	 * member variables and generates OpenGL texture and framebuffer objects.
+	 */
+	void init(uint32_t width, uint32_t height, uint32_t bpp, uint32_t stride, GLenum texture_unit, uint32_t texture_unit_uniform_id)
+	{
+		image_ = EGL_NO_IMAGE_KHR;
+		width_ = width;
+		height_ = height;
+		bpp_ = bpp;
+		stride_ = stride;
+		framesize_ = stride_ * height_;
+		texture_unit_ = texture_unit;
+		texture_unit_uniform_id_ = texture_unit_uniform_id;
+
+		glGenTextures(1, &texture_);
+		glGenFramebuffers(1, &fbo_);
+	}
+};
+
+/**
+ * \class eGL
+ * \brief Helper class for managing OpenGL ES operations
+ *
+ * It provides:
+ *
+ * - EGL context setup and management
+ * - Extension function pointer retrieval
+ * - Shader compilation and program linking
+ * - DMA-BUF texture creation and management
+ * - Synchronisation primitives
+ *
+ * This class is designed to work with zero-copy buffers via DMA-BUF file
+ * descriptors.
+ */
+class eGL
+{
+public:
+	/**
+	 * \brief Construct an EGL helper
+	 *
+	 * Creates an eGL instance with uninitialised context. Call initEGLContext()
+	 * to set up the EGL display, context, and load extension functions.
+	 */
+	eGL();
+
+	/**
+	 * \brief Destroy the EGL helper
+	 *
+	 * Destroys the EGL context and surface if they were successfully created.
+	 */
+	~eGL();
+
+	/**
+	 * \brief Initialise the EGL context
+	 * \param[in] gbmContext Pointer to initialised GBM context
+	 *
+	 * Sets up the EGL display from the GBM device, creates an OpenGL ES 2.0
+	 * context, and retrieves function pointers for required extensions
+	 * including:
+	 * - eglCreateImageKHR / eglDestroyImageKHR
+	 * - glEGLImageTargetTexture2DOES
+	 * - eglCreateSyncKHR / eglDestroySyncKHR / eglClientWaitSyncKHR
+	 *
+	 * \return 0 on success, or -ENODEV on failure
+	 */
+	int initEGLContext(GBM *gbmContext);
+
+	/**
+	 * \brief Clean up EGL resources
+	 *
+	 * Destroys the EGL sync object. Must be called from the same thread
+	 * that created the EGL context.
+	 */
+	void cleanUp();
+
+	/**
+	 * \brief Create an input DMA-BUF backed texture
+	 * \param[in,out] eglImage EGL image to associate with the DMA-BUF
+	 * \param[in] fd DMA-BUF file descriptor
+	 *
+	 * Creates an EGL image from a DMA-BUF file descriptor and binds it to
+	 * a 2D texture for use as an input texture in shaders. The texture is
+	 * configured with nearest filtering and clamp-to-edge wrapping.
+	 *
+	 * \return 0 on success, or -ENODEV on failure
+	 */
+	int createInputDMABufTexture2D(eGLImage *eglImage, int fd);
+
+	/**
+	 * \brief Create an output DMA-BUF backed texture
+	 * \param[in,out] eglImage EGL image to associate with the DMA-BUF
+	 * \param[in] fd DMA-BUF file descriptor
+	 *
+	 * Creates an EGL image from a DMA-BUF file descriptor and binds it to
+	 * a 2D texture, then attaches it to a framebuffer object for use as a
+	 * render target. This enables zero-copy rendering directly to the
+	 * DMA-BUF.
+	 *
+	 * \return 0 on success, or -ENODEV on failure
+	 */
+	int createOutputDMABufTexture2D(eGLImage *eglImage, int fd);
+
+	/**
+	 * \brief Destroy a DMA-BUF texture's EGL image
+	 * \param[in,out] eglImage EGL image to destroy
+	 *
+	 * Destroys the EGL image associated with a DMA-BUF texture. The OpenGL
+	 * texture and framebuffer objects are destroyed separately in the
+	 * eGLImage destructor.
+	 */
+	void destroyDMABufTexture(eGLImage *eglImage);
+
+	/**
+	 * \brief Create a 2D texture from a memory buffer
+	 * \param[in,out] eglImage EGL image to associate with the texture
+	 * \param[in] format OpenGL internal format (e.g., GL_RGB, GL_RGBA)
+	 * \param[in] width Texture width in pixels
+	 * \param[in] height Texture height in pixels
+	 * \param[in] data Pointer to pixel data, or nullptr for uninitialised texture
+	 *
+	 * Creates a 2D texture from a CPU-accessible memory buffer. The texture
+	 * is configured with nearest filtering and clamp-to-edge wrapping. This
+	 * is useful for uploading static data like lookup tables or uniform color
+	 * matrices to the GPU.
+	 */
+	void createTexture2D(eGLImage *eglImage, GLint format, uint32_t width, uint32_t height, void *data);
+
+	/**
+	 * \brief Create a 1D texture from a memory buffer
+	 * \param[in,out] eglImage EGL image to associate with the texture
+	 * \param[in] format OpenGL internal format
+	 * \param[in] width Texture width in pixels
+	 * \param[in] data Pointer to pixel data
+	 *
+	 * Creates a 1D texture (implemented as a 2D texture with height=1) from
+	 * a CPU-accessible memory buffer. Useful for lookup tables in shaders.
+	 */
+	void createTexture1D(eGLImage *eglImage, GLint format, uint32_t width, void *data);
+
+	/**
+	 * \brief Add a preprocessor definition to shader environment
+	 * \param[in,out] shaderEnv Vector of shader environment strings
+	 * \param[in] str Preprocessor definition string (e.g., "#define APPLY_RGB_PARAMETERS")
+	 *
+	 * Appends a preprocessor definition to the shader environment vector.
+	 * These definitions are prepended to shader source code during compilation.
+	 */
+	void pushEnv(std::vector<std::string> &shaderEnv, const char *str);
+
+	/**
+	 * \brief Make the EGL context current for the calling thread
+	 *
+	 * Binds the EGL context to the current thread, allowing OpenGL ES
+	 * operations to be performed. Must be called from the thread that
+	 * will perform rendering operations.
+	 */
+	void makeCurrent();
+
+	/**
+	 * \brief Compile a vertex shader
+	 * \param[out] shaderId OpenGL shader object ID
+	 * \param[in] shaderData Pointer to shader source code
+	 * \param[in] shaderDataLen Length of shader source in bytes
+	 * \param[in] shaderEnv Span of preprocessor definitions to prepend
+	 *
+	 * Compiles a vertex shader from source code with optional preprocessor
+	 * definitions. On compilation failure, logs the shader info log.
+	 *
+	 * \return 0 on success, or -EINVAL on compilation failure
+	 */
+	int compileVertexShader(GLuint &shaderId, unsigned char *shaderData,
+				unsigned int shaderDataLen,
+				Span<const std::string> shaderEnv);
+
+	/**
+	 * \brief Compile a fragment shader
+	 * \param[out] shaderId OpenGL shader object ID
+	 * \param[in] shaderData Pointer to shader source code
+	 * \param[in] shaderDataLen Length of shader source in bytes
+	 * \param[in] shaderEnv Span of preprocessor definitions to prepend
+	 *
+	 * Compiles a fragment shader from source code with optional preprocessor
+	 * definitions. On compilation failure, logs the shader info log.
+	 *
+	 * \return 0 on success, or -EINVAL on compilation failure
+	 */
+	int compileFragmentShader(GLuint &shaderId, unsigned char *shaderData,
+				  unsigned int shaderDataLen,
+				  Span<const std::string> shaderEnv);
+
+	/**
+	 * \brief Link a shader program
+	 * \param[out] programId OpenGL program object ID
+	 * \param[in] fragmentshaderId Compiled fragment shader ID
+	 * \param[in] vertexshaderId Compiled vertex shader ID
+	 *
+	 * Links vertex and fragment shaders into an executable shader program.
+	 * On link failure, logs the program info log and deletes the program.
+	 *
+	 * \return 0 on success, or -ENODEV on link failure
+	 */
+	int linkProgram(GLuint &programId, GLuint fragmentshaderId, GLuint vertexshaderId);
+
+	/**
+	 * \brief Dump shader source code to the log
+	 * \param[in] shaderId OpenGL shader object ID
+	 *
+	 * Retrieves and logs the complete source code of a compiled shader.
+	 * Useful for debugging shader compilation issues.
+	 */
+	void dumpShaderSource(GLuint shaderId);
+
+	/**
+	 * \brief Activate a shader program for rendering
+	 * \param[in] programId OpenGL program object ID
+	 *
+	 * Sets the specified program as the current rendering program. All
+	 * subsequent draw calls will use this program's shaders.
+	 */
+	void useProgram(GLuint programId);
+
+	/**
+	 * \brief Delete a shader program
+	 * \param[in] programId OpenGL program object ID
+	 *
+	 * Deletes a shader program and frees associated resources. The program
+	 * must not be currently in use.
+	 */
+	void deleteProgram(GLuint programId);
+
+	/**
+	 * \brief Synchronise rendering output
+	 *
+	 * Flushes OpenGL commands and waits for rendering to complete using an
+	 * EGL fence sync object. This ensures all rendering operations have
+	 * finished before the CPU accesses the output buffers.
+	 *
+	 * \return 0 on success
+	 */
+	int syncOutput();
+
+private:
+	LIBCAMERA_DISABLE_COPY_AND_MOVE(eGL)
+
+	int fd_;		/**< File descriptor \todo remove this */
+	pid_t tid_;		/**< Thread ID of the thread associated with this EGL context */
+
+	EGLDisplay display_;	/**< EGL display handle */
+	EGLContext context_;	/**< EGL context handle */
+	EGLSurface surface_;	/**< EGL sufrace handle */
+	EGLSyncKHR sync_;	/**< EGL sync object for output sychonisation */
+
+	/**
+	 * \brief Compile a shader of specified type
+	 * \param[in] shaderType GL_VERTEX_SHADER or GL_FRAGMENT_SHADER
+	 * \param[out] shaderId OpenGL shader object ID
+	 * \param[in] shaderData Pointer to shader source code
+	 * \param[in] shaderDataLen Length of shader source in bytes
+	 * \param[in] shaderEnv Span of preprocessor definitions to prepend
+	 *
+	 * Internal helper function for shader compilation. Prepends environment
+	 * definitions to the shader source and compiles the shader.
+	 *
+	 * \return 0 on success, or -EINVAL on compilation failure
+	 */
+	int compileShader(int shaderType, GLuint &shaderId, unsigned char *shaderData,
+			  unsigned int shaderDataLen,
+			  Span<const std::string> shaderEnv);
+
+	/**
+	 * \brief Create a DMA-BUF backed 2D texture
+	 * \param[in,out] eglImage EGL image to associate with the DMA-BUF
+	 * \param[in] fd DMA-BUF file descriptor
+	 * \param[in] output If true, create framebuffer for render target
+	 *
+	 * Internal implementation for creating DMA-BUF textures. Creates an EGL
+	 * image from the DMA-BUF and binds it to a 2D texture. If output is true,
+	 * also creates and attaches a framebuffer object.
+	 *
+	 * \return 0 on success, or -ENODEV on failure
+	 */
+	int createDMABufTexture2D(eGLImage *eglImage, int fd, bool output);
+
+	PFNEGLEXPORTDMABUFIMAGEMESAPROC eglExportDMABUFImageMESA;
+	PFNGLEGLIMAGETARGETTEXTURE2DOESPROC glEGLImageTargetTexture2DOES;
+
+	PFNEGLCREATEIMAGEKHRPROC eglCreateImageKHR;
+	PFNEGLDESTROYIMAGEKHRPROC eglDestroyImageKHR;
+
+	PFNEGLCREATESYNCKHRPROC eglCreateSyncKHR;
+	PFNEGLDESTROYSYNCKHRPROC eglDestroySyncKHR;
+	PFNEGLCLIENTWAITSYNCKHRPROC eglClientWaitSyncKHR;
+};
+} //namespace libcamera
diff --git a/src/libcamera/egl.cpp b/src/libcamera/egl.cpp
new file mode 100644
index 000000000..52d6a6249
--- /dev/null
+++ b/src/libcamera/egl.cpp
@@ -0,0 +1,436 @@ 
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Linaro Ltd.
+ *
+ * Authors:
+ * Bryan O'Donoghue <bryan.odonoghue@linaro.org>
+ *
+ * egl.cpp - Helper class for managing eGL interactions.
+ */
+
+#include "libcamera/base/thread.h"
+#include "libcamera/internal/egl.h"
+
+#include <fcntl.h>
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+#include <unistd.h>
+
+#include <libdrm/drm_fourcc.h>
+#include <linux/dma-buf.h>
+#include <linux/dma-heap.h>
+
+namespace libcamera {
+
+LOG_DEFINE_CATEGORY(eGL)
+
+eGL::eGL()
+{
+	context_ = EGL_NO_CONTEXT;
+	surface_ = EGL_NO_SURFACE;
+	display_ = EGL_NO_DISPLAY;
+}
+
+eGL::~eGL()
+{
+	if (context_ != EGL_NO_CONTEXT)
+		eglDestroyContext(display_, context_);
+
+	if (surface_ != EGL_NO_SURFACE)
+		eglDestroySurface(display_, surface_);
+
+}
+
+int eGL::syncOutput(void)
+{
+	ASSERT(tid_ == Thread::currentId());
+
+	glFlush();
+	eglClientWaitSyncKHR(display_, sync_, 0, EGL_FOREVER_KHR);
+
+	return 0;
+}
+
+// Create linear image attached to previous BO object
+int eGL::createDMABufTexture2D(eGLImage *eglImage, int fd, bool output)
+{
+	int ret = 0;
+
+	ASSERT(tid_ == Thread::currentId());
+
+	// clang-format off
+	EGLint image_attrs[] = {
+		EGL_WIDTH, (EGLint)eglImage->width_,
+		EGL_HEIGHT, (EGLint)eglImage->height_,
+		EGL_LINUX_DRM_FOURCC_EXT, DRM_FORMAT_ARGB8888,
+		EGL_DMA_BUF_PLANE0_FD_EXT, fd,
+		EGL_DMA_BUF_PLANE0_OFFSET_EXT, 0,
+		EGL_DMA_BUF_PLANE0_PITCH_EXT, (EGLint)eglImage->stride_,
+		EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT, 0,
+		EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT, 0,
+		EGL_NONE,
+	};
+	// clang-format on
+
+	eglImage->image_ = eglCreateImageKHR(display_, EGL_NO_CONTEXT,
+					     EGL_LINUX_DMA_BUF_EXT,
+					     NULL, image_attrs);
+
+	if (eglImage->image_ == EGL_NO_IMAGE_KHR) {
+		LOG(eGL, Error) << "eglCreateImageKHR fail";
+		ret = -ENODEV;
+		goto done;
+	}
+
+	// Bind texture unit and texture
+	glActiveTexture(eglImage->texture_unit_);
+	glBindTexture(GL_TEXTURE_2D, eglImage->texture_);
+
+	// Generate texture with filter semantics
+	glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, eglImage->image_);
+
+	// Nearest filtering
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+
+	// Wrap to edge to avoid edge artifacts
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+
+	if (output) {
+		// Generate a framebuffer from our texture direct to dma-buf handle buffer
+		glBindFramebuffer(GL_FRAMEBUFFER, eglImage->fbo_);
+		glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, eglImage->texture_, 0);
+
+		GLenum err = glCheckFramebufferStatus(GL_FRAMEBUFFER);
+		if (err!= GL_FRAMEBUFFER_COMPLETE) {
+			LOG(eGL, Error) << "glFrameBufferTexture2D error " << err;
+			eglDestroyImageKHR(display_, eglImage->image_);
+			ret = -ENODEV;
+			goto done;
+		}
+	}
+done:
+	return ret;
+}
+
+int eGL::createInputDMABufTexture2D(eGLImage *eglImage, int fd)
+{
+	ASSERT(tid_ == Thread::currentId());
+
+	return createDMABufTexture2D(eglImage, fd, false);
+}
+int eGL::createOutputDMABufTexture2D(eGLImage *eglImage, int fd)
+{
+	ASSERT(tid_ == Thread::currentId());
+
+	return createDMABufTexture2D(eglImage, fd, true);
+}
+
+void eGL::destroyDMABufTexture(eGLImage *eglImage)
+{
+	eglDestroyImage(display_, eglImage->image_);
+}
+
+// Generate a 2D texture from an input buffer directly
+void eGL::createTexture2D(eGLImage *eglImage, GLint format, uint32_t width, uint32_t height, void *data)
+{
+	ASSERT(tid_ == Thread::currentId());
+
+	glActiveTexture(eglImage->texture_unit_);
+	glBindTexture(GL_TEXTURE_2D, eglImage->texture_);
+
+	// Generate texture, bind, associate image to texture, configure, unbind
+	glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);
+
+	// Nearest filtering
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+
+	// Wrap to edge to avoid edge artifacts
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+}
+
+int eGL::initEGLContext(GBM *gbmContext)
+{
+	EGLint configAttribs[] = {
+		EGL_RED_SIZE, 8,
+		EGL_GREEN_SIZE, 8,
+		EGL_BLUE_SIZE, 8,
+		EGL_ALPHA_SIZE, 8,
+		EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
+		EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
+		EGL_NONE
+	};
+
+	EGLint contextAttribs[] = {
+		EGL_CONTEXT_MAJOR_VERSION, 2,
+		EGL_NONE
+	};
+
+	EGLint numConfigs;
+	EGLConfig config;
+	EGLint major;
+	EGLint minor;
+
+	if (!eglBindAPI(EGL_OPENGL_ES_API)) {
+		LOG(eGL, Error) << "API bind fail";
+		goto fail;
+	}
+
+	display_ = eglGetDisplay(gbmContext->getDevice());
+	if (display_ == EGL_NO_DISPLAY) {
+		LOG(eGL, Error) << "Unable to get EGL display";
+		goto fail;
+	}
+
+	if (eglInitialize(display_, &major, &minor) != EGL_TRUE) {
+		LOG(eGL, Error) << "eglInitialize fail";
+		goto fail;
+	}
+
+	LOG(eGL, Info) << "EGL: version " << major << "." << minor;
+	LOG(eGL, Info) << "EGL: EGL_VERSION: " << eglQueryString(display_, EGL_VERSION);
+	LOG(eGL, Info) << "EGL: EGL_VENDOR: " << eglQueryString(display_, EGL_VENDOR);
+	LOG(eGL, Info) << "EGL: EGL_CLIENT_APIS: " << eglQueryString(display_, EGL_CLIENT_APIS);
+	LOG(eGL, Info) << "EGL: EGL_EXTENSIONS: " << eglQueryString(display_, EGL_EXTENSIONS);
+
+	eglCreateImageKHR = (PFNEGLCREATEIMAGEKHRPROC)eglGetProcAddress("eglCreateImageKHR");
+	if (!eglCreateImageKHR) {
+		LOG(eGL, Error) << "eglCreateImageKHR not found";
+		goto fail;
+	}
+
+	eglDestroyImageKHR = (PFNEGLDESTROYIMAGEKHRPROC)eglGetProcAddress("eglDestroyImageKHR");
+	if (!eglDestroyImageKHR) {
+		LOG(eGL, Error) << "eglDestroyImageKHR not found";
+		goto fail;
+	}
+
+	glEGLImageTargetTexture2DOES = (PFNGLEGLIMAGETARGETTEXTURE2DOESPROC)eglGetProcAddress("glEGLImageTargetTexture2DOES");
+	if (!glEGLImageTargetTexture2DOES) {
+		LOG(eGL, Error) << "glEGLImageTargetTexture2DOES not found";
+		goto fail;
+	}
+
+	eglCreateSyncKHR = (PFNEGLCREATESYNCKHRPROC)eglGetProcAddress("eglCreateSyncKHR");
+	if (!eglCreateSyncKHR) {
+		LOG(eGL, Error) << "eglCreateSyncKHR not found";
+		goto fail;
+	}
+
+	eglDestroySyncKHR = (PFNEGLDESTROYSYNCKHRPROC)eglGetProcAddress("eglDestroySyncKHR");
+	if (!eglDestroySyncKHR) {
+		LOG(eGL, Error) << "eglDestroySyncKHR not found";
+		goto fail;
+	}
+
+	eglClientWaitSyncKHR = (PFNEGLCLIENTWAITSYNCKHRPROC)eglGetProcAddress("eglClientWaitSyncKHR");
+	if (!eglClientWaitSyncKHR) {
+		LOG(eGL, Error) << "eglClientWaitSyncKHR not found";
+		goto fail;
+	}
+
+	if (eglChooseConfig(display_, configAttribs, &config, 1, &numConfigs) != EGL_TRUE) {
+		LOG(eGL, Error) << "eglChooseConfig fail";
+		goto fail;
+	}
+
+	context_ = eglCreateContext(display_, config, EGL_NO_CONTEXT, contextAttribs);
+	if (context_ == EGL_NO_CONTEXT) {
+		LOG(eGL, Error) << "eglContext returned EGL_NO_CONTEXT";
+		goto fail;
+	}
+
+	tid_ = Thread::currentId();
+
+	makeCurrent();
+
+	sync_ = eglCreateSyncKHR(display_, EGL_SYNC_FENCE_KHR, NULL);
+	if (sync_ == EGL_NO_SYNC_KHR) {
+		LOG(eGL, Error) << "eglCreateSyncKHR fail";
+		goto fail;
+	}
+
+	return 0;
+fail:
+
+	return -ENODEV;
+}
+
+void eGL::cleanUp(void)
+{
+	ASSERT(tid_ == Thread::currentId());
+
+	if (sync_) {
+		makeCurrent();
+		eglDestroySyncKHR(display_, sync_);
+	}
+
+}
+
+void eGL::makeCurrent(void)
+{
+	ASSERT(tid_ == Thread::currentId());
+
+	if (eglMakeCurrent(display_, EGL_NO_SURFACE, EGL_NO_SURFACE, context_) != EGL_TRUE) {
+		LOG(eGL, Error) << "eglMakeCurrent fail";
+	}
+}
+
+void eGL::useProgram(GLuint programId)
+{
+	ASSERT(tid_ == Thread::currentId());
+
+	glUseProgram(programId);
+}
+
+void eGL::deleteProgram(GLuint programId)
+{
+	ASSERT(tid_ == Thread::currentId());
+
+	glDeleteProgram(programId);
+}
+
+void eGL::pushEnv(std::vector<std::string> &shaderEnv, const char *str)
+{
+	std::string addStr = str;
+
+	addStr.push_back('\n');
+	shaderEnv.push_back(std::move(addStr));
+}
+
+int eGL::compileVertexShader(GLuint &shaderId, unsigned char *shaderData,
+			     unsigned int shaderDataLen,
+			     Span<const std::string> shaderEnv)
+{
+	return compileShader(GL_VERTEX_SHADER, shaderId, shaderData, shaderDataLen, shaderEnv);
+}
+
+int eGL::compileFragmentShader(GLuint &shaderId, unsigned char *shaderData,
+			       unsigned int shaderDataLen,
+			       Span<const std::string> shaderEnv)
+{
+	return compileShader(GL_FRAGMENT_SHADER, shaderId, shaderData, shaderDataLen, shaderEnv);
+}
+
+int eGL::compileShader(int shaderType, GLuint &shaderId, unsigned char *shaderData,
+		       unsigned int shaderDataLen,
+		       Span<const std::string> shaderEnv)
+{
+	GLint success;
+	size_t i;
+
+	ASSERT(tid_ == Thread::currentId());
+
+	auto count = 1 + shaderEnv.size();
+	auto shaderSourceData = std::make_unique<const GLchar*[]>(count);
+	auto shaderDataLengths = std::make_unique<GLint[]>(count);
+
+	// Prefix defines before main body of shader
+	for (i = 0; i < shaderEnv.size(); i++) {
+		shaderSourceData[i] = shaderEnv[i].c_str();
+		shaderDataLengths[i] = shaderEnv[i].length();
+	}
+
+	// Now the main body of the shader program
+	shaderSourceData[i] = reinterpret_cast<const GLchar*>(shaderData);
+	shaderDataLengths[i] = shaderDataLen;
+
+	// And create the shader
+	shaderId = glCreateShader(shaderType);
+	glShaderSource(shaderId, count, shaderSourceData.get(), shaderDataLengths.get());
+	glCompileShader(shaderId);
+
+	// Check status
+	glGetShaderiv(shaderId, GL_COMPILE_STATUS, &success);
+	if (success == GL_FALSE) {
+		GLint sizeLog = 0;
+
+		glGetShaderiv(shaderId, GL_INFO_LOG_LENGTH, &sizeLog);
+		auto infoLog = std::make_unique<GLchar[]>(sizeLog);
+
+		glGetShaderInfoLog(shaderId, sizeLog, &sizeLog, infoLog.get());
+		LOG(eGL, Error) << infoLog.get();
+	}
+
+	return (success == GL_TRUE) ? 0 : -EINVAL;
+}
+
+void eGL::dumpShaderSource(GLuint shaderId)
+{
+	GLint shaderLength = 0;
+
+	ASSERT(tid_ == Thread::currentId());
+
+	glGetShaderiv(shaderId, GL_SHADER_SOURCE_LENGTH, &shaderLength);
+
+	LOG(eGL, Debug) << "Shader length is " << shaderLength;
+
+	if (shaderLength > 0) {
+		auto shaderSource = std::make_unique<GLchar []>(shaderLength);
+
+		glGetShaderSource(shaderId, shaderLength, &shaderLength, shaderSource.get());
+		if (shaderLength) {
+			LOG(eGL, Debug) << "Shader source = " << shaderSource.get();
+		}
+	}
+}
+
+int eGL::linkProgram(GLuint &programId, GLuint vertexshaderId, GLuint fragmentshaderId)
+{
+	GLint success;
+	GLenum err;
+
+	ASSERT(tid_ == Thread::currentId());
+
+	programId = glCreateProgram();
+	if (!programId)
+		goto fail;
+
+	glAttachShader(programId, vertexshaderId);
+	if ((err = glGetError()) != GL_NO_ERROR) {
+		LOG(eGL, Error) << "Attach compute vertex shader fail";
+		goto fail;
+	}
+
+	glAttachShader(programId, fragmentshaderId);
+	if ((err = glGetError()) != GL_NO_ERROR) {
+		LOG(eGL, Error) << "Attach compute vertex shader fail";
+		goto fail;
+	}
+
+	glLinkProgram(programId);
+	if ((err = glGetError()) != GL_NO_ERROR) {
+		LOG(eGL, Error) << "Link program fail";
+		goto fail;
+	}
+
+	glDetachShader(programId, fragmentshaderId);
+	glDetachShader(programId, vertexshaderId);
+
+	// Check status
+	glGetProgramiv(programId, GL_LINK_STATUS, &success);
+	if (success == GL_FALSE) {
+		GLint sizeLog = 0;
+		GLchar *infoLog;
+
+		glGetProgramiv(programId, GL_INFO_LOG_LENGTH, &sizeLog);
+		infoLog = new GLchar[sizeLog];
+
+		glGetProgramInfoLog(programId, sizeLog, &sizeLog, infoLog);
+		LOG(eGL, Error) << infoLog;
+
+		delete[] infoLog;
+		goto fail;
+	}
+
+	return 0;
+fail:
+	if (programId)
+		glDeleteProgram(programId);
+
+	return -ENODEV;
+}
+} // namespace libcamera
diff --git a/src/libcamera/meson.build b/src/libcamera/meson.build
index fe60c8752..b7be38a8f 100644
--- a/src/libcamera/meson.build
+++ b/src/libcamera/meson.build
@@ -79,6 +79,27 @@  if libgbm.found() and gbm_works
     ])
 endif
 
+libegl = cc.find_library('EGL', required : false)
+libglesv2 = cc.find_library('GLESv2', required : false)
+mesa_works = cc.check_header('EGL/egl.h', required: false)
+
+if libegl.found() and mesa_works
+    config_h.set('HAVE_LIBEGL', 1)
+endif
+
+if libglesv2.found() and mesa_works
+    config_h.set('HAVE_GLESV2', 1)
+endif
+
+if mesa_works and gbm_works
+    libcamera_internal_sources += files([
+        'egl.cpp',
+    ])
+    gles_headless_enabled = true
+else
+    gles_headless_enabled = false
+endif
+
 subdir('base')
 subdir('converter')
 subdir('ipa')
@@ -186,7 +207,9 @@  libcamera_deps += [
     libcamera_base_private,
     libcrypto,
     libdl,
+    libegl,
     libgbm,
+    libglesv2,
     liblttng,
     libudev,
     libyaml,