{"id":25517,"url":"https://patchwork.libcamera.org/api/1.1/patches/25517/?format=json","web_url":"https://patchwork.libcamera.org/patch/25517/","project":{"id":1,"url":"https://patchwork.libcamera.org/api/1.1/projects/1/?format=json","name":"libcamera","link_name":"libcamera","list_id":"libcamera_core","list_email":"libcamera-devel@lists.libcamera.org","web_url":"","scm_url":"","webscm_url":""},"msgid":"<20251212002937.3118-3-bryan.odonoghue@linaro.org>","date":"2025-12-12T00:29:13","name":"[v8,02/26] libcamera: software_isp: egl: Add a eGL base helper class","commit_ref":null,"pull_url":null,"state":"superseded","archived":false,"hash":"61d0841177d00c3eba4646a004886d6b528ea016","submitter":{"id":175,"url":"https://patchwork.libcamera.org/api/1.1/people/175/?format=json","name":"Bryan O'Donoghue","email":"bryan.odonoghue@linaro.org"},"delegate":null,"mbox":"https://patchwork.libcamera.org/patch/25517/mbox/","series":[{"id":5657,"url":"https://patchwork.libcamera.org/api/1.1/series/5657/?format=json","web_url":"https://patchwork.libcamera.org/project/libcamera/list/?series=5657","date":"2025-12-12T00:29:11","name":"Add GLES 2.0 GPUISP to libcamera","version":8,"mbox":"https://patchwork.libcamera.org/series/5657/mbox/"}],"comments":"https://patchwork.libcamera.org/api/patches/25517/comments/","check":"pending","checks":"https://patchwork.libcamera.org/api/patches/25517/checks/","tags":{},"headers":{"Return-Path":"<libcamera-devel-bounces@lists.libcamera.org>","X-Original-To":"parsemail@patchwork.libcamera.org","Delivered-To":"parsemail@patchwork.libcamera.org","Received":["from lancelot.ideasonboard.com (lancelot.ideasonboard.com\n\t[92.243.16.209])\n\tby patchwork.libcamera.org (Postfix) with ESMTPS id 984DABD1F1\n\tfor <parsemail@patchwork.libcamera.org>;\n\tFri, 12 Dec 2025 00:33:31 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 55BD161623;\n\tFri, 12 Dec 2025 01:33:31 +0100 (CET)","from mail-pl1-x631.google.com (mail-pl1-x631.google.com\n\t[IPv6:2607:f8b0:4864:20::631])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id D050761613\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tFri, 12 Dec 2025 01:33:29 +0100 (CET)","by mail-pl1-x631.google.com with SMTP id\n\td9443c01a7336-29f0f875bc5so7641425ad.3\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tThu, 11 Dec 2025 16:33:29 -0800 (PST)","from inspiron14p-linux (p99250-ipoefx.ipoe.ocn.ne.jp.\n\t[153.246.134.249]) by smtp.gmail.com with ESMTPSA id\n\t41be03b00d2f7-c0c26eb0f6bsm3317282a12.14.2025.12.11.16.33.23\n\t(version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);\n\tThu, 11 Dec 2025 16:33:27 -0800 (PST)"],"Authentication-Results":"lancelot.ideasonboard.com; dkim=pass (2048-bit key;\n\tunprotected) header.d=linaro.org header.i=@linaro.org\n\theader.b=\"zBYLl7a4\"; dkim-atps=neutral","DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=linaro.org; s=google; t=1765499608; x=1766104408;\n\tdarn=lists.libcamera.org; \n\th=content-transfer-encoding:mime-version:references:in-reply-to\n\t:message-id:date:subject:cc:to:from:from:to:cc:subject:date\n\t:message-id:reply-to;\n\tbh=g1ksjKvh42ffwWP1wYtLNAXmrFfPoVxptDMDo5IhR18=;\n\tb=zBYLl7a4ePlcHQuTqOb6V8dXTpQuKVFoxanrhiXZedUGHxCHXPgvNNI1MKUWdN8wMO\n\tXFWKnTM2ur0Y/B2bJDqigftVmAFebgpdfqHkfojOjyxru8lkJgt8i6tx4j1ZD9buMYch\n\tmo9s/CEETQtZNF4nxBFj1I7ypkERXC5dPV1RBYr9V3Ct07KWelnvp/nGSP8XpLnyWThy\n\t9AwppLS6vs5yRvAEDL/Edh15YoB+Fuljac1a93jSdoQIqMFFmYqckknsgNnNTKirN7Ry\n\tjH6MgUfiOyjHfzn7gLcIpEmweBdmr5AuQmMm/DqRWr0i6g7k91Azp7ocnJ8fWS6/pouW\n\trGtw==","X-Google-DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=1e100.net; s=20230601; t=1765499608; x=1766104408;\n\th=content-transfer-encoding:mime-version:references:in-reply-to\n\t:message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from\n\t:to:cc:subject:date:message-id:reply-to;\n\tbh=g1ksjKvh42ffwWP1wYtLNAXmrFfPoVxptDMDo5IhR18=;\n\tb=tiCyqQ3AAonyVpoEIhZfMBhkWlHoi16V+9+zjX+SJT85tCCYgsIW+16/n8OAnO49WD\n\t5+nYxFPH07JlN5gvgS3gLz1vKQxiwUfH56HSG36GyG6iRHmTdqK1SJgmLYXg2BaVCOM2\n\tKxpIONUk9T9evRIBGCrjEGRO60ndY0N0sJ7pyFU6Q3Il74jE/VplMboHN4Xb9PhLBDBP\n\t+2jEX4rBdQtzZiQMZukfEwRxA4BzO9nLFj5cxMMbU6EHL25DkxxqVEifK7xPwFsRMfgO\n\t7yBn6MY/CbUAegPc9Ri8NiNzAWFoDIL2F5HuPLKjftDNHOWNQED2ziZnH+CG/4v7Pt4v\n\tsaKQ==","X-Gm-Message-State":"AOJu0YzLDK3/F2xCGCZpn7XLoEuevmoovJRI0LimuD3G3s37jpDaeNTJ\n\tVSehpM/El6UQx4ZM+OWRbnn35vP7xQgUxORUlmHcAf99Cn1xnETK2fvDpuMefIH8lENVPO/1iVL\n\tM0Cun","X-Gm-Gg":"AY/fxX5Ps+luxSftf6zo+EOu4BrH0DxzJnDfaSQaFHmvSsKmLyAq9sTm8z+XI6GprmU\n\tbuYUHilS2Yuo0Gu6V64UzVPXe+8yrEEtoPY/1Ms3puQTaUu8Ul2RyfeK26giNoci35b4voXpz8k\n\t0qXHGJLilM2VJkxn6Qz45r4QWYIk/IUvgUwwDLXlHJSr6WqnXbvSW2HDz0l018Qcw68aWbBWq4a\n\ticePpF/L9nh3rSr8mY2iSSsbLkVHSFc52c+WokBjo/LfLsvhGPbVifmjtDa8vBDByrSjmXFCH70\n\tyKe4oyKW5FV30eX30IO9MWTvEde+CEVGlXB4P5whv6l6IVJx9JVn/esvKXggWWGOJgO0vACu9K2\n\tuL06bJm6Iheh36gj0IOzrex9dg1axecw/S0t3zP5x5eno0pOqKdbRYsNLawxg5BLrLc/2Ntfjcd\n\tnKupR2Gh1ZMaul15FgvjOANKdiew8N9dllr8Eq8qeo2JRlPdHTVsESBeR4eTnWyq4W5h/Qwg==","X-Google-Smtp-Source":"AGHT+IGJQ26sWIo5rSJvLcM5O63gPZ8T+hcCp5nfDsttGL7wnPawaHIq+Ay927JVpldO7TBOK2vapg==","X-Received":"by 2002:a17:903:2f8b:b0:295:8a09:bcf2 with SMTP id\n\td9443c01a7336-29f23dfeb5bmr3374345ad.8.1765499607595; \n\tThu, 11 Dec 2025 16:33:27 -0800 (PST)","From":"Bryan O'Donoghue <bryan.odonoghue@linaro.org>","To":"libcamera-devel@lists.libcamera.org","Cc":"pavel@ucw.cz, Bryan O'Donoghue <bryan.odonoghue@linaro.org>,\n\tKieran Bingham <kieran.bingham@ideasonboard.com>,\n\tHans de Goede <johannes.goede@oss.qualcomm.com>,\n\tMilan Zamazal <mzamazal@redhat.com>","Subject":"[PATCH v8 02/26] libcamera: software_isp: egl: Add a eGL base helper\n\tclass","Date":"Fri, 12 Dec 2025 00:29:13 +0000","Message-ID":"<20251212002937.3118-3-bryan.odonoghue@linaro.org>","X-Mailer":"git-send-email 2.52.0","In-Reply-To":"<20251212002937.3118-1-bryan.odonoghue@linaro.org>","References":"<20251212002937.3118-1-bryan.odonoghue@linaro.org>","MIME-Version":"1.0","Content-Transfer-Encoding":"8bit","X-BeenThere":"libcamera-devel@lists.libcamera.org","X-Mailman-Version":"2.1.29","Precedence":"list","List-Id":"<libcamera-devel.lists.libcamera.org>","List-Unsubscribe":"<https://lists.libcamera.org/options/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=unsubscribe>","List-Archive":"<https://lists.libcamera.org/pipermail/libcamera-devel/>","List-Post":"<mailto:libcamera-devel@lists.libcamera.org>","List-Help":"<mailto:libcamera-devel-request@lists.libcamera.org?subject=help>","List-Subscribe":"<https://lists.libcamera.org/listinfo/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=subscribe>","Errors-To":"libcamera-devel-bounces@lists.libcamera.org","Sender":"\"libcamera-devel\" <libcamera-devel-bounces@lists.libcamera.org>"},"content":"Introduce an eGL base helper class which provides an eGL context based on a\npassed width and height.\n\nThe initGLContext function could be overloaded to provide an interface to a\nreal display.\n\nA set of helper functions is provided to compile and link GLSL shaders.\nlinkShaderProgram currently compiles vertex/fragment pairs but could be\noverloaded or passed a parameter to link a compute shader instead.\n\nBreaking the eGL interface away from debayering - allows to use the eGL\ncontext inside of a dma-buf heap cleanly, reuse that context inside of a\ndebayer layer and conceivably reuse the context in a multi-stage shader\npass.\n\nSmall note the image_attrs[] array doesn't pass checkstyle.py however the\nelements of the array are in pairs.\n\nAcked-by: Kieran Bingham <kieran.bingham@ideasonboard.com>\n[bod: Takes fix from Hans for constructor stride bpp]\n[bod: Drops eglClientWaitSync in favour of glFinish Robert/Milan]\nCo-developed-by: Hans de Goede <johannes.goede@oss.qualcomm.com>\nSigned-off-by: Hans de Goede <johannes.goede@oss.qualcomm.com>\nCo-developed-by: Milan Zamazal <mzamazal@redhat.com>\nSigned-off-by: Milan Zamazal <mzamazal@redhat.com>\nSigned-off-by: Bryan O'Donoghue <bryan.odonoghue@linaro.org>\n---\n include/libcamera/internal/egl.h | 186 ++++++++++\n src/libcamera/egl.cpp            | 619 +++++++++++++++++++++++++++++++\n src/libcamera/meson.build        |  23 ++\n 3 files changed, 828 insertions(+)\n create mode 100644 include/libcamera/internal/egl.h\n create mode 100644 src/libcamera/egl.cpp","diff":"diff --git a/include/libcamera/internal/egl.h b/include/libcamera/internal/egl.h\nnew file mode 100644\nindex 000000000..542787dc5\n--- /dev/null\n+++ b/include/libcamera/internal/egl.h\n@@ -0,0 +1,186 @@\n+/* SPDX-License-Identifier: LGPL-2.1-or-later */\n+/*\n+ * Copyright (C) 2024, Linaro Ltd.\n+ *\n+ * Authors:\n+ * Bryan O'Donoghue <bryan.odonoghue@linaro.org>\n+ *\n+ * egl_context.cpp - Helper class for managing eGL interactions.\n+ */\n+\n+#pragma once\n+\n+#include <sys/types.h>\n+#include <unistd.h>\n+\n+#include <libcamera/base/log.h>\n+#include <libcamera/base/span.h>\n+\n+#include \"libcamera/base/utils.h\"\n+#include \"libcamera/internal/gbm.h\"\n+\n+#define EGL_EGLEXT_PROTOTYPES\n+#include <EGL/egl.h>\n+#include <EGL/eglext.h>\n+#define GL_GLEXT_PROTOTYPES\n+#include <GLES2/gl2.h>\n+#include <GLES2/gl2ext.h>\n+\n+namespace libcamera {\n+\n+LOG_DECLARE_CATEGORY(eGL)\n+\n+/**\n+ * \\class eGLImage\n+ * \\brief Helper class for managing EGL image resources\n+ *\n+ * The eGLImage class encapsulates OpenGL ES texture and framebuffer objects\n+ * along with their associated EGL image. It aggregates handles, descriptors,\n+ * and routines for managing textures that can be associated with shader\n+ * uniform IDs.\n+ *\n+ * This class is particularly useful for managing DMA-BUF backed textures\n+ * in zero-copy rendering pipelines, where textures are bound to specific\n+ * texture units and can be used as both input textures and render targets.\n+ */\n+class eGLImage\n+{\n+public:\n+\t/**\n+\t * \\brief Construct an eGLImage with explicit stride\n+\t * \\param[in] width Image width in pixels\n+\t * \\param[in] height Image height in pixels\n+\t * \\param[in] bpp Bytes per pixel\n+\t * \\param[in] stride Row stride in bytes\n+\t * \\param[in] texture_unit OpenGL texture unit (e.g., GL_TEXTURE0)\n+\t * \\param[in] texture_unit_uniform_id Shader uniform ID for this texture unit\n+\t *\n+\t * Creates an eGLImage with the specified dimensions and stride. The stride\n+\t * may differ from width * bpp due to alignment.\n+\t */\n+\teGLImage(uint32_t width, uint32_t height, uint32_t bpp, uint32_t stride, GLenum texture_unit, uint32_t texture_unit_uniform_id)\n+\t{\n+\t\tinit(width, height, bpp, stride, texture_unit, texture_unit_uniform_id);\n+\t}\n+\t/**\n+\t * \\brief Construct an eGLImage with automatic stride calculation\n+\t * \\param[in] width Image width in pixels\n+\t * \\param[in] height Image height in pixels\n+\t * \\param[in] bpp Bytes per pixel\n+\t * \\param[in] texture_unit OpenGL texture unit (e.g., GL_TEXTURE0)\n+\t * \\param[in] texture_unit_uniform_id Shader uniform ID for this texture unit\n+\t *\n+\t * Creates an eGLImage with automatic stride calculation. The stride is\n+\t * aligned to 256 bytes because 256 byte alignment is a common baseline alignment for GPUs.\n+\t */\n+\teGLImage(uint32_t width, uint32_t height, uint32_t bpp, GLenum texture_unit, uint32_t texture_unit_uniform_id)\n+\t{\n+\t\tuint32_t stride = libcamera::utils::alignUp(width * bpp / 8, 256);\n+\n+\t\tinit(width, height, bpp, stride, texture_unit, texture_unit_uniform_id);\n+\t}\n+\n+\t/**\n+\t * \\brief Destroy the eGLImage\n+\t *\n+\t * Cleans up OpenGL resources by deleting the framebuffer object and\n+\t * texture.\n+\t */\n+\t~eGLImage()\n+\t{\n+\t\tglDeleteFramebuffers(1, &fbo_);\n+\t\tglDeleteTextures(1, &texture_);\n+\t}\n+\n+\tuint32_t width_;\t\t\t/**< Image width in pixels */\n+\tuint32_t height_;\t\t\t/**< Image height in pixels */\n+\tuint32_t stride_;\t\t\t/**< Row stride in bytes */\n+\tuint32_t offset_;\t\t\t/**< Buffer offset (reserved for future use) */\n+\tuint32_t framesize_;\t\t\t/**< Total frame size in bytes (stride * height) */\n+\tuint32_t bpp_;\t\t\t\t/**< Bytes per pixel */\n+\tuint32_t texture_unit_uniform_id_;\t/**< Shader uniform id for texture unit */\n+\tGLenum texture_unit_;\t\t\t/**< Texture unit associated with this image eg (GL_TEXTURE0) */\n+\tGLuint texture_;\t\t\t/**< OpenGL texture object ID */\n+\tGLuint fbo_;\t\t\t\t/**< OpenGL frame buffer object ID */\n+\tEGLImageKHR image_;\t\t\t/**< EGL Image handle */\n+\n+private:\n+\tLIBCAMERA_DISABLE_COPY_AND_MOVE(eGLImage)\n+\n+\t/**\n+\t * \\brief Initialise eGLImage state\n+\t * \\param[in] width Image width in pixels\n+\t * \\param[in] height Image height in pixels\n+\t * \\param[in] bpp Bytes per pixel\n+\t * \\param[in] stride Row stride in bytes\n+\t * \\param[in] texture_unit OpenGL texture unit\n+\t * \\param[in] texture_unit_uniform_id Shader uniform ID\n+\t *\n+\t * Common initialisation routine called by both constructors. Sets up\n+\t * member variables and generates OpenGL texture and framebuffer objects.\n+\t */\n+\tvoid init(uint32_t width, uint32_t height, uint32_t bpp, uint32_t stride, GLenum texture_unit, uint32_t texture_unit_uniform_id)\n+\t{\n+\t\timage_ = EGL_NO_IMAGE_KHR;\n+\t\twidth_ = width;\n+\t\theight_ = height;\n+\t\tbpp_ = bpp;\n+\t\tstride_ = stride;\n+\t\tframesize_ = stride_ * height_;\n+\t\ttexture_unit_ = texture_unit;\n+\t\ttexture_unit_uniform_id_ = texture_unit_uniform_id;\n+\n+\t\tglGenTextures(1, &texture_);\n+\t\tglGenFramebuffers(1, &fbo_);\n+\t}\n+};\n+\n+class eGL\n+{\n+public:\n+\teGL();\n+\t~eGL();\n+\n+\tint initEGLContext(GBM *gbmContext);\n+\tvoid cleanUp();\n+\n+\tint createInputDMABufTexture2D(std::unique_ptr<eGLImage> &eglImage, int fd);\n+\tint createOutputDMABufTexture2D(std::unique_ptr<eGLImage> &eglImage, int fd);\n+\tvoid destroyDMABufTexture(std::unique_ptr<eGLImage> &eglImage);\n+\tvoid createTexture2D(std::unique_ptr<eGLImage> &eglImage, GLint format, uint32_t width, uint32_t height, void *data);\n+\n+\tvoid pushEnv(std::vector<std::string> &shaderEnv, const char *str);\n+\tvoid makeCurrent();\n+\n+\tint compileVertexShader(GLuint &shaderId, const unsigned char *shaderData,\n+\t\t\t\tunsigned int shaderDataLen,\n+\t\t\t\tSpan<const std::string> shaderEnv);\n+\tint compileFragmentShader(GLuint &shaderId, const unsigned char *shaderData,\n+\t\t\t\t  unsigned int shaderDataLen,\n+\t\t\t\t  Span<const std::string> shaderEnv);\n+\tint linkProgram(GLuint &programId, GLuint fragmentshaderId, GLuint vertexshaderId);\n+\tvoid dumpShaderSource(GLuint shaderId);\n+\tvoid useProgram(GLuint programId);\n+\tvoid deleteProgram(GLuint programId);\n+\tint syncOutput();\n+\n+private:\n+\tLIBCAMERA_DISABLE_COPY_AND_MOVE(eGL)\n+\n+\tpid_t tid_;\n+\n+\tEGLDisplay display_;\n+\tEGLContext context_;\n+\tEGLSurface surface_;\n+\n+\tint compileShader(int shaderType, GLuint &shaderId, const unsigned char *shaderData,\n+\t\t\t  unsigned int shaderDataLen,\n+\t\t\t  Span<const std::string> shaderEnv);\n+\n+\tint createDMABufTexture2D(std::unique_ptr<eGLImage> &eglImage, int fd, bool output);\n+\n+\tPFNGLEGLIMAGETARGETTEXTURE2DOESPROC glEGLImageTargetTexture2DOES;\n+\tPFNEGLCREATEIMAGEKHRPROC eglCreateImageKHR;\n+\tPFNEGLDESTROYIMAGEKHRPROC eglDestroyImageKHR;\n+};\n+} //namespace libcamera\ndiff --git a/src/libcamera/egl.cpp b/src/libcamera/egl.cpp\nnew file mode 100644\nindex 000000000..2ef1156eb\n--- /dev/null\n+++ b/src/libcamera/egl.cpp\n@@ -0,0 +1,619 @@\n+/* SPDX-License-Identifier: LGPL-2.1-or-later */\n+/*\n+ * Copyright (C) 2024, Linaro Ltd.\n+ *\n+ * Authors:\n+ * Bryan O'Donoghue <bryan.odonoghue@linaro.org>\n+ *\n+ * egl.cpp - Helper class for managing eGL interactions.\n+ */\n+\n+#include \"libcamera/internal/egl.h\"\n+\n+#include <fcntl.h>\n+#include <sys/ioctl.h>\n+#include <sys/mman.h>\n+#include <unistd.h>\n+\n+#include <linux/dma-buf.h>\n+#include <linux/dma-heap.h>\n+\n+#include <libcamera/base/thread.h>\n+\n+#include <libdrm/drm_fourcc.h>\n+\n+namespace libcamera {\n+\n+LOG_DEFINE_CATEGORY(eGL)\n+\n+/**\n+ * \\class eGL\n+ * \\brief Helper class for managing OpenGL ES operations\n+ *\n+ * It provides:\n+ *\n+ * - EGL context setup and management\n+ * - Extension function pointer retrieval\n+ * - Shader compilation and program linking\n+ * - DMA-BUF texture creation and management\n+ * - Synchronisation primitives\n+ *\n+ * This class is designed to work with zero-copy buffers via DMA-BUF file\n+ * descriptors.\n+ */\n+\n+/**\n+ *\\var eGL::tid_\n+ *\\brief Thread ID of the thread associated with this EGL context\n+ */\n+\n+/**\n+ *\\var eGL::display_\n+ *\\brief EGL display handle\n+ */\n+\n+/**\n+ *\\var eGL::context_\n+ *\\brief EGL context handle\n+ */\n+\n+/**\n+ *\\var eGL::surface_\n+ *\\brief EGL sufrace handle\n+ */\n+\n+/**\n+ * \\brief Construct an EGL helper\n+ *\n+ * Creates an eGL instance with uninitialised context. Call initEGLContext()\n+ * to set up the EGL display, context, and load extension functions.\n+ */\n+eGL::eGL()\n+{\n+\tcontext_ = EGL_NO_CONTEXT;\n+\tsurface_ = EGL_NO_SURFACE;\n+\tdisplay_ = EGL_NO_DISPLAY;\n+}\n+\n+/**\n+ * \\brief Destroy the EGL helper\n+ *\n+ * Destroys the EGL context and surface if they were successfully created.\n+ */\n+eGL::~eGL()\n+{\n+\tif (context_ != EGL_NO_CONTEXT)\n+\t\teglDestroyContext(display_, context_);\n+\n+\tif (surface_ != EGL_NO_SURFACE)\n+\t\teglDestroySurface(display_, surface_);\n+}\n+\n+/**\n+ * \\brief Synchronise rendering output\n+ *\n+ * Sychronise here. Calls glFinish() right now.\n+ *\n+ * \\return 0 on success\n+ */\n+int eGL::syncOutput(void)\n+{\n+\tASSERT(tid_ == Thread::currentId());\n+\n+\tglFinish();\n+\n+\treturn 0;\n+}\n+\n+/**\n+ * \\brief Create a DMA-BUF backed 2D texture\n+ * \\param[in,out] eglImage EGL image to associate with the DMA-BUF\n+ * \\param[in] fd DMA-BUF file descriptor\n+ * \\param[in] output If true, create framebuffer for render target\n+ *\n+ * Internal implementation for creating DMA-BUF textures. Creates an EGL\n+ * image from the DMA-BUF and binds it to a 2D texture. If output is true,\n+ * also creates and attaches a framebuffer object.\n+ *\n+ * \\return 0 on success, or -ENODEV on failure\n+ */\n+int eGL::createDMABufTexture2D(std::unique_ptr<eGLImage> &eglImage, int fd, bool output)\n+{\n+\tint ret = 0;\n+\n+\tASSERT(tid_ == Thread::currentId());\n+\n+\t// clang-format off\n+\tEGLint image_attrs[] = {\n+\t\tEGL_WIDTH, (EGLint)eglImage->width_,\n+\t\tEGL_HEIGHT, (EGLint)eglImage->height_,\n+\t\tEGL_LINUX_DRM_FOURCC_EXT, DRM_FORMAT_ARGB8888,\n+\t\tEGL_DMA_BUF_PLANE0_FD_EXT, fd,\n+\t\tEGL_DMA_BUF_PLANE0_OFFSET_EXT, 0,\n+\t\tEGL_DMA_BUF_PLANE0_PITCH_EXT, (EGLint)eglImage->stride_,\n+\t\tEGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT, 0,\n+\t\tEGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT, 0,\n+\t\tEGL_NONE,\n+\t};\n+\t// clang-format on\n+\n+\teglImage->image_ = eglCreateImageKHR(display_, EGL_NO_CONTEXT,\n+\t\t\t\t\t     EGL_LINUX_DMA_BUF_EXT,\n+\t\t\t\t\t     NULL, image_attrs);\n+\n+\tif (eglImage->image_ == EGL_NO_IMAGE_KHR) {\n+\t\tLOG(eGL, Error) << \"eglCreateImageKHR fail\";\n+\t\tret = -ENODEV;\n+\t\tgoto done;\n+\t}\n+\n+\t// Bind texture unit and texture\n+\tglActiveTexture(eglImage->texture_unit_);\n+\tglBindTexture(GL_TEXTURE_2D, eglImage->texture_);\n+\n+\t// Generate texture with filter semantics\n+\tglEGLImageTargetTexture2DOES(GL_TEXTURE_2D, eglImage->image_);\n+\n+\t// Nearest filtering\n+\tglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);\n+\tglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);\n+\n+\t// Wrap to edge to avoid edge artifacts\n+\tglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);\n+\tglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);\n+\n+\tif (output) {\n+\t\t// Generate a framebuffer from our texture direct to dma-buf handle buffer\n+\t\tglBindFramebuffer(GL_FRAMEBUFFER, eglImage->fbo_);\n+\t\tglFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, eglImage->texture_, 0);\n+\n+\t\tGLenum err = glCheckFramebufferStatus(GL_FRAMEBUFFER);\n+\t\tif (err != GL_FRAMEBUFFER_COMPLETE) {\n+\t\t\tLOG(eGL, Error) << \"glFrameBufferTexture2D error \" << err;\n+\t\t\teglDestroyImageKHR(display_, eglImage->image_);\n+\t\t\tret = -ENODEV;\n+\t\t\tgoto done;\n+\t\t}\n+\t}\n+done:\n+\treturn ret;\n+}\n+\n+/**\n+ * \\brief Create an input DMA-BUF backed texture\n+ * \\param[in,out] eglImage EGL image to associate with the DMA-BUF\n+ * \\param[in] fd DMA-BUF file descriptor\n+ *\n+ * Creates an EGL image from a DMA-BUF file descriptor and binds it to\n+ * a 2D texture for use as an input texture in shaders. The texture is\n+ * configured with nearest filtering and clamp-to-edge wrapping.\n+ *\n+ * \\return 0 on success, or -ENODEV on failure\n+ */\n+int eGL::createInputDMABufTexture2D(std::unique_ptr<eGLImage> &eglImage, int fd)\n+{\n+\tASSERT(tid_ == Thread::currentId());\n+\n+\treturn createDMABufTexture2D(eglImage, fd, false);\n+}\n+\n+/**\n+ * \\brief Create an output DMA-BUF backed texture\n+ * \\param[in,out] eglImage EGL image to associate with the DMA-BUF\n+ * \\param[in] fd DMA-BUF file descriptor\n+ *\n+ * Creates an EGL image from a DMA-BUF file descriptor and binds it to\n+ * a 2D texture, then attaches it to a framebuffer object for use as a\n+ * render target. This enables zero-copy rendering directly to the\n+ * DMA-BUF.\n+ *\n+ * \\return 0 on success, or -ENODEV on failure\n+ */\n+int eGL::createOutputDMABufTexture2D(std::unique_ptr<eGLImage> &eglImage, int fd)\n+{\n+\tASSERT(tid_ == Thread::currentId());\n+\n+\treturn createDMABufTexture2D(eglImage, fd, true);\n+}\n+\n+/**\n+ * \\brief Destroy a DMA-BUF texture's EGL image\n+ * \\param[in,out] eglImage EGL image to destroy\n+ *\n+ * Destroys the EGL image associated with a DMA-BUF texture. The OpenGL\n+ * texture and framebuffer objects are destroyed separately in the\n+ * eGLImage destructor.\n+ */\n+void eGL::destroyDMABufTexture(std::unique_ptr<eGLImage> &eglImage)\n+{\n+\teglDestroyImage(display_, eglImage->image_);\n+}\n+\n+\n+/**\n+ * \\brief Create a 2D texture from a memory buffer\n+ * \\param[in,out] eglImage EGL image to associate with the texture\n+ * \\param[in] format OpenGL internal format (e.g., GL_RGB, GL_RGBA)\n+ * \\param[in] width Texture width in pixels\n+ * \\param[in] height Texture height in pixels\n+ * \\param[in] data Pointer to pixel data, or nullptr for uninitialised texture\n+ *\n+ * Creates a 2D texture from a CPU-accessible memory buffer. The texture\n+ * is configured with nearest filtering and clamp-to-edge wrapping. This\n+ * is useful for uploading static data like lookup tables or uniform color\n+ * matrices to the GPU.\n+ */\n+void eGL::createTexture2D(std::unique_ptr<eGLImage> &eglImage, GLint format, uint32_t width, uint32_t height, void *data)\n+{\n+\tASSERT(tid_ == Thread::currentId());\n+\n+\tglActiveTexture(eglImage->texture_unit_);\n+\tglBindTexture(GL_TEXTURE_2D, eglImage->texture_);\n+\n+\t// Generate texture, bind, associate image to texture, configure, unbind\n+\tglTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);\n+\n+\t// Nearest filtering\n+\tglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);\n+\tglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);\n+\n+\t// Wrap to edge to avoid edge artifacts\n+\tglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);\n+\tglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);\n+}\n+\n+/**\n+ * \\brief Initialise the EGL context\n+ * \\param[in] gbmContext Pointer to initialised GBM context\n+ *\n+ * Sets up the EGL display from the GBM device, creates an OpenGL ES 2.0\n+ * context, and retrieves function pointers for required extensions\n+ * including:\n+ * - eglCreateImageKHR / eglDestroyImageKHR\n+ * - glEGLImageTargetTexture2DOES\n+ *\n+ * \\return 0 on success, or -ENODEV on failure\n+ */\n+int eGL::initEGLContext(GBM *gbmContext)\n+{\n+\tEGLint configAttribs[] = {\n+\t\tEGL_RED_SIZE, 8,\n+\t\tEGL_GREEN_SIZE, 8,\n+\t\tEGL_BLUE_SIZE, 8,\n+\t\tEGL_ALPHA_SIZE, 8,\n+\t\tEGL_SURFACE_TYPE, EGL_WINDOW_BIT,\n+\t\tEGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,\n+\t\tEGL_NONE\n+\t};\n+\n+\tEGLint contextAttribs[] = {\n+\t\tEGL_CONTEXT_MAJOR_VERSION, 2,\n+\t\tEGL_NONE\n+\t};\n+\n+\tEGLint numConfigs;\n+\tEGLConfig config;\n+\tEGLint major;\n+\tEGLint minor;\n+\n+\tif (!eglBindAPI(EGL_OPENGL_ES_API)) {\n+\t\tLOG(eGL, Error) << \"API bind fail\";\n+\t\tgoto fail;\n+\t}\n+\n+\tdisplay_ = eglGetDisplay(gbmContext->getDevice());\n+\tif (display_ == EGL_NO_DISPLAY) {\n+\t\tLOG(eGL, Error) << \"Unable to get EGL display\";\n+\t\tgoto fail;\n+\t}\n+\n+\tif (eglInitialize(display_, &major, &minor) != EGL_TRUE) {\n+\t\tLOG(eGL, Error) << \"eglInitialize fail\";\n+\t\tgoto fail;\n+\t}\n+\n+\tLOG(eGL, Info) << \"EGL: version \" << major << \".\" << minor;\n+\tLOG(eGL, Info) << \"EGL: EGL_VERSION: \" << eglQueryString(display_, EGL_VERSION);\n+\tLOG(eGL, Info) << \"EGL: EGL_VENDOR: \" << eglQueryString(display_, EGL_VENDOR);\n+\tLOG(eGL, Info) << \"EGL: EGL_CLIENT_APIS: \" << eglQueryString(display_, EGL_CLIENT_APIS);\n+\tLOG(eGL, Info) << \"EGL: EGL_EXTENSIONS: \" << eglQueryString(display_, EGL_EXTENSIONS);\n+\n+\teglCreateImageKHR = (PFNEGLCREATEIMAGEKHRPROC)eglGetProcAddress(\"eglCreateImageKHR\");\n+\tif (!eglCreateImageKHR) {\n+\t\tLOG(eGL, Error) << \"eglCreateImageKHR not found\";\n+\t\tgoto fail;\n+\t}\n+\n+\teglDestroyImageKHR = (PFNEGLDESTROYIMAGEKHRPROC)eglGetProcAddress(\"eglDestroyImageKHR\");\n+\tif (!eglDestroyImageKHR) {\n+\t\tLOG(eGL, Error) << \"eglDestroyImageKHR not found\";\n+\t\tgoto fail;\n+\t}\n+\n+\tglEGLImageTargetTexture2DOES = (PFNGLEGLIMAGETARGETTEXTURE2DOESPROC)eglGetProcAddress(\"glEGLImageTargetTexture2DOES\");\n+\tif (!glEGLImageTargetTexture2DOES) {\n+\t\tLOG(eGL, Error) << \"glEGLImageTargetTexture2DOES not found\";\n+\t\tgoto fail;\n+\t}\n+\n+\tif (eglChooseConfig(display_, configAttribs, &config, 1, &numConfigs) != EGL_TRUE) {\n+\t\tLOG(eGL, Error) << \"eglChooseConfig fail\";\n+\t\tgoto fail;\n+\t}\n+\n+\tcontext_ = eglCreateContext(display_, config, EGL_NO_CONTEXT, contextAttribs);\n+\tif (context_ == EGL_NO_CONTEXT) {\n+\t\tLOG(eGL, Error) << \"eglContext returned EGL_NO_CONTEXT\";\n+\t\tgoto fail;\n+\t}\n+\n+\ttid_ = Thread::currentId();\n+\n+\tmakeCurrent();\n+\n+\treturn 0;\n+fail:\n+\n+\treturn -ENODEV;\n+}\n+\n+\n+/**\n+ * \\brief Clean up EGL resources\n+ *\n+ * Destroys the EGL sync object. Must be called from the same thread\n+ * that created the EGL context.\n+ */\n+void eGL::cleanUp(void) {}\n+\n+\n+/**\n+ * \\brief Make the EGL context current for the calling thread\n+ *\n+ * Binds the EGL context to the current thread, allowing OpenGL ES\n+ * operations to be performed. Must be called from the thread that\n+ * will perform rendering operations.\n+ */\n+void eGL::makeCurrent(void)\n+{\n+\tASSERT(tid_ == Thread::currentId());\n+\n+\tif (eglMakeCurrent(display_, EGL_NO_SURFACE, EGL_NO_SURFACE, context_) != EGL_TRUE) {\n+\t\tLOG(eGL, Error) << \"eglMakeCurrent fail\";\n+\t}\n+}\n+\n+\n+/**\n+ * \\brief Activate a shader program for rendering\n+ * \\param[in] programId OpenGL program object ID\n+ *\n+ * Sets the specified program as the current rendering program. All\n+ * subsequent draw calls will use this program's shaders.\n+ */\n+void eGL::useProgram(GLuint programId)\n+{\n+\tASSERT(tid_ == Thread::currentId());\n+\n+\tglUseProgram(programId);\n+}\n+\n+/**\n+ * \\brief Delete a shader program\n+ * \\param[in] programId OpenGL program object ID\n+ *\n+ * Deletes a shader program and frees associated resources. The program\n+ * must not be currently in use.\n+ */\n+void eGL::deleteProgram(GLuint programId)\n+{\n+\tASSERT(tid_ == Thread::currentId());\n+\n+\tglDeleteProgram(programId);\n+}\n+\n+/**\n+ * \\brief Add a preprocessor definition to shader environment\n+ * \\param[in,out] shaderEnv Vector of shader environment strings\n+ * \\param[in] str Preprocessor definition string (e.g., \"#define APPLY_RGB_PARAMETERS\")\n+ *\n+ * Appends a preprocessor definition to the shader environment vector.\n+ * These definitions are prepended to shader source code during compilation.\n+ */\n+void eGL::pushEnv(std::vector<std::string> &shaderEnv, const char *str)\n+{\n+\tstd::string addStr = str;\n+\n+\taddStr.push_back('\\n');\n+\tshaderEnv.push_back(std::move(addStr));\n+}\n+\n+/**\n+ * \\brief Compile a vertex shader\n+ * \\param[out] shaderId OpenGL shader object ID\n+ * \\param[in] shaderData Pointer to shader source code\n+ * \\param[in] shaderDataLen Length of shader source in bytes\n+ * \\param[in] shaderEnv Span of preprocessor definitions to prepend\n+ *\n+ * Compiles a vertex shader from source code with optional preprocessor\n+ * definitions. On compilation failure, logs the shader info log.\n+ *\n+ * \\return 0 on success, or -EINVAL on compilation failure\n+ */\n+int eGL::compileVertexShader(GLuint &shaderId, const unsigned char *shaderData,\n+\t\t\t     unsigned int shaderDataLen,\n+\t\t\t     Span<const std::string> shaderEnv)\n+{\n+\treturn compileShader(GL_VERTEX_SHADER, shaderId, shaderData, shaderDataLen, shaderEnv);\n+}\n+\n+/**\n+ * \\brief Compile a fragment shader\n+ * \\param[out] shaderId OpenGL shader object ID\n+ * \\param[in] shaderData Pointer to shader source code\n+ * \\param[in] shaderDataLen Length of shader source in bytes\n+ * \\param[in] shaderEnv Span of preprocessor definitions to prepend\n+ *\n+ * Compiles a fragment shader from source code with optional preprocessor\n+ * definitions. On compilation failure, logs the shader info log.\n+ *\n+ * \\return 0 on success, or -EINVAL on compilation failure\n+ */\n+int eGL::compileFragmentShader(GLuint &shaderId, const unsigned char *shaderData,\n+\t\t\t       unsigned int shaderDataLen,\n+\t\t\t       Span<const std::string> shaderEnv)\n+{\n+\treturn compileShader(GL_FRAGMENT_SHADER, shaderId, shaderData, shaderDataLen, shaderEnv);\n+}\n+\n+/**\n+ * \\brief Compile a shader of specified type\n+ * \\param[in] shaderType GL_VERTEX_SHADER or GL_FRAGMENT_SHADER\n+ * \\param[out] shaderId OpenGL shader object ID\n+ * \\param[in] shaderData Pointer to shader source code\n+ * \\param[in] shaderDataLen Length of shader source in bytes\n+ * \\param[in] shaderEnv Span of preprocessor definitions to prepend\n+ *\n+ * Internal helper function for shader compilation. Prepends environment\n+ * definitions to the shader source and compiles the shader.\n+ *\n+ * \\return 0 on success, or -EINVAL on compilation failure\n+ */\n+int eGL::compileShader(int shaderType, GLuint &shaderId, const unsigned char *shaderData,\n+\t\t       unsigned int shaderDataLen,\n+\t\t       Span<const std::string> shaderEnv)\n+{\n+\tGLint success;\n+\tsize_t i;\n+\n+\tASSERT(tid_ == Thread::currentId());\n+\n+\tauto count = 1 + shaderEnv.size();\n+\tauto shaderSourceData = std::make_unique<const GLchar *[]>(count);\n+\tauto shaderDataLengths = std::make_unique<GLint[]>(count);\n+\n+\t// Prefix defines before main body of shader\n+\tfor (i = 0; i < shaderEnv.size(); i++) {\n+\t\tshaderSourceData[i] = shaderEnv[i].c_str();\n+\t\tshaderDataLengths[i] = shaderEnv[i].length();\n+\t}\n+\n+\t// Now the main body of the shader program\n+\tshaderSourceData[i] = reinterpret_cast<const GLchar *>(shaderData);\n+\tshaderDataLengths[i] = shaderDataLen;\n+\n+\t// And create the shader\n+\tshaderId = glCreateShader(shaderType);\n+\tglShaderSource(shaderId, count, shaderSourceData.get(), shaderDataLengths.get());\n+\tglCompileShader(shaderId);\n+\n+\t// Check status\n+\tglGetShaderiv(shaderId, GL_COMPILE_STATUS, &success);\n+\tif (success == GL_FALSE) {\n+\t\tGLint sizeLog = 0;\n+\n+\t\tglGetShaderiv(shaderId, GL_INFO_LOG_LENGTH, &sizeLog);\n+\t\tauto infoLog = std::make_unique<GLchar []>(sizeLog);\n+\n+\t\tglGetShaderInfoLog(shaderId, sizeLog, &sizeLog, infoLog.get());\n+\t\tLOG(eGL, Error) << infoLog.get();\n+\t}\n+\n+\treturn (success == GL_TRUE) ? 0 : -EINVAL;\n+}\n+\n+/**\n+ * \\brief Dump shader source code to the log\n+ * \\param[in] shaderId OpenGL shader object ID\n+ *\n+ * Retrieves and logs the complete source code of a compiled shader.\n+ * Useful for debugging shader compilation issues.\n+ */\n+void eGL::dumpShaderSource(GLuint shaderId)\n+{\n+\tGLint shaderLength = 0;\n+\n+\tASSERT(tid_ == Thread::currentId());\n+\n+\tglGetShaderiv(shaderId, GL_SHADER_SOURCE_LENGTH, &shaderLength);\n+\n+\tLOG(eGL, Debug) << \"Shader length is \" << shaderLength;\n+\n+\tif (shaderLength > 0) {\n+\t\tauto shaderSource = std::make_unique<GLchar []>(shaderLength);\n+\n+\t\tglGetShaderSource(shaderId, shaderLength, &shaderLength, shaderSource.get());\n+\t\tif (shaderLength) {\n+\t\t\tLOG(eGL, Debug) << \"Shader source = \" << shaderSource.get();\n+\t\t}\n+\t}\n+}\n+\n+\n+/**\n+ * \\brief Link a shader program\n+ * \\param[out] programId OpenGL program object ID\n+ * \\param[in] fragmentshaderId Compiled fragment shader ID\n+ * \\param[in] vertexshaderId Compiled vertex shader ID\n+ *\n+ * Links vertex and fragment shaders into an executable shader program.\n+ * On link failure, logs the program info log and deletes the program.\n+ *\n+ * \\return 0 on success, or -ENODEV on link failure\n+ */\n+int eGL::linkProgram(GLuint &programId, GLuint vertexshaderId, GLuint fragmentshaderId)\n+{\n+\tGLint success;\n+\tGLenum err;\n+\n+\tASSERT(tid_ == Thread::currentId());\n+\n+\tprogramId = glCreateProgram();\n+\tif (!programId)\n+\t\tgoto fail;\n+\n+\tglAttachShader(programId, vertexshaderId);\n+\tif ((err = glGetError()) != GL_NO_ERROR) {\n+\t\tLOG(eGL, Error) << \"Attach compute vertex shader fail\";\n+\t\tgoto fail;\n+\t}\n+\n+\tglAttachShader(programId, fragmentshaderId);\n+\tif ((err = glGetError()) != GL_NO_ERROR) {\n+\t\tLOG(eGL, Error) << \"Attach compute vertex shader fail\";\n+\t\tgoto fail;\n+\t}\n+\n+\tglLinkProgram(programId);\n+\tif ((err = glGetError()) != GL_NO_ERROR) {\n+\t\tLOG(eGL, Error) << \"Link program fail\";\n+\t\tgoto fail;\n+\t}\n+\n+\tglDetachShader(programId, fragmentshaderId);\n+\tglDetachShader(programId, vertexshaderId);\n+\n+\t// Check status\n+\tglGetProgramiv(programId, GL_LINK_STATUS, &success);\n+\tif (success == GL_FALSE) {\n+\t\tGLint sizeLog = 0;\n+\t\tGLchar *infoLog;\n+\n+\t\tglGetProgramiv(programId, GL_INFO_LOG_LENGTH, &sizeLog);\n+\t\tinfoLog = new GLchar[sizeLog];\n+\n+\t\tglGetProgramInfoLog(programId, sizeLog, &sizeLog, infoLog);\n+\t\tLOG(eGL, Error) << infoLog;\n+\n+\t\tdelete[] infoLog;\n+\t\tgoto fail;\n+\t}\n+\n+\treturn 0;\n+fail:\n+\tif (programId)\n+\t\tglDeleteProgram(programId);\n+\n+\treturn -ENODEV;\n+}\n+} // namespace libcamera\ndiff --git a/src/libcamera/meson.build b/src/libcamera/meson.build\nindex 685213c78..71ab39c25 100644\n--- a/src/libcamera/meson.build\n+++ b/src/libcamera/meson.build\n@@ -79,6 +79,27 @@ if libgbm.found() and gbm_works\n     ])\n endif\n \n+libegl = cc.find_library('EGL', required : false)\n+libglesv2 = cc.find_library('GLESv2', required : false)\n+mesa_works = cc.check_header('EGL/egl.h', required: false)\n+\n+if libegl.found() and mesa_works\n+    config_h.set('HAVE_LIBEGL', 1)\n+endif\n+\n+if libglesv2.found() and mesa_works\n+    config_h.set('HAVE_GLESV2', 1)\n+endif\n+\n+if mesa_works and gbm_works\n+    libcamera_internal_sources += files([\n+        'egl.cpp',\n+    ])\n+    gles_headless_enabled = true\n+else\n+    gles_headless_enabled = false\n+endif\n+\n subdir('base')\n subdir('converter')\n subdir('ipa')\n@@ -187,7 +208,9 @@ libcamera_deps += [\n     libcamera_base_private,\n     libcrypto,\n     libdl,\n+    libegl,\n     libgbm,\n+    libglesv2,\n     liblttng,\n     libudev,\n     libyaml,\n","prefixes":["v8","02/26"]}