[RFC,6/7] layer: Add layer to inject AeEnable control
diff mbox series

Message ID 20250626095944.1746345-7-paul.elder@ideasonboard.com
State New
Headers show
Series
  • Add Layers support
Related show

Commit Message

Paul Elder June 26, 2025, 9:59 a.m. UTC
Add a layer to implement the AeEnable control, so that we can remove it
from all IPAs and the Camera class, so that it is transparent to all
parties and it is available for applications to use.

Signed-off-by: Paul Elder <paul.elder@ideasonboard.com>

---
For layers like this that we forsee to be "always built-in" we might
want a way to build it into libcamera instead of into separate shared
object files. In any case, this also serves as a demo of how I envision
a layer implementation to work.
---
 src/layer/inject_controls/inject_controls.cpp | 164 ++++++++++++++++++
 src/layer/inject_controls/inject_controls.h   |  29 ++++
 src/layer/inject_controls/meson.build         |  15 ++
 src/layer/meson.build                         |   2 +
 4 files changed, 210 insertions(+)
 create mode 100644 src/layer/inject_controls/inject_controls.cpp
 create mode 100644 src/layer/inject_controls/inject_controls.h
 create mode 100644 src/layer/inject_controls/meson.build

Comments

Barnabás Pőcze June 26, 2025, 10:39 a.m. UTC | #1
Hi

2025. 06. 26. 11:59 keltezéssel, Paul Elder írta:
> Add a layer to implement the AeEnable control, so that we can remove it
> from all IPAs and the Camera class, so that it is transparent to all
> parties and it is available for applications to use.
> 
> Signed-off-by: Paul Elder <paul.elder@ideasonboard.com>
> 
> ---
> For layers like this that we forsee to be "always built-in" we might
> want a way to build it into libcamera instead of into separate shared
> object files. In any case, this also serves as a demo of how I envision
> a layer implementation to work.
> ---
>   src/layer/inject_controls/inject_controls.cpp | 164 ++++++++++++++++++
>   src/layer/inject_controls/inject_controls.h   |  29 ++++
>   src/layer/inject_controls/meson.build         |  15 ++
>   src/layer/meson.build                         |   2 +
>   4 files changed, 210 insertions(+)
>   create mode 100644 src/layer/inject_controls/inject_controls.cpp
>   create mode 100644 src/layer/inject_controls/inject_controls.h
>   create mode 100644 src/layer/inject_controls/meson.build
> 
> diff --git a/src/layer/inject_controls/inject_controls.cpp b/src/layer/inject_controls/inject_controls.cpp
> new file mode 100644
> index 000000000000..133a8f51a1f6
> --- /dev/null
> +++ b/src/layer/inject_controls/inject_controls.cpp
> @@ -0,0 +1,164 @@
> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> +/*
> + * Copyright (C) 2025, Ideas On Board Oy
> + *
> + * Layer implementation for injecting controls
> + */
> +
> +#include "inject_controls.h"
> +
> +#include <algorithm>
> +#include <set>
> +
> +#include <libcamera/layer.h>
> +
> +#include <libcamera/control_ids.h>
> +#include <libcamera/controls.h>
> +#include <libcamera/request.h>
> +
> +namespace layer {
> +
> +namespace inject_controls {

Any reason for the namespaces? I think an anonymous namespace
should be sufficient.


> +
> +libcamera::ControlInfoMap::Map controls(libcamera::ControlInfoMap &ctrls)
> +{
> +	auto it = ctrls.find(&libcamera::controls::ExposureTimeMode);
> +	if (it != ctrls.end()) {
> +		for (auto entry : it->second.values()) {
> +			if (entry == libcamera::ControlValue(libcamera::controls::ExposureTimeModeAuto))
> +				aeAvailable_ = true;
> +			if (entry == libcamera::ControlValue(libcamera::controls::ExposureTimeModeManual))
> +				meAvailable_ = true;
> +		}
> +	}
> +
> +	it = ctrls.find(&libcamera::controls::AnalogueGainMode);
> +	if (it != ctrls.end()) {
> +		for (auto entry : it->second.values()) {
> +			if (entry == libcamera::ControlValue(libcamera::controls::AnalogueGainModeAuto))
> +				agAvailable_ = true;
> +			if (entry == libcamera::ControlValue(libcamera::controls::AnalogueGainModeManual))
> +				mgAvailable_ = true;
> +		}
> +	}
> +
> +	std::set<bool> values;
> +	if (aeAvailable_ || agAvailable_)
> +		values.insert(true);
> +	if (meAvailable_ || mgAvailable_)
> +		values.insert(false);
> +
> +	if (values.empty())
> +		return {};
> +
> +	if (values.size() == 1) {
> +		bool value = *values.begin();
> +		return { { &libcamera::controls::AeEnable,
> +			   libcamera::ControlInfo(value, value, value) } };
> +	}
> +
> +	return { { &libcamera::controls::AeEnable, libcamera::ControlInfo(false, true, true) } };
> +}
> +
> +void queueRequest(libcamera::Request *request)
> +{
> +	libcamera::ControlList &ctrls = request->controls();
> +	auto aeEnable = ctrls.get<bool>(libcamera::controls::AeEnable);
> +	if (!aeEnable)
> +		return;
> +
> +	if (*aeEnable) {
> +		if (aeAvailable_) {
> +			ctrls.set(libcamera::controls::ExposureTimeMode,
> +				  libcamera::controls::ExposureTimeModeAuto);
> +		}
> +
> +		if (agAvailable_) {
> +			ctrls.set(libcamera::controls::AnalogueGainMode,
> +				  libcamera::controls::AnalogueGainModeAuto);
> +		}
> +	} else {
> +		if (meAvailable_) {
> +			ctrls.set(libcamera::controls::ExposureTimeMode,
> +				  libcamera::controls::ExposureTimeModeManual);
> +		}
> +
> +		if (mgAvailable_) {
> +			ctrls.set(libcamera::controls::AnalogueGainMode,
> +				  libcamera::controls::AnalogueGainModeManual);
> +		}
> +	}
> +}
> +
> +void requestCompleted(libcamera::Request *request)
> +{
> +	libcamera::ControlList &metadata = request->metadata();
> +
> +	auto eMode = metadata.get<int>(libcamera::controls::ExposureTimeMode);
> +	auto aMode = metadata.get<int>(libcamera::controls::AnalogueGainMode);
> +
> +	if (!eMode && !aMode)
> +		return;
> +
> +	bool ae = eMode && eMode == libcamera::controls::ExposureTimeModeAuto;
> +	bool me = eMode && eMode == libcamera::controls::ExposureTimeModeManual;
> +	bool ag = aMode && aMode == libcamera::controls::AnalogueGainModeAuto;
> +	bool mg = aMode && aMode == libcamera::controls::AnalogueGainModeManual;
> +
> +	/* Exposure time not reported at all; use gain only */
> +	if (!ae && !me) {
> +		metadata.set(libcamera::controls::AeEnable, ag);
> +		return;
> +	}
> +
> +	/* Analogue gain not reported at all; use exposure time only */
> +	if (!ag && !mg) {
> +		metadata.set(libcamera::controls::AeEnable, ae);
> +		return;
> +	}
> +
> +	/*
> +	 * Gain mode and exposure mode are not equal; therefore at least one is
> +	 * manual, so set AeEnable to false
> +	 */
> +	if (ag != ae) {
> +		metadata.set(libcamera::controls::AeEnable, false);
> +		return;
> +	}
> +
> +	/* ag and ae are equal, so just choose one */
> +	metadata.set(libcamera::controls::AeEnable, ag);
> +	return;
> +}
> +
> +} /* namespace inject_controls */
> +
> +} /* namespace layer */
> +
> +namespace libcamera {

Any reason for this namespace? It does not have any
effect due to the `extern "C"`.


> +
> +extern "C" {
> +
> +struct Layer layerInfo {

Could you modify this as follows:

   [[gnu::visibility("default")]]
   extern const Layer layerInfo {

? (See below for explanation.)


> +	.name = "inject_controls",
> +	.layerAPIVersion = 1,
> +	.init = nullptr,
> +	.bufferCompleted = nullptr,
> +	.requestCompleted = layer::inject_controls::requestCompleted,
> +	.disconnected = nullptr,
> +	.acquire = nullptr,
> +	.release = nullptr,
> +	.controls = layer::inject_controls::controls,
> +	.properties = nullptr,
> +	.streams = nullptr,
> +	.generateConfiguration = nullptr,
> +	.configure = nullptr,
> +	.createRequest = nullptr,
> +	.queueRequest = layer::inject_controls::queueRequest,
> +	.start = nullptr,
> +	.stop = nullptr,
> +};
> +
> +}
> +
> +} /* namespace libcamera */
> diff --git a/src/layer/inject_controls/inject_controls.h b/src/layer/inject_controls/inject_controls.h
> new file mode 100644
> index 000000000000..a65cda897836
> --- /dev/null
> +++ b/src/layer/inject_controls/inject_controls.h
> @@ -0,0 +1,29 @@
> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> +/*
> + * Copyright (C) 2025, Ideas On Board Oy
> + *
> + * Layer implementation for injecting controls
> + */
> +
> +#pragma once
> +
> +#include <libcamera/controls.h>
> +#include <libcamera/request.h>
> +
> +namespace layer {
> +
> +namespace inject_controls {
> +
> +libcamera::ControlInfoMap::Map controls(libcamera::ControlInfoMap &ctrls);
> +void queueRequest(libcamera::Request *);
> +void requestCompleted(libcamera::Request *);
> +
> +bool initialized_ = false;
> +bool aeAvailable_ = false;
> +bool meAvailable_ = false;
> +bool agAvailable_ = false;
> +bool mgAvailable_ = false;
> +
> +} /* namespace inject_controls */
> +
> +} /* namespace layer */
> diff --git a/src/layer/inject_controls/meson.build b/src/layer/inject_controls/meson.build
> new file mode 100644
> index 000000000000..72f22e184923
> --- /dev/null
> +++ b/src/layer/inject_controls/meson.build
> @@ -0,0 +1,15 @@
> +# SPDX-License-Identifier: CC0-1.0
> +
> +layer_name = 'layer_inject_controls'
> +
> +layer_inject_sources = files([
> +    'inject_controls.h',
> +    'inject_controls.cpp',
> +])
> +
> +mod = shared_module(layer_name, layer_inject_sources,
> +                    name_prefix : '',
> +                    include_directories : [layer_includes],
> +                    dependencies : libcamera_public,
> +                    install : true,
> +                    install_dir : layer_install_dir)

Could you add `gnu_symbol_visibility: 'hidden'`? I think it would be
nice to start making every symbol hidden unless explicitly needed.


> diff --git a/src/layer/meson.build b/src/layer/meson.build
> index dee5e5ac5804..d5793f8c4525 100644
> --- a/src/layer/meson.build
> +++ b/src/layer/meson.build
> @@ -8,3 +8,5 @@ layer_install_dir = libcamera_libdir / 'layers'
>   
>   config_h.set('LAYER_DIR',
>                '"' + get_option('prefix') / layer_install_dir + '"')
> +
> +subdir('inject_controls')


Regards,
Barnabás Pőcze
Paul Elder June 27, 2025, 8:49 a.m. UTC | #2
Quoting Barnabás Pőcze (2025-06-26 19:39:00)
> Hi
> 
> 2025. 06. 26. 11:59 keltezéssel, Paul Elder írta:
> > Add a layer to implement the AeEnable control, so that we can remove it
> > from all IPAs and the Camera class, so that it is transparent to all
> > parties and it is available for applications to use.
> > 
> > Signed-off-by: Paul Elder <paul.elder@ideasonboard.com>
> > 
> > ---
> > For layers like this that we forsee to be "always built-in" we might
> > want a way to build it into libcamera instead of into separate shared
> > object files. In any case, this also serves as a demo of how I envision
> > a layer implementation to work.
> > ---
> >   src/layer/inject_controls/inject_controls.cpp | 164 ++++++++++++++++++
> >   src/layer/inject_controls/inject_controls.h   |  29 ++++
> >   src/layer/inject_controls/meson.build         |  15 ++
> >   src/layer/meson.build                         |   2 +
> >   4 files changed, 210 insertions(+)
> >   create mode 100644 src/layer/inject_controls/inject_controls.cpp
> >   create mode 100644 src/layer/inject_controls/inject_controls.h
> >   create mode 100644 src/layer/inject_controls/meson.build
> > 
> > diff --git a/src/layer/inject_controls/inject_controls.cpp b/src/layer/inject_controls/inject_controls.cpp
> > new file mode 100644
> > index 000000000000..133a8f51a1f6
> > --- /dev/null
> > +++ b/src/layer/inject_controls/inject_controls.cpp
> > @@ -0,0 +1,164 @@
> > +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> > +/*
> > + * Copyright (C) 2025, Ideas On Board Oy
> > + *
> > + * Layer implementation for injecting controls
> > + */
> > +
> > +#include "inject_controls.h"
> > +
> > +#include <algorithm>
> > +#include <set>
> > +
> > +#include <libcamera/layer.h>
> > +
> > +#include <libcamera/control_ids.h>
> > +#include <libcamera/controls.h>
> > +#include <libcamera/request.h>
> > +
> > +namespace layer {
> > +
> > +namespace inject_controls {
> 
> Any reason for the namespaces? I think an anonymous namespace
> should be sufficient.

No particular reason.

> 
> 
> > +
> > +libcamera::ControlInfoMap::Map controls(libcamera::ControlInfoMap &ctrls)
> > +{
> > +     auto it = ctrls.find(&libcamera::controls::ExposureTimeMode);
> > +     if (it != ctrls.end()) {
> > +             for (auto entry : it->second.values()) {
> > +                     if (entry == libcamera::ControlValue(libcamera::controls::ExposureTimeModeAuto))
> > +                             aeAvailable_ = true;
> > +                     if (entry == libcamera::ControlValue(libcamera::controls::ExposureTimeModeManual))
> > +                             meAvailable_ = true;
> > +             }
> > +     }
> > +
> > +     it = ctrls.find(&libcamera::controls::AnalogueGainMode);
> > +     if (it != ctrls.end()) {
> > +             for (auto entry : it->second.values()) {
> > +                     if (entry == libcamera::ControlValue(libcamera::controls::AnalogueGainModeAuto))
> > +                             agAvailable_ = true;
> > +                     if (entry == libcamera::ControlValue(libcamera::controls::AnalogueGainModeManual))
> > +                             mgAvailable_ = true;
> > +             }
> > +     }
> > +
> > +     std::set<bool> values;
> > +     if (aeAvailable_ || agAvailable_)
> > +             values.insert(true);
> > +     if (meAvailable_ || mgAvailable_)
> > +             values.insert(false);
> > +
> > +     if (values.empty())
> > +             return {};
> > +
> > +     if (values.size() == 1) {
> > +             bool value = *values.begin();
> > +             return { { &libcamera::controls::AeEnable,
> > +                        libcamera::ControlInfo(value, value, value) } };
> > +     }
> > +
> > +     return { { &libcamera::controls::AeEnable, libcamera::ControlInfo(false, true, true) } };
> > +}
> > +
> > +void queueRequest(libcamera::Request *request)
> > +{
> > +     libcamera::ControlList &ctrls = request->controls();
> > +     auto aeEnable = ctrls.get<bool>(libcamera::controls::AeEnable);
> > +     if (!aeEnable)
> > +             return;
> > +
> > +     if (*aeEnable) {
> > +             if (aeAvailable_) {
> > +                     ctrls.set(libcamera::controls::ExposureTimeMode,
> > +                               libcamera::controls::ExposureTimeModeAuto);
> > +             }
> > +
> > +             if (agAvailable_) {
> > +                     ctrls.set(libcamera::controls::AnalogueGainMode,
> > +                               libcamera::controls::AnalogueGainModeAuto);
> > +             }
> > +     } else {
> > +             if (meAvailable_) {
> > +                     ctrls.set(libcamera::controls::ExposureTimeMode,
> > +                               libcamera::controls::ExposureTimeModeManual);
> > +             }
> > +
> > +             if (mgAvailable_) {
> > +                     ctrls.set(libcamera::controls::AnalogueGainMode,
> > +                               libcamera::controls::AnalogueGainModeManual);
> > +             }
> > +     }
> > +}
> > +
> > +void requestCompleted(libcamera::Request *request)
> > +{
> > +     libcamera::ControlList &metadata = request->metadata();
> > +
> > +     auto eMode = metadata.get<int>(libcamera::controls::ExposureTimeMode);
> > +     auto aMode = metadata.get<int>(libcamera::controls::AnalogueGainMode);
> > +
> > +     if (!eMode && !aMode)
> > +             return;
> > +
> > +     bool ae = eMode && eMode == libcamera::controls::ExposureTimeModeAuto;
> > +     bool me = eMode && eMode == libcamera::controls::ExposureTimeModeManual;
> > +     bool ag = aMode && aMode == libcamera::controls::AnalogueGainModeAuto;
> > +     bool mg = aMode && aMode == libcamera::controls::AnalogueGainModeManual;
> > +
> > +     /* Exposure time not reported at all; use gain only */
> > +     if (!ae && !me) {
> > +             metadata.set(libcamera::controls::AeEnable, ag);
> > +             return;
> > +     }
> > +
> > +     /* Analogue gain not reported at all; use exposure time only */
> > +     if (!ag && !mg) {
> > +             metadata.set(libcamera::controls::AeEnable, ae);
> > +             return;
> > +     }
> > +
> > +     /*
> > +      * Gain mode and exposure mode are not equal; therefore at least one is
> > +      * manual, so set AeEnable to false
> > +      */
> > +     if (ag != ae) {
> > +             metadata.set(libcamera::controls::AeEnable, false);
> > +             return;
> > +     }
> > +
> > +     /* ag and ae are equal, so just choose one */
> > +     metadata.set(libcamera::controls::AeEnable, ag);
> > +     return;
> > +}
> > +
> > +} /* namespace inject_controls */
> > +
> > +} /* namespace layer */
> > +
> > +namespace libcamera {
> 
> Any reason for this namespace? It does not have any
> effect due to the `extern "C"`.

(I copied it from IPAModule)

> 
> 
> > +
> > +extern "C" {
> > +
> > +struct Layer layerInfo {
> 
> Could you modify this as follows:
> 
>    [[gnu::visibility("default")]]
>    extern const Layer layerInfo {
> 
> ? (See below for explanation.)
> 
> 
> > +     .name = "inject_controls",
> > +     .layerAPIVersion = 1,
> > +     .init = nullptr,
> > +     .bufferCompleted = nullptr,
> > +     .requestCompleted = layer::inject_controls::requestCompleted,
> > +     .disconnected = nullptr,
> > +     .acquire = nullptr,
> > +     .release = nullptr,
> > +     .controls = layer::inject_controls::controls,
> > +     .properties = nullptr,
> > +     .streams = nullptr,
> > +     .generateConfiguration = nullptr,
> > +     .configure = nullptr,
> > +     .createRequest = nullptr,
> > +     .queueRequest = layer::inject_controls::queueRequest,
> > +     .start = nullptr,
> > +     .stop = nullptr,
> > +};
> > +
> > +}
> > +
> > +} /* namespace libcamera */
> > diff --git a/src/layer/inject_controls/inject_controls.h b/src/layer/inject_controls/inject_controls.h
> > new file mode 100644
> > index 000000000000..a65cda897836
> > --- /dev/null
> > +++ b/src/layer/inject_controls/inject_controls.h
> > @@ -0,0 +1,29 @@
> > +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> > +/*
> > + * Copyright (C) 2025, Ideas On Board Oy
> > + *
> > + * Layer implementation for injecting controls
> > + */
> > +
> > +#pragma once
> > +
> > +#include <libcamera/controls.h>
> > +#include <libcamera/request.h>
> > +
> > +namespace layer {
> > +
> > +namespace inject_controls {
> > +
> > +libcamera::ControlInfoMap::Map controls(libcamera::ControlInfoMap &ctrls);
> > +void queueRequest(libcamera::Request *);
> > +void requestCompleted(libcamera::Request *);
> > +
> > +bool initialized_ = false;
> > +bool aeAvailable_ = false;
> > +bool meAvailable_ = false;
> > +bool agAvailable_ = false;
> > +bool mgAvailable_ = false;
> > +
> > +} /* namespace inject_controls */
> > +
> > +} /* namespace layer */
> > diff --git a/src/layer/inject_controls/meson.build b/src/layer/inject_controls/meson.build
> > new file mode 100644
> > index 000000000000..72f22e184923
> > --- /dev/null
> > +++ b/src/layer/inject_controls/meson.build
> > @@ -0,0 +1,15 @@
> > +# SPDX-License-Identifier: CC0-1.0
> > +
> > +layer_name = 'layer_inject_controls'
> > +
> > +layer_inject_sources = files([
> > +    'inject_controls.h',
> > +    'inject_controls.cpp',
> > +])
> > +
> > +mod = shared_module(layer_name, layer_inject_sources,
> > +                    name_prefix : '',
> > +                    include_directories : [layer_includes],
> > +                    dependencies : libcamera_public,
> > +                    install : true,
> > +                    install_dir : layer_install_dir)
> 
> Could you add `gnu_symbol_visibility: 'hidden'`? I think it would be
> nice to start making every symbol hidden unless explicitly needed.

I see, ok.


Thanks,

Paul

> 
> 
> > diff --git a/src/layer/meson.build b/src/layer/meson.build
> > index dee5e5ac5804..d5793f8c4525 100644
> > --- a/src/layer/meson.build
> > +++ b/src/layer/meson.build
> > @@ -8,3 +8,5 @@ layer_install_dir = libcamera_libdir / 'layers'
> >   
> >   config_h.set('LAYER_DIR',
> >                '"' + get_option('prefix') / layer_install_dir + '"')
> > +
> > +subdir('inject_controls')
> 
> 
> Regards,
> Barnabás Pőcze
>

Patch
diff mbox series

diff --git a/src/layer/inject_controls/inject_controls.cpp b/src/layer/inject_controls/inject_controls.cpp
new file mode 100644
index 000000000000..133a8f51a1f6
--- /dev/null
+++ b/src/layer/inject_controls/inject_controls.cpp
@@ -0,0 +1,164 @@ 
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2025, Ideas On Board Oy
+ *
+ * Layer implementation for injecting controls
+ */
+
+#include "inject_controls.h"
+
+#include <algorithm>
+#include <set>
+
+#include <libcamera/layer.h>
+
+#include <libcamera/control_ids.h>
+#include <libcamera/controls.h>
+#include <libcamera/request.h>
+
+namespace layer {
+
+namespace inject_controls {
+
+libcamera::ControlInfoMap::Map controls(libcamera::ControlInfoMap &ctrls)
+{
+	auto it = ctrls.find(&libcamera::controls::ExposureTimeMode);
+	if (it != ctrls.end()) {
+		for (auto entry : it->second.values()) {
+			if (entry == libcamera::ControlValue(libcamera::controls::ExposureTimeModeAuto))
+				aeAvailable_ = true;
+			if (entry == libcamera::ControlValue(libcamera::controls::ExposureTimeModeManual))
+				meAvailable_ = true;
+		}
+	}
+
+	it = ctrls.find(&libcamera::controls::AnalogueGainMode);
+	if (it != ctrls.end()) {
+		for (auto entry : it->second.values()) {
+			if (entry == libcamera::ControlValue(libcamera::controls::AnalogueGainModeAuto))
+				agAvailable_ = true;
+			if (entry == libcamera::ControlValue(libcamera::controls::AnalogueGainModeManual))
+				mgAvailable_ = true;
+		}
+	}
+
+	std::set<bool> values;
+	if (aeAvailable_ || agAvailable_)
+		values.insert(true);
+	if (meAvailable_ || mgAvailable_)
+		values.insert(false);
+
+	if (values.empty())
+		return {};
+
+	if (values.size() == 1) {
+		bool value = *values.begin();
+		return { { &libcamera::controls::AeEnable,
+			   libcamera::ControlInfo(value, value, value) } };
+	}
+
+	return { { &libcamera::controls::AeEnable, libcamera::ControlInfo(false, true, true) } };
+}
+
+void queueRequest(libcamera::Request *request)
+{
+	libcamera::ControlList &ctrls = request->controls();
+	auto aeEnable = ctrls.get<bool>(libcamera::controls::AeEnable);
+	if (!aeEnable)
+		return;
+
+	if (*aeEnable) {
+		if (aeAvailable_) {
+			ctrls.set(libcamera::controls::ExposureTimeMode,
+				  libcamera::controls::ExposureTimeModeAuto);
+		}
+
+		if (agAvailable_) {
+			ctrls.set(libcamera::controls::AnalogueGainMode,
+				  libcamera::controls::AnalogueGainModeAuto);
+		}
+	} else {
+		if (meAvailable_) {
+			ctrls.set(libcamera::controls::ExposureTimeMode,
+				  libcamera::controls::ExposureTimeModeManual);
+		}
+
+		if (mgAvailable_) {
+			ctrls.set(libcamera::controls::AnalogueGainMode,
+				  libcamera::controls::AnalogueGainModeManual);
+		}
+	}
+}
+
+void requestCompleted(libcamera::Request *request)
+{
+	libcamera::ControlList &metadata = request->metadata();
+
+	auto eMode = metadata.get<int>(libcamera::controls::ExposureTimeMode);
+	auto aMode = metadata.get<int>(libcamera::controls::AnalogueGainMode);
+
+	if (!eMode && !aMode)
+		return;
+
+	bool ae = eMode && eMode == libcamera::controls::ExposureTimeModeAuto;
+	bool me = eMode && eMode == libcamera::controls::ExposureTimeModeManual;
+	bool ag = aMode && aMode == libcamera::controls::AnalogueGainModeAuto;
+	bool mg = aMode && aMode == libcamera::controls::AnalogueGainModeManual;
+
+	/* Exposure time not reported at all; use gain only */
+	if (!ae && !me) {
+		metadata.set(libcamera::controls::AeEnable, ag);
+		return;
+	}
+
+	/* Analogue gain not reported at all; use exposure time only */
+	if (!ag && !mg) {
+		metadata.set(libcamera::controls::AeEnable, ae);
+		return;
+	}
+
+	/*
+	 * Gain mode and exposure mode are not equal; therefore at least one is
+	 * manual, so set AeEnable to false
+	 */
+	if (ag != ae) {
+		metadata.set(libcamera::controls::AeEnable, false);
+		return;
+	}
+
+	/* ag and ae are equal, so just choose one */
+	metadata.set(libcamera::controls::AeEnable, ag);
+	return;
+}
+
+} /* namespace inject_controls */
+
+} /* namespace layer */
+
+namespace libcamera {
+
+extern "C" {
+
+struct Layer layerInfo {
+	.name = "inject_controls",
+	.layerAPIVersion = 1,
+	.init = nullptr,
+	.bufferCompleted = nullptr,
+	.requestCompleted = layer::inject_controls::requestCompleted,
+	.disconnected = nullptr,
+	.acquire = nullptr,
+	.release = nullptr,
+	.controls = layer::inject_controls::controls,
+	.properties = nullptr,
+	.streams = nullptr,
+	.generateConfiguration = nullptr,
+	.configure = nullptr,
+	.createRequest = nullptr,
+	.queueRequest = layer::inject_controls::queueRequest,
+	.start = nullptr,
+	.stop = nullptr,
+};
+
+}
+
+} /* namespace libcamera */
diff --git a/src/layer/inject_controls/inject_controls.h b/src/layer/inject_controls/inject_controls.h
new file mode 100644
index 000000000000..a65cda897836
--- /dev/null
+++ b/src/layer/inject_controls/inject_controls.h
@@ -0,0 +1,29 @@ 
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2025, Ideas On Board Oy
+ *
+ * Layer implementation for injecting controls
+ */
+
+#pragma once
+
+#include <libcamera/controls.h>
+#include <libcamera/request.h>
+
+namespace layer {
+
+namespace inject_controls {
+
+libcamera::ControlInfoMap::Map controls(libcamera::ControlInfoMap &ctrls);
+void queueRequest(libcamera::Request *);
+void requestCompleted(libcamera::Request *);
+
+bool initialized_ = false;
+bool aeAvailable_ = false;
+bool meAvailable_ = false;
+bool agAvailable_ = false;
+bool mgAvailable_ = false;
+
+} /* namespace inject_controls */
+
+} /* namespace layer */
diff --git a/src/layer/inject_controls/meson.build b/src/layer/inject_controls/meson.build
new file mode 100644
index 000000000000..72f22e184923
--- /dev/null
+++ b/src/layer/inject_controls/meson.build
@@ -0,0 +1,15 @@ 
+# SPDX-License-Identifier: CC0-1.0
+
+layer_name = 'layer_inject_controls'
+
+layer_inject_sources = files([
+    'inject_controls.h',
+    'inject_controls.cpp',
+])
+
+mod = shared_module(layer_name, layer_inject_sources,
+                    name_prefix : '',
+                    include_directories : [layer_includes],
+                    dependencies : libcamera_public,
+                    install : true,
+                    install_dir : layer_install_dir)
diff --git a/src/layer/meson.build b/src/layer/meson.build
index dee5e5ac5804..d5793f8c4525 100644
--- a/src/layer/meson.build
+++ b/src/layer/meson.build
@@ -8,3 +8,5 @@  layer_install_dir = libcamera_libdir / 'layers'
 
 config_h.set('LAYER_DIR',
              '"' + get_option('prefix') / layer_install_dir + '"')
+
+subdir('inject_controls')