[{"id":31991,"web_url":"https://patchwork.libcamera.org/comment/31991/","msgid":"<ee6ffc5f9ad69cb5738ecf85a0aa7a78eee8ded0.camel@ndufresne.ca>","date":"2024-10-31T18:15:28","subject":"Re: [PATCH v4 3/3] gstreamer: Generate controls from\n\tcontrol_ids_*.yaml files","submitter":{"id":30,"url":"https://patchwork.libcamera.org/api/people/30/","name":"Nicolas Dufresne","email":"nicolas@ndufresne.ca"},"content":"Hi,\n\nsorry for the late review, arguably bigger then the other patches.\n\nLe lundi 21 octobre 2024 à 18:45 +0200, Jaslo Ziska a écrit :\n> This commit implements gstreamer controls for the libcamera element by\n> generating the controls from the control_ids_*.yaml files using a new\n> gen-gst-controls.py script. The appropriate meson files are also changed\n> to automatically run the script when building.\n> \n> The gen-gst-controls.py script works similar to the gen-controls.py\n> script by parsing the control_ids_*.yaml files and generating C++ code\n> for each exposed control.\n> For the controls to be used as gstreamer properties the type for each\n> control needs to be translated to the appropriate glib type and a\n> GEnumValue is generated for each enum control. Then a\n> g_object_install_property(), _get_property() and _set_property()\n> function is generated for each control.\n> The vendor controls get prefixed with \"$vendor-\" in the final gstreamer\n> property name.\n> \n> The C++ code generated by the gen-gst-controls.py script is written into\n> the template gstlibcamerasrc-controls.cpp.in file. The matching\n> gstlibcamerasrc-controls.h header defines the GstCameraControls class\n> which handles the installation of the gstreamer properties as well as\n> keeping track of the control values and setting and getting the\n> controls. The content of these functions is generated in the Python\n> script.\n> \n> Finally the libcamerasrc element itself is edited to make use of the new\n> GstCameraControls class. The way this works is by defining a PROP_LAST\n> enum variant which is passed to the installProperties() function so the\n> properties are defined with the appropriate offset. When getting or\n> setting a property PROP_LAST is subtracted from the requested property\n> to translate the control back into a libcamera::controls:: enum\n> variant.\n> \n> Signed-off-by: Jaslo Ziska <jaslo@ziska.de>\n\nI actually reviewed from the generated file, and am happy with the results.\n\nReviewed-by: Nicolas Dufresne <nicolas.dufresne@collabora.com>\n\n> ---\n>  src/gstreamer/gstlibcamera-controls.cpp.in | 332 +++++++++++++++++++++\n>  src/gstreamer/gstlibcamera-controls.h      |  43 +++\n>  src/gstreamer/gstlibcamerasrc.cpp          |  22 +-\n>  src/gstreamer/meson.build                  |  10 +\n>  utils/codegen/controls.py                  |   8 +\n>  utils/codegen/gen-gst-controls.py          | 182 +++++++++++\n>  utils/codegen/meson.build                  |   1 +\n>  7 files changed, 595 insertions(+), 3 deletions(-)\n>  create mode 100644 src/gstreamer/gstlibcamera-controls.cpp.in\n>  create mode 100644 src/gstreamer/gstlibcamera-controls.h\n>  create mode 100755 utils/codegen/gen-gst-controls.py\n> \n> diff --git a/src/gstreamer/gstlibcamera-controls.cpp.in b/src/gstreamer/gstlibcamera-controls.cpp.in\n> new file mode 100644\n> index 00000000..ace36b71\n> --- /dev/null\n> +++ b/src/gstreamer/gstlibcamera-controls.cpp.in\n> @@ -0,0 +1,332 @@\n> +/* SPDX-License-Identifier: LGPL-2.1-or-later */\n> +/*\n> + * Copyright (C) 2024, Jaslo Ziska\n> + *\n> + * GStreamer Camera Controls\n> + *\n> + * This file is auto-generated. Do not edit.\n> + */\n> +\n> +#include <vector>\n> +\n> +#include <libcamera/control_ids.h>\n> +#include <libcamera/controls.h>\n> +#include <libcamera/geometry.h>\n> +\n> +#include \"gstlibcamera-controls.h\"\n> +\n> +using namespace libcamera;\n> +\n> +static void value_set_rectangle(GValue *value, const Rectangle &rect)\n> +{\n> +\tPoint top_left = rect.topLeft();\n> +\tSize size = rect.size();\n> +\n> +\tGValue x = G_VALUE_INIT;\n> +\tg_value_init(&x, G_TYPE_INT);\n> +\tg_value_set_int(&x, top_left.x);\n> +\tgst_value_array_append_and_take_value(value, &x);\n> +\n> +\tGValue y = G_VALUE_INIT;\n> +\tg_value_init(&y, G_TYPE_INT);\n> +\tg_value_set_int(&y, top_left.y);\n> +\tgst_value_array_append_and_take_value(value, &y);\n> +\n> +\tGValue width = G_VALUE_INIT;\n> +\tg_value_init(&width, G_TYPE_INT);\n> +\tg_value_set_int(&width, size.width);\n> +\tgst_value_array_append_and_take_value(value, &width);\n> +\n> +\tGValue height = G_VALUE_INIT;\n> +\tg_value_init(&height, G_TYPE_INT);\n> +\tg_value_set_int(&x, size.height);\n> +\tgst_value_array_append_and_take_value(value, &height);\n> +}\n> +\n> +static Rectangle value_get_rectangle(const GValue *value)\n> +{\n> +\tconst GValue *r;\n> +\tr = gst_value_array_get_value(value, 0);\n> +\tint x = g_value_get_int(r);\n> +\tr = gst_value_array_get_value(value, 1);\n> +\tint y = g_value_get_int(r);\n> +\tr = gst_value_array_get_value(value, 2);\n> +\tint w = g_value_get_int(r);\n> +\tr = gst_value_array_get_value(value, 3);\n> +\tint h = g_value_get_int(r);\n> +\n> +\treturn Rectangle(x, y, w, h);\n> +}\n> +\n> +{% for vendor, ctrls in controls %}\n> +{%- for ctrl in ctrls if ctrl.is_enum %}\n> +static const GEnumValue {{ ctrl.name|snake_case }}_types[] = {\n> +{%- for enum in ctrl.enum_values %}\n> +\t{\n> +\t\tcontrols::{{ ctrl.namespace }}{{ enum.name }},\n> +\t\t{{ enum.description|format_description|indent_str('\\t\\t') }},\n> +\t\t\"{{ enum.gst_name }}\"\n> +\t},\n> +{%- endfor %}\n> +\t{0, NULL, NULL}\n> +};\n> +\n> +#define TYPE_{{ ctrl.name|snake_case|upper }} \\\n> +\t({{ ctrl.name|snake_case }}_get_type())\n> +static GType {{ ctrl.name|snake_case }}_get_type()\n> +{\n> +\tstatic GType {{ ctrl.name|snake_case }}_type = 0;\n> +\n> +\tif (!{{ ctrl.name|snake_case }}_type)\n> +\t\t{{ ctrl.name|snake_case }}_type =\n> +\t\t\tg_enum_register_static(\"{{ ctrl.name }}\",\n> +\t\t\t\t\t       {{ ctrl.name|snake_case }}_types);\n> +\n> +\treturn {{ ctrl.name|snake_case }}_type;\n> +}\n> +{% endfor %}\n> +{%- endfor %}\n> +\n> +void GstCameraControls::installProperties(GObjectClass *klass, int lastPropId)\n> +{\n> +{%- for vendor, ctrls in controls %}\n> +{%- for ctrl in ctrls %}\n> +\n> +{%- set spec %}\n> +{%- if ctrl.is_rectangle -%}\n> +gst_param_spec_array(\n> +{%- else -%}\n> +g_param_spec_{{ ctrl.gtype }}(\n> +{%- endif -%}\n> +{%- if ctrl.is_array %}\n> +\t\"{{ ctrl.vendor_prefix }}{{ ctrl.name|kebab_case }}-value\",\n> +\t\"{{ ctrl.name }} Value\",\n> +\t\"One {{ ctrl.name }} element value\",\n> +{%- else %}\n> +\t\"{{ ctrl.vendor_prefix }}{{ ctrl.name|kebab_case }}\",\n> +\t\"{{ ctrl.name }}\",\n> +\t{{ ctrl.description|format_description|indent_str('\\t') }},\n> +{%- endif %}\n> +{%- if ctrl.is_enum %}\n> +\tTYPE_{{ ctrl.name|snake_case|upper }},\n> +\t{{ ctrl.default }},\n> +{%- elif ctrl.is_rectangle %}\n> +\tg_param_spec_int(\n> +\t\t\"rectangle-value\",\n> +\t\t\"Rectangle Value\",\n> +\t\t\"One rectangle value, either x, y, width or height.\",\n> +\t\t{{ ctrl.min }}, {{ ctrl.max }}, {{ ctrl.default }},\n> +\t\t(GParamFlags) (GST_PARAM_CONTROLLABLE | G_PARAM_READWRITE |\n> +\t\t\t       G_PARAM_STATIC_STRINGS)\n> +\t),\n> +{%- elif ctrl.gtype == 'boolean' %}\n> +\t{{ ctrl.default }},\n> +{%- elif ctrl.gtype in ['float', 'int', 'int64', 'uchar'] %}\n> +\t{{ ctrl.min }}, {{ ctrl.max }}, {{ ctrl.default }},\n> +{%- endif %}\n> +\t(GParamFlags) (GST_PARAM_CONTROLLABLE | G_PARAM_READWRITE |\n> +\t\t       G_PARAM_STATIC_STRINGS)\n> +)\n> +{%- endset %}\n> +\n> +\tg_object_class_install_property(\n> +\t\tklass,\n> +\t\tlastPropId + controls::{{ ctrl.namespace }}{{ ctrl.name|snake_case|upper }},\n> +{%- if ctrl.is_array %}\n> +\t\tgst_param_spec_array(\n> +\t\t\t\"{{ ctrl.vendor_prefix }}{{ ctrl.name|kebab_case }}\",\n> +\t\t\t\"{{ ctrl.name }}\",\n> +\t\t\t{{ ctrl.description|format_description|indent_str('\\t\\t\\t') }},\n> +\t\t\t{{ spec|indent_str('\\t\\t\\t') }},\n> +\t\t\t(GParamFlags) (GST_PARAM_CONTROLLABLE |\n> +\t\t\t\t       G_PARAM_READWRITE |\n> +\t\t\t\t       G_PARAM_STATIC_STRINGS)\n> +\t\t)\n> +{%- else %}\n> +\t\t{{ spec|indent_str('\\t\\t') }}\n> +{%- endif %}\n> +\t);\n> +{%- endfor %}\n> +{%- endfor %}\n> +}\n> +\n> +bool GstCameraControls::getProperty(guint propId, GValue *value,\n> +\t\t\t\t    [[maybe_unused]] GParamSpec *pspec)\n> +{\n> +\tif (!controls_acc_.contains(propId)) {\n> +\t\tGST_WARNING(\"Control '%s' is not available, default value will \"\n> +\t\t\t    \"be returned\",\n> +\t\t\t    controls::controls.at(propId)->name().c_str());\n> +\t\treturn true;\n> +\t}\n> +\tconst ControlValue &cv = controls_acc_.get(propId);\n> +\n> +\tswitch (propId) {\n> +{%- for vendor, ctrls in controls %}\n> +{%- for ctrl in ctrls %}\n> +\n> +\tcase controls::{{ ctrl.namespace }}{{ ctrl.name|snake_case|upper }}: {\n> +\t\tauto control = cv.get<{{ ctrl.type }}>();\n> +\n> +{%- if ctrl.is_array %}\n> +\t\tfor (size_t i = 0; i < control.size(); ++i) {\n> +\t\t\tGValue element = G_VALUE_INIT;\n> +{%- if ctrl.is_rectangle %}\n> +\t\t\tg_value_init(&element, GST_TYPE_PARAM_ARRAY_LIST);\n> +\t\t\tvalue_set_rectangle(&element, control[i]);\n> +{%- else %}\n> +\t\t\tg_value_init(&element, G_TYPE_{{ ctrl.gtype|upper }});\n> +\t\t\tg_value_set_{{ ctrl.gtype }}(&element, control[i]);\n> +{%- endif %}\n> +\t\t\tgst_value_array_append_and_take_value(value, &element);\n> +\t\t}\n> +{%- else %}\n> +{%- if ctrl.is_rectangle %}\n> +\t\tvalue_set_rectangle(value, control);\n> +{%- else %}\n> +\t\tg_value_set_{{ ctrl.gtype }}(value, control);\n> +{%- endif %}\n> +{%- endif %}\n> +\n> +\t\treturn true;\n> +\t}\n> +{%- endfor %}\n> +{%- endfor %}\n> +\n> +\tdefault:\n> +\t\treturn false;\n> +\t}\n> +}\n> +\n> +bool GstCameraControls::setProperty(guint propId, const GValue *value,\n> +\t\t\t\t    [[maybe_unused]] GParamSpec *pspec)\n> +{\n> +\t/*\n> +\t * Check whether the camera capabilities are already available.\n> +\t * They might not be available if the pipeline has not started yet.\n> +\t */\n> +\tif (!capabilities_.empty()) {\n> + \t\t/* If so, check that the control is supported by the camera. */\n> +\t\tconst ControlId *cid = capabilities_.idmap().at(propId);\n> +\t\tauto info = capabilities_.find(cid);\n> +\n> +\t\tif (info == capabilities_.end()) {\n> +\t\t\tGST_WARNING(\"Control '%s' is not supported by the \"\n> +\t\t\t\t    \"camera and will be ignored\",\n> +\t\t\t\t    cid->name().c_str());\n> +\t\t\treturn true;\n> +\t\t}\n> +\t}\n> +\n> +\tswitch (propId) {\n> +{%- for vendor, ctrls in controls %}\n> +{%- for ctrl in ctrls %}\n> +\n> +\tcase controls::{{ ctrl.namespace }}{{ ctrl.name|snake_case|upper }}: {\n> +\t\tControlValue control;\n> +{%- if ctrl.is_array %}\n> +\t\tsize_t size = gst_value_array_get_size(value);\n> +{%- if ctrl.size != 0 %}\n> +\t\tif (size != {{ ctrl.size }}) {\n> +\t\t\tGST_ERROR(\"Incorrect array size for control \"\n> +\t\t\t\t  \"'{{ ctrl.name|kebab_case }}', must be of \"\n> +\t\t\t\t  \"size {{ ctrl.size }}\");\n> +\t\t\treturn true;\n> +\t\t}\n> +{%- endif %}\n> +\n> +\t\tstd::vector<{{ ctrl.element_type }}> values(size);\n> +\t\tfor (size_t i = 0; i < size; ++i) {\n> +\t\t\tconst GValue *element =\n> +\t\t\t\tgst_value_array_get_value(value, i);\n> +{%- if ctrl.is_rectangle %}\n> +\t\t\tif (gst_value_array_get_size(element) != 4) {\n> +\t\t\t\tGST_ERROR(\"Rectangle in control \"\n> +\t\t\t\t\t  \"'{{ ctrl.name|kebab_case }}' at\"\n> +\t\t\t\t\t  \"index %zu must be an array of size 4\",\n> +\t\t\t\t\t  i);\n> +\t\t\t\treturn true;\n> +\t\t\t}\n> +\t\t\tvalues[i] = value_get_rectangle(element);\n> +{%- else %}\n> +\t\t\tvalues[i] = g_value_get_{{ ctrl.gtype }}(element);\n> +{%- endif %}\n> +\t\t}\n> +\n> +{%- if ctrl.size == 0 %}\n> +\t\tcontrol.set(Span<const {{ ctrl.element_type }}>(values.data(),\n> +\t\t\t\t\t\t\t\tsize));\n> +{%- else %}\n> +\t\tcontrol.set(Span<const {{ ctrl.element_type }},\n> +\t\t\t         {{ ctrl.size }}>(values.data(),\n> +\t\t\t\t\t\t  {{ ctrl.size }}));\n> +{%- endif %}\n> +{%- else %}\n> +{%- if ctrl.is_rectangle %}\n> +\t\tif (gst_value_array_get_size(value) != 4) {\n> +\t\t\tGST_ERROR(\"Rectangle in control \"\n> +\t\t\t\t  \"'{{ ctrl.name|kebab_case }}' must be an \"\n> +\t\t\t\t  \"array of size 4\");\n> +\t\t\treturn true;\n> +\t\t}\n> +\t\tRectangle val = value_get_rectangle(value);\n> +{%- else %}\n> +\t\tauto val = g_value_get_{{ ctrl.gtype }}(value);\n> +{%- endif %}\n> +\t\tcontrol.set(val);\n> +{%- endif %}\n> +\t\tcontrols_.set(propId, control);\n> +\t\tcontrols_acc_.set(propId, control);\n> +\t\treturn true;\n> +\t}\n> +{%- endfor %}\n> +{%- endfor %}\n> +\n> +\tdefault:\n> +\t\treturn false;\n> +\t}\n> +}\n> +\n> +void GstCameraControls::setCamera(const std::shared_ptr<libcamera::Camera> &cam)\n> +{\n> +\tcapabilities_ = cam->controls();\n> +\n> +\t/*\n> +\t * Check the controls which were set before the camera capabilities were\n> +\t * known. This is required because GStreamer may set properties before\n> +\t * the pipeline has started and thus before the camera was known.\n> +\t */\n> +\tControlList new_controls;\n> +\tfor (auto control = controls_acc_.begin();\n> +\t     control != controls_acc_.end();\n> +\t     ++control) {\n> +\t\tunsigned int id = control->first;\n> +\t\tControlValue value = control->second;\n> +\n> +\t\tconst ControlId *cid = capabilities_.idmap().at(id);\n> +\t\tauto info = capabilities_.find(cid);\n> +\n> +\t\t/* Only add controls which are supported. */\n> +\t\tif (info != capabilities_.end())\n> +\t\t\tnew_controls.set(id, value);\n> +\t\telse\n> +\t\t\tGST_WARNING(\"Control '%s' is not supported by the \"\n> +\t\t\t\t    \"camera and will be ignored\",\n> +\t\t\t\t    cid->name().c_str());\n> +\t}\n> +\n> +\tcontrols_acc_ = new_controls;\n> +\tcontrols_ = new_controls;\n> +}\n> +\n> +void GstCameraControls::applyControls(std::unique_ptr<libcamera::Request> &request)\n> +{\n> +\trequest->controls().merge(controls_);\n> +\tcontrols_.clear();\n> +}\n> +\n> +void GstCameraControls::readMetadata(libcamera::Request *request)\n> +{\n> +\tcontrols_acc_.merge(request->metadata(),\n> +\t\t\t    ControlList::MergePolicy::OverwriteExisting);\n> +}\n> diff --git a/src/gstreamer/gstlibcamera-controls.h b/src/gstreamer/gstlibcamera-controls.h\n> new file mode 100644\n> index 00000000..749220b5\n> --- /dev/null\n> +++ b/src/gstreamer/gstlibcamera-controls.h\n> @@ -0,0 +1,43 @@\n> +/* SPDX-License-Identifier: LGPL-2.1-or-later */\n> +/*\n> + * Copyright (C) 2023, Collabora Ltd.\n> + *     Author: Nicolas Dufresne <nicolas.dufresne@collabora.com>\n> + *\n> + * GStreamer Camera Controls\n> + */\n> +\n> +#pragma once\n> +\n> +#include <memory>\n> +\n> +#include <libcamera/camera.h>\n> +#include <libcamera/controls.h>\n> +#include <libcamera/request.h>\n> +\n> +#include \"gstlibcamerasrc.h\"\n> +\n> +namespace libcamera {\n> +\n> +class GstCameraControls\n> +{\n> +public:\n> +\tstatic void installProperties(GObjectClass *klass, int lastProp);\n> +\n> +\tbool getProperty(guint propId, GValue *value, GParamSpec *pspec);\n> +\tbool setProperty(guint propId, const GValue *value, GParamSpec *pspec);\n> +\n> +\tvoid setCamera(const std::shared_ptr<libcamera::Camera> &cam);\n> +\n> +\tvoid applyControls(std::unique_ptr<libcamera::Request> &request);\n> +\tvoid readMetadata(libcamera::Request *request);\n> +\n> +private:\n> +\t/* Supported controls and limits of camera. */\n> +\tControlInfoMap capabilities_;\n> +\t/* Set of user modified controls. */\n> +\tControlList controls_;\n> +\t/* Accumulator of all controls ever set and metadata returned by camera */\n> +\tControlList controls_acc_;\n> +};\n> +\n> +} /* namespace libcamera */\n> diff --git a/src/gstreamer/gstlibcamerasrc.cpp b/src/gstreamer/gstlibcamerasrc.cpp\n> index 40b787c8..8efa25f4 100644\n> --- a/src/gstreamer/gstlibcamerasrc.cpp\n> +++ b/src/gstreamer/gstlibcamerasrc.cpp\n> @@ -37,10 +37,11 @@\n>  \n>  #include <gst/base/base.h>\n>  \n> +#include \"gstlibcamera-controls.h\"\n> +#include \"gstlibcamera-utils.h\"\n>  #include \"gstlibcameraallocator.h\"\n>  #include \"gstlibcamerapad.h\"\n>  #include \"gstlibcamerapool.h\"\n> -#include \"gstlibcamera-utils.h\"\n>  \n>  using namespace libcamera;\n>  \n> @@ -128,6 +129,7 @@ struct GstLibcameraSrcState {\n>  \n>  \tControlList initControls_;\n>  \tguint group_id_;\n> +\tGstCameraControls controls_;\n>  \n>  \tint queueRequest();\n>  \tvoid requestCompleted(Request *request);\n> @@ -153,6 +155,7 @@ struct _GstLibcameraSrc {\n>  enum {\n>  \tPROP_0,\n>  \tPROP_CAMERA_NAME,\n> +\tPROP_LAST\n>  };\n>  \n>  static void gst_libcamera_src_child_proxy_init(gpointer g_iface,\n> @@ -183,6 +186,9 @@ int GstLibcameraSrcState::queueRequest()\n>  \tif (!request)\n>  \t\treturn -ENOMEM;\n>  \n> +\t/* Apply controls */\n> +\tcontrols_.applyControls(request);\n> +\n>  \tstd::unique_ptr<RequestWrap> wrap =\n>  \t\tstd::make_unique<RequestWrap>(std::move(request));\n>  \n> @@ -226,6 +232,9 @@ GstLibcameraSrcState::requestCompleted(Request *request)\n>  \n>  \t{\n>  \t\tGLibLocker locker(&lock_);\n> +\n> +\t\tcontrols_.readMetadata(request);\n> +\n>  \t\twrap = std::move(queuedRequests_.front());\n>  \t\tqueuedRequests_.pop();\n>  \t}\n> @@ -408,6 +417,8 @@ gst_libcamera_src_open(GstLibcameraSrc *self)\n>  \t\treturn false;\n>  \t}\n>  \n> +\tself->state->controls_.setCamera(cam);\n> +\n>  \tcam->requestCompleted.connect(self->state, &GstLibcameraSrcState::requestCompleted);\n>  \n>  \t/* No need to lock here, we didn't start our threads yet. */\n> @@ -722,6 +733,7 @@ gst_libcamera_src_set_property(GObject *object, guint prop_id,\n>  {\n>  \tGLibLocker lock(GST_OBJECT(object));\n>  \tGstLibcameraSrc *self = GST_LIBCAMERA_SRC(object);\n> +\tGstLibcameraSrcState *state = self->state;\n>  \n>  \tswitch (prop_id) {\n>  \tcase PROP_CAMERA_NAME:\n> @@ -729,7 +741,8 @@ gst_libcamera_src_set_property(GObject *object, guint prop_id,\n>  \t\tself->camera_name = g_value_dup_string(value);\n>  \t\tbreak;\n>  \tdefault:\n> -\t\tG_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);\n> +\t\tif (!state->controls_.setProperty(prop_id - PROP_LAST, value, pspec))\n> +\t\t\tG_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);\n>  \t\tbreak;\n>  \t}\n>  }\n> @@ -740,13 +753,15 @@ gst_libcamera_src_get_property(GObject *object, guint prop_id, GValue *value,\n>  {\n>  \tGLibLocker lock(GST_OBJECT(object));\n>  \tGstLibcameraSrc *self = GST_LIBCAMERA_SRC(object);\n> +\tGstLibcameraSrcState *state = self->state;\n>  \n>  \tswitch (prop_id) {\n>  \tcase PROP_CAMERA_NAME:\n>  \t\tg_value_set_string(value, self->camera_name);\n>  \t\tbreak;\n>  \tdefault:\n> -\t\tG_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);\n> +\t\tif (!state->controls_.getProperty(prop_id - PROP_LAST, value, pspec))\n> +\t\t\tG_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);\n>  \t\tbreak;\n>  \t}\n>  }\n> @@ -947,6 +962,7 @@ gst_libcamera_src_class_init(GstLibcameraSrcClass *klass)\n>  \t\t\t\t\t\t\t     | G_PARAM_STATIC_STRINGS));\n>  \tg_object_class_install_property(object_class, PROP_CAMERA_NAME, spec);\n>  \n> +\tGstCameraControls::installProperties(object_class, PROP_LAST);\n>  }\n>  \n>  /* GstChildProxy implementation */\n> diff --git a/src/gstreamer/meson.build b/src/gstreamer/meson.build\n> index c2a01e7b..6b7e53b5 100644\n> --- a/src/gstreamer/meson.build\n> +++ b/src/gstreamer/meson.build\n> @@ -25,6 +25,16 @@ libcamera_gst_sources = [\n>      'gstlibcamerasrc.cpp',\n>  ]\n>  \n> +# Generate gstreamer control properties\n> +\n> +gen_gst_controls_template = files('gstlibcamera-controls.cpp.in')\n> +libcamera_gst_sources += custom_target('gstlibcamera-controls.cpp',\n> +                                       input : controls_files,\n> +                                       output : 'gstlibcamera-controls.cpp',\n> +                                       command : [gen_gst_controls, '-o', '@OUTPUT@',\n> +                                                  '-t', gen_gst_controls_template, '@INPUT@'],\n> +                                       env : py_build_env)\n> +\n>  libcamera_gst_cpp_args = [\n>      '-DVERSION=\"@0@\"'.format(libcamera_git_version),\n>      '-DPACKAGE=\"@0@\"'.format(meson.project_name()),\n> diff --git a/utils/codegen/controls.py b/utils/codegen/controls.py\n> index 7bafee59..03c77cc6 100644\n> --- a/utils/codegen/controls.py\n> +++ b/utils/codegen/controls.py\n> @@ -110,3 +110,11 @@ class Control(object):\n>              return f\"Span<const {typ}, {self.__size}>\"\n>          else:\n>              return f\"Span<const {typ}>\"\n> +\n> +    @property\n> +    def element_type(self):\n> +        return self.__data.get('type')\n> +\n> +    @property\n> +    def size(self):\n> +        return self.__size\n> diff --git a/utils/codegen/gen-gst-controls.py b/utils/codegen/gen-gst-controls.py\n> new file mode 100755\n> index 00000000..2601a675\n> --- /dev/null\n> +++ b/utils/codegen/gen-gst-controls.py\n> @@ -0,0 +1,182 @@\n> +#!/usr/bin/env python3\n> +# SPDX-License-Identifier: GPL-2.0-or-later\n> +# Copyright (C) 2019, Google Inc.\n> +# Copyright (C) 2024, Jaslo Ziska\n> +#\n> +# Authors:\n> +# Laurent Pinchart <laurent.pinchart@ideasonboard.com>\n> +# Jaslo Ziska <jaslo@ziska.de>\n> +#\n> +# Generate gstreamer control properties from YAML\n> +\n> +import argparse\n> +import jinja2\n> +import re\n> +import sys\n> +import yaml\n> +\n> +from controls import Control\n> +\n> +\n> +exposed_controls = [\n> +    'AeEnable', 'AeMeteringMode', 'AeConstraintMode', 'AeExposureMode',\n> +    'ExposureValue', 'ExposureTime', 'AnalogueGain', 'AeFlickerPeriod',\n> +    'Brightness', 'Contrast', 'AwbEnable', 'AwbMode', 'ColourGains',\n> +    'Saturation', 'Sharpness', 'ColourCorrectionMatrix', 'ScalerCrop',\n> +    'DigitalGain', 'AfMode', 'AfRange', 'AfSpeed', 'AfMetering', 'AfWindows',\n> +    'LensPosition', 'Gamma',\n> +]\n> +\n> +\n> +def find_common_prefix(strings):\n> +    prefix = strings[0]\n> +\n> +    for string in strings[1:]:\n> +        while string[:len(prefix)] != prefix and prefix:\n> +            prefix = prefix[:len(prefix) - 1]\n> +        if not prefix:\n> +            break\n> +\n> +    return prefix\n> +\n> +\n> +def format_description(description):\n> +    # Substitute doxygen keywords \\sa (see also) and \\todo\n> +    description = re.sub(r'\\\\sa((?: \\w+)+)',\n> +                         lambda match: 'See also: ' + ', '.join(\n> +                             map(kebab_case, match.group(1).strip().split(' '))\n> +                         ) + '.', description)\n> +    description = re.sub(r'\\\\todo', 'Todo:', description)\n> +\n> +    description = description.strip().split('\\n')\n> +    return '\\n'.join([\n> +        '\"' + line.replace('\\\\', r'\\\\').replace('\"', r'\\\"') + ' \"' for line in description if line\n> +    ]).rstrip()\n> +\n> +\n> +# Custom filter to allow indenting by a string prior to Jinja version 3.0\n> +#\n> +# This function can be removed and the calls to indent_str() replaced by the\n> +# built-in indent() filter when dropping Jinja versions older than 3.0\n> +def indent_str(s, indention):\n> +    s += '\\n'\n> +\n> +    lines = s.splitlines()\n> +    rv = lines.pop(0)\n> +\n> +    if lines:\n> +        rv += '\\n' + '\\n'.join(\n> +            indention + line if line else line for line in lines\n> +        )\n> +\n> +    return rv\n> +\n> +\n> +def snake_case(s):\n> +    return ''.join([\n> +        c.isupper() and ('_' + c.lower()) or c for c in s\n> +    ]).strip('_')\n> +\n> +\n> +def kebab_case(s):\n> +    return snake_case(s).replace('_', '-')\n> +\n> +\n> +def extend_control(ctrl):\n> +    if ctrl.vendor != 'libcamera':\n> +        ctrl.namespace = f'{ctrl.vendor}::'\n> +        ctrl.vendor_prefix = f'{ctrl.vendor}-'\n> +    else:\n> +        ctrl.namespace = ''\n> +        ctrl.vendor_prefix = ''\n> +\n> +    ctrl.is_array = ctrl.size is not None\n> +\n> +    if ctrl.is_enum:\n> +        # Remove common prefix from enum variant names\n> +        prefix = find_common_prefix([enum.name for enum in ctrl.enum_values])\n> +        for enum in ctrl.enum_values:\n> +            enum.gst_name = kebab_case(enum.name.removeprefix(prefix))\n> +\n> +        ctrl.gtype = 'enum'\n> +        ctrl.default = '0'\n> +    elif ctrl.element_type == 'bool':\n> +        ctrl.gtype = 'boolean'\n> +        ctrl.default = 'false'\n> +    elif ctrl.element_type == 'float':\n> +        ctrl.gtype = 'float'\n> +        ctrl.default = '0'\n> +        ctrl.min = '-G_MAXFLOAT'\n> +        ctrl.max = 'G_MAXFLOAT'\n> +    elif ctrl.element_type == 'int32_t':\n> +        ctrl.gtype = 'int'\n> +        ctrl.default = '0'\n> +        ctrl.min = 'G_MININT'\n> +        ctrl.max = 'G_MAXINT'\n> +    elif ctrl.element_type == 'int64_t':\n> +        ctrl.gtype = 'int64'\n> +        ctrl.default = '0'\n> +        ctrl.min = 'G_MININT64'\n> +        ctrl.max = 'G_MAXINT64'\n> +    elif ctrl.element_type == 'uint8_t':\n> +        ctrl.gtype = 'uchar'\n> +        ctrl.default = '0'\n> +        ctrl.min = '0'\n> +        ctrl.max = 'G_MAXUINT8'\n> +    elif ctrl.element_type == 'Rectangle':\n> +        ctrl.is_rectangle = True\n> +        ctrl.default = '0'\n> +        ctrl.min = '0'\n> +        ctrl.max = 'G_MAXINT'\n> +    else:\n> +        raise RuntimeError(f'The type `{ctrl.element_type}` is unknown')\n> +\n> +    return ctrl\n> +\n> +\n> +def main(argv):\n> +    # Parse command line arguments\n> +    parser = argparse.ArgumentParser()\n> +    parser.add_argument('--output', '-o', metavar='file', type=str,\n> +                        help='Output file name. Defaults to standard output if not specified.')\n> +    parser.add_argument('--template', '-t', dest='template', type=str, required=True,\n> +                        help='Template file name.')\n> +    parser.add_argument('input', type=str, nargs='+',\n> +                        help='Input file name.')\n> +\n> +    args = parser.parse_args(argv[1:])\n> +\n> +    controls = {}\n> +    for input in args.input:\n> +        data = yaml.safe_load(open(input, 'rb').read())\n> +\n> +        vendor = data['vendor']\n> +        ctrls = controls.setdefault(vendor, [])\n> +\n> +        for ctrl in data['controls']:\n> +            ctrl = Control(*ctrl.popitem(), vendor)\n> +\n> +            if ctrl.name in exposed_controls:\n> +                ctrls.append(extend_control(ctrl))\n> +\n> +    data = {'controls': list(controls.items())}\n> +\n> +    env = jinja2.Environment()\n> +    env.filters['format_description'] = format_description\n> +    env.filters['indent_str'] = indent_str\n> +    env.filters['snake_case'] = snake_case\n> +    env.filters['kebab_case'] = kebab_case\n> +    template = env.from_string(open(args.template, 'r', encoding='utf-8').read())\n> +    string = template.render(data)\n> +\n> +    if args.output:\n> +        with open(args.output, 'w', encoding='utf-8') as output:\n> +            output.write(string)\n> +    else:\n> +        sys.stdout.write(string)\n> +\n> +    return 0\n> +\n> +\n> +if __name__ == '__main__':\n> +    sys.exit(main(sys.argv))\n> diff --git a/utils/codegen/meson.build b/utils/codegen/meson.build\n> index adf33bba..904dd66d 100644\n> --- a/utils/codegen/meson.build\n> +++ b/utils/codegen/meson.build\n> @@ -11,6 +11,7 @@ py_modules += ['jinja2', 'yaml']\n>  \n>  gen_controls = files('gen-controls.py')\n>  gen_formats = files('gen-formats.py')\n> +gen_gst_controls = files('gen-gst-controls.py')\n>  gen_header = files('gen-header.sh')\n>  gen_ipa_pub_key = files('gen-ipa-pub-key.py')\n>  gen_tracepoints = files('gen-tp-header.py')","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 9583EC3237\n\tfor <parsemail@patchwork.libcamera.org>;\n\tThu, 31 Oct 2024 18:15:36 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 92072653A1;\n\tThu, 31 Oct 2024 19:15:35 +0100 (CET)","from mail-qv1-xf31.google.com (mail-qv1-xf31.google.com\n\t[IPv6:2607:f8b0:4864:20::f31])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 11D1560360\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tThu, 31 Oct 2024 19:15:33 +0100 (CET)","by mail-qv1-xf31.google.com with SMTP id\n\t6a1803df08f44-6cbe9914487so7474356d6.1\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tThu, 31 Oct 2024 11:15:32 -0700 (PDT)","from nicolas-tpx395.localdomain ([2606:6d00:15:862e::580])\n\tby smtp.gmail.com with ESMTPSA id\n\t6a1803df08f44-6d353fc50e6sm10395986d6.35.2024.10.31.11.15.29\n\t(version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);\n\tThu, 31 Oct 2024 11:15:29 -0700 (PDT)"],"Authentication-Results":"lancelot.ideasonboard.com; dkim=pass (2048-bit key;\n\tunprotected) header.d=ndufresne-ca.20230601.gappssmtp.com\n\theader.i=@ndufresne-ca.20230601.gappssmtp.com\n\theader.b=\"I+NTkspA\"; dkim-atps=neutral","DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=ndufresne-ca.20230601.gappssmtp.com; s=20230601; t=1730398532;\n\tx=1731003332; darn=lists.libcamera.org; \n\th=mime-version:user-agent:content-transfer-encoding:references\n\t:in-reply-to:date:to:from:subject:message-id:from:to:cc:subject:date\n\t:message-id:reply-to;\n\tbh=WO6bPNHFzZf43AIBSLGrmGvgqMwroMtMjZ/gwFPetb0=;\n\tb=I+NTkspAk2b6Yni4HmAJW5kGwthXTVSgW4uELFk7MfDYQc5eyds4+GMq7Np0faOXB9\n\tsxYhzCgOiFePmd1odAdEkzUV4By/q7w4lOlQ6d8sWbbVq4ilm5laZRMEQMobFR8N5aXp\n\tJ1SMbncOFt27NLAkMxxh9m6GwQIU8ODroCYS5xeGvF03KgJNr8V1uTCiRM5h1KsuqO0T\n\tsUWTRGdYf9BZxD5ScTY6vWobVCeupDHpNrhq0clKYp+/PZr8bg9Vm6U3Auq/tYnoxoW5\n\tc3fXPAc5ap1A5o1ECvOPJO+UpeDxXwAHoRzw6BdbDzd2gOjqkAbpNo9GNpNjv9Z/AH3y\n\thLiA==","X-Google-DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=1e100.net; s=20230601; t=1730398532; x=1731003332;\n\th=mime-version:user-agent:content-transfer-encoding:references\n\t:in-reply-to:date:to:from:subject:message-id:x-gm-message-state:from\n\t:to:cc:subject:date:message-id:reply-to;\n\tbh=WO6bPNHFzZf43AIBSLGrmGvgqMwroMtMjZ/gwFPetb0=;\n\tb=LRbSQ+22+8SR5KzTN1hcIq1ni6hEwVvmk6StjtYNJLRCkOUZbEgxMvxJJTBco4af9z\n\taVsvI8bHmpV148OqBbH8nm7IjVvZ+pBqbUVVKP+Y+dlWH2uBg3CNYTY+rfWf3cIPQHOw\n\tnLqaa9TwlJOa9vkhIq1O7r6XHzLQrO2k75+zVGX6uHVkVGrKxUDyMV7Zby7tf7JfMd8b\n\ts4v/RXFB//sJtmmzP33aMCwSoZr2Av2jLCVXz8ONL2V6TeplVp6/1k1ih0hVC8JHZlXo\n\tViVbUzvSbQPnTHkoGTvA5fPnEHwtm0K96PwNwg6OmRUd3Ev+dsJAxmaWkVVyhQ9l1fdP\n\tooPg==","X-Forwarded-Encrypted":"i=1;\n\tAJvYcCUu1BWkzPQGTUfqdKjdBNGg8jt/hTGx4W0uNfBsAM6BZ/R4lU35BwtnmKCr6DFqNeMiWYFhLlm7h1XgA8qZJWA=@lists.libcamera.org","X-Gm-Message-State":"AOJu0YzsO7DF8ZvyUpQdn0pr04RS6PsJSLG2AQipCkC1b2Ld8NqZxMPX\n\tCLjrPltJ9dobgrC3x6r+fdMi5CLGFcPClM4ZOxO6ho025oJt8Mk/H7UGXozQbMC1vxS/SXr4WFO\n\tt","X-Google-Smtp-Source":"AGHT+IGW07y3H8BQHbE8vCSSX8qxGRYzp3Q1khyWbUwwLn118xGHbi0ngz1Ux2+epQroZjisMLxOQA==","X-Received":"by 2002:a05:6214:5d85:b0:6cb:d4e6:2507 with SMTP id\n\t6a1803df08f44-6d351ad1148mr50777246d6.22.1730398530193; \n\tThu, 31 Oct 2024 11:15:30 -0700 (PDT)","Message-ID":"<ee6ffc5f9ad69cb5738ecf85a0aa7a78eee8ded0.camel@ndufresne.ca>","Subject":"Re: [PATCH v4 3/3] gstreamer: Generate controls from\n\tcontrol_ids_*.yaml files","From":"Nicolas Dufresne <nicolas@ndufresne.ca>","To":"Jaslo Ziska <jaslo@ziska.de>, libcamera-devel@lists.libcamera.org","Date":"Thu, 31 Oct 2024 14:15:28 -0400","In-Reply-To":"<20241021164946.11111-4-jaslo@ziska.de>","References":"<20241021164946.11111-1-jaslo@ziska.de>\n\t<20241021164946.11111-4-jaslo@ziska.de>","Content-Type":"text/plain; charset=\"UTF-8\"","Content-Transfer-Encoding":"quoted-printable","User-Agent":"Evolution 3.54.1 (3.54.1-1.fc41) ","MIME-Version":"1.0","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>"}}]