[v2,24/37] libcamera: software_isp: egl: Introduce an eGL base helper class
diff mbox series

Message ID 20250824-b4-v0-5-2-gpuisp-v2-a-v2-24-96f4576c814e@linaro.org
State New
Headers show
Series
  • Add GLES 2.0 GPUISP to libcamera
Related show

Commit Message

Bryan O'Donoghue Aug. 24, 2025, 12:48 a.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.

Signed-off-by: Bryan O'Donoghue <bryan.odonoghue@linaro.org>
---
 include/libcamera/internal/egl.h | 133 +++++++++++++
 src/libcamera/egl.cpp            | 408 +++++++++++++++++++++++++++++++++++++++
 src/libcamera/meson.build        |  23 +++
 3 files changed, 564 insertions(+)

Comments

Robert Mader Aug. 28, 2025, 9:35 p.m. UTC | #1
Thanks a lot for the series / all your work!

I just tested it on a OnePlus6 / Freedreno - there's one issue, see below.

On 24.08.25 02:48, Bryan O'Donoghue wrote:
> 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.
>
> Signed-off-by: Bryan O'Donoghue<bryan.odonoghue@linaro.org>
> ---
>   include/libcamera/internal/egl.h | 133 +++++++++++++
>   src/libcamera/egl.cpp            | 408 +++++++++++++++++++++++++++++++++++++++
>   src/libcamera/meson.build        |  23 +++
>   3 files changed, 564 insertions(+)
>
> diff --git a/include/libcamera/internal/egl.h b/include/libcamera/internal/egl.h
> new file mode 100644
> index 0000000000000000000000000000000000000000..2c48e28e14aaa1fa8029dd18662c8ce24e6a550c
> --- /dev/null
> +++ b/include/libcamera/internal/egl.h
> @@ -0,0 +1,133 @@
> +/* 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 <unistd.h>
> +
> +#include <libcamera/base/log.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
> +{
> +public:
> +	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);
> +	}
> +
> +	eGLImage(uint32_t width, uint32_t height, uint32_t bpp, GLenum texture_unit, uint32_t texture_unit_uniform_id)
> +	{
> +		uint32_t stride = ALIGN_TO(width * bpp, 256);
> +
> +		init(width, height, bpp, stride, texture_unit, texture_unit_uniform_id);
> +	}
> +
> +	~eGLImage()
> +	{
> +		glDeleteFramebuffers(1, &fbo_);
> +		glDeleteTextures(1, &texture_);
> +	}
> +
> +	uint32_t width_;
> +	uint32_t height_;
> +	uint32_t stride_;
> +	uint32_t offset_;
> +	uint32_t framesize_;
> +	uint32_t bpp_;
> +	uint32_t texture_unit_uniform_id_;
> +	GLenum texture_unit_;
> +	GLuint texture_;
> +	GLuint fbo_;
> +	EGLImageKHR image_;
> +
> +private:
> +	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
> +{
> +public:
> +	eGL();
> +	~eGL();
> +
> +	int initEGLContext(GBM *gbmContext);
> +	void cleanUp();
> +	int createInputDMABufTexture2D(eGLImage *eglImage, int fd);
> +	int createOutputDMABufTexture2D(eGLImage *eglImage, int fd);
> +	void destroyDMABufTexture(eGLImage *eglImage);
> +	void createTexture2D(eGLImage *eglImage, GLint format, uint32_t width, uint32_t height, void *data);
> +	void createTexture1D(eGLImage *eglImage, GLint format, uint32_t width, void *data);
> +
> +	void pushEnv(std::vector<std::string> &shaderEnv, const char *str);
> +	void makeCurrent();
> +	void swapBuffers();
> +
> +	int compileVertexShader(GLuint &shaderId, unsigned char *shaderData,
> +				unsigned int shaderDataLen,
> +				std::vector<std::string> shaderEnv);
> +	int compileFragmentShader(GLuint &shaderId, unsigned char *shaderData,
> +				  unsigned int shaderDataLen,
> +				  std::vector<std::string> shaderEnv);
> +	int linkProgram(GLuint &programIdd, GLuint fragmentshaderId, GLuint vertexshaderId);
> +	void dumpShaderSource(GLuint shaderId);
> +	void useProgram(GLuint programId);
> +	int syncOutput();
> +
> +private:
> +	int fd_;
> +
> +	EGLDisplay display_;
> +	EGLContext context_;
> +	EGLSurface surface_;
> +	EGLSyncKHR sync_;
> +
> +	int compileShader(int shaderType, GLuint &shaderId, unsigned char *shaderData,
> +			  unsigned int shaderDataLen,
> +			  std::vector<std::string> shaderEnv);
> +
> +	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 0000000000000000000000000000000000000000..b56c65a33837bb31a3e19613f4abb52d77880301
> --- /dev/null
> +++ b/src/libcamera/egl.cpp
> @@ -0,0 +1,408 @@
> +/* 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/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()
> +{
> +}
> +
> +eGL::~eGL()
> +{
> +}
> +
> +int eGL::syncOutput(void)
> +{
> +	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;
> +
> +	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_NONE, EGL_NONE,	/* modifier lo */
> +		EGL_NONE, EGL_NONE,	/* modifier hi */

These two cause eglCreateImageKHR() to fail on this platform. Simply 
replacing it with

         EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT, 0,
         EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT, 0,

does the trick, though.

> +		EGL_NONE,
> +	};
> +
> +	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;
> +			ret = -ENODEV;
> +			goto done;
> +		}
> +	}
> +done:
> +	return ret;
> +}
> +
> +int eGL::createInputDMABufTexture2D(eGLImage *eglImage, int fd)
> +{
> +	return createDMABufTexture2D(eglImage, fd, false);
> +}
> +int eGL::createOutputDMABufTexture2D(eGLImage *eglImage, int fd)
> +{
> +	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)
> +{
> +	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;
> +	}
> +
> +	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)
> +{
> +	if (sync_) {
> +		makeCurrent();
> +		eglDestroySyncKHR(display_, sync_);
> +	}
> +
> +}
> +void eGL::makeCurrent(void)
> +{
> +	if (eglMakeCurrent(display_, EGL_NO_SURFACE, EGL_NO_SURFACE, context_) != EGL_TRUE) {
> +		LOG(eGL, Error) << "eglMakeCurrent fail";
> +	}
> +}
> +
> +void eGL::swapBuffers(void)
> +{
> +	if (eglSwapBuffers(display_, surface_) != EGL_TRUE) {
> +		LOG(eGL, Error) << "eglSwapBuffers fail";
> +	}
> +}
> +
> +void eGL::useProgram(GLuint programId)
> +{
> +	glUseProgram(programId);
> +}
> +
> +void eGL::pushEnv(std::vector<std::string> &shaderEnv, const char *str)
> +{
> +	std::string addStr = str;
> +
> +	addStr.push_back('\n');
> +	shaderEnv.push_back(addStr);
> +}
> +
> +int eGL::compileVertexShader(GLuint &shaderId, unsigned char *shaderData,
> +			     unsigned int shaderDataLen,
> +			     std::vector<std::string> shaderEnv)
> +{
> +	return compileShader(GL_VERTEX_SHADER, shaderId, shaderData, shaderDataLen, shaderEnv);
> +}
> +
> +int eGL::compileFragmentShader(GLuint &shaderId, unsigned char *shaderData,
> +			       unsigned int shaderDataLen,
> +			       std::vector<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,
> +		       std::vector<std::string> shaderEnv)
> +{
> +	GLchar **shaderSourceData;
> +	GLint *shaderDataLengths;
> +	GLint success;
> +	GLsizei count;
> +	size_t i;
> +
> +	count = 1 + shaderEnv.size();
> +	shaderSourceData = new GLchar *[count];
> +	shaderDataLengths = new GLint[count];
> +
> +	// Prefix defines before main body of shader
> +	for (i = 0; i < shaderEnv.size(); i++) {
> +		shaderSourceData[i] = (GLchar *)shaderEnv[i].c_str();
> +		shaderDataLengths[i] = shaderEnv[i].length();
> +	}
> +
> +	// Now the main body of the shader program
> +	shaderSourceData[i] = (GLchar *)shaderData;
> +	shaderDataLengths[i] = shaderDataLen;
> +
> +	// And create the shader
> +	shaderId = glCreateShader(shaderType);
> +	glShaderSource(shaderId, count, shaderSourceData, shaderDataLengths);
> +	glCompileShader(shaderId);
> +
> +	// Check status
> +	glGetShaderiv(shaderId, GL_COMPILE_STATUS, &success);
> +	if (success == GL_FALSE) {
> +		GLint sizeLog = 0;
> +		GLchar *infoLog;
> +
> +		glGetShaderiv(shaderId, GL_INFO_LOG_LENGTH, &sizeLog);
> +		infoLog = new GLchar[sizeLog];
> +
> +		glGetShaderInfoLog(shaderId, sizeLog, &sizeLog, infoLog);
> +		LOG(eGL, Error) << infoLog;
> +
> +		delete[] infoLog;
> +	}
> +
> +	delete[] shaderSourceData;
> +	delete[] shaderDataLengths;
> +
> +	return !(success == GL_TRUE);
> +}
> +
> +void eGL::dumpShaderSource(GLuint shaderId)
> +{
> +	GLint shaderLength = 0;
> +	GLchar *shaderSource;
> +
> +	glGetShaderiv(shaderId, GL_SHADER_SOURCE_LENGTH, &shaderLength);
> +
> +	LOG(eGL, Debug) << "Shader length is " << shaderLength;
> +
> +	if (shaderLength > 0) {
> +		shaderSource = new GLchar[shaderLength];
> +		if (!shaderSource)
> +			return;
> +
> +		glGetShaderSource(shaderId, shaderLength, &shaderLength, shaderSource);
> +		if (shaderLength) {
> +			LOG(eGL, Debug) << "Shader source = " << shaderSource;
> +		}
> +		delete[] shaderSource;
> +	}
> +}
> +
> +int eGL::linkProgram(GLuint &programId, GLuint vertexshaderId, GLuint fragmentshaderId)
> +{
> +	GLint success;
> +	GLenum err;
> +
> +	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:
> +	return -ENODEV;
> +}
> +} // namespace libcamera
> diff --git a/src/libcamera/meson.build b/src/libcamera/meson.build
> index 64270b571cea2ece8d4dfea8a23b8edf60de70d3..705c328a27f68377573916a77573335f4f4ab3cc 100644
> --- a/src/libcamera/meson.build
> +++ b/src/libcamera/meson.build
> @@ -78,6 +78,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')
> @@ -191,7 +212,9 @@ libcamera_deps += [
>       libcamera_base_private,
>       libcrypto,
>       libdl,
> +    libegl,
>       libgbm,
> +    libglesv2,
>       liblttng,
>       libudev,
>       libyaml,
>
Robert Mader Aug. 31, 2025, 1:58 p.m. UTC | #2
On 24.08.25 02:48, Bryan O'Donoghue wrote:
> 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.
>
> Signed-off-by: Bryan O'Donoghue<bryan.odonoghue@linaro.org>
> ---
>   include/libcamera/internal/egl.h | 133 +++++++++++++
>   src/libcamera/egl.cpp            | 408 +++++++++++++++++++++++++++++++++++++++
>   src/libcamera/meson.build        |  23 +++
>   3 files changed, 564 insertions(+)
>
> diff --git a/include/libcamera/internal/egl.h b/include/libcamera/internal/egl.h
> new file mode 100644
> index 0000000000000000000000000000000000000000..2c48e28e14aaa1fa8029dd18662c8ce24e6a550c
> --- /dev/null
> +++ b/include/libcamera/internal/egl.h
> @@ -0,0 +1,133 @@
> +/* 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 <unistd.h>
> +
> +#include <libcamera/base/log.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
> +{
> +public:
> +	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);
> +	}
> +
> +	eGLImage(uint32_t width, uint32_t height, uint32_t bpp, GLenum texture_unit, uint32_t texture_unit_uniform_id)
> +	{
> +		uint32_t stride = ALIGN_TO(width * bpp, 256);
> +
> +		init(width, height, bpp, stride, texture_unit, texture_unit_uniform_id);
> +	}
> +
> +	~eGLImage()
> +	{
> +		glDeleteFramebuffers(1, &fbo_);
> +		glDeleteTextures(1, &texture_);
> +	}
> +
> +	uint32_t width_;
> +	uint32_t height_;
> +	uint32_t stride_;
> +	uint32_t offset_;
> +	uint32_t framesize_;
> +	uint32_t bpp_;
> +	uint32_t texture_unit_uniform_id_;
> +	GLenum texture_unit_;
> +	GLuint texture_;
> +	GLuint fbo_;
> +	EGLImageKHR image_;
> +
> +private:
> +	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
> +{
> +public:
> +	eGL();
> +	~eGL();
> +
> +	int initEGLContext(GBM *gbmContext);
> +	void cleanUp();
> +	int createInputDMABufTexture2D(eGLImage *eglImage, int fd);
> +	int createOutputDMABufTexture2D(eGLImage *eglImage, int fd);
> +	void destroyDMABufTexture(eGLImage *eglImage);
> +	void createTexture2D(eGLImage *eglImage, GLint format, uint32_t width, uint32_t height, void *data);
> +	void createTexture1D(eGLImage *eglImage, GLint format, uint32_t width, void *data);
> +
> +	void pushEnv(std::vector<std::string> &shaderEnv, const char *str);
> +	void makeCurrent();
> +	void swapBuffers();
> +
> +	int compileVertexShader(GLuint &shaderId, unsigned char *shaderData,
> +				unsigned int shaderDataLen,
> +				std::vector<std::string> shaderEnv);
> +	int compileFragmentShader(GLuint &shaderId, unsigned char *shaderData,
> +				  unsigned int shaderDataLen,
> +				  std::vector<std::string> shaderEnv);
> +	int linkProgram(GLuint &programIdd, GLuint fragmentshaderId, GLuint vertexshaderId);
> +	void dumpShaderSource(GLuint shaderId);
> +	void useProgram(GLuint programId);
> +	int syncOutput();
> +
> +private:
> +	int fd_;
> +
> +	EGLDisplay display_;
> +	EGLContext context_;
> +	EGLSurface surface_;
> +	EGLSyncKHR sync_;
> +
> +	int compileShader(int shaderType, GLuint &shaderId, unsigned char *shaderData,
> +			  unsigned int shaderDataLen,
> +			  std::vector<std::string> shaderEnv);
> +
> +	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 0000000000000000000000000000000000000000..b56c65a33837bb31a3e19613f4abb52d77880301
> --- /dev/null
> +++ b/src/libcamera/egl.cpp
> @@ -0,0 +1,408 @@
> +/* 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/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()
> +{
> +}
> +
> +eGL::~eGL()
> +{
> +}
> +
> +int eGL::syncOutput(void)
> +{
> +	glFlush();
> +	eglClientWaitSyncKHR(display_, sync_, 0, EGL_FOREVER_KHR);

I think there are several issue here:

 1. eglClientWaitSyncKHR() with a 0 flag - i.e. no wait condition - is a
    no-op.
 2. The only supported flag in Mesa is EGL_SYNC_FLUSH_COMMANDS_BIT_KHR
    and all it does is triggering a flush if needed - i.e. it should be
    equivalent to the glFlush() above if I'm not mistaken.
    (https://gitlab.freedesktop.org/mesa/mesa/-/blob/9a03aee7035e7f474ac33edc92954d45e68670e4/src/egl/drivers/dri2/egl_dri2.c#L2932-2963)
 3. Flushing alone does not guarantee that the work has actually
    completed, but only that the commands have been send to the GPU.
    That means the output buffer might still not be ready for
    consumption after this command. This is no problem in cases where
    the buffer gets imported into GL/VK/V4L2 etc. by the client -
    assuming a driver with implicit sync support - but can be an issue
    if the buffer is used by a client without implicit sync support -
    most importantly when reading from CPU / mmapping. Note that
    DMA_BUF_IOCTL_SYNC (which a client is expected to use in this case)
    explicitly does *not* cover implicit sync, see the quote below. If
    I'm not mistaken the only clean solution here - without essentially
    breaking API and requiring consumers to support implicit sync - is
    to explicitly wait for the buffer-write to be complete before
    handing over to the client. And the best option I see for that is
    dma_buf_export_sync_file
    <https://www.kernel.org/doc/html/latest/driver-api/dma-buf.html#c.dma_buf_export_sync_file>

 4. If we introduce such waiting, it'll probably make sense to ensure
    that work on the next buffer can already begin - i.e. we'd want
    things to become more async.

Regards,

Robert

> The synchronization provided via DMA_BUF_IOCTL_SYNC only provides 
> cache coherency. It does not prevent other processes or devices from 
> accessing the memory at the same time. If synchronization with a GPU 
> or other device driver is required, it is the client’s responsibility 
> to wait for buffer to be ready for reading or writing before calling 
> this ioctl with DMA_BUF_SYNC_START.
from 
https://www.kernel.org/doc/html/latest/driver-api/dma-buf.html#c.dma_buf_sync

> +
> +	return 0;
> +}
> +
> +// Create linear image attached to previous BO object
> +int eGL::createDMABufTexture2D(eGLImage *eglImage, int fd, bool output)
> +{
> +	int ret = 0;
> +
> +	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_NONE, EGL_NONE,	/* modifier lo */
> +		EGL_NONE, EGL_NONE,	/* modifier hi */
> +		EGL_NONE,
> +	};
> +
> +	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;
> +			ret = -ENODEV;
> +			goto done;
> +		}
> +	}
> +done:
> +	return ret;
> +}
> +
> +int eGL::createInputDMABufTexture2D(eGLImage *eglImage, int fd)
> +{
> +	return createDMABufTexture2D(eglImage, fd, false);
> +}
> +int eGL::createOutputDMABufTexture2D(eGLImage *eglImage, int fd)
> +{
> +	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)
> +{
> +	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;
> +	}
> +
> +	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)
> +{
> +	if (sync_) {
> +		makeCurrent();
> +		eglDestroySyncKHR(display_, sync_);
> +	}
> +
> +}
> +void eGL::makeCurrent(void)
> +{
> +	if (eglMakeCurrent(display_, EGL_NO_SURFACE, EGL_NO_SURFACE, context_) != EGL_TRUE) {
> +		LOG(eGL, Error) << "eglMakeCurrent fail";
> +	}
> +}
> +
> +void eGL::swapBuffers(void)
> +{
> +	if (eglSwapBuffers(display_, surface_) != EGL_TRUE) {
> +		LOG(eGL, Error) << "eglSwapBuffers fail";
> +	}
> +}
> +
> +void eGL::useProgram(GLuint programId)
> +{
> +	glUseProgram(programId);
> +}
> +
> +void eGL::pushEnv(std::vector<std::string> &shaderEnv, const char *str)
> +{
> +	std::string addStr = str;
> +
> +	addStr.push_back('\n');
> +	shaderEnv.push_back(addStr);
> +}
> +
> +int eGL::compileVertexShader(GLuint &shaderId, unsigned char *shaderData,
> +			     unsigned int shaderDataLen,
> +			     std::vector<std::string> shaderEnv)
> +{
> +	return compileShader(GL_VERTEX_SHADER, shaderId, shaderData, shaderDataLen, shaderEnv);
> +}
> +
> +int eGL::compileFragmentShader(GLuint &shaderId, unsigned char *shaderData,
> +			       unsigned int shaderDataLen,
> +			       std::vector<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,
> +		       std::vector<std::string> shaderEnv)
> +{
> +	GLchar **shaderSourceData;
> +	GLint *shaderDataLengths;
> +	GLint success;
> +	GLsizei count;
> +	size_t i;
> +
> +	count = 1 + shaderEnv.size();
> +	shaderSourceData = new GLchar *[count];
> +	shaderDataLengths = new GLint[count];
> +
> +	// Prefix defines before main body of shader
> +	for (i = 0; i < shaderEnv.size(); i++) {
> +		shaderSourceData[i] = (GLchar *)shaderEnv[i].c_str();
> +		shaderDataLengths[i] = shaderEnv[i].length();
> +	}
> +
> +	// Now the main body of the shader program
> +	shaderSourceData[i] = (GLchar *)shaderData;
> +	shaderDataLengths[i] = shaderDataLen;
> +
> +	// And create the shader
> +	shaderId = glCreateShader(shaderType);
> +	glShaderSource(shaderId, count, shaderSourceData, shaderDataLengths);
> +	glCompileShader(shaderId);
> +
> +	// Check status
> +	glGetShaderiv(shaderId, GL_COMPILE_STATUS, &success);
> +	if (success == GL_FALSE) {
> +		GLint sizeLog = 0;
> +		GLchar *infoLog;
> +
> +		glGetShaderiv(shaderId, GL_INFO_LOG_LENGTH, &sizeLog);
> +		infoLog = new GLchar[sizeLog];
> +
> +		glGetShaderInfoLog(shaderId, sizeLog, &sizeLog, infoLog);
> +		LOG(eGL, Error) << infoLog;
> +
> +		delete[] infoLog;
> +	}
> +
> +	delete[] shaderSourceData;
> +	delete[] shaderDataLengths;
> +
> +	return !(success == GL_TRUE);
> +}
> +
> +void eGL::dumpShaderSource(GLuint shaderId)
> +{
> +	GLint shaderLength = 0;
> +	GLchar *shaderSource;
> +
> +	glGetShaderiv(shaderId, GL_SHADER_SOURCE_LENGTH, &shaderLength);
> +
> +	LOG(eGL, Debug) << "Shader length is " << shaderLength;
> +
> +	if (shaderLength > 0) {
> +		shaderSource = new GLchar[shaderLength];
> +		if (!shaderSource)
> +			return;
> +
> +		glGetShaderSource(shaderId, shaderLength, &shaderLength, shaderSource);
> +		if (shaderLength) {
> +			LOG(eGL, Debug) << "Shader source = " << shaderSource;
> +		}
> +		delete[] shaderSource;
> +	}
> +}
> +
> +int eGL::linkProgram(GLuint &programId, GLuint vertexshaderId, GLuint fragmentshaderId)
> +{
> +	GLint success;
> +	GLenum err;
> +
> +	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:
> +	return -ENODEV;
> +}
> +} // namespace libcamera
> diff --git a/src/libcamera/meson.build b/src/libcamera/meson.build
> index 64270b571cea2ece8d4dfea8a23b8edf60de70d3..705c328a27f68377573916a77573335f4f4ab3cc 100644
> --- a/src/libcamera/meson.build
> +++ b/src/libcamera/meson.build
> @@ -78,6 +78,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')
> @@ -191,7 +212,9 @@ libcamera_deps += [
>       libcamera_base_private,
>       libcrypto,
>       libdl,
> +    libegl,
>       libgbm,
> +    libglesv2,
>       liblttng,
>       libudev,
>       libyaml,
>
Robert Mader Aug. 31, 2025, 2:56 p.m. UTC | #3
On 31.08.25 15:58, Robert Mader wrote:
>
> I think there are several issue here:
>
>  1. eglClientWaitSyncKHR() with a 0 flag - i.e. no wait condition - is
>     a no-op.
>  2. The only supported flag in Mesa is EGL_SYNC_FLUSH_COMMANDS_BIT_KHR
>     and all it does is triggering a flush if needed - i.e. it should
>     be equivalent to the glFlush() above if I'm not mistaken.
>     (https://gitlab.freedesktop.org/mesa/mesa/-/blob/9a03aee7035e7f474ac33edc92954d45e68670e4/src/egl/drivers/dri2/egl_dri2.c#L2932-2963)
>  3. Flushing alone does not guarantee that the work has actually
>     completed, but only that the commands have been send to the GPU.
>     That means the output buffer might still not be ready for
>     consumption after this command. This is no problem in cases where
>     the buffer gets imported into GL/VK/V4L2 etc. by the client -
>     assuming a driver with implicit sync support - but can be an issue
>     if the buffer is used by a client without implicit sync support -
>     most importantly when reading from CPU / mmapping. Note that
>     DMA_BUF_IOCTL_SYNC (which a client is expected to use in this
>     case) explicitly does *not* cover implicit sync, see the quote
>     below. If I'm not mistaken the only clean solution here - without
>     essentially breaking API and requiring consumers to support
>     implicit sync - is to explicitly wait for the buffer-write to be
>     complete before handing over to the client. And the best option I
>     see for that is dma_buf_export_sync_file
>     <https://www.kernel.org/doc/html/latest/driver-api/dma-buf.html#c.dma_buf_export_sync_file>
>
>  4. If we introduce such waiting, it'll probably make sense to ensure
>     that work on the next buffer can already begin - i.e. we'd want
>     things to become more async.
>
> Regards,
>
> Robert
>
Hm, actually calling glFinish() should do the trick IIUC (instead of 
using dma_buf_export_sync_file).

> glFinish does not return until the effects of all previously called GL 
> commands are complete. Such effects include all changes to GL state, 
> all changes to connection state, and all changes to the frame buffer 
> contents.
Robert Mader Aug. 31, 2025, 3:26 p.m. UTC | #4
Urgh, I was wrong, sorry for the noise. EGL_SYNC_FENCE_KHR implies 
EGL_SYNC_PRIOR_COMMANDS_COMPLETE_KHR, which again should block until 
writing to the buffer is complete. So this is all correct.

The only comment I've left is that since syncOutput() is called in 
debayerGPU(), making sure that stats_->processFrame() can run in another 
thread in parallel, as mentioned in another mail before, would likely be 
a worthwhile optimization.

On 31.08.25 16:56, Robert Mader wrote:
> On 31.08.25 15:58, Robert Mader wrote:
>>
>> I think there are several issue here:
>>
>>  1. eglClientWaitSyncKHR() with a 0 flag - i.e. no wait condition -
>>     is a no-op.
>>  2. The only supported flag in Mesa is
>>     EGL_SYNC_FLUSH_COMMANDS_BIT_KHR and all it does is triggering a
>>     flush if needed - i.e. it should be equivalent to the glFlush()
>>     above if I'm not mistaken.
>>     (https://gitlab.freedesktop.org/mesa/mesa/-/blob/9a03aee7035e7f474ac33edc92954d45e68670e4/src/egl/drivers/dri2/egl_dri2.c#L2932-2963)
>>  3. Flushing alone does not guarantee that the work has actually
>>     completed, but only that the commands have been send to the GPU.
>>     That means the output buffer might still not be ready for
>>     consumption after this command. This is no problem in cases where
>>     the buffer gets imported into GL/VK/V4L2 etc. by the client -
>>     assuming a driver with implicit sync support - but can be an
>>     issue if the buffer is used by a client without implicit sync
>>     support - most importantly when reading from CPU / mmapping. Note
>>     that DMA_BUF_IOCTL_SYNC (which a client is expected to use in
>>     this case) explicitly does *not* cover implicit sync, see the
>>     quote below. If I'm not mistaken the only clean solution here -
>>     without essentially breaking API and requiring consumers to
>>     support implicit sync - is to explicitly wait for the
>>     buffer-write to be complete before handing over to the client.
>>     And the best option I see for that is dma_buf_export_sync_file
>>     <https://www.kernel.org/doc/html/latest/driver-api/dma-buf.html#c.dma_buf_export_sync_file>
>>
>>  4. If we introduce such waiting, it'll probably make sense to ensure
>>     that work on the next buffer can already begin - i.e. we'd want
>>     things to become more async.
>>
>> Regards,
>>
>> Robert
>>
> Hm, actually calling glFinish() should do the trick IIUC (instead of 
> using dma_buf_export_sync_file).
>
>> glFinish does not return until the effects of all previously called 
>> GL commands are complete. Such effects include all changes to GL 
>> state, all changes to connection state, and all changes to the frame 
>> buffer contents.

Patch
diff mbox series

diff --git a/include/libcamera/internal/egl.h b/include/libcamera/internal/egl.h
new file mode 100644
index 0000000000000000000000000000000000000000..2c48e28e14aaa1fa8029dd18662c8ce24e6a550c
--- /dev/null
+++ b/include/libcamera/internal/egl.h
@@ -0,0 +1,133 @@ 
+/* 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 <unistd.h>
+
+#include <libcamera/base/log.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
+{
+public:
+	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);
+	}
+
+	eGLImage(uint32_t width, uint32_t height, uint32_t bpp, GLenum texture_unit, uint32_t texture_unit_uniform_id)
+	{
+		uint32_t stride = ALIGN_TO(width * bpp, 256);
+
+		init(width, height, bpp, stride, texture_unit, texture_unit_uniform_id);
+	}
+
+	~eGLImage()
+	{
+		glDeleteFramebuffers(1, &fbo_);
+		glDeleteTextures(1, &texture_);
+	}
+
+	uint32_t width_;
+	uint32_t height_;
+	uint32_t stride_;
+	uint32_t offset_;
+	uint32_t framesize_;
+	uint32_t bpp_;
+	uint32_t texture_unit_uniform_id_;
+	GLenum texture_unit_;
+	GLuint texture_;
+	GLuint fbo_;
+	EGLImageKHR image_;
+
+private:
+	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
+{
+public:
+	eGL();
+	~eGL();
+
+	int initEGLContext(GBM *gbmContext);
+	void cleanUp();
+	int createInputDMABufTexture2D(eGLImage *eglImage, int fd);
+	int createOutputDMABufTexture2D(eGLImage *eglImage, int fd);
+	void destroyDMABufTexture(eGLImage *eglImage);
+	void createTexture2D(eGLImage *eglImage, GLint format, uint32_t width, uint32_t height, void *data);
+	void createTexture1D(eGLImage *eglImage, GLint format, uint32_t width, void *data);
+
+	void pushEnv(std::vector<std::string> &shaderEnv, const char *str);
+	void makeCurrent();
+	void swapBuffers();
+
+	int compileVertexShader(GLuint &shaderId, unsigned char *shaderData,
+				unsigned int shaderDataLen,
+				std::vector<std::string> shaderEnv);
+	int compileFragmentShader(GLuint &shaderId, unsigned char *shaderData,
+				  unsigned int shaderDataLen,
+				  std::vector<std::string> shaderEnv);
+	int linkProgram(GLuint &programIdd, GLuint fragmentshaderId, GLuint vertexshaderId);
+	void dumpShaderSource(GLuint shaderId);
+	void useProgram(GLuint programId);
+	int syncOutput();
+
+private:
+	int fd_;
+
+	EGLDisplay display_;
+	EGLContext context_;
+	EGLSurface surface_;
+	EGLSyncKHR sync_;
+
+	int compileShader(int shaderType, GLuint &shaderId, unsigned char *shaderData,
+			  unsigned int shaderDataLen,
+			  std::vector<std::string> shaderEnv);
+
+	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 0000000000000000000000000000000000000000..b56c65a33837bb31a3e19613f4abb52d77880301
--- /dev/null
+++ b/src/libcamera/egl.cpp
@@ -0,0 +1,408 @@ 
+/* 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/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()
+{
+}
+
+eGL::~eGL()
+{
+}
+
+int eGL::syncOutput(void)
+{
+	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;
+
+	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_NONE, EGL_NONE,	/* modifier lo */
+		EGL_NONE, EGL_NONE,	/* modifier hi */
+		EGL_NONE,
+	};
+
+	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;
+			ret = -ENODEV;
+			goto done;
+		}
+	}
+done:
+	return ret;
+}
+
+int eGL::createInputDMABufTexture2D(eGLImage *eglImage, int fd)
+{
+	return createDMABufTexture2D(eglImage, fd, false);
+}
+int eGL::createOutputDMABufTexture2D(eGLImage *eglImage, int fd)
+{
+	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)
+{
+	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;
+	}
+
+	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)
+{
+	if (sync_) {
+		makeCurrent();
+		eglDestroySyncKHR(display_, sync_);
+	}
+
+}
+void eGL::makeCurrent(void)
+{
+	if (eglMakeCurrent(display_, EGL_NO_SURFACE, EGL_NO_SURFACE, context_) != EGL_TRUE) {
+		LOG(eGL, Error) << "eglMakeCurrent fail";
+	}
+}
+
+void eGL::swapBuffers(void)
+{
+	if (eglSwapBuffers(display_, surface_) != EGL_TRUE) {
+		LOG(eGL, Error) << "eglSwapBuffers fail";
+	}
+}
+
+void eGL::useProgram(GLuint programId)
+{
+	glUseProgram(programId);
+}
+
+void eGL::pushEnv(std::vector<std::string> &shaderEnv, const char *str)
+{
+	std::string addStr = str;
+
+	addStr.push_back('\n');
+	shaderEnv.push_back(addStr);
+}
+
+int eGL::compileVertexShader(GLuint &shaderId, unsigned char *shaderData,
+			     unsigned int shaderDataLen,
+			     std::vector<std::string> shaderEnv)
+{
+	return compileShader(GL_VERTEX_SHADER, shaderId, shaderData, shaderDataLen, shaderEnv);
+}
+
+int eGL::compileFragmentShader(GLuint &shaderId, unsigned char *shaderData,
+			       unsigned int shaderDataLen,
+			       std::vector<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,
+		       std::vector<std::string> shaderEnv)
+{
+	GLchar **shaderSourceData;
+	GLint *shaderDataLengths;
+	GLint success;
+	GLsizei count;
+	size_t i;
+
+	count = 1 + shaderEnv.size();
+	shaderSourceData = new GLchar *[count];
+	shaderDataLengths = new GLint[count];
+
+	// Prefix defines before main body of shader
+	for (i = 0; i < shaderEnv.size(); i++) {
+		shaderSourceData[i] = (GLchar *)shaderEnv[i].c_str();
+		shaderDataLengths[i] = shaderEnv[i].length();
+	}
+
+	// Now the main body of the shader program
+	shaderSourceData[i] = (GLchar *)shaderData;
+	shaderDataLengths[i] = shaderDataLen;
+
+	// And create the shader
+	shaderId = glCreateShader(shaderType);
+	glShaderSource(shaderId, count, shaderSourceData, shaderDataLengths);
+	glCompileShader(shaderId);
+
+	// Check status
+	glGetShaderiv(shaderId, GL_COMPILE_STATUS, &success);
+	if (success == GL_FALSE) {
+		GLint sizeLog = 0;
+		GLchar *infoLog;
+
+		glGetShaderiv(shaderId, GL_INFO_LOG_LENGTH, &sizeLog);
+		infoLog = new GLchar[sizeLog];
+
+		glGetShaderInfoLog(shaderId, sizeLog, &sizeLog, infoLog);
+		LOG(eGL, Error) << infoLog;
+
+		delete[] infoLog;
+	}
+
+	delete[] shaderSourceData;
+	delete[] shaderDataLengths;
+
+	return !(success == GL_TRUE);
+}
+
+void eGL::dumpShaderSource(GLuint shaderId)
+{
+	GLint shaderLength = 0;
+	GLchar *shaderSource;
+
+	glGetShaderiv(shaderId, GL_SHADER_SOURCE_LENGTH, &shaderLength);
+
+	LOG(eGL, Debug) << "Shader length is " << shaderLength;
+
+	if (shaderLength > 0) {
+		shaderSource = new GLchar[shaderLength];
+		if (!shaderSource)
+			return;
+
+		glGetShaderSource(shaderId, shaderLength, &shaderLength, shaderSource);
+		if (shaderLength) {
+			LOG(eGL, Debug) << "Shader source = " << shaderSource;
+		}
+		delete[] shaderSource;
+	}
+}
+
+int eGL::linkProgram(GLuint &programId, GLuint vertexshaderId, GLuint fragmentshaderId)
+{
+	GLint success;
+	GLenum err;
+
+	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:
+	return -ENODEV;
+}
+} // namespace libcamera
diff --git a/src/libcamera/meson.build b/src/libcamera/meson.build
index 64270b571cea2ece8d4dfea8a23b8edf60de70d3..705c328a27f68377573916a77573335f4f4ab3cc 100644
--- a/src/libcamera/meson.build
+++ b/src/libcamera/meson.build
@@ -78,6 +78,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')
@@ -191,7 +212,9 @@  libcamera_deps += [
     libcamera_base_private,
     libcrypto,
     libdl,
+    libegl,
     libgbm,
+    libglesv2,
     liblttng,
     libudev,
     libyaml,