Show a patch.

GET /api/1.1/patches/20768/?format=api
HTTP 200 OK
Allow: GET, PUT, PATCH, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept

{
    "id": 20768,
    "url": "https://patchwork.libcamera.org/api/1.1/patches/20768/?format=api",
    "web_url": "https://patchwork.libcamera.org/patch/20768/",
    "project": {
        "id": 1,
        "url": "https://patchwork.libcamera.org/api/1.1/projects/1/?format=api",
        "name": "libcamera",
        "link_name": "libcamera",
        "list_id": "libcamera_core",
        "list_email": "libcamera-devel@lists.libcamera.org",
        "web_url": "",
        "scm_url": "",
        "webscm_url": ""
    },
    "msgid": "<20240805100038.11972-3-jaslo@ziska.de>",
    "date": "2024-08-05T09:28:37",
    "name": "[2/3] gstreamer: Generate controls from control_ids_*.yaml files",
    "commit_ref": null,
    "pull_url": null,
    "state": "superseded",
    "archived": false,
    "hash": "694a155a6ebefe6c93f9c5e3f4fd0bbbc8486a56",
    "submitter": {
        "id": 173,
        "url": "https://patchwork.libcamera.org/api/1.1/people/173/?format=api",
        "name": "Jaslo Ziska",
        "email": "jaslo@ziska.de"
    },
    "delegate": null,
    "mbox": "https://patchwork.libcamera.org/patch/20768/mbox/",
    "series": [
        {
            "id": 4485,
            "url": "https://patchwork.libcamera.org/api/1.1/series/4485/?format=api",
            "web_url": "https://patchwork.libcamera.org/project/libcamera/list/?series=4485",
            "date": "2024-08-05T09:28:35",
            "name": "gstreamer: Generate controls from control_ids_*.yaml files",
            "version": 1,
            "mbox": "https://patchwork.libcamera.org/series/4485/mbox/"
        }
    ],
    "comments": "https://patchwork.libcamera.org/api/patches/20768/comments/",
    "check": "pending",
    "checks": "https://patchwork.libcamera.org/api/patches/20768/checks/",
    "tags": {},
    "headers": {
        "Return-Path": "<libcamera-devel-bounces@lists.libcamera.org>",
        "X-Original-To": "parsemail@patchwork.libcamera.org",
        "Delivered-To": "parsemail@patchwork.libcamera.org",
        "Received": [
            "from lancelot.ideasonboard.com (lancelot.ideasonboard.com\n\t[92.243.16.209])\n\tby patchwork.libcamera.org (Postfix) with ESMTPS id BDD3AC323E\n\tfor <parsemail@patchwork.libcamera.org>;\n\tMon,  5 Aug 2024 10:01:48 +0000 (UTC)",
            "from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 5E08A63381;\n\tMon,  5 Aug 2024 12:01:48 +0200 (CEST)",
            "from mo4-p00-ob.smtp.rzone.de (mo4-p00-ob.smtp.rzone.de\n\t[81.169.146.217])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 0A9696337E\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tMon,  5 Aug 2024 12:01:47 +0200 (CEST)",
            "from archlinux.fritz.box by smtp.strato.de (RZmta 51.1.0 AUTH)\n\twith ESMTPSA id zb9f0a075A1kvkK\n\t(using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256 bits))\n\t(Client did not present a certificate);\n\tMon, 5 Aug 2024 12:01:46 +0200 (CEST)"
        ],
        "Authentication-Results": "lancelot.ideasonboard.com;\n\tdkim=fail reason=\"signature verification failed\" (2048-bit key;\n\tunprotected) header.d=ziska.de header.i=@ziska.de header.b=\"Cf78g8L8\";\n\tdkim=permerror (0-bit key) header.d=ziska.de header.i=@ziska.de\n\theader.b=\"HebAEFhc\"; dkim-atps=neutral",
        "ARC-Seal": "i=1; a=rsa-sha256; t=1722852106; cv=none;\n\td=strato.com; s=strato-dkim-0002;\n\tb=B7qNlkFUSfPdm5rdszkqvJSL8RyGs09TCKWZQrB3vF+6dBtwdLurTtfv171/BJTUD4\n\tYb90Qo3Qs79wR5ItMUK0iv1HnVGr0Zc4byN3zSvc1xa+O5o8SliNyo5eGbwox2WeJ7h6\n\t8r3RLYuhgvtMexNrfyEuN1qkio8Q7UPmeVlpA3R3dAOnjV7V3ueXsTuXKd/DcixEZxWz\n\tMYoKWZX8Cow7pIv0GsLxjISlsddT/SRdjLmVdkSfOX0FTuNesurY5lJC29lz/MhbNE07\n\tJwub1AQfOdIAetgE4yxvRBUorQZW6YNrRnJR1dovMgqsPDKCJ8Od65maxFvEPtljv9qO\n\tG03Q==",
        "ARC-Message-Signature": "i=1; a=rsa-sha256; c=relaxed/relaxed; t=1722852106;\n\ts=strato-dkim-0002; d=strato.com;\n\th=References:In-Reply-To:Message-ID:Date:Subject:Cc:To:From:Cc:Date:\n\tFrom:Subject:Sender;\n\tbh=3SHd+fgDM0zwoJ6gc3Yv4Pl6CBD3qYQKnUY+lVuijqk=;\n\tb=KLH7Ek7ii4WRJnnJM4uc/MYPTjfTkH4oqBtRjsf74vw+MeoIg8ASq2nK2U2NfXWPkw\n\tHGRVh1U48jNe8kbDF6HF3TRi8b+Q/dqzHv4dzBI/tINLnrA2j74cdj1Xzf/U074bN12b\n\tIcET2ivuz1oa0iq9coImZheajzg11X/+OtGStTJEqlubO6Aj5tEoAh9xVxYHj2nH3NVq\n\tJN19Xj2qyfaCU4wsDzFS1J0g3Cj2g+lS/+8Yqi5caX/B08vnY+Z8jTDwcOkb+odSjISo\n\tSzLyBlPjy1u2Wg2XD957jTjPt9w2n3o2dZWDfK6fzZXHh2M9qjD+Z/aY68dtaiP6sIeQ\n\t0O2g==",
        "ARC-Authentication-Results": "i=1; strato.com;\n    arc=none;\n    dkim=none",
        "X-RZG-CLASS-ID": "mo00",
        "DKIM-Signature": [
            "v=1; a=rsa-sha256; c=relaxed/relaxed; t=1722852106;\n\ts=strato-dkim-0002; d=ziska.de;\n\th=References:In-Reply-To:Message-ID:Date:Subject:Cc:To:From:Cc:Date:\n\tFrom:Subject:Sender;\n\tbh=3SHd+fgDM0zwoJ6gc3Yv4Pl6CBD3qYQKnUY+lVuijqk=;\n\tb=Cf78g8L8hMcH2tx5mU3oDlC767K/zpU5mX4yQNMC+yiWZJn72eEmpZSCke9DKEsxqb\n\tXQfCyoDk42CcPabA9dsalIvDxXB1/iKJClxqfgfLwcIPaW0iixClGTGFwixkMBff62vY\n\tgMRKdFVdVUMD4DtQ3kjCc1lzPg7Y5UqPlXkS/TkTZzPajjHSQ/X1q0EgF9OKw4xQY/Ju\n\t1Jp0jtf4H8q0ydPe+B1HYantStwFBjvrmoukPIDDr3Z51pFWrRssBzyjt9TidWSIODbF\n\tuuQz/i3PYIfGEBxzigRNBpHW9YH3xwSbdxdlTFjg8WOTjHrgNS2mODXRTCXAQeLlO3fq\n\tjWew==",
            "v=1; a=ed25519-sha256; c=relaxed/relaxed; t=1722852106;\n\ts=strato-dkim-0003; d=ziska.de;\n\th=References:In-Reply-To:Message-ID:Date:Subject:Cc:To:From:Cc:Date:\n\tFrom:Subject:Sender;\n\tbh=3SHd+fgDM0zwoJ6gc3Yv4Pl6CBD3qYQKnUY+lVuijqk=;\n\tb=HebAEFhc0FgnjX0Kbfn4YAYz+XFNhu/xef5EbDJjXAs9UbIiIEcBIqr/3yqP5fow4j\n\ti9Oufk7ssmBYzsZNp6DQ=="
        ],
        "X-RZG-AUTH": "\":Jm0XeU+IYfb0x77LHmrjN5Wlb7TBwusDqIM6Hizy8VdfzvKi4yoFC9cCg4qxBvJaP2L5sFjJoIK+3CsR3+pCW/FVb/tK\"",
        "From": "Jaslo Ziska <jaslo@ziska.de>",
        "To": "libcamera-devel@lists.libcamera.org",
        "Cc": "Jaslo Ziska <jaslo@ziska.de>",
        "Subject": "[PATCH 2/3] gstreamer: Generate controls from control_ids_*.yaml\n\tfiles",
        "Date": "Mon,  5 Aug 2024 11:28:37 +0200",
        "Message-ID": "<20240805100038.11972-3-jaslo@ziska.de>",
        "X-Mailer": "git-send-email 2.46.0",
        "In-Reply-To": "<20240805100038.11972-1-jaslo@ziska.de>",
        "References": "<20240805100038.11972-1-jaslo@ziska.de>",
        "MIME-Version": "1.0",
        "Content-Transfer-Encoding": "8bit",
        "Content-Type": "text/plain; charset=\"us-ascii\"",
        "X-BeenThere": "libcamera-devel@lists.libcamera.org",
        "X-Mailman-Version": "2.1.29",
        "Precedence": "list",
        "List-Id": "<libcamera-devel.lists.libcamera.org>",
        "List-Unsubscribe": "<https://lists.libcamera.org/options/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=unsubscribe>",
        "List-Archive": "<https://lists.libcamera.org/pipermail/libcamera-devel/>",
        "List-Post": "<mailto:libcamera-devel@lists.libcamera.org>",
        "List-Help": "<mailto:libcamera-devel-request@lists.libcamera.org?subject=help>",
        "List-Subscribe": "<https://lists.libcamera.org/listinfo/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=subscribe>",
        "Errors-To": "libcamera-devel-bounces@lists.libcamera.org",
        "Sender": "\"libcamera-devel\" <libcamera-devel-bounces@lists.libcamera.org>"
    },
    "content": "This commit implements gstreamer controls for the libcamera element by\ngenerating the controls from the control_ids_*.yaml files using a new\ngen-gst-controls.py script. The appropriate meson files are also changed\nto automatically run the script when building.\n\nThe gen-gst-controls.py script works similar to the gen-controls.py\nscript by parsing the control_ids_*.yaml files and generating C++ code\nfor each control.\nFor the controls to be used as gstreamer properties the type for each\ncontrol needs to be translated to the appropriate glib type and a\nGEnumValue is generated for each enum control. Then a\ng_object_install_property(), _get_property() and _set_property()\nfunction is generated for each control.\nThe vendor controls get prefixed with \"$vendor-\" in the final gstreamer\nproperty name.\n\nThe C++ code generated by the gen-gst-controls.py script is written into\nthe template gstlibcamerasrc-controls.cpp.in file. The matching\ngstlibcamerasrc-controls.h header defines the GstCameraControls class\nwhich handles the installation of the gstreamer properties as well as\nkeeping track of the control values and setting and getting the\ncontrols. The content of these functions is generated in the Python\nscript.\n\nFinally the libcamerasrc element itself is edited to make use of the new\nGstCameraControls class. The way this works is by defining a PROP_LAST\nenum variant which is passed to the installProperties() function so the\nproperties are defined with the appropriate offset. When getting or\nsetting a property PROP_LAST is subtracted from the requested property\nto translate the control back into a libcamera::controls:: enum\nvariant.\n\nSigned-off-by: Jaslo Ziska <jaslo@ziska.de>\n---\n src/gstreamer/gstlibcamera-controls.cpp.in |  46 +++\n src/gstreamer/gstlibcamera-controls.h      |  36 ++\n src/gstreamer/gstlibcamerasrc.cpp          |  17 +-\n src/gstreamer/meson.build                  |  14 +\n utils/gen-gst-controls.py                  | 398 +++++++++++++++++++++\n utils/meson.build                          |   1 +\n 6 files changed, 509 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/gen-gst-controls.py",
    "diff": "diff --git a/src/gstreamer/gstlibcamera-controls.cpp.in b/src/gstreamer/gstlibcamera-controls.cpp.in\nnew file mode 100644\nindex 00000000..ff93f5c3\n--- /dev/null\n+++ b/src/gstreamer/gstlibcamera-controls.cpp.in\n@@ -0,0 +1,46 @@\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+ * gstlibcamera-controls.cpp - GStreamer Camera Controls\n+ *\n+ * This file is auto-generated. Do not edit.\n+ */\n+\n+#include \"gstlibcamera-controls.h\"\n+\n+#include <libcamera/control_ids.h>\n+\n+using namespace libcamera;\n+\n+${enum_def}\n+\n+void GstCameraControls::installProperties(GObjectClass *klass, int lastPropId)\n+{\n+${install_properties}\n+}\n+\n+bool GstCameraControls::getProperty(guint propId, GValue *value, GParamSpec *pspec)\n+{\n+\tswitch (propId) {\n+${get_properties}\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+\tswitch (propId) {\n+${set_properties}\n+\tdefault:\n+\t\treturn false;\n+\t}\n+}\n+\n+void GstCameraControls::applyControls(std::unique_ptr<libcamera::Request> &request)\n+{\n+\trequest->controls().merge(controls_);\n+}\ndiff --git a/src/gstreamer/gstlibcamera-controls.h b/src/gstreamer/gstlibcamera-controls.h\nnew file mode 100644\nindex 00000000..4e1d5bf9\n--- /dev/null\n+++ b/src/gstreamer/gstlibcamera-controls.h\n@@ -0,0 +1,36 @@\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+ * gstlibcamera-controls.h - GStreamer Camera Controls\n+ */\n+\n+#pragma once\n+\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+\tGstCameraControls() {};\n+\t~GstCameraControls() {};\n+\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 applyControls(std::unique_ptr<libcamera::Request> &request);\n+\n+private:\n+\t/* set of user modified controls */\n+\tControlList controls_;\n+};\n+\n+} /* namespace libcamera */\ndiff --git a/src/gstreamer/gstlibcamerasrc.cpp b/src/gstreamer/gstlibcamerasrc.cpp\nindex 5a3e2989..85dab67f 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@@ -722,6 +728,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 +736,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 +748,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 +957,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 */\ndiff --git a/src/gstreamer/meson.build b/src/gstreamer/meson.build\nindex c2a01e7b..e6c20124 100644\n--- a/src/gstreamer/meson.build\n+++ b/src/gstreamer/meson.build\n@@ -25,6 +25,20 @@ libcamera_gst_sources = [\n     'gstlibcamerasrc.cpp',\n ]\n \n+# Generate gstreamer control properties\n+\n+gen_gst_controls_input_files = []\n+gen_gst_controls_template = files('gstlibcamera-controls.cpp.in')\n+foreach file : controls_files\n+    gen_gst_controls_input_files += files('../libcamera/' + file)\n+endforeach\n+\n+libcamera_gst_sources += custom_target('gstlibcamera-controls.cpp',\n+                                       input : gen_gst_controls_input_files,\n+                                       output : 'gstlibcamera-controls.cpp',\n+                                       command : [gen_gst_controls, '-o', '@OUTPUT@',\n+                                                  '-t', gen_gst_controls_template, '@INPUT@'])\n+\n libcamera_gst_cpp_args = [\n     '-DVERSION=\"@0@\"'.format(libcamera_git_version),\n     '-DPACKAGE=\"@0@\"'.format(meson.project_name()),\ndiff --git a/utils/gen-gst-controls.py b/utils/gen-gst-controls.py\nnew file mode 100755\nindex 00000000..d0c12b50\n--- /dev/null\n+++ b/utils/gen-gst-controls.py\n@@ -0,0 +1,398 @@\n+#!/usr/bin/env python3\n+\n+import argparse\n+import re\n+import string\n+import sys\n+import yaml\n+\n+\n+class ControlEnum(object):\n+    def __init__(self, data):\n+        self.__data = data\n+\n+    @property\n+    def description(self):\n+        \"\"\"The enum description\"\"\"\n+        return self.__data.get('description')\n+\n+    @property\n+    def name(self):\n+        \"\"\"The enum name\"\"\"\n+        return self.__data.get('name')\n+\n+    @property\n+    def value(self):\n+        \"\"\"The enum value\"\"\"\n+        return self.__data.get('value')\n+\n+\n+class Control(object):\n+    def __init__(self, name, data, vendor):\n+        self.__name = name\n+        self.__data = data\n+        self.__enum_values = None\n+        self.__size = None\n+        self.__vendor = vendor\n+\n+        enum_values = data.get('enum')\n+        if enum_values is not None:\n+            self.__enum_values = [ControlEnum(enum) for enum in enum_values]\n+\n+        size = self.__data.get('size')\n+        if size is not None:\n+            if len(size) == 0:\n+                raise RuntimeError(f'Control `{self.__name}` size must have at least one dimension')\n+\n+            # Compute the total number of elements in the array. If any of the\n+            # array dimension is a string, the array is variable-sized.\n+            num_elems = 1\n+            for dim in size:\n+                if type(dim) is str:\n+                    num_elems = 0\n+                    break\n+\n+                dim = int(dim)\n+                if dim <= 0:\n+                    raise RuntimeError(f'Control `{self.__name}` size must have positive values only')\n+\n+                num_elems *= dim\n+\n+            self.__size = num_elems\n+\n+    @property\n+    def description(self):\n+        \"\"\"The control description\"\"\"\n+        return self.__data.get('description')\n+\n+    @property\n+    def enum_values(self):\n+        \"\"\"The enum values, if the control is an enumeration\"\"\"\n+        if self.__enum_values is None:\n+            return\n+        for enum in self.__enum_values:\n+            yield enum\n+\n+    @property\n+    def is_enum(self):\n+        \"\"\"Is the control an enumeration\"\"\"\n+        return self.__enum_values is not None\n+\n+    @property\n+    def vendor(self):\n+        \"\"\"The vendor string, or None\"\"\"\n+        return self.__vendor\n+\n+    @property\n+    def name(self):\n+        \"\"\"The control name (CamelCase)\"\"\"\n+        return self.__name\n+\n+    @property\n+    def type(self):\n+        typ = self.__data.get('type')\n+        size = self.__data.get('size')\n+\n+        if typ == 'string':\n+            return 'std::string'\n+\n+        if self.__size is None:\n+            return typ\n+\n+        if self.__size:\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+        typ = self.__data.get('type')\n+        return typ\n+\n+    @property\n+    def size(self):\n+        return self.__size\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, indent = 0):\n+    # Substitute doxygen keywords \\sa (see also) and \\todo\n+    description = re.sub(r'\\\\sa((?: \\w+)+)',\n+                         lambda match: 'See also: ' + ', '.join(map(kebab_case, match.group(1).strip().split(' '))) + '.', description)\n+    description = re.sub(r'\\\\todo', 'Todo:', description)\n+\n+    description = description.strip().split('\\n')\n+    return '\\n'.join([indent * '\\t' + '\"' + line.replace('\\\\', r'\\\\').replace('\"', r'\\\"') + ' \"' for line in description if line]).rstrip()\n+\n+\n+def snake_case(s):\n+    return ''.join([c.isupper() and ('_' + c.lower()) or c for c in s]).strip('_')\n+\n+\n+def kebab_case(s):\n+    return snake_case(s).replace('_', '-')\n+\n+\n+def indent(s, n):\n+    lines = s.split('\\n')\n+    return '\\n'.join([n * '\\t' + line for line in lines])\n+\n+\n+def generate_cpp(controls):\n+    # Beginning of a GEnumValue definition for enum controls\n+    enum_values_start = string.Template('static const GEnumValue ${name_snake_case}_types[] = {')\n+    # Definition of the GEnumValue variant for each enum control variant\n+    # Because the description might have multiple lines it will get indented below\n+    enum_values_values = string.Template('''{\n+\\tcontrols${vendor_namespace}::${name},\n+${description},\n+\\t\"${nick}\"\n+},''')\n+    # End of a GEnumValue definition and definition of the matching _get_type() function\n+    enum_values_end = string.Template('''\\t{0, NULL, NULL}\n+};\n+\n+#define TYPE_${name_upper} (${name_snake_case}_get_type())\n+static GType ${name_snake_case}_get_type(void)\n+{\n+\\tstatic GType ${name_snake_case}_type = 0;\n+\n+\\tif (!${name_snake_case}_type)\n+\\t\\t${name_snake_case}_type = g_enum_register_static(\"${name}\", ${name_snake_case}_types);\n+\n+\\treturn ${name_snake_case}_type;\n+}\n+''')\n+\n+    # Creation of the type spec for the different types of controls\n+    # The description (and the element_spec for the array) might have multiple lines and will get indented below\n+    spec_array = string.Template('''gst_param_spec_array(\n+\\t\"${spec_name}\",\n+\\t\"${nick}\",\n+${description},\n+${element_spec},\n+\\t(GParamFlags) (GST_PARAM_CONTROLLABLE | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)\n+)''')\n+    spec_bool = string.Template('''g_param_spec_boolean(\n+\\t\"${spec_name}\",\n+\\t\"${nick}\",\n+${description},\n+\\t${default},\n+\\t(GParamFlags) (GST_PARAM_CONTROLLABLE | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)\n+)''')\n+    spec_enum = string.Template('''g_param_spec_enum(\n+\\t\"${spec_name}\",\n+\\t\"${nick}\",\n+${description},\n+\\tTYPE_${enum},\n+\\t${default},\n+\\t(GParamFlags) (GST_PARAM_CONTROLLABLE | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)\n+)''')\n+    spec_numeric = string.Template('''g_param_spec_${gtype}(\n+\\t\"${spec_name}\",\n+\\t\"${nick}\",\n+${description},\n+\\t${min},\n+\\t${max},\n+\\t${default},\n+\\t(GParamFlags) (GST_PARAM_CONTROLLABLE | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)\n+)''')\n+\n+    # The g_object_class_install_property() function call for each control\n+    install_property = string.Template('''\\tg_object_class_install_property(\n+\\t\\tklass,\n+\\t\\tlastPropId + controls${vendor_namespace}::${name_upper},\n+${spec}\n+\\t);''')\n+\n+    # The _get_property() switch cases for each control\n+    get_property = string.Template('''case controls${vendor_namespace}::${name_upper}: {\n+\\tauto val = controls_.get(controls${vendor_namespace}::${name});\n+\\tauto spec = G_PARAM_SPEC_${gtype_upper}(pspec);\n+\\tg_value_set_${gtype}(value, val.value_or(spec->default_value));\n+\\treturn true;\n+}''')\n+    get_array_property = string.Template('''case controls${vendor_namespace}::${name_upper}: {\n+\\tauto val = controls_.get(controls${vendor_namespace}::${name});\n+\\tif (val) {\n+\\t\\tfor (size_t i = 0; i < val->size(); ++i) {\n+\\t\\t\\tGValue v = G_VALUE_INIT;\n+\\t\\t\\tg_value_init(&v, G_TYPE_${gtype_upper});\n+\\t\\t\\tg_value_set_${gtype}(&v, (*val)[i]);\n+\\t\\t\\tgst_value_array_append_and_take_value(value, &v);\n+\\t\\t}\n+\\t}\n+\\treturn true;\n+}''')\n+\n+    # The _set_property() switch cases for each control\n+    set_property = string.Template('''case controls${vendor_namespace}::${name_upper}:\n+\\tcontrols_.set(controls${vendor_namespace}::${name}, g_value_get_${gtype}(value));\n+\\treturn true;''')\n+    set_array_property = string.Template('''case controls${vendor_namespace}::${name_upper}: {\n+\\tsize_t size = gst_value_array_get_size(value);\n+\\tstd::vector<${type}> val(size);\n+\\tfor (size_t i = 0; i < size; ++i) {\n+\\t\\tconst GValue *v = gst_value_array_get_value(value, i);\n+\\t\\tval[i] = g_value_get_${gtype}(v);\n+\\t}\n+\\tcontrols_.set(controls${vendor_namespace}::${name}, Span<const ${type}, ${size}>(val.data(), size));\n+\\treturn true;\n+}''')\n+\n+    enum_def = []\n+    install_properties = []\n+    get_properties = []\n+    set_properties = []\n+\n+    for ctrl in controls:\n+        is_array = ctrl.size is not None\n+        size = ctrl.size\n+        if size == 0:\n+            size = 'dynamic_extent'\n+\n+        # Determine the matching glib type for each C++ type used in the controls\n+        gtype = ''\n+        if ctrl.is_enum:\n+            gtype = 'enum'\n+        elif ctrl.element_type == 'bool':\n+            gtype = 'boolean'\n+        elif ctrl.element_type == 'float':\n+            gtype = 'float'\n+        elif ctrl.element_type == 'int32_t':\n+            gtype = 'int'\n+        elif ctrl.element_type == 'int64_t':\n+            gtype = 'int64'\n+        elif ctrl.element_type == 'uint8_t':\n+            gtype = 'uchar'\n+        elif ctrl.element_type == 'Rectangle':\n+            # TODO: Handle Rectangle\n+            continue\n+        else:\n+            raise RuntimeError(f'The type `{ctrl.element_type}` is unknown')\n+\n+        vendor_prefix = ''\n+        vendor_namespace = ''\n+        if ctrl.vendor != 'libcamera':\n+            vendor_prefix = ctrl.vendor + '-'\n+            vendor_namespace = '::' + ctrl.vendor\n+\n+        name_snake_case = snake_case(ctrl.name)\n+        name_upper = name_snake_case.upper()\n+\n+        info = {\n+            'name': ctrl.name,\n+            'vendor_namespace': vendor_namespace,\n+            'name_snake_case': name_snake_case,\n+            'name_upper': name_upper,\n+            'spec_name': vendor_prefix + kebab_case(ctrl.name),\n+            'nick': ''.join([c.isupper() and (' ' + c) or c for c in ctrl.name]).strip(' '),\n+            'description': format_description(ctrl.description, indent=1),\n+            'gtype': gtype,\n+            'gtype_upper': gtype.upper(),\n+            'type': ctrl.element_type,\n+            'size': size,\n+        }\n+\n+        if ctrl.is_enum:\n+            enum_def.append(enum_values_start.substitute(info))\n+\n+            common_prefix = find_common_prefix([enum.name for enum in ctrl.enum_values])\n+\n+            for enum in ctrl.enum_values:\n+                values_info = {\n+                    'name': enum.name,\n+                    'vendor_namespace': vendor_namespace,\n+                    'description': format_description(enum.description, indent=1),\n+                    'nick': kebab_case(enum.name.removeprefix(common_prefix)),\n+                }\n+                enum_def.append(indent(enum_values_values.substitute(values_info), 1))\n+\n+            enum_def.append(enum_values_end.substitute(info))\n+\n+        spec = ''\n+        if ctrl.is_enum:\n+            spec = spec_enum.substitute({'enum': name_upper, 'default': 0, **info})\n+        elif gtype == 'boolean':\n+            spec = spec_bool.substitute({'default': 'false', **info})\n+        elif gtype == 'float':\n+            spec = spec_numeric.substitute({'min': '-G_MAXFLOAT', 'max': 'G_MAXFLOAT', 'default': 0, **info})\n+        elif gtype == 'int':\n+            spec = spec_numeric.substitute({'min': 'G_MININT', 'max': 'G_MAXINT', 'default': 0, **info})\n+        elif gtype == 'int64':\n+            spec = spec_numeric.substitute({'min': 'G_MININT64', 'max': 'G_MAXINT64', 'default': 0, **info})\n+        elif gtype == 'uchar':\n+            spec = spec_numeric.substitute({'min': '0', 'max': 'G_MAXUINT8', 'default': 0, **info})\n+\n+        if is_array:\n+            spec = spec_array.substitute({'element_spec': indent(spec, 1), **info})\n+\n+        install_properties.append(install_property.substitute({'spec': indent(spec, 2), **info}))\n+\n+        if is_array:\n+            get_properties.append(indent(get_array_property.substitute(info), 1))\n+            set_properties.append(indent(set_array_property.substitute(info), 1))\n+        else:\n+            get_properties.append(indent(get_property.substitute(info), 1))\n+            set_properties.append(indent(set_property.substitute(info), 1))\n+\n+    return {\n+        'enum_def': '\\n'.join(enum_def),\n+        'install_properties': '\\n'.join(install_properties),\n+        'get_properties': '\\n'.join(get_properties),\n+        'set_properties': '\\n'.join(set_properties),\n+    }\n+\n+\n+def fill_template(template, data):\n+    template = open(template, 'rb').read()\n+    template = template.decode('utf-8')\n+    template = string.Template(template)\n+    return template.substitute(data)\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+    args = parser.parse_args(argv[1:])\n+\n+    controls = []\n+    for input in args.input:\n+        data = open(input, 'rb').read()\n+        vendor = yaml.safe_load(data)['vendor']\n+        ctrls = yaml.safe_load(data)['controls']\n+        controls = controls + [Control(*ctrl.popitem(), vendor) for ctrl in ctrls]\n+\n+    data = generate_cpp(controls)\n+\n+    data = fill_template(args.template, data)\n+\n+    if args.output:\n+        output = open(args.output, 'wb')\n+        output.write(data.encode('utf-8'))\n+        output.close()\n+    else:\n+        sys.stdout.write(data)\n+\n+    return 0\n+\n+\n+if __name__ == '__main__':\n+    sys.exit(main(sys.argv))\ndiff --git a/utils/meson.build b/utils/meson.build\nindex 8e28ada7..89597f7f 100644\n--- a/utils/meson.build\n+++ b/utils/meson.build\n@@ -9,6 +9,7 @@ py_modules += ['yaml']\n gen_controls = files('gen-controls.py')\n gen_formats = files('gen-formats.py')\n gen_header = files('gen-header.sh')\n+gen_gst_controls = files('gen-gst-controls.py')\n \n ## Module signing\n gen_ipa_priv_key = files('gen-ipa-priv-key.sh')\n",
    "prefixes": [
        "2/3"
    ]
}