[PATCH v4 3/3] gstreamer: Generate controls from control_ids_*.yaml files

Nicolas Dufresne nicolas at ndufresne.ca
Thu Oct 31 19:15:28 CET 2024


Hi,

sorry for the late review, arguably bigger then the other patches.

Le lundi 21 octobre 2024 à 18:45 +0200, Jaslo Ziska a écrit :
> This commit implements gstreamer controls for the libcamera element by
> generating the controls from the control_ids_*.yaml files using a new
> gen-gst-controls.py script. The appropriate meson files are also changed
> to automatically run the script when building.
> 
> The gen-gst-controls.py script works similar to the gen-controls.py
> script by parsing the control_ids_*.yaml files and generating C++ code
> for each exposed control.
> For the controls to be used as gstreamer properties the type for each
> control needs to be translated to the appropriate glib type and a
> GEnumValue is generated for each enum control. Then a
> g_object_install_property(), _get_property() and _set_property()
> function is generated for each control.
> The vendor controls get prefixed with "$vendor-" in the final gstreamer
> property name.
> 
> The C++ code generated by the gen-gst-controls.py script is written into
> the template gstlibcamerasrc-controls.cpp.in file. The matching
> gstlibcamerasrc-controls.h header defines the GstCameraControls class
> which handles the installation of the gstreamer properties as well as
> keeping track of the control values and setting and getting the
> controls. The content of these functions is generated in the Python
> script.
> 
> Finally the libcamerasrc element itself is edited to make use of the new
> GstCameraControls class. The way this works is by defining a PROP_LAST
> enum variant which is passed to the installProperties() function so the
> properties are defined with the appropriate offset. When getting or
> setting a property PROP_LAST is subtracted from the requested property
> to translate the control back into a libcamera::controls:: enum
> variant.
> 
> Signed-off-by: Jaslo Ziska <jaslo at ziska.de>

I actually reviewed from the generated file, and am happy with the results.

Reviewed-by: Nicolas Dufresne <nicolas.dufresne at collabora.com>

> ---
>  src/gstreamer/gstlibcamera-controls.cpp.in | 332 +++++++++++++++++++++
>  src/gstreamer/gstlibcamera-controls.h      |  43 +++
>  src/gstreamer/gstlibcamerasrc.cpp          |  22 +-
>  src/gstreamer/meson.build                  |  10 +
>  utils/codegen/controls.py                  |   8 +
>  utils/codegen/gen-gst-controls.py          | 182 +++++++++++
>  utils/codegen/meson.build                  |   1 +
>  7 files changed, 595 insertions(+), 3 deletions(-)
>  create mode 100644 src/gstreamer/gstlibcamera-controls.cpp.in
>  create mode 100644 src/gstreamer/gstlibcamera-controls.h
>  create mode 100755 utils/codegen/gen-gst-controls.py
> 
> diff --git a/src/gstreamer/gstlibcamera-controls.cpp.in b/src/gstreamer/gstlibcamera-controls.cpp.in
> new file mode 100644
> index 00000000..ace36b71
> --- /dev/null
> +++ b/src/gstreamer/gstlibcamera-controls.cpp.in
> @@ -0,0 +1,332 @@
> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> +/*
> + * Copyright (C) 2024, Jaslo Ziska
> + *
> + * GStreamer Camera Controls
> + *
> + * This file is auto-generated. Do not edit.
> + */
> +
> +#include <vector>
> +
> +#include <libcamera/control_ids.h>
> +#include <libcamera/controls.h>
> +#include <libcamera/geometry.h>
> +
> +#include "gstlibcamera-controls.h"
> +
> +using namespace libcamera;
> +
> +static void value_set_rectangle(GValue *value, const Rectangle &rect)
> +{
> +	Point top_left = rect.topLeft();
> +	Size size = rect.size();
> +
> +	GValue x = G_VALUE_INIT;
> +	g_value_init(&x, G_TYPE_INT);
> +	g_value_set_int(&x, top_left.x);
> +	gst_value_array_append_and_take_value(value, &x);
> +
> +	GValue y = G_VALUE_INIT;
> +	g_value_init(&y, G_TYPE_INT);
> +	g_value_set_int(&y, top_left.y);
> +	gst_value_array_append_and_take_value(value, &y);
> +
> +	GValue width = G_VALUE_INIT;
> +	g_value_init(&width, G_TYPE_INT);
> +	g_value_set_int(&width, size.width);
> +	gst_value_array_append_and_take_value(value, &width);
> +
> +	GValue height = G_VALUE_INIT;
> +	g_value_init(&height, G_TYPE_INT);
> +	g_value_set_int(&x, size.height);
> +	gst_value_array_append_and_take_value(value, &height);
> +}
> +
> +static Rectangle value_get_rectangle(const GValue *value)
> +{
> +	const GValue *r;
> +	r = gst_value_array_get_value(value, 0);
> +	int x = g_value_get_int(r);
> +	r = gst_value_array_get_value(value, 1);
> +	int y = g_value_get_int(r);
> +	r = gst_value_array_get_value(value, 2);
> +	int w = g_value_get_int(r);
> +	r = gst_value_array_get_value(value, 3);
> +	int h = g_value_get_int(r);
> +
> +	return Rectangle(x, y, w, h);
> +}
> +
> +{% for vendor, ctrls in controls %}
> +{%- for ctrl in ctrls if ctrl.is_enum %}
> +static const GEnumValue {{ ctrl.name|snake_case }}_types[] = {
> +{%- for enum in ctrl.enum_values %}
> +	{
> +		controls::{{ ctrl.namespace }}{{ enum.name }},
> +		{{ enum.description|format_description|indent_str('\t\t') }},
> +		"{{ enum.gst_name }}"
> +	},
> +{%- endfor %}
> +	{0, NULL, NULL}
> +};
> +
> +#define TYPE_{{ ctrl.name|snake_case|upper }} \
> +	({{ ctrl.name|snake_case }}_get_type())
> +static GType {{ ctrl.name|snake_case }}_get_type()
> +{
> +	static GType {{ ctrl.name|snake_case }}_type = 0;
> +
> +	if (!{{ ctrl.name|snake_case }}_type)
> +		{{ ctrl.name|snake_case }}_type =
> +			g_enum_register_static("{{ ctrl.name }}",
> +					       {{ ctrl.name|snake_case }}_types);
> +
> +	return {{ ctrl.name|snake_case }}_type;
> +}
> +{% endfor %}
> +{%- endfor %}
> +
> +void GstCameraControls::installProperties(GObjectClass *klass, int lastPropId)
> +{
> +{%- for vendor, ctrls in controls %}
> +{%- for ctrl in ctrls %}
> +
> +{%- set spec %}
> +{%- if ctrl.is_rectangle -%}
> +gst_param_spec_array(
> +{%- else -%}
> +g_param_spec_{{ ctrl.gtype }}(
> +{%- endif -%}
> +{%- if ctrl.is_array %}
> +	"{{ ctrl.vendor_prefix }}{{ ctrl.name|kebab_case }}-value",
> +	"{{ ctrl.name }} Value",
> +	"One {{ ctrl.name }} element value",
> +{%- else %}
> +	"{{ ctrl.vendor_prefix }}{{ ctrl.name|kebab_case }}",
> +	"{{ ctrl.name }}",
> +	{{ ctrl.description|format_description|indent_str('\t') }},
> +{%- endif %}
> +{%- if ctrl.is_enum %}
> +	TYPE_{{ ctrl.name|snake_case|upper }},
> +	{{ ctrl.default }},
> +{%- elif ctrl.is_rectangle %}
> +	g_param_spec_int(
> +		"rectangle-value",
> +		"Rectangle Value",
> +		"One rectangle value, either x, y, width or height.",
> +		{{ ctrl.min }}, {{ ctrl.max }}, {{ ctrl.default }},
> +		(GParamFlags) (GST_PARAM_CONTROLLABLE | G_PARAM_READWRITE |
> +			       G_PARAM_STATIC_STRINGS)
> +	),
> +{%- elif ctrl.gtype == 'boolean' %}
> +	{{ ctrl.default }},
> +{%- elif ctrl.gtype in ['float', 'int', 'int64', 'uchar'] %}
> +	{{ ctrl.min }}, {{ ctrl.max }}, {{ ctrl.default }},
> +{%- endif %}
> +	(GParamFlags) (GST_PARAM_CONTROLLABLE | G_PARAM_READWRITE |
> +		       G_PARAM_STATIC_STRINGS)
> +)
> +{%- endset %}
> +
> +	g_object_class_install_property(
> +		klass,
> +		lastPropId + controls::{{ ctrl.namespace }}{{ ctrl.name|snake_case|upper }},
> +{%- if ctrl.is_array %}
> +		gst_param_spec_array(
> +			"{{ ctrl.vendor_prefix }}{{ ctrl.name|kebab_case }}",
> +			"{{ ctrl.name }}",
> +			{{ ctrl.description|format_description|indent_str('\t\t\t') }},
> +			{{ spec|indent_str('\t\t\t') }},
> +			(GParamFlags) (GST_PARAM_CONTROLLABLE |
> +				       G_PARAM_READWRITE |
> +				       G_PARAM_STATIC_STRINGS)
> +		)
> +{%- else %}
> +		{{ spec|indent_str('\t\t') }}
> +{%- endif %}
> +	);
> +{%- endfor %}
> +{%- endfor %}
> +}
> +
> +bool GstCameraControls::getProperty(guint propId, GValue *value,
> +				    [[maybe_unused]] GParamSpec *pspec)
> +{
> +	if (!controls_acc_.contains(propId)) {
> +		GST_WARNING("Control '%s' is not available, default value will "
> +			    "be returned",
> +			    controls::controls.at(propId)->name().c_str());
> +		return true;
> +	}
> +	const ControlValue &cv = controls_acc_.get(propId);
> +
> +	switch (propId) {
> +{%- for vendor, ctrls in controls %}
> +{%- for ctrl in ctrls %}
> +
> +	case controls::{{ ctrl.namespace }}{{ ctrl.name|snake_case|upper }}: {
> +		auto control = cv.get<{{ ctrl.type }}>();
> +
> +{%- if ctrl.is_array %}
> +		for (size_t i = 0; i < control.size(); ++i) {
> +			GValue element = G_VALUE_INIT;
> +{%- if ctrl.is_rectangle %}
> +			g_value_init(&element, GST_TYPE_PARAM_ARRAY_LIST);
> +			value_set_rectangle(&element, control[i]);
> +{%- else %}
> +			g_value_init(&element, G_TYPE_{{ ctrl.gtype|upper }});
> +			g_value_set_{{ ctrl.gtype }}(&element, control[i]);
> +{%- endif %}
> +			gst_value_array_append_and_take_value(value, &element);
> +		}
> +{%- else %}
> +{%- if ctrl.is_rectangle %}
> +		value_set_rectangle(value, control);
> +{%- else %}
> +		g_value_set_{{ ctrl.gtype }}(value, control);
> +{%- endif %}
> +{%- endif %}
> +
> +		return true;
> +	}
> +{%- endfor %}
> +{%- endfor %}
> +
> +	default:
> +		return false;
> +	}
> +}
> +
> +bool GstCameraControls::setProperty(guint propId, const GValue *value,
> +				    [[maybe_unused]] GParamSpec *pspec)
> +{
> +	/*
> +	 * Check whether the camera capabilities are already available.
> +	 * They might not be available if the pipeline has not started yet.
> +	 */
> +	if (!capabilities_.empty()) {
> + 		/* If so, check that the control is supported by the camera. */
> +		const ControlId *cid = capabilities_.idmap().at(propId);
> +		auto info = capabilities_.find(cid);
> +
> +		if (info == capabilities_.end()) {
> +			GST_WARNING("Control '%s' is not supported by the "
> +				    "camera and will be ignored",
> +				    cid->name().c_str());
> +			return true;
> +		}
> +	}
> +
> +	switch (propId) {
> +{%- for vendor, ctrls in controls %}
> +{%- for ctrl in ctrls %}
> +
> +	case controls::{{ ctrl.namespace }}{{ ctrl.name|snake_case|upper }}: {
> +		ControlValue control;
> +{%- if ctrl.is_array %}
> +		size_t size = gst_value_array_get_size(value);
> +{%- if ctrl.size != 0 %}
> +		if (size != {{ ctrl.size }}) {
> +			GST_ERROR("Incorrect array size for control "
> +				  "'{{ ctrl.name|kebab_case }}', must be of "
> +				  "size {{ ctrl.size }}");
> +			return true;
> +		}
> +{%- endif %}
> +
> +		std::vector<{{ ctrl.element_type }}> values(size);
> +		for (size_t i = 0; i < size; ++i) {
> +			const GValue *element =
> +				gst_value_array_get_value(value, i);
> +{%- if ctrl.is_rectangle %}
> +			if (gst_value_array_get_size(element) != 4) {
> +				GST_ERROR("Rectangle in control "
> +					  "'{{ ctrl.name|kebab_case }}' at"
> +					  "index %zu must be an array of size 4",
> +					  i);
> +				return true;
> +			}
> +			values[i] = value_get_rectangle(element);
> +{%- else %}
> +			values[i] = g_value_get_{{ ctrl.gtype }}(element);
> +{%- endif %}
> +		}
> +
> +{%- if ctrl.size == 0 %}
> +		control.set(Span<const {{ ctrl.element_type }}>(values.data(),
> +								size));
> +{%- else %}
> +		control.set(Span<const {{ ctrl.element_type }},
> +			         {{ ctrl.size }}>(values.data(),
> +						  {{ ctrl.size }}));
> +{%- endif %}
> +{%- else %}
> +{%- if ctrl.is_rectangle %}
> +		if (gst_value_array_get_size(value) != 4) {
> +			GST_ERROR("Rectangle in control "
> +				  "'{{ ctrl.name|kebab_case }}' must be an "
> +				  "array of size 4");
> +			return true;
> +		}
> +		Rectangle val = value_get_rectangle(value);
> +{%- else %}
> +		auto val = g_value_get_{{ ctrl.gtype }}(value);
> +{%- endif %}
> +		control.set(val);
> +{%- endif %}
> +		controls_.set(propId, control);
> +		controls_acc_.set(propId, control);
> +		return true;
> +	}
> +{%- endfor %}
> +{%- endfor %}
> +
> +	default:
> +		return false;
> +	}
> +}
> +
> +void GstCameraControls::setCamera(const std::shared_ptr<libcamera::Camera> &cam)
> +{
> +	capabilities_ = cam->controls();
> +
> +	/*
> +	 * Check the controls which were set before the camera capabilities were
> +	 * known. This is required because GStreamer may set properties before
> +	 * the pipeline has started and thus before the camera was known.
> +	 */
> +	ControlList new_controls;
> +	for (auto control = controls_acc_.begin();
> +	     control != controls_acc_.end();
> +	     ++control) {
> +		unsigned int id = control->first;
> +		ControlValue value = control->second;
> +
> +		const ControlId *cid = capabilities_.idmap().at(id);
> +		auto info = capabilities_.find(cid);
> +
> +		/* Only add controls which are supported. */
> +		if (info != capabilities_.end())
> +			new_controls.set(id, value);
> +		else
> +			GST_WARNING("Control '%s' is not supported by the "
> +				    "camera and will be ignored",
> +				    cid->name().c_str());
> +	}
> +
> +	controls_acc_ = new_controls;
> +	controls_ = new_controls;
> +}
> +
> +void GstCameraControls::applyControls(std::unique_ptr<libcamera::Request> &request)
> +{
> +	request->controls().merge(controls_);
> +	controls_.clear();
> +}
> +
> +void GstCameraControls::readMetadata(libcamera::Request *request)
> +{
> +	controls_acc_.merge(request->metadata(),
> +			    ControlList::MergePolicy::OverwriteExisting);
> +}
> diff --git a/src/gstreamer/gstlibcamera-controls.h b/src/gstreamer/gstlibcamera-controls.h
> new file mode 100644
> index 00000000..749220b5
> --- /dev/null
> +++ b/src/gstreamer/gstlibcamera-controls.h
> @@ -0,0 +1,43 @@
> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> +/*
> + * Copyright (C) 2023, Collabora Ltd.
> + *     Author: Nicolas Dufresne <nicolas.dufresne at collabora.com>
> + *
> + * GStreamer Camera Controls
> + */
> +
> +#pragma once
> +
> +#include <memory>
> +
> +#include <libcamera/camera.h>
> +#include <libcamera/controls.h>
> +#include <libcamera/request.h>
> +
> +#include "gstlibcamerasrc.h"
> +
> +namespace libcamera {
> +
> +class GstCameraControls
> +{
> +public:
> +	static void installProperties(GObjectClass *klass, int lastProp);
> +
> +	bool getProperty(guint propId, GValue *value, GParamSpec *pspec);
> +	bool setProperty(guint propId, const GValue *value, GParamSpec *pspec);
> +
> +	void setCamera(const std::shared_ptr<libcamera::Camera> &cam);
> +
> +	void applyControls(std::unique_ptr<libcamera::Request> &request);
> +	void readMetadata(libcamera::Request *request);
> +
> +private:
> +	/* Supported controls and limits of camera. */
> +	ControlInfoMap capabilities_;
> +	/* Set of user modified controls. */
> +	ControlList controls_;
> +	/* Accumulator of all controls ever set and metadata returned by camera */
> +	ControlList controls_acc_;
> +};
> +
> +} /* namespace libcamera */
> diff --git a/src/gstreamer/gstlibcamerasrc.cpp b/src/gstreamer/gstlibcamerasrc.cpp
> index 40b787c8..8efa25f4 100644
> --- a/src/gstreamer/gstlibcamerasrc.cpp
> +++ b/src/gstreamer/gstlibcamerasrc.cpp
> @@ -37,10 +37,11 @@
>  
>  #include <gst/base/base.h>
>  
> +#include "gstlibcamera-controls.h"
> +#include "gstlibcamera-utils.h"
>  #include "gstlibcameraallocator.h"
>  #include "gstlibcamerapad.h"
>  #include "gstlibcamerapool.h"
> -#include "gstlibcamera-utils.h"
>  
>  using namespace libcamera;
>  
> @@ -128,6 +129,7 @@ struct GstLibcameraSrcState {
>  
>  	ControlList initControls_;
>  	guint group_id_;
> +	GstCameraControls controls_;
>  
>  	int queueRequest();
>  	void requestCompleted(Request *request);
> @@ -153,6 +155,7 @@ struct _GstLibcameraSrc {
>  enum {
>  	PROP_0,
>  	PROP_CAMERA_NAME,
> +	PROP_LAST
>  };
>  
>  static void gst_libcamera_src_child_proxy_init(gpointer g_iface,
> @@ -183,6 +186,9 @@ int GstLibcameraSrcState::queueRequest()
>  	if (!request)
>  		return -ENOMEM;
>  
> +	/* Apply controls */
> +	controls_.applyControls(request);
> +
>  	std::unique_ptr<RequestWrap> wrap =
>  		std::make_unique<RequestWrap>(std::move(request));
>  
> @@ -226,6 +232,9 @@ GstLibcameraSrcState::requestCompleted(Request *request)
>  
>  	{
>  		GLibLocker locker(&lock_);
> +
> +		controls_.readMetadata(request);
> +
>  		wrap = std::move(queuedRequests_.front());
>  		queuedRequests_.pop();
>  	}
> @@ -408,6 +417,8 @@ gst_libcamera_src_open(GstLibcameraSrc *self)
>  		return false;
>  	}
>  
> +	self->state->controls_.setCamera(cam);
> +
>  	cam->requestCompleted.connect(self->state, &GstLibcameraSrcState::requestCompleted);
>  
>  	/* No need to lock here, we didn't start our threads yet. */
> @@ -722,6 +733,7 @@ gst_libcamera_src_set_property(GObject *object, guint prop_id,
>  {
>  	GLibLocker lock(GST_OBJECT(object));
>  	GstLibcameraSrc *self = GST_LIBCAMERA_SRC(object);
> +	GstLibcameraSrcState *state = self->state;
>  
>  	switch (prop_id) {
>  	case PROP_CAMERA_NAME:
> @@ -729,7 +741,8 @@ gst_libcamera_src_set_property(GObject *object, guint prop_id,
>  		self->camera_name = g_value_dup_string(value);
>  		break;
>  	default:
> -		G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
> +		if (!state->controls_.setProperty(prop_id - PROP_LAST, value, pspec))
> +			G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
>  		break;
>  	}
>  }
> @@ -740,13 +753,15 @@ gst_libcamera_src_get_property(GObject *object, guint prop_id, GValue *value,
>  {
>  	GLibLocker lock(GST_OBJECT(object));
>  	GstLibcameraSrc *self = GST_LIBCAMERA_SRC(object);
> +	GstLibcameraSrcState *state = self->state;
>  
>  	switch (prop_id) {
>  	case PROP_CAMERA_NAME:
>  		g_value_set_string(value, self->camera_name);
>  		break;
>  	default:
> -		G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
> +		if (!state->controls_.getProperty(prop_id - PROP_LAST, value, pspec))
> +			G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
>  		break;
>  	}
>  }
> @@ -947,6 +962,7 @@ gst_libcamera_src_class_init(GstLibcameraSrcClass *klass)
>  							     | G_PARAM_STATIC_STRINGS));
>  	g_object_class_install_property(object_class, PROP_CAMERA_NAME, spec);
>  
> +	GstCameraControls::installProperties(object_class, PROP_LAST);
>  }
>  
>  /* GstChildProxy implementation */
> diff --git a/src/gstreamer/meson.build b/src/gstreamer/meson.build
> index c2a01e7b..6b7e53b5 100644
> --- a/src/gstreamer/meson.build
> +++ b/src/gstreamer/meson.build
> @@ -25,6 +25,16 @@ libcamera_gst_sources = [
>      'gstlibcamerasrc.cpp',
>  ]
>  
> +# Generate gstreamer control properties
> +
> +gen_gst_controls_template = files('gstlibcamera-controls.cpp.in')
> +libcamera_gst_sources += custom_target('gstlibcamera-controls.cpp',
> +                                       input : controls_files,
> +                                       output : 'gstlibcamera-controls.cpp',
> +                                       command : [gen_gst_controls, '-o', '@OUTPUT@',
> +                                                  '-t', gen_gst_controls_template, '@INPUT@'],
> +                                       env : py_build_env)
> +
>  libcamera_gst_cpp_args = [
>      '-DVERSION="@0@"'.format(libcamera_git_version),
>      '-DPACKAGE="@0@"'.format(meson.project_name()),
> diff --git a/utils/codegen/controls.py b/utils/codegen/controls.py
> index 7bafee59..03c77cc6 100644
> --- a/utils/codegen/controls.py
> +++ b/utils/codegen/controls.py
> @@ -110,3 +110,11 @@ class Control(object):
>              return f"Span<const {typ}, {self.__size}>"
>          else:
>              return f"Span<const {typ}>"
> +
> +    @property
> +    def element_type(self):
> +        return self.__data.get('type')
> +
> +    @property
> +    def size(self):
> +        return self.__size
> diff --git a/utils/codegen/gen-gst-controls.py b/utils/codegen/gen-gst-controls.py
> new file mode 100755
> index 00000000..2601a675
> --- /dev/null
> +++ b/utils/codegen/gen-gst-controls.py
> @@ -0,0 +1,182 @@
> +#!/usr/bin/env python3
> +# SPDX-License-Identifier: GPL-2.0-or-later
> +# Copyright (C) 2019, Google Inc.
> +# Copyright (C) 2024, Jaslo Ziska
> +#
> +# Authors:
> +# Laurent Pinchart <laurent.pinchart at ideasonboard.com>
> +# Jaslo Ziska <jaslo at ziska.de>
> +#
> +# Generate gstreamer control properties from YAML
> +
> +import argparse
> +import jinja2
> +import re
> +import sys
> +import yaml
> +
> +from controls import Control
> +
> +
> +exposed_controls = [
> +    'AeEnable', 'AeMeteringMode', 'AeConstraintMode', 'AeExposureMode',
> +    'ExposureValue', 'ExposureTime', 'AnalogueGain', 'AeFlickerPeriod',
> +    'Brightness', 'Contrast', 'AwbEnable', 'AwbMode', 'ColourGains',
> +    'Saturation', 'Sharpness', 'ColourCorrectionMatrix', 'ScalerCrop',
> +    'DigitalGain', 'AfMode', 'AfRange', 'AfSpeed', 'AfMetering', 'AfWindows',
> +    'LensPosition', 'Gamma',
> +]
> +
> +
> +def find_common_prefix(strings):
> +    prefix = strings[0]
> +
> +    for string in strings[1:]:
> +        while string[:len(prefix)] != prefix and prefix:
> +            prefix = prefix[:len(prefix) - 1]
> +        if not prefix:
> +            break
> +
> +    return prefix
> +
> +
> +def format_description(description):
> +    # Substitute doxygen keywords \sa (see also) and \todo
> +    description = re.sub(r'\\sa((?: \w+)+)',
> +                         lambda match: 'See also: ' + ', '.join(
> +                             map(kebab_case, match.group(1).strip().split(' '))
> +                         ) + '.', description)
> +    description = re.sub(r'\\todo', 'Todo:', description)
> +
> +    description = description.strip().split('\n')
> +    return '\n'.join([
> +        '"' + line.replace('\\', r'\\').replace('"', r'\"') + ' "' for line in description if line
> +    ]).rstrip()
> +
> +
> +# Custom filter to allow indenting by a string prior to Jinja version 3.0
> +#
> +# This function can be removed and the calls to indent_str() replaced by the
> +# built-in indent() filter when dropping Jinja versions older than 3.0
> +def indent_str(s, indention):
> +    s += '\n'
> +
> +    lines = s.splitlines()
> +    rv = lines.pop(0)
> +
> +    if lines:
> +        rv += '\n' + '\n'.join(
> +            indention + line if line else line for line in lines
> +        )
> +
> +    return rv
> +
> +
> +def snake_case(s):
> +    return ''.join([
> +        c.isupper() and ('_' + c.lower()) or c for c in s
> +    ]).strip('_')
> +
> +
> +def kebab_case(s):
> +    return snake_case(s).replace('_', '-')
> +
> +
> +def extend_control(ctrl):
> +    if ctrl.vendor != 'libcamera':
> +        ctrl.namespace = f'{ctrl.vendor}::'
> +        ctrl.vendor_prefix = f'{ctrl.vendor}-'
> +    else:
> +        ctrl.namespace = ''
> +        ctrl.vendor_prefix = ''
> +
> +    ctrl.is_array = ctrl.size is not None
> +
> +    if ctrl.is_enum:
> +        # Remove common prefix from enum variant names
> +        prefix = find_common_prefix([enum.name for enum in ctrl.enum_values])
> +        for enum in ctrl.enum_values:
> +            enum.gst_name = kebab_case(enum.name.removeprefix(prefix))
> +
> +        ctrl.gtype = 'enum'
> +        ctrl.default = '0'
> +    elif ctrl.element_type == 'bool':
> +        ctrl.gtype = 'boolean'
> +        ctrl.default = 'false'
> +    elif ctrl.element_type == 'float':
> +        ctrl.gtype = 'float'
> +        ctrl.default = '0'
> +        ctrl.min = '-G_MAXFLOAT'
> +        ctrl.max = 'G_MAXFLOAT'
> +    elif ctrl.element_type == 'int32_t':
> +        ctrl.gtype = 'int'
> +        ctrl.default = '0'
> +        ctrl.min = 'G_MININT'
> +        ctrl.max = 'G_MAXINT'
> +    elif ctrl.element_type == 'int64_t':
> +        ctrl.gtype = 'int64'
> +        ctrl.default = '0'
> +        ctrl.min = 'G_MININT64'
> +        ctrl.max = 'G_MAXINT64'
> +    elif ctrl.element_type == 'uint8_t':
> +        ctrl.gtype = 'uchar'
> +        ctrl.default = '0'
> +        ctrl.min = '0'
> +        ctrl.max = 'G_MAXUINT8'
> +    elif ctrl.element_type == 'Rectangle':
> +        ctrl.is_rectangle = True
> +        ctrl.default = '0'
> +        ctrl.min = '0'
> +        ctrl.max = 'G_MAXINT'
> +    else:
> +        raise RuntimeError(f'The type `{ctrl.element_type}` is unknown')
> +
> +    return ctrl
> +
> +
> +def main(argv):
> +    # Parse command line arguments
> +    parser = argparse.ArgumentParser()
> +    parser.add_argument('--output', '-o', metavar='file', type=str,
> +                        help='Output file name. Defaults to standard output if not specified.')
> +    parser.add_argument('--template', '-t', dest='template', type=str, required=True,
> +                        help='Template file name.')
> +    parser.add_argument('input', type=str, nargs='+',
> +                        help='Input file name.')
> +
> +    args = parser.parse_args(argv[1:])
> +
> +    controls = {}
> +    for input in args.input:
> +        data = yaml.safe_load(open(input, 'rb').read())
> +
> +        vendor = data['vendor']
> +        ctrls = controls.setdefault(vendor, [])
> +
> +        for ctrl in data['controls']:
> +            ctrl = Control(*ctrl.popitem(), vendor)
> +
> +            if ctrl.name in exposed_controls:
> +                ctrls.append(extend_control(ctrl))
> +
> +    data = {'controls': list(controls.items())}
> +
> +    env = jinja2.Environment()
> +    env.filters['format_description'] = format_description
> +    env.filters['indent_str'] = indent_str
> +    env.filters['snake_case'] = snake_case
> +    env.filters['kebab_case'] = kebab_case
> +    template = env.from_string(open(args.template, 'r', encoding='utf-8').read())
> +    string = template.render(data)
> +
> +    if args.output:
> +        with open(args.output, 'w', encoding='utf-8') as output:
> +            output.write(string)
> +    else:
> +        sys.stdout.write(string)
> +
> +    return 0
> +
> +
> +if __name__ == '__main__':
> +    sys.exit(main(sys.argv))
> diff --git a/utils/codegen/meson.build b/utils/codegen/meson.build
> index adf33bba..904dd66d 100644
> --- a/utils/codegen/meson.build
> +++ b/utils/codegen/meson.build
> @@ -11,6 +11,7 @@ py_modules += ['jinja2', 'yaml']
>  
>  gen_controls = files('gen-controls.py')
>  gen_formats = files('gen-formats.py')
> +gen_gst_controls = files('gen-gst-controls.py')
>  gen_header = files('gen-header.sh')
>  gen_ipa_pub_key = files('gen-ipa-pub-key.py')
>  gen_tracepoints = files('gen-tp-header.py')



More information about the libcamera-devel mailing list