[22/35] libcamera: software_isp: egl: Introduce an eGL base helper class
diff mbox series

Message ID 20250611013245.133785-23-bryan.odonoghue@linaro.org
State New
Headers show
Series
  • Add GLES 2.0 GPUISP to libcamera
Related show

Commit Message

Bryan O'Donoghue June 11, 2025, 1:32 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 | 110 +++++++++
 src/libcamera/egl.cpp            | 369 +++++++++++++++++++++++++++++++
 src/libcamera/meson.build        |  23 ++
 3 files changed, 502 insertions(+)
 create mode 100644 include/libcamera/internal/egl.h
 create mode 100644 src/libcamera/egl.cpp

Comments

Bryan O'Donoghue June 16, 2025, 7:15 p.m. UTC | #1
On 11/06/2025 02:32, 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 | 110 +++++++++
>   src/libcamera/egl.cpp            | 369 +++++++++++++++++++++++++++++++
>   src/libcamera/meson.build        |  23 ++
>   3 files changed, 502 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 00000000..04d637d8
> --- /dev/null
> +++ b/include/libcamera/internal/egl.h
> @@ -0,0 +1,110 @@
> +/* 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, GLenum texture_unit, uint32_t texture_unit_uniform_id)
> +	{
> +		image_ = EGL_NO_IMAGE_KHR;
> +		width_ = width;
> +		height_ = height;
> +		bpp_ = bpp;
> +		stride_ = width_ * bpp_ / 4;
> +		framesize_ = stride_ * height_;
> +		texture_unit_ = texture_unit;
> +		texture_unit_uniform_id_ = texture_unit_uniform_id;
> +
> +		glGenTextures(1, &texture_);
> +	}
> +
> +	~eGLImage()
> +	{
> +		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_;
> +	EGLImageKHR image_;
> +};
> +
> +class eGL
> +{
> +public:
> +	eGL();
> +	~eGL();
> +
> +	int initEGLContext(GBM *gbmContext);
> +	int createDMABufTexture2D(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);
> +
> +private:
> +	int fd_;
> +
> +	EGLDisplay display_;
> +	EGLContext context_;
> +	EGLSurface surface_;
> +
> +	int compileShader(int shaderType, GLuint &shaderId, unsigned char *shaderData,
> +			  unsigned int shaderDataLen,
> +			  std::vector<std::string> shaderEnv);
> +
> +	PFNEGLEXPORTDMABUFIMAGEMESAPROC eglExportDMABUFImageMESA;
> +	PFNGLEGLIMAGETARGETTEXTURE2DOESPROC glEGLImageTargetTexture2DOES;
> +
> +	PFNEGLCREATEIMAGEKHRPROC eglCreateImageKHR;
> +	PFNEGLDESTROYIMAGEKHRPROC eglDestroyImageKHR;
> +
> +	PFNEGLCLIENTWAITSYNCKHRPROC eglClientWaitSyncKHR;
> +	PFNEGLCREATESYNCKHRPROC eglCreateSyncKHR;
> +};
> +} //namespace libcamera
> diff --git a/src/libcamera/egl.cpp b/src/libcamera/egl.cpp
> new file mode 100644
> index 00000000..89ece148
> --- /dev/null
> +++ b/src/libcamera/egl.cpp
> @@ -0,0 +1,369 @@
> +/* 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 <linux/dma-buf.h>
> +#include <linux/dma-heap.h>
> +
> +namespace libcamera {
> +
> +LOG_DEFINE_CATEGORY(eGL)
> +
> +eGL::eGL()
> +{
> +}
> +
> +eGL::~eGL()
> +{
> +}

Defaults can be dropped.

> +// Create linear image attached to previous BO object
> +int eGL::createDMABufTexture2D(eGLImage *eglImage, int fd)
> +{
> +	int ret = 0;
> +
> +	eglImage->stride_ = eglImage->width_ * eglImage->height_;
> +	eglImage->offset_ = 0;
> +	eglImage->framesize_ = eglImage->height_ * eglImage->stride_;
> +
> +	LOG(eGL, Info)
> +		<< " stride " << eglImage->stride_ << " width " << eglImage->width_ << " height " << eglImage->height_ << " offset " << eglImage->offset_ << " framesize " << eglImage->framesize_;
> +
> +	// TODO: use the dma buf handle from udma heap here directly
> +	// should work for both input and output with fencing
> +	EGLint image_attrs[] = {
> +		EGL_WIDTH, (EGLint)eglImage->width_,
> +		EGL_HEIGHT, (EGLint)eglImage->height_,
> +		EGL_LINUX_DRM_FOURCC_EXT, (int)GBM_FORMAT_ARGB8888,
> +		EGL_DMA_BUF_PLANE0_FD_EXT, fd,
> +		EGL_DMA_BUF_PLANE0_OFFSET_EXT, 0,
> +		EGL_DMA_BUF_PLANE0_PITCH_EXT, (EGLint)eglImage->framesize_,
> +		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;
> +	}
> +
> +	// Generate texture, bind, associate image to texture, configure, unbind
> +	glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, eglImage->image_);
> +	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
> +	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

Not to self don't forget to set these parameters for GL_TEXTURE_1D.

> +
> +done:
> +	return ret;
> +}
> +
> +void eGL::destroyDMABufTexture(eGLImage *eglImage)
> +{
> +	eglDestroyImage(display_, eglImage->image_);
> +}
> +
> +//
> +// Generate a 2D texture from an input buffer directly
> +void eGL::createTexture2D(eGLImage *eglImage, 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, GL_LUMINANCE, width, height, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, data);

Looks like squashing Milan's change for the format - GL_LUMINANCE hasn't 
made it into this patch.

> +	// 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;
> +	}
> +
> +	//TODO: use optional eglGetPlatformDisplayEXT ?
> +	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);
> +
> +	//TODO: interrogate strings to make sure we aren't hooking unsupported functions
> +	//      and remember to error out if a function we depend on isn't found.
> +	//      we don't use these functions right now but expect to for DMA backed
> +	//      texture generation and render-to-texture. One thing we can do is differentiate
> +	//      between DMA and non-DMA texture generation based on the presence of these functions
> +	//      In reality most - all ? - mesa implementations have these extensions so
> +	//      probably no fallback will be required
> +	eglCreateImageKHR = (PFNEGLCREATEIMAGEKHRPROC)eglGetProcAddress("eglCreateImageKHR");
> +	if (!eglCreateImageKHR)
> +		LOG(eGL, Warning) << "eglCreateImageKHR not found";
> +
> +	eglDestroyImageKHR = (PFNEGLDESTROYIMAGEKHRPROC)eglGetProcAddress("eglDestroyImageKHR");
> +	if (!eglDestroyImageKHR)
> +		LOG(eGL, Warning) << "eglDestroyImageKHR not found";
> +
> +	eglExportDMABUFImageMESA = (PFNEGLEXPORTDMABUFIMAGEMESAPROC)eglGetProcAddress("eglExportDMABUFImageMESA");
> +	if (!eglExportDMABUFImageMESA)
> +		LOG(eGL, Warning) << "eglExportDMABUFImageMESA not found";
> +
> +	glEGLImageTargetTexture2DOES = (PFNGLEGLIMAGETARGETTEXTURE2DOESPROC)eglGetProcAddress("glEGLImageTargetTexture2DOES");
> +	if (!glEGLImageTargetTexture2DOES)
> +		LOG(eGL, Warning) << "glEGLImageTargetTexture2DOES not found";
> +
> +	eglClientWaitSyncKHR = (PFNEGLCLIENTWAITSYNCKHRPROC)eglGetProcAddress("eglClientWaitSyncKHR");
> +	if (!eglClientWaitSyncKHR)
> +		LOG(eGL, Warning) << "eglClientWaitSyncKHR not found";
> +
> +	eglCreateSyncKHR = (PFNEGLCREATESYNCKHRPROC)eglGetProcAddress("eglCreateSyncKHR");
> +	if (!eglCreateSyncKHR)
> +		LOG(eGL, Warning) << "eglCreateSyncKHR not found";
> +
> +	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;
> +	}
> +
> +	surface_ = eglCreateWindowSurface(display_, config,
> +					  (EGLNativeWindowType)gbmContext->getSurface(),
> +					  NULL);
> +	if (surface_ == EGL_NO_SURFACE) {
> +		LOG(eGL, Error) << "eglCreateWindowSurface fail";
> +		goto fail;
> +	}
> +
> +	makeCurrent();
> +	swapBuffers();
> +
> +	return 0;
> +fail:
> +
> +	return -ENODEV;
> +}
> +
> +void eGL::makeCurrent(void)
> +{
> +	if (eglMakeCurrent(display_, surface_, 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 0d004694..491eb734 100644
> --- a/src/libcamera/meson.build
> +++ b/src/libcamera/meson.build
> @@ -77,6 +77,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)
Recall Laurent said dependency() was the preferred way.

---
bod
Barnabás Pőcze June 17, 2025, 12:33 p.m. UTC | #2
Hi

2025. 06. 11. 3:32 keltezéssel, Bryan O'Donoghue írta:
> 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 | 110 +++++++++
>   src/libcamera/egl.cpp            | 369 +++++++++++++++++++++++++++++++
>   src/libcamera/meson.build        |  23 ++
>   3 files changed, 502 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 00000000..04d637d8
> --- /dev/null
> +++ b/include/libcamera/internal/egl.h
> @@ -0,0 +1,110 @@
> +/* 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, GLenum texture_unit, uint32_t texture_unit_uniform_id)

Could you use the member init list?


> +	{
> +		image_ = EGL_NO_IMAGE_KHR;
> +		width_ = width;
> +		height_ = height;
> +		bpp_ = bpp;
> +		stride_ = width_ * bpp_ / 4;
> +		framesize_ = stride_ * height_;
> +		texture_unit_ = texture_unit;
> +		texture_unit_uniform_id_ = texture_unit_uniform_id;
> +
> +		glGenTextures(1, &texture_);
> +	}
> +
> +	~eGLImage()
> +	{
> +		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_;
> +	EGLImageKHR image_;

  = EGL_NO_IMAGE_KHR;



> +};

Copy ctor/assignment should be deleted; move ctor/assignment should be implemented or deleted (if not needed).


> +
> +class eGL
> +{
> +public:
> +	eGL();
> +	~eGL();
> +
> +	int initEGLContext(GBM *gbmContext);
> +	int createDMABufTexture2D(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);

Span<const std::string> ?


> +	int compileFragmentShader(GLuint &shaderId, unsigned char *shaderData,
> +				  unsigned int shaderDataLen,
> +				  std::vector<std::string> shaderEnv);

And here?


> +	int linkProgram(GLuint &programIdd, GLuint fragmentshaderId, GLuint vertexshaderId);

programIdd -> programId


> +	void dumpShaderSource(GLuint shaderId);
> +	void useProgram(GLuint programId);
> +
> +private:
> +	int fd_;
> +
> +	EGLDisplay display_;
> +	EGLContext context_;
> +	EGLSurface surface_;
> +
> +	int compileShader(int shaderType, GLuint &shaderId, unsigned char *shaderData,
> +			  unsigned int shaderDataLen,
> +			  std::vector<std::string> shaderEnv);

Span<const std::string> here as well?


> +
> +	PFNEGLEXPORTDMABUFIMAGEMESAPROC eglExportDMABUFImageMESA;
> +	PFNGLEGLIMAGETARGETTEXTURE2DOESPROC glEGLImageTargetTexture2DOES;
> +
> +	PFNEGLCREATEIMAGEKHRPROC eglCreateImageKHR;
> +	PFNEGLDESTROYIMAGEKHRPROC eglDestroyImageKHR;
> +
> +	PFNEGLCLIENTWAITSYNCKHRPROC eglClientWaitSyncKHR;
> +	PFNEGLCREATESYNCKHRPROC eglCreateSyncKHR;
> +};

Copy ctor/assignment should be deleted; move ctor/assignment should be implemented or deleted (if not needed).


> +} //namespace libcamera
> diff --git a/src/libcamera/egl.cpp b/src/libcamera/egl.cpp
> new file mode 100644
> index 00000000..89ece148
> --- /dev/null
> +++ b/src/libcamera/egl.cpp
> @@ -0,0 +1,369 @@
> +/* 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 <linux/dma-buf.h>
> +#include <linux/dma-heap.h>
> +
> +namespace libcamera {
> +
> +LOG_DEFINE_CATEGORY(eGL)
> +
> +eGL::eGL()
> +{
> +}
> +
> +eGL::~eGL()
> +{
> +}
> +
> +// Create linear image attached to previous BO object
> +int eGL::createDMABufTexture2D(eGLImage *eglImage, int fd)
> +{
> +	int ret = 0;
> +
> +	eglImage->stride_ = eglImage->width_ * eglImage->height_;
> +	eglImage->offset_ = 0;
> +	eglImage->framesize_ = eglImage->height_ * eglImage->stride_;
> +
> +	LOG(eGL, Info)
> +		<< " stride " << eglImage->stride_ << " width " << eglImage->width_ << " height " << eglImage->height_ << " offset " << eglImage->offset_ << " framesize " << eglImage->framesize_;
> +
> +	// TODO: use the dma buf handle from udma heap here directly
> +	// should work for both input and output with fencing
> +	EGLint image_attrs[] = {> +		EGL_WIDTH, (EGLint)eglImage->width_,
> +		EGL_HEIGHT, (EGLint)eglImage->height_,
> +		EGL_LINUX_DRM_FOURCC_EXT, (int)GBM_FORMAT_ARGB8888,
> +		EGL_DMA_BUF_PLANE0_FD_EXT, fd,
> +		EGL_DMA_BUF_PLANE0_OFFSET_EXT, 0,
> +		EGL_DMA_BUF_PLANE0_PITCH_EXT, (EGLint)eglImage->framesize_,
> +		EGL_NONE, EGL_NONE,	/* modifier lo */
> +		EGL_NONE, EGL_NONE,	/* modifier hi */
> +		EGL_NONE,
> +	};


I think if you add `// clang-format off` and `// clang-format on` around it
and then checkstyle.py should not complain.


> +
> +	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;
> +	}
> +
> +	// Generate texture, bind, associate image to texture, configure, unbind
> +	glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, eglImage->image_);
> +	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
> +	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
> +
> +done:
> +	return ret;
> +}
> +
> +void eGL::destroyDMABufTexture(eGLImage *eglImage)
> +{
> +	eglDestroyImage(display_, eglImage->image_);
> +}
> +
> +//
> +// Generate a 2D texture from an input buffer directly
> +void eGL::createTexture2D(eGLImage *eglImage, 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, GL_LUMINANCE, width, height, 0, GL_LUMINANCE, 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;
> +	}
> +
> +	//TODO: use optional eglGetPlatformDisplayEXT ?
> +	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);
> +
> +	//TODO: interrogate strings to make sure we aren't hooking unsupported functions
> +	//      and remember to error out if a function we depend on isn't found.
> +	//      we don't use these functions right now but expect to for DMA backed
> +	//      texture generation and render-to-texture. One thing we can do is differentiate
> +	//      between DMA and non-DMA texture generation based on the presence of these functions
> +	//      In reality most - all ? - mesa implementations have these extensions so
> +	//      probably no fallback will be required
> +	eglCreateImageKHR = (PFNEGLCREATEIMAGEKHRPROC)eglGetProcAddress("eglCreateImageKHR");
> +	if (!eglCreateImageKHR)
> +		LOG(eGL, Warning) << "eglCreateImageKHR not found";
> +
> +	eglDestroyImageKHR = (PFNEGLDESTROYIMAGEKHRPROC)eglGetProcAddress("eglDestroyImageKHR");
> +	if (!eglDestroyImageKHR)
> +		LOG(eGL, Warning) << "eglDestroyImageKHR not found";
> +
> +	eglExportDMABUFImageMESA = (PFNEGLEXPORTDMABUFIMAGEMESAPROC)eglGetProcAddress("eglExportDMABUFImageMESA");
> +	if (!eglExportDMABUFImageMESA)
> +		LOG(eGL, Warning) << "eglExportDMABUFImageMESA not found";
> +
> +	glEGLImageTargetTexture2DOES = (PFNGLEGLIMAGETARGETTEXTURE2DOESPROC)eglGetProcAddress("glEGLImageTargetTexture2DOES");
> +	if (!glEGLImageTargetTexture2DOES)
> +		LOG(eGL, Warning) << "glEGLImageTargetTexture2DOES not found";
> +
> +	eglClientWaitSyncKHR = (PFNEGLCLIENTWAITSYNCKHRPROC)eglGetProcAddress("eglClientWaitSyncKHR");
> +	if (!eglClientWaitSyncKHR)
> +		LOG(eGL, Warning) << "eglClientWaitSyncKHR not found";
> +
> +	eglCreateSyncKHR = (PFNEGLCREATESYNCKHRPROC)eglGetProcAddress("eglCreateSyncKHR");
> +	if (!eglCreateSyncKHR)
> +		LOG(eGL, Warning) << "eglCreateSyncKHR not found";
> +
> +	if (eglChooseConfig(display_, configAttribs, &config, 1, &numConfigs) != EGL_TRUE) {
> +		LOG(eGL, Error) << "eglChooseConfig fail";
> +		goto fail;
> +	}
> +
> +	context_ = eglCreateContext(display_, config, EGL_NO_CONTEXT, contextAttribs);

Does this not need `eglDestroyContext()` or similar in `~eGL()`?


> +	if (context_ == EGL_NO_CONTEXT) {
> +		LOG(eGL, Error) << "eglContext returned EGL_NO_CONTEXT";
> +		goto fail;
> +	}
> +
> +	surface_ = eglCreateWindowSurface(display_, config,
> +					  (EGLNativeWindowType)gbmContext->getSurface(),
> +					  NULL);

Same question here with `eglDestroySurface()`?


> +	if (surface_ == EGL_NO_SURFACE) {
> +		LOG(eGL, Error) << "eglCreateWindowSurface fail";
> +		goto fail;
> +	}
> +
> +	makeCurrent();
> +	swapBuffers();
> +
> +	return 0;
> +fail:
> +
> +	return -ENODEV;
> +}
> +
> +void eGL::makeCurrent(void)
> +{
> +	if (eglMakeCurrent(display_, surface_, 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);

std::move(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];

auto shaderSourceData = std::make_unique<const GLchar *[]>(count);
etc.


> +
> +	// Prefix defines before main body of shader
> +	for (i = 0; i < shaderEnv.size(); i++) {
> +		shaderSourceData[i] = (GLchar *)shaderEnv[i].c_str();

The above cast shouldn't be needed if you change the type. `glShaderSource()`
should take `const GLchar * const *`, so if you have `const GLchar **`,
that should work.


> +		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];

Same here.


> +
> +		glGetShaderInfoLog(shaderId, sizeLog, &sizeLog, infoLog);
> +		LOG(eGL, Error) << infoLog;
> +
> +		delete[] infoLog;
> +	}
> +
> +	delete[] shaderSourceData;
> +	delete[] shaderDataLengths;
> +
> +	return !(success == GL_TRUE);

Should `glDeleteShader()` be called in case of failure?


> +}
> +
> +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];

Same here.


> +		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;

I think there should be another failure label that calls `glDeleteProgram()`.


> +
> +	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];

std::unique_ptr<GLchar[]> here as well.


> +
> +		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 0d004694..491eb734 100644
> --- a/src/libcamera/meson.build
> +++ b/src/libcamera/meson.build
> @@ -77,6 +77,27 @@ if libgbm.found() and gbm_works
>       ])
>   endif
>   
> +libegl = cc.find_library('EGL', required : false)

egl_dep = dependency('egl', required: false) ?


> +libglesv2 = cc.find_library('GLESv2', required : false)
> +mesa_works = cc.check_header('EGL/egl.h', required: false)

cc.check_header(..., dependency: egl_dep) ?


> +
> +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')
> @@ -198,7 +219,9 @@ libcamera_deps += [
>       libcamera_base_private,
>       libcrypto,
>       libdl,
> +    libegl,
>       libgbm,
> +    libglesv2,
>       liblttng,
>       libudev,
>       libyaml,


Regards,
Barnabás Pőcze

Patch
diff mbox series

diff --git a/include/libcamera/internal/egl.h b/include/libcamera/internal/egl.h
new file mode 100644
index 00000000..04d637d8
--- /dev/null
+++ b/include/libcamera/internal/egl.h
@@ -0,0 +1,110 @@ 
+/* 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, GLenum texture_unit, uint32_t texture_unit_uniform_id)
+	{
+		image_ = EGL_NO_IMAGE_KHR;
+		width_ = width;
+		height_ = height;
+		bpp_ = bpp;
+		stride_ = width_ * bpp_ / 4;
+		framesize_ = stride_ * height_;
+		texture_unit_ = texture_unit;
+		texture_unit_uniform_id_ = texture_unit_uniform_id;
+
+		glGenTextures(1, &texture_);
+	}
+
+	~eGLImage()
+	{
+		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_;
+	EGLImageKHR image_;
+};
+
+class eGL
+{
+public:
+	eGL();
+	~eGL();
+
+	int initEGLContext(GBM *gbmContext);
+	int createDMABufTexture2D(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);
+
+private:
+	int fd_;
+
+	EGLDisplay display_;
+	EGLContext context_;
+	EGLSurface surface_;
+
+	int compileShader(int shaderType, GLuint &shaderId, unsigned char *shaderData,
+			  unsigned int shaderDataLen,
+			  std::vector<std::string> shaderEnv);
+
+	PFNEGLEXPORTDMABUFIMAGEMESAPROC eglExportDMABUFImageMESA;
+	PFNGLEGLIMAGETARGETTEXTURE2DOESPROC glEGLImageTargetTexture2DOES;
+
+	PFNEGLCREATEIMAGEKHRPROC eglCreateImageKHR;
+	PFNEGLDESTROYIMAGEKHRPROC eglDestroyImageKHR;
+
+	PFNEGLCLIENTWAITSYNCKHRPROC eglClientWaitSyncKHR;
+	PFNEGLCREATESYNCKHRPROC eglCreateSyncKHR;
+};
+} //namespace libcamera
diff --git a/src/libcamera/egl.cpp b/src/libcamera/egl.cpp
new file mode 100644
index 00000000..89ece148
--- /dev/null
+++ b/src/libcamera/egl.cpp
@@ -0,0 +1,369 @@ 
+/* 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 <linux/dma-buf.h>
+#include <linux/dma-heap.h>
+
+namespace libcamera {
+
+LOG_DEFINE_CATEGORY(eGL)
+
+eGL::eGL()
+{
+}
+
+eGL::~eGL()
+{
+}
+
+// Create linear image attached to previous BO object
+int eGL::createDMABufTexture2D(eGLImage *eglImage, int fd)
+{
+	int ret = 0;
+
+	eglImage->stride_ = eglImage->width_ * eglImage->height_;
+	eglImage->offset_ = 0;
+	eglImage->framesize_ = eglImage->height_ * eglImage->stride_;
+
+	LOG(eGL, Info)
+		<< " stride " << eglImage->stride_ << " width " << eglImage->width_ << " height " << eglImage->height_ << " offset " << eglImage->offset_ << " framesize " << eglImage->framesize_;
+
+	// TODO: use the dma buf handle from udma heap here directly
+	// should work for both input and output with fencing
+	EGLint image_attrs[] = {
+		EGL_WIDTH, (EGLint)eglImage->width_,
+		EGL_HEIGHT, (EGLint)eglImage->height_,
+		EGL_LINUX_DRM_FOURCC_EXT, (int)GBM_FORMAT_ARGB8888,
+		EGL_DMA_BUF_PLANE0_FD_EXT, fd,
+		EGL_DMA_BUF_PLANE0_OFFSET_EXT, 0,
+		EGL_DMA_BUF_PLANE0_PITCH_EXT, (EGLint)eglImage->framesize_,
+		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;
+	}
+
+	// Generate texture, bind, associate image to texture, configure, unbind
+	glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, eglImage->image_);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+
+done:
+	return ret;
+}
+
+void eGL::destroyDMABufTexture(eGLImage *eglImage)
+{
+	eglDestroyImage(display_, eglImage->image_);
+}
+
+//
+// Generate a 2D texture from an input buffer directly
+void eGL::createTexture2D(eGLImage *eglImage, 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, GL_LUMINANCE, width, height, 0, GL_LUMINANCE, 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;
+	}
+
+	//TODO: use optional eglGetPlatformDisplayEXT ?
+	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);
+
+	//TODO: interrogate strings to make sure we aren't hooking unsupported functions
+	//      and remember to error out if a function we depend on isn't found.
+	//      we don't use these functions right now but expect to for DMA backed
+	//      texture generation and render-to-texture. One thing we can do is differentiate
+	//      between DMA and non-DMA texture generation based on the presence of these functions
+	//      In reality most - all ? - mesa implementations have these extensions so
+	//      probably no fallback will be required
+	eglCreateImageKHR = (PFNEGLCREATEIMAGEKHRPROC)eglGetProcAddress("eglCreateImageKHR");
+	if (!eglCreateImageKHR)
+		LOG(eGL, Warning) << "eglCreateImageKHR not found";
+
+	eglDestroyImageKHR = (PFNEGLDESTROYIMAGEKHRPROC)eglGetProcAddress("eglDestroyImageKHR");
+	if (!eglDestroyImageKHR)
+		LOG(eGL, Warning) << "eglDestroyImageKHR not found";
+
+	eglExportDMABUFImageMESA = (PFNEGLEXPORTDMABUFIMAGEMESAPROC)eglGetProcAddress("eglExportDMABUFImageMESA");
+	if (!eglExportDMABUFImageMESA)
+		LOG(eGL, Warning) << "eglExportDMABUFImageMESA not found";
+
+	glEGLImageTargetTexture2DOES = (PFNGLEGLIMAGETARGETTEXTURE2DOESPROC)eglGetProcAddress("glEGLImageTargetTexture2DOES");
+	if (!glEGLImageTargetTexture2DOES)
+		LOG(eGL, Warning) << "glEGLImageTargetTexture2DOES not found";
+
+	eglClientWaitSyncKHR = (PFNEGLCLIENTWAITSYNCKHRPROC)eglGetProcAddress("eglClientWaitSyncKHR");
+	if (!eglClientWaitSyncKHR)
+		LOG(eGL, Warning) << "eglClientWaitSyncKHR not found";
+
+	eglCreateSyncKHR = (PFNEGLCREATESYNCKHRPROC)eglGetProcAddress("eglCreateSyncKHR");
+	if (!eglCreateSyncKHR)
+		LOG(eGL, Warning) << "eglCreateSyncKHR not found";
+
+	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;
+	}
+
+	surface_ = eglCreateWindowSurface(display_, config,
+					  (EGLNativeWindowType)gbmContext->getSurface(),
+					  NULL);
+	if (surface_ == EGL_NO_SURFACE) {
+		LOG(eGL, Error) << "eglCreateWindowSurface fail";
+		goto fail;
+	}
+
+	makeCurrent();
+	swapBuffers();
+
+	return 0;
+fail:
+
+	return -ENODEV;
+}
+
+void eGL::makeCurrent(void)
+{
+	if (eglMakeCurrent(display_, surface_, 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 0d004694..491eb734 100644
--- a/src/libcamera/meson.build
+++ b/src/libcamera/meson.build
@@ -77,6 +77,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')
@@ -198,7 +219,9 @@  libcamera_deps += [
     libcamera_base_private,
     libcrypto,
     libdl,
+    libegl,
     libgbm,
+    libglesv2,
     liblttng,
     libudev,
     libyaml,