[v3] libcamera: egl: Pass EGLDisplay to eGL constructor to avoid double init
diff mbox series

Message ID 20260729025557.2261274-1-qi.hou@oss.nxp.com
State Accepted
Headers show
Series
  • [v3] libcamera: egl: Pass EGLDisplay to eGL constructor to avoid double init
Related show

Commit Message

Qi Hou (OSS) July 29, 2026, 2:55 a.m. UTC
From: Qi Hou <qi.hou@nxp.com>

isAvailable() previously called probeDisplay() to run a full EGL
initialisation sequence and then immediately terminated the display with
eglTerminate(). When SoftwareIsp subsequently created a DebayerEGL
instance, initEGLContext() would call probeDisplay() again, repeating
the full initialisation.

Fix this by reusing the display obtained during the availability check.
probeDisplay() is promoted to a public static method and isAvailable()
is removed. The eGL constructor now takes the EGLDisplay as a parameter
and stores it directly, so initEGLContext() no longer needs to call
probeDisplay(). DebayerEGL is updated to accept and forward the display
to eGL, and SoftwareIsp calls eGL::probeDisplay() once, passing the
result straight to DebayerEGL when EGL is available.

Signed-off-by: Qi Hou <qi.hou@nxp.com>
---
 include/libcamera/internal/egl.h            |  5 +--
 src/libcamera/egl.cpp                       | 42 +++++++--------------
 src/libcamera/software_isp/debayer_egl.cpp  |  5 ++-
 src/libcamera/software_isp/debayer_egl.h    |  2 +-
 src/libcamera/software_isp/software_isp.cpp |  5 ++-
 5 files changed, 23 insertions(+), 36 deletions(-)

Comments

Barnabás Pőcze July 29, 2026, 10:05 a.m. UTC | #1
2026. 07. 29. 4:55 keltezéssel, qi.hou@oss.nxp.com írta:
> From: Qi Hou <qi.hou@nxp.com>
> 
> isAvailable() previously called probeDisplay() to run a full EGL
> initialisation sequence and then immediately terminated the display with
> eglTerminate(). When SoftwareIsp subsequently created a DebayerEGL
> instance, initEGLContext() would call probeDisplay() again, repeating
> the full initialisation.
> 
> Fix this by reusing the display obtained during the availability check.
> probeDisplay() is promoted to a public static method and isAvailable()
> is removed. The eGL constructor now takes the EGLDisplay as a parameter
> and stores it directly, so initEGLContext() no longer needs to call
> probeDisplay(). DebayerEGL is updated to accept and forward the display
> to eGL, and SoftwareIsp calls eGL::probeDisplay() once, passing the
> result straight to DebayerEGL when EGL is available.
> 
> Signed-off-by: Qi Hou <qi.hou@nxp.com>
> ---

Tested-by: Barnabás Pőcze <barnabas.pocze@ideasonboard.com> # intel-ipu6 + ov2740 + mesa 26.1.5

It seems to work, no output with `MESA_DEBUG=1`, and I might have very
well missed it, but I couldn't find any obvious prohibition of what
is suggested here.


>   include/libcamera/internal/egl.h            |  5 +--
>   src/libcamera/egl.cpp                       | 42 +++++++--------------
>   src/libcamera/software_isp/debayer_egl.cpp  |  5 ++-
>   src/libcamera/software_isp/debayer_egl.h    |  2 +-
>   src/libcamera/software_isp/software_isp.cpp |  5 ++-
>   5 files changed, 23 insertions(+), 36 deletions(-)
> 
> diff --git a/include/libcamera/internal/egl.h b/include/libcamera/internal/egl.h
> index 1e31d490b..7ef1ca0d9 100644
> --- a/include/libcamera/internal/egl.h
> +++ b/include/libcamera/internal/egl.h
> @@ -99,11 +99,11 @@ private:
>   class eGL
>   {
>   public:
> -	eGL();
> +	eGL(EGLDisplay display);
>   	~eGL();
>   
>   	int initEGLContext();
> -	static bool isAvailable();
> +	static EGLDisplay probeDisplay();
>   
>   	int createInputDMABufTexture2D(eGLImage &eglImage, int fd);
>   	int createOutputDMABufTexture2D(eGLImage &eglImage, int fd);
> @@ -137,7 +137,6 @@ private:
>   	EGLContext context_ = EGL_NO_CONTEXT;
>   	EGLSurface surface_ = EGL_NO_SURFACE;
>   
> -	static EGLDisplay probeDisplay();
>   	int compileShader(int shaderType, GLuint &shaderId, Span<const unsigned char> shaderData,
>   			  Span<const std::string> shaderEnv);
>   
> diff --git a/src/libcamera/egl.cpp b/src/libcamera/egl.cpp
> index 7ec7a654d..01e51cda8 100644
> --- a/src/libcamera/egl.cpp
> +++ b/src/libcamera/egl.cpp
> @@ -64,12 +64,15 @@ LOG_DEFINE_CATEGORY(eGL)
>   
>   /**
>    * \brief Construct an EGL helper
> + * \param[in] display The EGL display to use
>    *
>    * Creates an eGL instance with uninitialised context. Call initEGLContext()
> - * to set up the EGL display, context, and load extension functions.
> + * to set up the EGL context, and load extension functions.
>    */
> -eGL::eGL()
> +eGL::eGL(EGLDisplay display)
> +	: display_(display)
>   {
> +	ASSERT(display_ != EGL_NO_DISPLAY);
>   }
>   
>   /**
> @@ -299,15 +302,17 @@ void eGL::createTexture2D(eGLImage &eglImage, void *data)
>   	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
>   }
>   
> +/**
> + * \brief Try to create an EGL display for surfaceless rendering
> + *
> + * Tries to create an EGL display for surfaceless rendering.
> + *
> + * \return A EGL display handle if successful, otherwise \a EGL_NO_DISPLAY
> + */
>   EGLDisplay eGL::probeDisplay()
>   {
>   	EGLDisplay display;
>   
> -	if (!eglBindAPI(EGL_OPENGL_ES_API)) {
> -		LOG(eGL, Info) << "API bind fail";
> -		return EGL_NO_DISPLAY;
> -	}
> -
>   	display = eglGetPlatformDisplay(EGL_PLATFORM_SURFACELESS_MESA,
>   					EGL_DEFAULT_DISPLAY,
>   					nullptr);
> @@ -325,24 +330,6 @@ EGLDisplay eGL::probeDisplay()
>   	return display;
>   }
>   
> -/**
> - * \brief Probe whether EGL surfaceless rendering is available
> - *
> - * Checks if an EGL surfaceless display can be obtained and initialised.
> - * The display is immediately terminated so that no resources are leaked.
> - *
> - * \return True if EGL surfaceless rendering is available, false otherwise
> - */
> -bool eGL::isAvailable()
> -{
> -	EGLDisplay display = probeDisplay();
> -	if (display == EGL_NO_DISPLAY)
> -		return false;
> -
> -	eglTerminate(display);
> -	return true;
> -}
> -
>   /**
>    * \brief Update a 2D texture already created
>    * \param[in,out] eglImage EGL image to associate with the texture
> @@ -403,9 +390,8 @@ int eGL::initEGLContext()
>   	EGLint numConfigs;
>   	EGLConfig config;
>   
> -	display_ = probeDisplay();
> -	if (display_ == EGL_NO_DISPLAY) {
> -		LOG(eGL, Error) << "Unable to probe display";
> +	if (!eglBindAPI(EGL_OPENGL_ES_API)) {
> +		LOG(eGL, Error) << "API bind fail";
>   		goto fail;
>   	}
>   
> diff --git a/src/libcamera/software_isp/debayer_egl.cpp b/src/libcamera/software_isp/debayer_egl.cpp
> index dd2041219..20b478b1c 100644
> --- a/src/libcamera/software_isp/debayer_egl.cpp
> +++ b/src/libcamera/software_isp/debayer_egl.cpp
> @@ -40,9 +40,10 @@ namespace libcamera {
>    * \brief Construct a DebayerEGL object
>    * \param[in] stats Statistics processing object
>    * \param[in] cm The camera manager
> + * \param[in] display The EGL display to use
>    */
> -DebayerEGL::DebayerEGL(std::unique_ptr<SwStatsCpu> stats, const CameraManager &cm)
> -	: Debayer(cm), stats_(std::move(stats))
> +DebayerEGL::DebayerEGL(std::unique_ptr<SwStatsCpu> stats, const CameraManager &cm, EGLDisplay display)
> +	: Debayer(cm), stats_(std::move(stats)), egl_(display)
>   {
>   }
>   
> diff --git a/src/libcamera/software_isp/debayer_egl.h b/src/libcamera/software_isp/debayer_egl.h
> index e613639f5..30e51a477 100644
> --- a/src/libcamera/software_isp/debayer_egl.h
> +++ b/src/libcamera/software_isp/debayer_egl.h
> @@ -40,7 +40,7 @@ class CameraManager;
>   class DebayerEGL : public Debayer
>   {
>   public:
> -	DebayerEGL(std::unique_ptr<SwStatsCpu> stats, const CameraManager &cm);
> +	DebayerEGL(std::unique_ptr<SwStatsCpu> stats, const CameraManager &cm, EGLDisplay display);
>   	~DebayerEGL();
>   
>   	int configure(const StreamConfiguration &inputCfg,
> diff --git a/src/libcamera/software_isp/software_isp.cpp b/src/libcamera/software_isp/software_isp.cpp
> index c73a16ce0..c7165771c 100644
> --- a/src/libcamera/software_isp/software_isp.cpp
> +++ b/src/libcamera/software_isp/software_isp.cpp
> @@ -120,8 +120,9 @@ SoftwareIsp::SoftwareIsp(PipelineHandler *pipe, const CameraSensor *sensor,
>   	}
>   
>   	if (!softISPMode || softISPMode == "gpu") {
> -		if (eGL::isAvailable()) {
> -			debayer_ = std::make_unique<DebayerEGL>(std::move(stats), cm);
> +		auto display = eGL::probeDisplay();
> +		if (display != EGL_NO_DISPLAY) {
> +			debayer_ = std::make_unique<DebayerEGL>(std::move(stats), cm, display);
>   		} else {
>   			LOG(SoftwareIsp, Info)
>   				<< "EGL not available, falling back to CPU debayer";
Jacopo Mondi July 31, 2026, 1:01 p.m. UTC | #2
Hi Qi Hou

On Wed, Jul 29, 2026 at 11:55:57AM +0900, qi.hou@oss.nxp.com wrote:
> From: Qi Hou <qi.hou@nxp.com>
>
> isAvailable() previously called probeDisplay() to run a full EGL
> initialisation sequence and then immediately terminated the display with
> eglTerminate(). When SoftwareIsp subsequently created a DebayerEGL
> instance, initEGLContext() would call probeDisplay() again, repeating
> the full initialisation.
>
> Fix this by reusing the display obtained during the availability check.
> probeDisplay() is promoted to a public static method and isAvailable()
> is removed. The eGL constructor now takes the EGLDisplay as a parameter
> and stores it directly, so initEGLContext() no longer needs to call
> probeDisplay(). DebayerEGL is updated to accept and forward the display
> to eGL, and SoftwareIsp calls eGL::probeDisplay() once, passing the
> result straight to DebayerEGL when EGL is available.
>
> Signed-off-by: Qi Hou <qi.hou@nxp.com>
> ---
>  include/libcamera/internal/egl.h            |  5 +--
>  src/libcamera/egl.cpp                       | 42 +++++++--------------
>  src/libcamera/software_isp/debayer_egl.cpp  |  5 ++-
>  src/libcamera/software_isp/debayer_egl.h    |  2 +-
>  src/libcamera/software_isp/software_isp.cpp |  5 ++-
>  5 files changed, 23 insertions(+), 36 deletions(-)
>
> diff --git a/include/libcamera/internal/egl.h b/include/libcamera/internal/egl.h
> index 1e31d490b..7ef1ca0d9 100644
> --- a/include/libcamera/internal/egl.h
> +++ b/include/libcamera/internal/egl.h
> @@ -99,11 +99,11 @@ private:
>  class eGL
>  {
>  public:
> -	eGL();
> +	eGL(EGLDisplay display);
>  	~eGL();
>
>  	int initEGLContext();
> -	static bool isAvailable();
> +	static EGLDisplay probeDisplay();
>
>  	int createInputDMABufTexture2D(eGLImage &eglImage, int fd);
>  	int createOutputDMABufTexture2D(eGLImage &eglImage, int fd);
> @@ -137,7 +137,6 @@ private:
>  	EGLContext context_ = EGL_NO_CONTEXT;
>  	EGLSurface surface_ = EGL_NO_SURFACE;
>
> -	static EGLDisplay probeDisplay();
>  	int compileShader(int shaderType, GLuint &shaderId, Span<const unsigned char> shaderData,
>  			  Span<const std::string> shaderEnv);
>
> diff --git a/src/libcamera/egl.cpp b/src/libcamera/egl.cpp
> index 7ec7a654d..01e51cda8 100644
> --- a/src/libcamera/egl.cpp
> +++ b/src/libcamera/egl.cpp
> @@ -64,12 +64,15 @@ LOG_DEFINE_CATEGORY(eGL)
>
>  /**
>   * \brief Construct an EGL helper
> + * \param[in] display The EGL display to use
>   *
>   * Creates an eGL instance with uninitialised context. Call initEGLContext()
> - * to set up the EGL display, context, and load extension functions.
> + * to set up the EGL context, and load extension functions.
>   */
> -eGL::eGL()
> +eGL::eGL(EGLDisplay display)
> +	: display_(display)

If I'm not mistaken (not an expert of this area) EGLDisplay is a
pointer

/usr/include/EGL/egl.h:typedef void *EGLDisplay;

so we are not concerned about passing it around by value, right ?

>  {
> +	ASSERT(display_ != EGL_NO_DISPLAY);
>  }
>
>  /**
> @@ -299,15 +302,17 @@ void eGL::createTexture2D(eGLImage &eglImage, void *data)
>  	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
>  }
>
> +/**
> + * \brief Try to create an EGL display for surfaceless rendering
> + *
> + * Tries to create an EGL display for surfaceless rendering.
> + *
> + * \return A EGL display handle if successful, otherwise \a EGL_NO_DISPLAY
> + */
>  EGLDisplay eGL::probeDisplay()
>  {
>  	EGLDisplay display;
>
> -	if (!eglBindAPI(EGL_OPENGL_ES_API)) {
> -		LOG(eGL, Info) << "API bind fail";
> -		return EGL_NO_DISPLAY;
> -	}
> -
>  	display = eglGetPlatformDisplay(EGL_PLATFORM_SURFACELESS_MESA,
>  					EGL_DEFAULT_DISPLAY,
>  					nullptr);
> @@ -325,24 +330,6 @@ EGLDisplay eGL::probeDisplay()
>  	return display;
>  }
>
> -/**
> - * \brief Probe whether EGL surfaceless rendering is available
> - *
> - * Checks if an EGL surfaceless display can be obtained and initialised.
> - * The display is immediately terminated so that no resources are leaked.
> - *
> - * \return True if EGL surfaceless rendering is available, false otherwise
> - */
> -bool eGL::isAvailable()
> -{
> -	EGLDisplay display = probeDisplay();
> -	if (display == EGL_NO_DISPLAY)
> -		return false;
> -
> -	eglTerminate(display);
> -	return true;
> -}
> -
>  /**
>   * \brief Update a 2D texture already created
>   * \param[in,out] eglImage EGL image to associate with the texture
> @@ -403,9 +390,8 @@ int eGL::initEGLContext()
>  	EGLint numConfigs;
>  	EGLConfig config;
>
> -	display_ = probeDisplay();
> -	if (display_ == EGL_NO_DISPLAY) {
> -		LOG(eGL, Error) << "Unable to probe display";
> +	if (!eglBindAPI(EGL_OPENGL_ES_API)) {
> +		LOG(eGL, Error) << "API bind fail";
>  		goto fail;

Should this check remain in probeDisplay() ?

If this check fails (in probeDisplay()) you return EGL_NO_DISPLAY and
the eGL class constructor now has

	ASSERT(display_ != EGL_NO_DISPLAY);

so if you get here you should be safe ?

Or have I missed something ?

Thanks
  j

>  	}
>
> diff --git a/src/libcamera/software_isp/debayer_egl.cpp b/src/libcamera/software_isp/debayer_egl.cpp
> index dd2041219..20b478b1c 100644
> --- a/src/libcamera/software_isp/debayer_egl.cpp
> +++ b/src/libcamera/software_isp/debayer_egl.cpp
> @@ -40,9 +40,10 @@ namespace libcamera {
>   * \brief Construct a DebayerEGL object
>   * \param[in] stats Statistics processing object
>   * \param[in] cm The camera manager
> + * \param[in] display The EGL display to use
>   */
> -DebayerEGL::DebayerEGL(std::unique_ptr<SwStatsCpu> stats, const CameraManager &cm)
> -	: Debayer(cm), stats_(std::move(stats))
> +DebayerEGL::DebayerEGL(std::unique_ptr<SwStatsCpu> stats, const CameraManager &cm, EGLDisplay display)
> +	: Debayer(cm), stats_(std::move(stats)), egl_(display)
>  {
>  }
>
> diff --git a/src/libcamera/software_isp/debayer_egl.h b/src/libcamera/software_isp/debayer_egl.h
> index e613639f5..30e51a477 100644
> --- a/src/libcamera/software_isp/debayer_egl.h
> +++ b/src/libcamera/software_isp/debayer_egl.h
> @@ -40,7 +40,7 @@ class CameraManager;
>  class DebayerEGL : public Debayer
>  {
>  public:
> -	DebayerEGL(std::unique_ptr<SwStatsCpu> stats, const CameraManager &cm);
> +	DebayerEGL(std::unique_ptr<SwStatsCpu> stats, const CameraManager &cm, EGLDisplay display);
>  	~DebayerEGL();
>
>  	int configure(const StreamConfiguration &inputCfg,
> diff --git a/src/libcamera/software_isp/software_isp.cpp b/src/libcamera/software_isp/software_isp.cpp
> index c73a16ce0..c7165771c 100644
> --- a/src/libcamera/software_isp/software_isp.cpp
> +++ b/src/libcamera/software_isp/software_isp.cpp
> @@ -120,8 +120,9 @@ SoftwareIsp::SoftwareIsp(PipelineHandler *pipe, const CameraSensor *sensor,
>  	}
>
>  	if (!softISPMode || softISPMode == "gpu") {
> -		if (eGL::isAvailable()) {
> -			debayer_ = std::make_unique<DebayerEGL>(std::move(stats), cm);
> +		auto display = eGL::probeDisplay();
> +		if (display != EGL_NO_DISPLAY) {
> +			debayer_ = std::make_unique<DebayerEGL>(std::move(stats), cm, display);
>  		} else {
>  			LOG(SoftwareIsp, Info)
>  				<< "EGL not available, falling back to CPU debayer";
> --
> 2.34.1
>
Barnabás Pőcze July 31, 2026, 1:04 p.m. UTC | #3
2026. 07. 31. 15:01 keltezéssel, Jacopo Mondi írta:
> Hi Qi Hou
> 
> On Wed, Jul 29, 2026 at 11:55:57AM +0900, qi.hou@oss.nxp.com wrote:
>> From: Qi Hou <qi.hou@nxp.com>
>>
>> isAvailable() previously called probeDisplay() to run a full EGL
>> initialisation sequence and then immediately terminated the display with
>> eglTerminate(). When SoftwareIsp subsequently created a DebayerEGL
>> instance, initEGLContext() would call probeDisplay() again, repeating
>> the full initialisation.
>>
>> Fix this by reusing the display obtained during the availability check.
>> probeDisplay() is promoted to a public static method and isAvailable()
>> is removed. The eGL constructor now takes the EGLDisplay as a parameter
>> and stores it directly, so initEGLContext() no longer needs to call
>> probeDisplay(). DebayerEGL is updated to accept and forward the display
>> to eGL, and SoftwareIsp calls eGL::probeDisplay() once, passing the
>> result straight to DebayerEGL when EGL is available.
>>
>> Signed-off-by: Qi Hou <qi.hou@nxp.com>
>> ---
>>   include/libcamera/internal/egl.h            |  5 +--
>>   src/libcamera/egl.cpp                       | 42 +++++++--------------
>>   src/libcamera/software_isp/debayer_egl.cpp  |  5 ++-
>>   src/libcamera/software_isp/debayer_egl.h    |  2 +-
>>   src/libcamera/software_isp/software_isp.cpp |  5 ++-
>>   5 files changed, 23 insertions(+), 36 deletions(-)
>>
>> diff --git a/include/libcamera/internal/egl.h b/include/libcamera/internal/egl.h
>> index 1e31d490b..7ef1ca0d9 100644
>> --- a/include/libcamera/internal/egl.h
>> +++ b/include/libcamera/internal/egl.h
>> @@ -99,11 +99,11 @@ private:
>>   class eGL
>>   {
>>   public:
>> -	eGL();
>> +	eGL(EGLDisplay display);
>>   	~eGL();
>>
>>   	int initEGLContext();
>> -	static bool isAvailable();
>> +	static EGLDisplay probeDisplay();
>>
>>   	int createInputDMABufTexture2D(eGLImage &eglImage, int fd);
>>   	int createOutputDMABufTexture2D(eGLImage &eglImage, int fd);
>> @@ -137,7 +137,6 @@ private:
>>   	EGLContext context_ = EGL_NO_CONTEXT;
>>   	EGLSurface surface_ = EGL_NO_SURFACE;
>>
>> -	static EGLDisplay probeDisplay();
>>   	int compileShader(int shaderType, GLuint &shaderId, Span<const unsigned char> shaderData,
>>   			  Span<const std::string> shaderEnv);
>>
>> diff --git a/src/libcamera/egl.cpp b/src/libcamera/egl.cpp
>> index 7ec7a654d..01e51cda8 100644
>> --- a/src/libcamera/egl.cpp
>> +++ b/src/libcamera/egl.cpp
>> @@ -64,12 +64,15 @@ LOG_DEFINE_CATEGORY(eGL)
>>
>>   /**
>>    * \brief Construct an EGL helper
>> + * \param[in] display The EGL display to use
>>    *
>>    * Creates an eGL instance with uninitialised context. Call initEGLContext()
>> - * to set up the EGL display, context, and load extension functions.
>> + * to set up the EGL context, and load extension functions.
>>    */
>> -eGL::eGL()
>> +eGL::eGL(EGLDisplay display)
>> +	: display_(display)
> 
> If I'm not mistaken (not an expert of this area) EGLDisplay is a
> pointer
> 
> /usr/include/EGL/egl.h:typedef void *EGLDisplay;
> 
> so we are not concerned about passing it around by value, right ?
> 
>>   {
>> +	ASSERT(display_ != EGL_NO_DISPLAY);
>>   }
>>
>>   /**
>> @@ -299,15 +302,17 @@ void eGL::createTexture2D(eGLImage &eglImage, void *data)
>>   	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
>>   }
>>
>> +/**
>> + * \brief Try to create an EGL display for surfaceless rendering
>> + *
>> + * Tries to create an EGL display for surfaceless rendering.
>> + *
>> + * \return A EGL display handle if successful, otherwise \a EGL_NO_DISPLAY
>> + */
>>   EGLDisplay eGL::probeDisplay()
>>   {
>>   	EGLDisplay display;
>>
>> -	if (!eglBindAPI(EGL_OPENGL_ES_API)) {
>> -		LOG(eGL, Info) << "API bind fail";
>> -		return EGL_NO_DISPLAY;
>> -	}
>> -
>>   	display = eglGetPlatformDisplay(EGL_PLATFORM_SURFACELESS_MESA,
>>   					EGL_DEFAULT_DISPLAY,
>>   					nullptr);
>> @@ -325,24 +330,6 @@ EGLDisplay eGL::probeDisplay()
>>   	return display;
>>   }
>>
>> -/**
>> - * \brief Probe whether EGL surfaceless rendering is available
>> - *
>> - * Checks if an EGL surfaceless display can be obtained and initialised.
>> - * The display is immediately terminated so that no resources are leaked.
>> - *
>> - * \return True if EGL surfaceless rendering is available, false otherwise
>> - */
>> -bool eGL::isAvailable()
>> -{
>> -	EGLDisplay display = probeDisplay();
>> -	if (display == EGL_NO_DISPLAY)
>> -		return false;
>> -
>> -	eglTerminate(display);
>> -	return true;
>> -}
>> -
>>   /**
>>    * \brief Update a 2D texture already created
>>    * \param[in,out] eglImage EGL image to associate with the texture
>> @@ -403,9 +390,8 @@ int eGL::initEGLContext()
>>   	EGLint numConfigs;
>>   	EGLConfig config;
>>
>> -	display_ = probeDisplay();
>> -	if (display_ == EGL_NO_DISPLAY) {
>> -		LOG(eGL, Error) << "Unable to probe display";
>> +	if (!eglBindAPI(EGL_OPENGL_ES_API)) {
>> +		LOG(eGL, Error) << "API bind fail";
>>   		goto fail;
> 
> Should this check remain in probeDisplay() ?
> 
> If this check fails (in probeDisplay()) you return EGL_NO_DISPLAY and
> the eGL class constructor now has
> 
> 	ASSERT(display_ != EGL_NO_DISPLAY);
> 
> so if you get here you should be safe ?
> 
> Or have I missed something ?

I suggested moving it in the previous version because
   * it affects per-thread state in the egl library,
   * the display creation should work without it (as far as I can tell).


> 
> Thanks
>    j
> 
>>   	}
>>
>> diff --git a/src/libcamera/software_isp/debayer_egl.cpp b/src/libcamera/software_isp/debayer_egl.cpp
>> index dd2041219..20b478b1c 100644
>> --- a/src/libcamera/software_isp/debayer_egl.cpp
>> +++ b/src/libcamera/software_isp/debayer_egl.cpp
>> @@ -40,9 +40,10 @@ namespace libcamera {
>>    * \brief Construct a DebayerEGL object
>>    * \param[in] stats Statistics processing object
>>    * \param[in] cm The camera manager
>> + * \param[in] display The EGL display to use
>>    */
>> -DebayerEGL::DebayerEGL(std::unique_ptr<SwStatsCpu> stats, const CameraManager &cm)
>> -	: Debayer(cm), stats_(std::move(stats))
>> +DebayerEGL::DebayerEGL(std::unique_ptr<SwStatsCpu> stats, const CameraManager &cm, EGLDisplay display)
>> +	: Debayer(cm), stats_(std::move(stats)), egl_(display)
>>   {
>>   }
>>
>> diff --git a/src/libcamera/software_isp/debayer_egl.h b/src/libcamera/software_isp/debayer_egl.h
>> index e613639f5..30e51a477 100644
>> --- a/src/libcamera/software_isp/debayer_egl.h
>> +++ b/src/libcamera/software_isp/debayer_egl.h
>> @@ -40,7 +40,7 @@ class CameraManager;
>>   class DebayerEGL : public Debayer
>>   {
>>   public:
>> -	DebayerEGL(std::unique_ptr<SwStatsCpu> stats, const CameraManager &cm);
>> +	DebayerEGL(std::unique_ptr<SwStatsCpu> stats, const CameraManager &cm, EGLDisplay display);
>>   	~DebayerEGL();
>>
>>   	int configure(const StreamConfiguration &inputCfg,
>> diff --git a/src/libcamera/software_isp/software_isp.cpp b/src/libcamera/software_isp/software_isp.cpp
>> index c73a16ce0..c7165771c 100644
>> --- a/src/libcamera/software_isp/software_isp.cpp
>> +++ b/src/libcamera/software_isp/software_isp.cpp
>> @@ -120,8 +120,9 @@ SoftwareIsp::SoftwareIsp(PipelineHandler *pipe, const CameraSensor *sensor,
>>   	}
>>
>>   	if (!softISPMode || softISPMode == "gpu") {
>> -		if (eGL::isAvailable()) {
>> -			debayer_ = std::make_unique<DebayerEGL>(std::move(stats), cm);
>> +		auto display = eGL::probeDisplay();
>> +		if (display != EGL_NO_DISPLAY) {
>> +			debayer_ = std::make_unique<DebayerEGL>(std::move(stats), cm, display);
>>   		} else {
>>   			LOG(SoftwareIsp, Info)
>>   				<< "EGL not available, falling back to CPU debayer";
>> --
>> 2.34.1
>>
Jacopo Mondi July 31, 2026, 1:08 p.m. UTC | #4
Hi Barnabás

On Fri, Jul 31, 2026 at 03:04:23PM +0200, Barnabás Pőcze wrote:
> 2026. 07. 31. 15:01 keltezéssel, Jacopo Mondi írta:
> > Hi Qi Hou
> >
> > On Wed, Jul 29, 2026 at 11:55:57AM +0900, qi.hou@oss.nxp.com wrote:
> > > From: Qi Hou <qi.hou@nxp.com>
> > >
> > > isAvailable() previously called probeDisplay() to run a full EGL
> > > initialisation sequence and then immediately terminated the display with
> > > eglTerminate(). When SoftwareIsp subsequently created a DebayerEGL
> > > instance, initEGLContext() would call probeDisplay() again, repeating
> > > the full initialisation.
> > >
> > > Fix this by reusing the display obtained during the availability check.
> > > probeDisplay() is promoted to a public static method and isAvailable()
> > > is removed. The eGL constructor now takes the EGLDisplay as a parameter
> > > and stores it directly, so initEGLContext() no longer needs to call
> > > probeDisplay(). DebayerEGL is updated to accept and forward the display
> > > to eGL, and SoftwareIsp calls eGL::probeDisplay() once, passing the
> > > result straight to DebayerEGL when EGL is available.
> > >
> > > Signed-off-by: Qi Hou <qi.hou@nxp.com>
> > > ---
> > >   include/libcamera/internal/egl.h            |  5 +--
> > >   src/libcamera/egl.cpp                       | 42 +++++++--------------
> > >   src/libcamera/software_isp/debayer_egl.cpp  |  5 ++-
> > >   src/libcamera/software_isp/debayer_egl.h    |  2 +-
> > >   src/libcamera/software_isp/software_isp.cpp |  5 ++-
> > >   5 files changed, 23 insertions(+), 36 deletions(-)
> > >
> > > diff --git a/include/libcamera/internal/egl.h b/include/libcamera/internal/egl.h
> > > index 1e31d490b..7ef1ca0d9 100644
> > > --- a/include/libcamera/internal/egl.h
> > > +++ b/include/libcamera/internal/egl.h
> > > @@ -99,11 +99,11 @@ private:
> > >   class eGL
> > >   {
> > >   public:
> > > -	eGL();
> > > +	eGL(EGLDisplay display);
> > >   	~eGL();
> > >
> > >   	int initEGLContext();
> > > -	static bool isAvailable();
> > > +	static EGLDisplay probeDisplay();
> > >
> > >   	int createInputDMABufTexture2D(eGLImage &eglImage, int fd);
> > >   	int createOutputDMABufTexture2D(eGLImage &eglImage, int fd);
> > > @@ -137,7 +137,6 @@ private:
> > >   	EGLContext context_ = EGL_NO_CONTEXT;
> > >   	EGLSurface surface_ = EGL_NO_SURFACE;
> > >
> > > -	static EGLDisplay probeDisplay();
> > >   	int compileShader(int shaderType, GLuint &shaderId, Span<const unsigned char> shaderData,
> > >   			  Span<const std::string> shaderEnv);
> > >
> > > diff --git a/src/libcamera/egl.cpp b/src/libcamera/egl.cpp
> > > index 7ec7a654d..01e51cda8 100644
> > > --- a/src/libcamera/egl.cpp
> > > +++ b/src/libcamera/egl.cpp
> > > @@ -64,12 +64,15 @@ LOG_DEFINE_CATEGORY(eGL)
> > >
> > >   /**
> > >    * \brief Construct an EGL helper
> > > + * \param[in] display The EGL display to use
> > >    *
> > >    * Creates an eGL instance with uninitialised context. Call initEGLContext()
> > > - * to set up the EGL display, context, and load extension functions.
> > > + * to set up the EGL context, and load extension functions.
> > >    */
> > > -eGL::eGL()
> > > +eGL::eGL(EGLDisplay display)
> > > +	: display_(display)
> >
> > If I'm not mistaken (not an expert of this area) EGLDisplay is a
> > pointer
> >
> > /usr/include/EGL/egl.h:typedef void *EGLDisplay;
> >
> > so we are not concerned about passing it around by value, right ?
> >
> > >   {
> > > +	ASSERT(display_ != EGL_NO_DISPLAY);
> > >   }
> > >
> > >   /**
> > > @@ -299,15 +302,17 @@ void eGL::createTexture2D(eGLImage &eglImage, void *data)
> > >   	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
> > >   }
> > >
> > > +/**
> > > + * \brief Try to create an EGL display for surfaceless rendering
> > > + *
> > > + * Tries to create an EGL display for surfaceless rendering.
> > > + *
> > > + * \return A EGL display handle if successful, otherwise \a EGL_NO_DISPLAY
> > > + */
> > >   EGLDisplay eGL::probeDisplay()
> > >   {
> > >   	EGLDisplay display;
> > >
> > > -	if (!eglBindAPI(EGL_OPENGL_ES_API)) {
> > > -		LOG(eGL, Info) << "API bind fail";
> > > -		return EGL_NO_DISPLAY;
> > > -	}
> > > -
> > >   	display = eglGetPlatformDisplay(EGL_PLATFORM_SURFACELESS_MESA,
> > >   					EGL_DEFAULT_DISPLAY,
> > >   					nullptr);
> > > @@ -325,24 +330,6 @@ EGLDisplay eGL::probeDisplay()
> > >   	return display;
> > >   }
> > >
> > > -/**
> > > - * \brief Probe whether EGL surfaceless rendering is available
> > > - *
> > > - * Checks if an EGL surfaceless display can be obtained and initialised.
> > > - * The display is immediately terminated so that no resources are leaked.
> > > - *
> > > - * \return True if EGL surfaceless rendering is available, false otherwise
> > > - */
> > > -bool eGL::isAvailable()
> > > -{
> > > -	EGLDisplay display = probeDisplay();
> > > -	if (display == EGL_NO_DISPLAY)
> > > -		return false;
> > > -
> > > -	eglTerminate(display);
> > > -	return true;
> > > -}
> > > -
> > >   /**
> > >    * \brief Update a 2D texture already created
> > >    * \param[in,out] eglImage EGL image to associate with the texture
> > > @@ -403,9 +390,8 @@ int eGL::initEGLContext()
> > >   	EGLint numConfigs;
> > >   	EGLConfig config;
> > >
> > > -	display_ = probeDisplay();
> > > -	if (display_ == EGL_NO_DISPLAY) {
> > > -		LOG(eGL, Error) << "Unable to probe display";
> > > +	if (!eglBindAPI(EGL_OPENGL_ES_API)) {
> > > +		LOG(eGL, Error) << "API bind fail";
> > >   		goto fail;
> >
> > Should this check remain in probeDisplay() ?
> >
> > If this check fails (in probeDisplay()) you return EGL_NO_DISPLAY and
> > the eGL class constructor now has
> >
> > 	ASSERT(display_ != EGL_NO_DISPLAY);
> >
> > so if you get here you should be safe ?
> >
> > Or have I missed something ?
>
> I suggested moving it in the previous version because
>   * it affects per-thread state in the egl library,
>   * the display creation should work without it (as far as I can tell).
>

Ah yes, I just read your comment on v2..

are you happy with me collecting this patch in this form then ?

>
> >
> > Thanks
> >    j
> >
> > >   	}
> > >
> > > diff --git a/src/libcamera/software_isp/debayer_egl.cpp b/src/libcamera/software_isp/debayer_egl.cpp
> > > index dd2041219..20b478b1c 100644
> > > --- a/src/libcamera/software_isp/debayer_egl.cpp
> > > +++ b/src/libcamera/software_isp/debayer_egl.cpp
> > > @@ -40,9 +40,10 @@ namespace libcamera {
> > >    * \brief Construct a DebayerEGL object
> > >    * \param[in] stats Statistics processing object
> > >    * \param[in] cm The camera manager
> > > + * \param[in] display The EGL display to use
> > >    */
> > > -DebayerEGL::DebayerEGL(std::unique_ptr<SwStatsCpu> stats, const CameraManager &cm)
> > > -	: Debayer(cm), stats_(std::move(stats))
> > > +DebayerEGL::DebayerEGL(std::unique_ptr<SwStatsCpu> stats, const CameraManager &cm, EGLDisplay display)
> > > +	: Debayer(cm), stats_(std::move(stats)), egl_(display)
> > >   {
> > >   }
> > >
> > > diff --git a/src/libcamera/software_isp/debayer_egl.h b/src/libcamera/software_isp/debayer_egl.h
> > > index e613639f5..30e51a477 100644
> > > --- a/src/libcamera/software_isp/debayer_egl.h
> > > +++ b/src/libcamera/software_isp/debayer_egl.h
> > > @@ -40,7 +40,7 @@ class CameraManager;
> > >   class DebayerEGL : public Debayer
> > >   {
> > >   public:
> > > -	DebayerEGL(std::unique_ptr<SwStatsCpu> stats, const CameraManager &cm);
> > > +	DebayerEGL(std::unique_ptr<SwStatsCpu> stats, const CameraManager &cm, EGLDisplay display);
> > >   	~DebayerEGL();
> > >
> > >   	int configure(const StreamConfiguration &inputCfg,
> > > diff --git a/src/libcamera/software_isp/software_isp.cpp b/src/libcamera/software_isp/software_isp.cpp
> > > index c73a16ce0..c7165771c 100644
> > > --- a/src/libcamera/software_isp/software_isp.cpp
> > > +++ b/src/libcamera/software_isp/software_isp.cpp
> > > @@ -120,8 +120,9 @@ SoftwareIsp::SoftwareIsp(PipelineHandler *pipe, const CameraSensor *sensor,
> > >   	}
> > >
> > >   	if (!softISPMode || softISPMode == "gpu") {
> > > -		if (eGL::isAvailable()) {
> > > -			debayer_ = std::make_unique<DebayerEGL>(std::move(stats), cm);
> > > +		auto display = eGL::probeDisplay();
> > > +		if (display != EGL_NO_DISPLAY) {
> > > +			debayer_ = std::make_unique<DebayerEGL>(std::move(stats), cm, display);
> > >   		} else {
> > >   			LOG(SoftwareIsp, Info)
> > >   				<< "EGL not available, falling back to CPU debayer";
> > > --
> > > 2.34.1
> > >
>
Qi Hou (OSS) Aug. 4, 2026, 9:06 a.m. UTC | #5
Hi Jacopo Mondi,

Yes, you're right. `EGLDisplay` is `typedef void *EGLDisplay`, passing and storing it by value is the standard EGL idiom. No concern here.

Regards,
Qi Hou

-----Original Message-----
From: Jacopo Mondi <jacopo.mondi@ideasonboard.com> 
Sent: Friday, July 31, 2026 9:02 PM
To: Qi Hou (OSS) <qi.hou@oss.nxp.com>
Cc: libcamera-devel@lists.libcamera.org; Jared Hu <jared.hu@nxp.com>; Julien Vuillaumier <julien.vuillaumier@nxp.com>
Subject: Re: [PATCH v3] libcamera: egl: Pass EGLDisplay to eGL constructor to avoid double init

Hi Qi Hou

On Wed, Jul 29, 2026 at 11:55:57AM +0900, qi.hou@oss.nxp.com wrote:
> From: Qi Hou <qi.hou@nxp.com>
>
> isAvailable() previously called probeDisplay() to run a full EGL 
> initialisation sequence and then immediately terminated the display 
> with eglTerminate(). When SoftwareIsp subsequently created a 
> DebayerEGL instance, initEGLContext() would call probeDisplay() again, 
> repeating the full initialisation.
>
> Fix this by reusing the display obtained during the availability check.
> probeDisplay() is promoted to a public static method and isAvailable() 
> is removed. The eGL constructor now takes the EGLDisplay as a 
> parameter and stores it directly, so initEGLContext() no longer needs 
> to call probeDisplay(). DebayerEGL is updated to accept and forward 
> the display to eGL, and SoftwareIsp calls eGL::probeDisplay() once, 
> passing the result straight to DebayerEGL when EGL is available.
>
> Signed-off-by: Qi Hou <qi.hou@nxp.com>
> ---
>  include/libcamera/internal/egl.h            |  5 +--
>  src/libcamera/egl.cpp                       | 42 +++++++--------------
>  src/libcamera/software_isp/debayer_egl.cpp  |  5 ++-
>  src/libcamera/software_isp/debayer_egl.h    |  2 +-
>  src/libcamera/software_isp/software_isp.cpp |  5 ++-
>  5 files changed, 23 insertions(+), 36 deletions(-)
>
> diff --git a/include/libcamera/internal/egl.h 
> b/include/libcamera/internal/egl.h
> index 1e31d490b..7ef1ca0d9 100644
> --- a/include/libcamera/internal/egl.h
> +++ b/include/libcamera/internal/egl.h
> @@ -99,11 +99,11 @@ private:
>  class eGL
>  {
>  public:
> -	eGL();
> +	eGL(EGLDisplay display);
>  	~eGL();
>
>  	int initEGLContext();
> -	static bool isAvailable();
> +	static EGLDisplay probeDisplay();
>
>  	int createInputDMABufTexture2D(eGLImage &eglImage, int fd);
>  	int createOutputDMABufTexture2D(eGLImage &eglImage, int fd); @@ 
> -137,7 +137,6 @@ private:
>  	EGLContext context_ = EGL_NO_CONTEXT;
>  	EGLSurface surface_ = EGL_NO_SURFACE;
>
> -	static EGLDisplay probeDisplay();
>  	int compileShader(int shaderType, GLuint &shaderId, Span<const unsigned char> shaderData,
>  			  Span<const std::string> shaderEnv);
>
> diff --git a/src/libcamera/egl.cpp b/src/libcamera/egl.cpp index 
> 7ec7a654d..01e51cda8 100644
> --- a/src/libcamera/egl.cpp
> +++ b/src/libcamera/egl.cpp
> @@ -64,12 +64,15 @@ LOG_DEFINE_CATEGORY(eGL)
>
>  /**
>   * \brief Construct an EGL helper
> + * \param[in] display The EGL display to use
>   *
>   * Creates an eGL instance with uninitialised context. Call 
> initEGLContext()
> - * to set up the EGL display, context, and load extension functions.
> + * to set up the EGL context, and load extension functions.
>   */
> -eGL::eGL()
> +eGL::eGL(EGLDisplay display)
> +	: display_(display)

If I'm not mistaken (not an expert of this area) EGLDisplay is a pointer

/usr/include/EGL/egl.h:typedef void *EGLDisplay;

so we are not concerned about passing it around by value, right ?

>  {
> +	ASSERT(display_ != EGL_NO_DISPLAY);
>  }
>
>  /**
> @@ -299,15 +302,17 @@ void eGL::createTexture2D(eGLImage &eglImage, void *data)
>  	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);  
> }
>
> +/**
> + * \brief Try to create an EGL display for surfaceless rendering
> + *
> + * Tries to create an EGL display for surfaceless rendering.
> + *
> + * \return A EGL display handle if successful, otherwise \a 
> +EGL_NO_DISPLAY  */
>  EGLDisplay eGL::probeDisplay()
>  {
>  	EGLDisplay display;
>
> -	if (!eglBindAPI(EGL_OPENGL_ES_API)) {
> -		LOG(eGL, Info) << "API bind fail";
> -		return EGL_NO_DISPLAY;
> -	}
> -
>  	display = eglGetPlatformDisplay(EGL_PLATFORM_SURFACELESS_MESA,
>  					EGL_DEFAULT_DISPLAY,
>  					nullptr);
> @@ -325,24 +330,6 @@ EGLDisplay eGL::probeDisplay()
>  	return display;
>  }
>
> -/**
> - * \brief Probe whether EGL surfaceless rendering is available
> - *
> - * Checks if an EGL surfaceless display can be obtained and initialised.
> - * The display is immediately terminated so that no resources are leaked.
> - *
> - * \return True if EGL surfaceless rendering is available, false 
> otherwise
> - */
> -bool eGL::isAvailable()
> -{
> -	EGLDisplay display = probeDisplay();
> -	if (display == EGL_NO_DISPLAY)
> -		return false;
> -
> -	eglTerminate(display);
> -	return true;
> -}
> -
>  /**
>   * \brief Update a 2D texture already created
>   * \param[in,out] eglImage EGL image to associate with the texture @@ 
> -403,9 +390,8 @@ int eGL::initEGLContext()
>  	EGLint numConfigs;
>  	EGLConfig config;
>
> -	display_ = probeDisplay();
> -	if (display_ == EGL_NO_DISPLAY) {
> -		LOG(eGL, Error) << "Unable to probe display";
> +	if (!eglBindAPI(EGL_OPENGL_ES_API)) {
> +		LOG(eGL, Error) << "API bind fail";
>  		goto fail;

Should this check remain in probeDisplay() ?

If this check fails (in probeDisplay()) you return EGL_NO_DISPLAY and the eGL class constructor now has

	ASSERT(display_ != EGL_NO_DISPLAY);

so if you get here you should be safe ?

Or have I missed something ?

Thanks
  j

>  	}
>
> diff --git a/src/libcamera/software_isp/debayer_egl.cpp 
> b/src/libcamera/software_isp/debayer_egl.cpp
> index dd2041219..20b478b1c 100644
> --- a/src/libcamera/software_isp/debayer_egl.cpp
> +++ b/src/libcamera/software_isp/debayer_egl.cpp
> @@ -40,9 +40,10 @@ namespace libcamera {
>   * \brief Construct a DebayerEGL object
>   * \param[in] stats Statistics processing object
>   * \param[in] cm The camera manager
> + * \param[in] display The EGL display to use
>   */
> -DebayerEGL::DebayerEGL(std::unique_ptr<SwStatsCpu> stats, const CameraManager &cm)
> -	: Debayer(cm), stats_(std::move(stats))
> +DebayerEGL::DebayerEGL(std::unique_ptr<SwStatsCpu> stats, const CameraManager &cm, EGLDisplay display)
> +	: Debayer(cm), stats_(std::move(stats)), egl_(display)
>  {
>  }
>
> diff --git a/src/libcamera/software_isp/debayer_egl.h 
> b/src/libcamera/software_isp/debayer_egl.h
> index e613639f5..30e51a477 100644
> --- a/src/libcamera/software_isp/debayer_egl.h
> +++ b/src/libcamera/software_isp/debayer_egl.h
> @@ -40,7 +40,7 @@ class CameraManager;  class DebayerEGL : public 
> Debayer  {
>  public:
> -	DebayerEGL(std::unique_ptr<SwStatsCpu> stats, const CameraManager &cm);
> +	DebayerEGL(std::unique_ptr<SwStatsCpu> stats, const CameraManager 
> +&cm, EGLDisplay display);
>  	~DebayerEGL();
>
>  	int configure(const StreamConfiguration &inputCfg, diff --git 
> a/src/libcamera/software_isp/software_isp.cpp 
> b/src/libcamera/software_isp/software_isp.cpp
> index c73a16ce0..c7165771c 100644
> --- a/src/libcamera/software_isp/software_isp.cpp
> +++ b/src/libcamera/software_isp/software_isp.cpp
> @@ -120,8 +120,9 @@ SoftwareIsp::SoftwareIsp(PipelineHandler *pipe, const CameraSensor *sensor,
>  	}
>
>  	if (!softISPMode || softISPMode == "gpu") {
> -		if (eGL::isAvailable()) {
> -			debayer_ = std::make_unique<DebayerEGL>(std::move(stats), cm);
> +		auto display = eGL::probeDisplay();
> +		if (display != EGL_NO_DISPLAY) {
> +			debayer_ = std::make_unique<DebayerEGL>(std::move(stats), cm, 
> +display);
>  		} else {
>  			LOG(SoftwareIsp, Info)
>  				<< "EGL not available, falling back to CPU debayer";
> --
> 2.34.1
>

Patch
diff mbox series

diff --git a/include/libcamera/internal/egl.h b/include/libcamera/internal/egl.h
index 1e31d490b..7ef1ca0d9 100644
--- a/include/libcamera/internal/egl.h
+++ b/include/libcamera/internal/egl.h
@@ -99,11 +99,11 @@  private:
 class eGL
 {
 public:
-	eGL();
+	eGL(EGLDisplay display);
 	~eGL();
 
 	int initEGLContext();
-	static bool isAvailable();
+	static EGLDisplay probeDisplay();
 
 	int createInputDMABufTexture2D(eGLImage &eglImage, int fd);
 	int createOutputDMABufTexture2D(eGLImage &eglImage, int fd);
@@ -137,7 +137,6 @@  private:
 	EGLContext context_ = EGL_NO_CONTEXT;
 	EGLSurface surface_ = EGL_NO_SURFACE;
 
-	static EGLDisplay probeDisplay();
 	int compileShader(int shaderType, GLuint &shaderId, Span<const unsigned char> shaderData,
 			  Span<const std::string> shaderEnv);
 
diff --git a/src/libcamera/egl.cpp b/src/libcamera/egl.cpp
index 7ec7a654d..01e51cda8 100644
--- a/src/libcamera/egl.cpp
+++ b/src/libcamera/egl.cpp
@@ -64,12 +64,15 @@  LOG_DEFINE_CATEGORY(eGL)
 
 /**
  * \brief Construct an EGL helper
+ * \param[in] display The EGL display to use
  *
  * Creates an eGL instance with uninitialised context. Call initEGLContext()
- * to set up the EGL display, context, and load extension functions.
+ * to set up the EGL context, and load extension functions.
  */
-eGL::eGL()
+eGL::eGL(EGLDisplay display)
+	: display_(display)
 {
+	ASSERT(display_ != EGL_NO_DISPLAY);
 }
 
 /**
@@ -299,15 +302,17 @@  void eGL::createTexture2D(eGLImage &eglImage, void *data)
 	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
 }
 
+/**
+ * \brief Try to create an EGL display for surfaceless rendering
+ *
+ * Tries to create an EGL display for surfaceless rendering.
+ *
+ * \return A EGL display handle if successful, otherwise \a EGL_NO_DISPLAY
+ */
 EGLDisplay eGL::probeDisplay()
 {
 	EGLDisplay display;
 
-	if (!eglBindAPI(EGL_OPENGL_ES_API)) {
-		LOG(eGL, Info) << "API bind fail";
-		return EGL_NO_DISPLAY;
-	}
-
 	display = eglGetPlatformDisplay(EGL_PLATFORM_SURFACELESS_MESA,
 					EGL_DEFAULT_DISPLAY,
 					nullptr);
@@ -325,24 +330,6 @@  EGLDisplay eGL::probeDisplay()
 	return display;
 }
 
-/**
- * \brief Probe whether EGL surfaceless rendering is available
- *
- * Checks if an EGL surfaceless display can be obtained and initialised.
- * The display is immediately terminated so that no resources are leaked.
- *
- * \return True if EGL surfaceless rendering is available, false otherwise
- */
-bool eGL::isAvailable()
-{
-	EGLDisplay display = probeDisplay();
-	if (display == EGL_NO_DISPLAY)
-		return false;
-
-	eglTerminate(display);
-	return true;
-}
-
 /**
  * \brief Update a 2D texture already created
  * \param[in,out] eglImage EGL image to associate with the texture
@@ -403,9 +390,8 @@  int eGL::initEGLContext()
 	EGLint numConfigs;
 	EGLConfig config;
 
-	display_ = probeDisplay();
-	if (display_ == EGL_NO_DISPLAY) {
-		LOG(eGL, Error) << "Unable to probe display";
+	if (!eglBindAPI(EGL_OPENGL_ES_API)) {
+		LOG(eGL, Error) << "API bind fail";
 		goto fail;
 	}
 
diff --git a/src/libcamera/software_isp/debayer_egl.cpp b/src/libcamera/software_isp/debayer_egl.cpp
index dd2041219..20b478b1c 100644
--- a/src/libcamera/software_isp/debayer_egl.cpp
+++ b/src/libcamera/software_isp/debayer_egl.cpp
@@ -40,9 +40,10 @@  namespace libcamera {
  * \brief Construct a DebayerEGL object
  * \param[in] stats Statistics processing object
  * \param[in] cm The camera manager
+ * \param[in] display The EGL display to use
  */
-DebayerEGL::DebayerEGL(std::unique_ptr<SwStatsCpu> stats, const CameraManager &cm)
-	: Debayer(cm), stats_(std::move(stats))
+DebayerEGL::DebayerEGL(std::unique_ptr<SwStatsCpu> stats, const CameraManager &cm, EGLDisplay display)
+	: Debayer(cm), stats_(std::move(stats)), egl_(display)
 {
 }
 
diff --git a/src/libcamera/software_isp/debayer_egl.h b/src/libcamera/software_isp/debayer_egl.h
index e613639f5..30e51a477 100644
--- a/src/libcamera/software_isp/debayer_egl.h
+++ b/src/libcamera/software_isp/debayer_egl.h
@@ -40,7 +40,7 @@  class CameraManager;
 class DebayerEGL : public Debayer
 {
 public:
-	DebayerEGL(std::unique_ptr<SwStatsCpu> stats, const CameraManager &cm);
+	DebayerEGL(std::unique_ptr<SwStatsCpu> stats, const CameraManager &cm, EGLDisplay display);
 	~DebayerEGL();
 
 	int configure(const StreamConfiguration &inputCfg,
diff --git a/src/libcamera/software_isp/software_isp.cpp b/src/libcamera/software_isp/software_isp.cpp
index c73a16ce0..c7165771c 100644
--- a/src/libcamera/software_isp/software_isp.cpp
+++ b/src/libcamera/software_isp/software_isp.cpp
@@ -120,8 +120,9 @@  SoftwareIsp::SoftwareIsp(PipelineHandler *pipe, const CameraSensor *sensor,
 	}
 
 	if (!softISPMode || softISPMode == "gpu") {
-		if (eGL::isAvailable()) {
-			debayer_ = std::make_unique<DebayerEGL>(std::move(stats), cm);
+		auto display = eGL::probeDisplay();
+		if (display != EGL_NO_DISPLAY) {
+			debayer_ = std::make_unique<DebayerEGL>(std::move(stats), cm, display);
 		} else {
 			LOG(SoftwareIsp, Info)
 				<< "EGL not available, falling back to CPU debayer";