Patch Detail
Show a patch.
GET /api/1.1/patches/23488/?format=api
{ "id": 23488, "url": "https://patchwork.libcamera.org/api/1.1/patches/23488/?format=api", "web_url": "https://patchwork.libcamera.org/patch/23488/", "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": "<20250606164156.1442682-8-barnabas.pocze@ideasonboard.com>", "date": "2025-06-06T16:41:40", "name": "[RFC,v1,07/23] libcamera: Add `MetadataList`", "commit_ref": null, "pull_url": null, "state": "superseded", "archived": false, "hash": "4f0cdbbc17b2112ea22ad1b5dc1d7eee45524c6a", "submitter": { "id": 216, "url": "https://patchwork.libcamera.org/api/1.1/people/216/?format=api", "name": "Barnabás Pőcze", "email": "barnabas.pocze@ideasonboard.com" }, "delegate": null, "mbox": "https://patchwork.libcamera.org/patch/23488/mbox/", "series": [ { "id": 5210, "url": "https://patchwork.libcamera.org/api/1.1/series/5210/?format=api", "web_url": "https://patchwork.libcamera.org/project/libcamera/list/?series=5210", "date": "2025-06-06T16:41:33", "name": "libcamera: Add `MetadataList`", "version": 1, "mbox": "https://patchwork.libcamera.org/series/5210/mbox/" } ], "comments": "https://patchwork.libcamera.org/api/patches/23488/comments/", "check": "pending", "checks": "https://patchwork.libcamera.org/api/patches/23488/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 4F5B2C3292\n\tfor <parsemail@patchwork.libcamera.org>;\n\tFri, 6 Jun 2025 16:42:38 +0000 (UTC)", "from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 4486F68DB3;\n\tFri, 6 Jun 2025 18:42:37 +0200 (CEST)", "from perceval.ideasonboard.com (perceval.ideasonboard.com\n\t[213.167.242.64])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id EB89A68DC5\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tFri, 6 Jun 2025 18:42:16 +0200 (CEST)", "from pb-laptop.local (185.182.215.79.nat.pool.zt.hu\n\t[185.182.215.79])\n\tby perceval.ideasonboard.com (Postfix) with ESMTPSA id 664DB6DC\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tFri, 6 Jun 2025 18:42:12 +0200 (CEST)" ], "Authentication-Results": "lancelot.ideasonboard.com; dkim=pass (1024-bit key;\n\tunprotected) header.d=ideasonboard.com header.i=@ideasonboard.com\n\theader.b=\"BvRtlSyz\"; dkim-atps=neutral", "DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1749228132;\n\tbh=Reg56XeorKAGdSyu8t632fgzuDSyiABZV58MYlpP+jc=;\n\th=From:To:Subject:Date:In-Reply-To:References:From;\n\tb=BvRtlSyzJO7bFrYNqTLwBUx/H1u4aqU/3c2Jvg6kg2CJGR+MHW3rnTZLC3knL/0g+\n\tbf9ivYaMkK1rKBKFJ42PbAf4NisyxlJc3S445zI0n62yIBg8VKnny9KP/q4Rs6YQYi\n\tHWO0C2bXXvnuImUxmNTEDv3W38W0LCnM0aR4R8jQ=", "From": "=?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= <barnabas.pocze@ideasonboard.com>", "To": "libcamera-devel@lists.libcamera.org", "Subject": "[RFC PATCH v1 07/23] libcamera: Add `MetadataList`", "Date": "Fri, 6 Jun 2025 18:41:40 +0200", "Message-ID": "<20250606164156.1442682-8-barnabas.pocze@ideasonboard.com>", "X-Mailer": "git-send-email 2.49.0", "In-Reply-To": "<20250606164156.1442682-1-barnabas.pocze@ideasonboard.com>", "References": "<20250606164156.1442682-1-barnabas.pocze@ideasonboard.com>", "MIME-Version": "1.0", "Content-Type": "text/plain; charset=UTF-8", "Content-Transfer-Encoding": "8bit", "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": "Add a dedicated `MetadataList` type, whose purpose is to store the metadata\nreported by a camera for a given request. Previously, a `ControlList` was\nused for this purpose. The reason for introducing a separate type is to\nsimplify the access to the returned metadata during the entire lifetime\nof a request.\n\nSpecifically, for early metadata completion to be easily usable it should be\nguaranteed that any completed metadata item can be accessed and looked up\nat least until the associated requested is reused with `Request::reuse()`.\n\nHowever, when a metadata item is completed early, the pipeline handler\nmight still work on the request in the `CameraManager`'s private thread,\ntherefore there is an inherent synchronization issue when an application\naccesses early metadata.\n\nRestricting the user to only access the metadata items of a not yet completed\nrequest in the early metadata availability signal handler by ways of\ndocumenting or enforcing it at runtime could be an option, but it is not\ntoo convenient for the user.\n\nThe current `ControlList` implementation employs an `std::unordered_map`,\nso pointers remain stable when the container is modified, so an application\ncould keep accessing particular metadata items outside the signal handler,\nbut this fact is far from obvious, and the user would still not be able\nto make a copy of all metadata or do lookups based on the numeric ids or\nthe usual `libcamera::Control<>` objects, thus some type safety is lost.\n\nThe above also requires that each metadata item is only completed once for\na given request, but this does not appear to be serious limitation,\nand in fact, this restriction is enforced by `MetadataList`.\n\nThe introduced `MetadataList` supports single writer - multiple reader\nscenarios, and it can be set, looked-up, and copied in a wait-free fashion\nwithout introducing data races or other synchronization issues. This is\nachieved by requiring the possible set of metadata items to be known\n(such set is stored in a `MetadataListPlan` object). Based on the this\nplan, a single contiguous allocation is made to accommodate all potential\nmetadata items. Due to this single contiguous allocation that is not modified\nduring the lifetime of a `MetadataList` and atomic modifications, it is\npossible to easily gaurantee thread-safe set, lookup, and copy; assuming\nthere is only ever a single writer.\n\nSigned-off-by: Barnabás Pőcze <barnabas.pocze@ideasonboard.com>\n---\n include/libcamera/meson.build | 2 +\n include/libcamera/metadata_list.h | 619 +++++++++++++++++++++++++\n include/libcamera/metadata_list_plan.h | 109 +++++\n src/libcamera/meson.build | 1 +\n src/libcamera/metadata_list.cpp | 315 +++++++++++++\n test/controls/meson.build | 1 +\n test/controls/metadata_list.cpp | 171 +++++++\n 7 files changed, 1218 insertions(+)\n create mode 100644 include/libcamera/metadata_list.h\n create mode 100644 include/libcamera/metadata_list_plan.h\n create mode 100644 src/libcamera/metadata_list.cpp\n create mode 100644 test/controls/metadata_list.cpp", "diff": "diff --git a/include/libcamera/meson.build b/include/libcamera/meson.build\nindex 30ea76f94..410b548dd 100644\n--- a/include/libcamera/meson.build\n+++ b/include/libcamera/meson.build\n@@ -12,6 +12,8 @@ libcamera_public_headers = files([\n 'framebuffer_allocator.h',\n 'geometry.h',\n 'logging.h',\n+ 'metadata_list.h',\n+ 'metadata_list_plan.h',\n 'orientation.h',\n 'pixel_format.h',\n 'request.h',\ndiff --git a/include/libcamera/metadata_list.h b/include/libcamera/metadata_list.h\nnew file mode 100644\nindex 000000000..7514bd2ad\n--- /dev/null\n+++ b/include/libcamera/metadata_list.h\n@@ -0,0 +1,619 @@\n+/* SPDX-License-Identifier: LGPL-2.1-or-later */\n+/*\n+ * Copyright (C) 2025, Ideas On Board Oy\n+ *\n+ * Metadata list\n+ */\n+\n+#pragma once\n+\n+#include <algorithm>\n+#include <atomic>\n+#include <cassert>\n+#include <cstdint>\n+#include <cstring>\n+#include <new>\n+#include <optional>\n+#include <type_traits>\n+\n+#include <libcamera/base/details/align.h>\n+#include <libcamera/base/details/cxx20.h>\n+#include <libcamera/base/span.h>\n+\n+#include <libcamera/controls.h>\n+#include <libcamera/metadata_list_plan.h>\n+\n+// TODO: want this?\n+#if __has_include(<sanitizer/asan_interface.h>)\n+#if __SANITIZE_ADDRESS__ /* gcc */\n+#include <sanitizer/asan_interface.h>\n+#define HAS_ASAN 1\n+#elif defined(__has_feature)\n+#if __has_feature(address_sanitizer) /* clang */\n+#include <sanitizer/asan_interface.h>\n+#define HAS_ASAN 1\n+#endif\n+#endif\n+#endif\n+\n+namespace libcamera {\n+\n+class MetadataList\n+{\n+private:\n+\tstruct ValueParams {\n+\t\tControlType type;\n+\t\tbool isArray;\n+\t\tstd::uint32_t numElements;\n+\t};\n+\n+\tstruct Entry {\n+\t\tconst std::uint32_t tag;\n+\t\tconst std::uint32_t capacity;\n+\t\tconst std::uint32_t alignment;\n+\t\tconst ControlType type;\n+\t\tbool isArray;\n+\n+\t\tstatic constexpr std::uint32_t invalidOffset = -1;\n+\t\t/*\n+\t\t* Offset from the beginning of the allocation, and\n+\t\t* and _not_ relative to `contentOffset_`.\n+\t\t*/\n+\t\tstd::atomic_uint32_t headerOffset = invalidOffset;\n+\n+\t\t[[nodiscard]] std::optional<std::uint32_t> hasValue() const\n+\t\t{\n+\t\t\tauto offset = headerOffset.load(std::memory_order_relaxed);\n+\t\t\tif (offset == invalidOffset)\n+\t\t\t\treturn {};\n+\n+\t\t\treturn offset;\n+\t\t}\n+\n+\t\t[[nodiscard]] std::optional<std::uint32_t> acquireData() const\n+\t\t{\n+\t\t\tauto offset = hasValue();\n+\t\t\tif (offset) {\n+\t\t\t\t/* sync with release-store on `headerOffset` in `MetadataList::set()` */\n+\t\t\t\tstd::atomic_thread_fence(std::memory_order_acquire);\n+\t\t\t}\n+\n+\t\t\treturn offset;\n+\t\t}\n+\t};\n+\n+\tstruct ValueHeader {\n+\t\tstd::uint32_t tag;\n+\t\tstd::uint32_t size;\n+\t\tstd::uint32_t alignment;\n+\t\tValueParams params;\n+\t};\n+\n+\tstruct State {\n+\t\tstd::uint32_t count;\n+\t\tstd::uint32_t fill;\n+\t};\n+\n+public:\n+\texplicit MetadataList(const MetadataListPlan &plan)\n+\t\t: capacity_(plan.size()),\n+\t\t contentOffset_(MetadataList::contentOffset(capacity_)),\n+\t\t alloc_(contentOffset_)\n+\t{\n+\t\tfor (const auto &[tag, e] : plan) {\n+\t\t\tassert(details::cxx20::has_single_bit(e.alignment));\n+\n+\t\t\talloc_ += sizeof(ValueHeader);\n+\t\t\talloc_ += e.alignment - 1; // XXX: this is the maximum\n+\t\t\talloc_ += e.size;\n+\t\t\talloc_ += alignof(ValueHeader) - 1; // XXX: this is the maximum\n+\t\t}\n+\n+\t\tp_ = static_cast<std::byte *>(::operator new(alloc_));\n+\n+\t\tauto *entries = reinterpret_cast<Entry *>(p_ + entriesOffset());\n+\t\tauto it = plan.begin();\n+\n+\t\tfor (std::size_t i = 0; i < capacity_; i++, ++it) {\n+\t\t\tconst auto &[tag, e] = *it;\n+\n+\t\t\tnew (&entries[i]) Entry{\n+\t\t\t\t.tag = tag,\n+\t\t\t\t.capacity = e.size,\n+\t\t\t\t.alignment = e.alignment,\n+\t\t\t\t.type = e.type,\n+\t\t\t\t.isArray = e.isArray,\n+\t\t\t};\n+\t\t}\n+\n+#if HAS_ASAN\n+\t\t::__sanitizer_annotate_contiguous_container(\n+\t\t\tp_ + contentOffset_, p_ + alloc_,\n+\t\t\tp_ + alloc_, p_ + contentOffset_\n+\t\t);\n+#endif\n+\t}\n+\n+\tMetadataList(const MetadataList &other)\n+\t\t: capacity_(other.capacity_),\n+\t\t contentOffset_(other.contentOffset_),\n+\t\t alloc_(other.alloc_),\n+\t\t p_(static_cast<std::byte *>(::operator new(alloc_)))\n+\t{\n+\t\tauto *entries = reinterpret_cast<Entry *>(p_ + entriesOffset());\n+\t\tconst auto otherEntries = other.entries();\n+\n+\t\tfor (std::size_t i = 0; i < capacity_; i++) {\n+\t\t\tauto *e = new (&entries[i]) Entry{\n+\t\t\t\t.tag = otherEntries[i].tag,\n+\t\t\t\t.capacity = otherEntries[i].capacity,\n+\t\t\t\t.alignment = otherEntries[i].alignment,\n+\t\t\t\t.type = otherEntries[i].type,\n+\t\t\t\t.isArray = otherEntries[i].isArray,\n+\t\t\t};\n+\n+\t\t\tauto v = other.data_of(otherEntries[i]);\n+\t\t\tif (!v)\n+\t\t\t\tcontinue;\n+\n+\t\t\t[[maybe_unused]] auto r = set(*e, v);\n+\t\t\tassert(r == SetError());\n+\t\t}\n+\n+#if HAS_ASAN\n+\t\t::__sanitizer_annotate_contiguous_container(\n+\t\t\tp_ + contentOffset_, p_ + alloc_,\n+\t\t\tp_ + alloc_, p_ + contentOffset_\n+\t\t);\n+#endif\n+\t}\n+\n+\tMetadataList(MetadataList &&) = delete;\n+\n+\tMetadataList &operator=(const MetadataList &) = delete;\n+\tMetadataList &operator=(MetadataList &&) = delete;\n+\n+\t~MetadataList()\n+\t{\n+#if HAS_ASAN\n+\t\t/*\n+\t\t * The documentation says the range apparently has to be\n+\t\t * restored to its initial state before it is deallocated.\n+\t\t */\n+\t\t::__sanitizer_annotate_contiguous_container(\n+\t\t\tp_ + contentOffset_, p_ + alloc_,\n+\t\t\tp_ + contentOffset_ + state_.load(std::memory_order_relaxed).fill, p_ + alloc_\n+\t\t);\n+#endif\n+\n+\t\t::operator delete(p_, alloc_);\n+\t}\n+\n+\t// TODO: want these?\n+\t[[nodiscard]] std::size_t size() const { return state_.load(std::memory_order_relaxed).count; }\n+\t[[nodiscard]] bool empty() const { return size() == 0; }\n+\n+\tenum class SetError {\n+\t\tUnknownTag = 1,\n+\t\tAlreadySet,\n+\t\tDataTooLarge,\n+\t\tTypeMismatch,\n+\t};\n+\n+\t[[nodiscard]] SetError set(std::uint32_t tag, ControlValueView v)\n+\t{\n+\t\tauto *e = find(tag);\n+\t\tif (!e)\n+\t\t\treturn SetError::UnknownTag;\n+\n+\t\treturn set(*e, v);\n+\t}\n+\n+\ttemplate<typename T>\n+\t/* TODO: [[nodiscard]] */ SetError set(const Control<T> &ctrl, const details::cxx20::type_identity_t<T> &value)\n+\t{\n+\t\tusing TypeInfo = libcamera::details::control_type<T>;\n+\n+\t\tif constexpr (TypeInfo::size > 0) {\n+\t\t\tstatic_assert(std::is_trivially_copyable_v<typename T::value_type>);\n+\n+\t\t\treturn set(ctrl.id(), {\n+\t\t\t\tTypeInfo::value,\n+\t\t\t\ttrue,\n+\t\t\t\tvalue.size(),\n+\t\t\t\treinterpret_cast<const std::byte *>(value.data()),\n+\t\t\t});\n+\t\t} else {\n+\t\t\tstatic_assert(std::is_trivially_copyable_v<T>);\n+\n+\t\t\treturn set(ctrl.id(), {\n+\t\t\t\tTypeInfo::value,\n+\t\t\t\tfalse,\n+\t\t\t\t1,\n+\t\t\t\treinterpret_cast<const std::byte *>(&value),\n+\t\t\t});\n+\t\t}\n+\t}\n+\n+\ttemplate<typename T>\n+\t[[nodiscard]] decltype(auto) get(const Control<T> &ctrl) const\n+\t{\n+\t\tControlValueView v = get(ctrl.id());\n+\n+\t\treturn v ? std::optional(v.get<T>()) : std::nullopt;\n+\t}\n+\n+\t// TODO: operator ControlListView() const ?\n+\t// TODO: explicit operator ControlList() const ?\n+\n+\t[[nodiscard]] ControlValueView get(std::uint32_t tag) const\n+\t{\n+\t\tconst auto *e = find(tag);\n+\t\tif (!e)\n+\t\t\treturn {};\n+\n+\t\treturn data_of(*e);\n+\t}\n+\n+\tvoid clear()\n+\t{\n+\t\tfor (auto &e : entries())\n+\t\t\te.headerOffset.store(Entry::invalidOffset, std::memory_order_relaxed);\n+\n+\t\t[[maybe_unused]] auto s = state_.exchange({}, std::memory_order_relaxed);\n+\n+#if HAS_ASAN\n+\t\t::__sanitizer_annotate_contiguous_container(\n+\t\t\tp_ + contentOffset_, p_ + alloc_,\n+\t\t\tp_ + contentOffset_ + s.fill, p_ + contentOffset_\n+\t\t);\n+#endif\n+\t}\n+\n+\tclass iterator\n+\t{\n+\tpublic:\n+\t\tusing difference_type = std::ptrdiff_t;\n+\t\tusing value_type = std::pair<std::uint32_t, ControlValueView>;\n+\t\tusing pointer = void;\n+\t\tusing reference = value_type;\n+\t\tusing iterator_category = std::forward_iterator_tag;\n+\n+\t\titerator() = default;\n+\n+\t\titerator& operator++()\n+\t\t{\n+\t\t\tconst auto &h = header();\n+\n+\t\t\tp_ += sizeof(h);\n+\t\t\tp_ = details::align::up(p_, h.alignment);\n+\t\t\tp_ += h.size;\n+\t\t\tp_ = details::align::up(p_, alignof(decltype(h)));\n+\n+\t\t\treturn *this;\n+\t\t}\n+\n+\t\titerator operator++(int)\n+\t\t{\n+\t\t\tauto copy = *this;\n+\t\t\t++*this;\n+\t\t\treturn copy;\n+\t\t}\n+\n+\t\t[[nodiscard]] reference operator*() const\n+\t\t{\n+\t\t\tconst auto &h = header();\n+\t\t\tconst auto *data = details::align::up(p_ + sizeof(h), h.alignment);\n+\n+\t\t\treturn { h.tag, { h.params.type, h.params.isArray, h.params.numElements, data } };\n+\t\t}\n+\n+\t\t[[nodiscard]] bool operator==(const iterator &other) const\n+\t\t{\n+\t\t\treturn p_ == other.p_;\n+\t\t}\n+\n+\t\t[[nodiscard]] bool operator!=(const iterator &other) const\n+\t\t{\n+\t\t\treturn !(*this == other);\n+\t\t}\n+\n+\tprivate:\n+\t\titerator(const std::byte *p)\n+\t\t\t: p_(p)\n+\t\t{\n+\t\t}\n+\n+\t\t[[nodiscard]] const ValueHeader &header() const\n+\t\t{\n+\t\t\treturn *reinterpret_cast<const ValueHeader *>(p_);\n+\t\t}\n+\n+\t\tfriend MetadataList;\n+\n+\t\tconst std::byte *p_ = nullptr;\n+\t};\n+\n+\t[[nodiscard]] iterator begin() const\n+\t{\n+\t\treturn { p_ + contentOffset_ };\n+\t}\n+\n+\t[[nodiscard]] iterator end() const\n+\t{\n+\t\treturn { p_ + contentOffset_ + state_.load(std::memory_order_acquire).fill };\n+\t}\n+\n+\tclass Diff\n+\t{\n+\tpublic:\n+\t\t// TODO: want these?\n+\t\t[[nodiscard]] explicit operator bool() const { return !empty(); }\n+\t\t[[nodiscard]] bool empty() const { return start_ == stop_; }\n+\t\t[[nodiscard]] std::size_t size() const { return changed_; }\n+\t\t[[nodiscard]] const MetadataList &list() const { return *l_; }\n+\n+\t\t[[nodiscard]] ControlValueView get(std::uint32_t tag) const\n+\t\t{\n+\t\t\tconst auto *e = l_->find(tag);\n+\t\t\tif (!e)\n+\t\t\t\treturn {};\n+\n+\t\t\tauto o = e->acquireData();\n+\t\t\tif (!o)\n+\t\t\t\treturn {};\n+\n+\t\t\tif (!(start_ <= *o && *o < stop_))\n+\t\t\t\treturn {};\n+\n+\t\t\treturn l_->data_of(*o);\n+\t\t}\n+\n+\t\ttemplate<typename T>\n+\t\t[[nodiscard]] decltype(auto) get(const Control<T> &ctrl) const\n+\t\t{\n+\t\t\tControlValueView v = get(ctrl.id());\n+\n+\t\t\treturn v ? std::optional(v.get<T>()) : std::nullopt;\n+\t\t}\n+\n+\t\t[[nodiscard]] iterator begin() const\n+\t\t{\n+\t\t\treturn { l_->p_ + start_ };\n+\t\t}\n+\n+\t\t[[nodiscard]] iterator end() const\n+\t\t{\n+\t\t\treturn { l_->p_ + stop_ };\n+\t\t}\n+\n+\tprivate:\n+\t\tDiff(const MetadataList &l, std::size_t changed, std::size_t oldFill, std::size_t newFill)\n+\t\t\t: l_(&l),\n+\t\t\t changed_(changed),\n+\t\t\t start_(l.contentOffset_ + oldFill),\n+\t\t\t stop_(l.contentOffset_ + newFill)\n+\t\t{\n+\t\t}\n+\n+\t\tfriend MetadataList;\n+\t\tfriend struct Checkpoint;\n+\n+\t\tconst MetadataList *l_ = nullptr;\n+\t\tstd::size_t changed_;\n+\t\tstd::size_t start_;\n+\t\tstd::size_t stop_;\n+\t};\n+\n+\tDiff merge(const MetadataList &other)\n+\t{\n+\t\tconst auto entries = this->entries();\n+\t\tconst auto otherEntries = other.entries();\n+\t\tconst auto c = checkpoint();\n+\n+\t\tfor (std::size_t i = 0, j = 0; i < entries.size() && j < otherEntries.size(); ) {\n+\t\t\tif (entries[i].tag < otherEntries[j].tag) {\n+\t\t\t\ti += 1;\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\tif (entries[i].tag > otherEntries[j].tag) {\n+\t\t\t\tj += 1;\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\tassert(entries[i].alignment >= otherEntries[j].alignment);\n+\t\t\tassert(entries[i].capacity >= otherEntries[j].capacity);\n+\n+\t\t\tif (!entries[i].hasValue()) {\n+\t\t\t\tauto v = other.data_of(otherEntries[j]);\n+\t\t\t\tif (v) {\n+\t\t\t\t\t[[maybe_unused]] auto r = set(entries[i], v);\n+\t\t\t\t\tassert(r == SetError());\n+\t\t\t\t}\n+\t\t\t}\n+\n+\t\t\ti += 1;\n+\t\t\tj += 1;\n+\t\t}\n+\n+\t\treturn c.diffSince();\n+\t}\n+\n+\tDiff merge(const ControlList &other)\n+\t{\n+\t\t// TODO: check id map of `other`?\n+\n+\t\tconst auto c = checkpoint();\n+\n+\t\tfor (auto &&[tag, value] : other) {\n+\t\t\tauto *e = find(tag);\n+\t\t\tif (e) {\n+\t\t\t\t[[maybe_unused]] auto r = set(*e, value);\n+\t\t\t\tassert(r == SetError() || r == SetError::AlreadySet); // TODO: ?\n+\t\t\t}\n+\t\t}\n+\n+\t\treturn c.diffSince();\n+\t}\n+\n+\tclass Checkpoint\n+\t{\n+\tpublic:\n+\t\t// TODO: want this?\n+\t\t[[nodiscard]] const MetadataList &list() const { return *l_; }\n+\n+\t\t[[nodiscard]] Diff diffSince() const\n+\t\t{\n+\t\t\t/* sync with release-store on `state_` in `set()` */\n+\t\t\tconst auto curr = l_->state_.load(std::memory_order_acquire);\n+\n+\t\t\tassert(s_.count <= curr.count);\n+\t\t\tassert(s_.fill <= curr.fill);\n+\n+\t\t\treturn {\n+\t\t\t\t*l_,\n+\t\t\t\tcurr.count - s_.count,\n+\t\t\t\ts_.fill,\n+\t\t\t\tcurr.fill,\n+\t\t\t};\n+\t\t}\n+\n+\tprivate:\n+\t\tCheckpoint(const MetadataList &l)\n+\t\t\t: l_(&l),\n+\t\t\t s_(l.state_.load(std::memory_order_relaxed))\n+\t\t{\n+\t\t}\n+\n+\t\tfriend MetadataList;\n+\n+\t\tconst MetadataList *l_ = nullptr;\n+\t\tState s_ = {};\n+\t};\n+\n+\t[[nodiscard]] Checkpoint checkpoint() const\n+\t{\n+\t\treturn { *this };\n+\t}\n+\n+private:\n+\t[[nodiscard]] static constexpr std::size_t entriesOffset()\n+\t{\n+\t\treturn 0;\n+\t}\n+\n+\t[[nodiscard]] static constexpr std::size_t contentOffset(std::size_t entries)\n+\t{\n+\t\treturn details::align::up(entriesOffset() + entries * sizeof(Entry), alignof(ValueHeader));\n+\t}\n+\n+\t[[nodiscard]] Span<Entry> entries() const\n+\t{\n+\t\treturn { reinterpret_cast<Entry *>(p_ + entriesOffset()), capacity_ };\n+\t}\n+\n+\t[[nodiscard]] Entry *find(std::uint32_t tag) const\n+\t{\n+\t\tconst auto entries = this->entries();\n+\t\tauto it = std::lower_bound(entries.begin(), entries.end(), tag, [](const auto &e, const auto &t) {\n+\t\t\treturn e.tag < t;\n+\t\t});\n+\n+\t\tif (it == entries.end() || it->tag != tag)\n+\t\t\treturn nullptr;\n+\n+\t\treturn &*it;\n+\t}\n+\n+\t[[nodiscard]] ControlValueView data_of(const Entry &e) const\n+\t{\n+\t\tconst auto o = e.acquireData();\n+\t\treturn o ? data_of(*o) : ControlValueView{ };\n+\t}\n+\n+\t[[nodiscard]] ControlValueView data_of(std::size_t headerOffset) const\n+\t{\n+\t\tassert(headerOffset <= alloc_ - sizeof(ValueHeader));\n+\t\tassert(details::align::is(p_ + headerOffset, alignof(ValueHeader)));\n+\n+\t\tconst auto *vh = reinterpret_cast<const ValueHeader *>(p_ + headerOffset);\n+\t\tconst auto *p = reinterpret_cast<const std::byte *>(vh) + sizeof(*vh);\n+\t\tstd::size_t avail = p_ + alloc_ - p;\n+\n+\t\tconst auto *data = details::align::up(vh->size, vh->alignment, p, &avail);\n+\t\tassert(data);\n+\n+\t\treturn { vh->params.type, vh->params.isArray, vh->params.numElements, data };\n+\t}\n+\n+\t[[nodiscard]] SetError set(Entry &e, ControlValueView v)\n+\t{\n+\t\tif (e.hasValue())\n+\t\t\treturn SetError::AlreadySet;\n+\t\tif (e.type != v.type() || e.isArray != v.isArray())\n+\t\t\treturn SetError::TypeMismatch;\n+\n+\t\tconst auto src = v.data();\n+\t\tif (e.isArray) {\n+\t\t\tif (src.size_bytes() > e.capacity)\n+\t\t\t\treturn SetError::DataTooLarge;\n+\t\t} else {\n+\t\t\tassert(src.size_bytes() == e.capacity);\n+\t\t}\n+\n+\t\tauto s = state_.load(std::memory_order_relaxed);\n+\t\tstd::byte *oldEnd = p_ + contentOffset_ + s.fill;\n+\t\tstd::byte *p = oldEnd;\n+\n+\t\tauto *headerPtr = details::align::up<ValueHeader>(p);\n+\t\tauto *dataPtr = details::align::up(src.size_bytes(), e.alignment, p);\n+\t\tdetails::align::up(0, alignof(ValueHeader), p);\n+\n+#if HAS_ASAN\n+\t\t::__sanitizer_annotate_contiguous_container(\n+\t\t\tp_ + contentOffset_, p_ + alloc_,\n+\t\t\toldEnd, p\n+\t\t);\n+#endif\n+\n+\t\tnew (headerPtr) ValueHeader{\n+\t\t\t.tag = e.tag,\n+\t\t\t.size = std::uint32_t(src.size_bytes()),\n+\t\t\t.alignment = e.alignment,\n+\t\t\t.params = {\n+\t\t\t\t.type = v.type(),\n+\t\t\t\t.isArray = v.isArray(),\n+\t\t\t\t.numElements = std::uint32_t(v.numElements()),\n+\t\t\t},\n+\t\t};\n+\t\tstd::memcpy(dataPtr, src.data(), src.size_bytes());\n+\t\te.headerOffset.store(reinterpret_cast<std::byte *>(headerPtr) - p_, std::memory_order_release);\n+\n+\t\ts.fill += p - oldEnd;\n+\t\ts.count += 1;\n+\n+\t\tstate_.store(s, std::memory_order_release);\n+\n+\t\treturn {};\n+\t}\n+\n+\tstd::size_t capacity_ = 0;\n+\tstd::size_t contentOffset_ = -1;\n+\tstd::size_t alloc_ = 0;\n+\tstd::atomic<State> state_ = State{};\n+\tstd::byte *p_ = nullptr;\n+\t// TODO: ControlIdMap in any way shape or form?\n+\n+\t/*\n+\t * If this is problematic on a 32-bit architecture, then\n+\t * `count` can be stored in a separate atomic variable\n+\t * but then `Diff::changed_` must be removed since the fill\n+\t * level and item count cannot be retrieved atomically.\n+\t */\n+\tstatic_assert(decltype(state_)::is_always_lock_free);\n+};\n+\n+} /* namespace libcamera */\n+\n+#undef HAS_ASAN\ndiff --git a/include/libcamera/metadata_list_plan.h b/include/libcamera/metadata_list_plan.h\nnew file mode 100644\nindex 000000000..9ad4ae87e\n--- /dev/null\n+++ b/include/libcamera/metadata_list_plan.h\n@@ -0,0 +1,109 @@\n+/* SPDX-License-Identifier: LGPL-2.1-or-later */\n+/*\n+ * Copyright (C) 2025, Ideas On Board Oy\n+ */\n+\n+#pragma once\n+\n+#include <cassert>\n+#include <cstddef>\n+#include <cstdint>\n+#include <limits>\n+#include <map>\n+#include <type_traits>\n+\n+#include <libcamera/base/details/cxx20.h>\n+\n+#include <libcamera/controls.h>\n+\n+namespace libcamera {\n+\n+class MetadataListPlan\n+{\n+public:\n+\t[[nodiscard]] bool empty() const { return items_.empty(); }\n+\t[[nodiscard]] std::size_t size() const { return items_.size(); }\n+\t[[nodiscard]] decltype(auto) begin() const { return items_.begin(); }\n+\t[[nodiscard]] decltype(auto) end() const { return items_.end(); }\n+\tvoid clear() { items_.clear(); }\n+\n+\ttemplate<\n+\t\ttypename T,\n+\t\tstd::enable_if_t<libcamera::details::control_type<T>::size != libcamera::dynamic_extent> * = nullptr\n+\t>\n+\tdecltype(auto) add(const Control<T> &ctrl)\n+\t{\n+\t\tif constexpr (libcamera::details::control_type<T>::size > 0) {\n+\t\t\tstatic_assert(libcamera::details::control_type<T>::size != libcamera::dynamic_extent);\n+\n+\t\t\treturn add<typename T::value_type>(\n+\t\t\t\tctrl.id(),\n+\t\t\t\tlibcamera::details::control_type<T>::size,\n+\t\t\t\ttrue\n+\t\t\t);\n+\t\t} else {\n+\t\t\treturn add<T>(ctrl.id(), 1, false);\n+\t\t}\n+\t}\n+\n+\ttemplate<\n+\t\ttypename T,\n+\t\tstd::enable_if_t<libcamera::details::control_type<T>::size == libcamera::dynamic_extent> * = nullptr\n+\t>\n+\tdecltype(auto) add(const Control<T> &ctrl, std::size_t count)\n+\t{\n+\t\treturn add<typename T::value_type>(ctrl.id(), count, true);\n+\t}\n+\n+#ifndef __DOXYGEN__\n+\tMetadataListPlan &add(std::uint32_t tag,\n+\t\t\t std::size_t size, std::size_t count, std::size_t alignment,\n+\t\t\t ControlType type, bool isArray)\n+\t{\n+\t\tassert(size > 0 && size <= std::numeric_limits<std::uint32_t>::max());\n+\t\tassert(count <= std::numeric_limits<std::uint32_t>::max() / size);\n+\t\tassert(alignment <= std::numeric_limits<std::uint32_t>::max());\n+\t\tassert(details::cxx20::has_single_bit(alignment));\n+\t\tassert(isArray || count == 1);\n+\n+\t\titems_.insert_or_assign(tag, Entry{\n+\t\t\t.size = std::uint32_t(size * count),\n+\t\t\t.alignment = std::uint32_t(alignment),\n+\t\t\t.type = type,\n+\t\t\t.isArray = isArray,\n+\t\t});\n+\n+\t\treturn *this;\n+\t}\n+#endif\n+\n+\tbool remove(std::uint32_t tag)\n+\t{\n+\t\treturn items_.erase(tag);\n+\t}\n+\n+\tbool remove(const ControlId &ctrl)\n+\t{\n+\t\treturn remove(ctrl.id());\n+\t}\n+\n+private:\n+\tstruct Entry {\n+\t\tstd::uint32_t size;\n+\t\tstd::uint32_t alignment; // TODO: is this necessary?\n+\t\tControlType type;\n+\t\tbool isArray;\n+\t};\n+\n+\tstd::map<std::uint32_t, Entry> items_;\n+\n+\ttemplate<typename T>\n+\tdecltype(auto) add(std::uint32_t tag, std::size_t count, bool isArray)\n+\t{\n+\t\tstatic_assert(std::is_trivially_copyable_v<T>);\n+\n+\t\treturn add(tag, sizeof(T), count, alignof(T), details::control_type<T>::value, isArray);\n+\t}\n+};\n+\n+} /* namespace libcamera */\ndiff --git a/src/libcamera/meson.build b/src/libcamera/meson.build\nindex 202db1efe..d7a850907 100644\n--- a/src/libcamera/meson.build\n+++ b/src/libcamera/meson.build\n@@ -9,6 +9,7 @@ libcamera_public_sources = files([\n 'framebuffer.cpp',\n 'framebuffer_allocator.cpp',\n 'geometry.cpp',\n+ 'metadata_list.cpp',\n 'orientation.cpp',\n 'pixel_format.cpp',\n 'request.cpp',\ndiff --git a/src/libcamera/metadata_list.cpp b/src/libcamera/metadata_list.cpp\nnew file mode 100644\nindex 000000000..ccfe37318\n--- /dev/null\n+++ b/src/libcamera/metadata_list.cpp\n@@ -0,0 +1,315 @@\n+/* SPDX-License-Identifier: LGPL-2.1-or-later */\n+/*\n+ * Copyright (C) 2025, Ideas On Board Oy\n+ */\n+\n+#include <libcamera/metadata_list.h>\n+\n+namespace libcamera {\n+\n+/**\n+ * \\class MetadataListPlan\n+ * \\brief Class to hold the possible set of metadata items for a \\ref MetadataList\n+ */\n+\n+/**\n+ * \\fn MetadataListPlan::begin() const\n+ * \\brief Retrieve the begin iterator\n+ */\n+\n+/**\n+ * \\fn MetadataListPlan::end() const\n+ * \\brief Retrieve the end iterator\n+ */\n+\n+/**\n+ * \\fn MetadataListPlan::size() const\n+ * \\brief Retrieve the number of controls\n+ */\n+\n+/**\n+ * \\fn MetadataListPlan::empty() const\n+ * \\brief Check if empty\n+ */\n+\n+/**\n+ * \\fn MetadataListPlan::clear()\n+ * \\brief Remove all controls\n+ */\n+\n+/**\n+ * \\fn MetadataListPlan::add(const Control<T> &ctrl)\n+ * \\brief Add a control to the metadata list plan\n+ */\n+\n+/**\n+ * \\fn MetadataListPlan::add(const Control<T> &ctrl, std::size_t count)\n+ * \\brief Add a dynamically-sized control to the metadata list plan\n+ * \\param[in] ctrl The control\n+ * \\param[in] count The maximum number of elements\n+ *\n+ * Add the dynamically-sized control \\a ctrl to the metadata list plan with a maximum\n+ * capacity of \\a count elements.\n+ */\n+\n+/**\n+ * \\fn MetadataListPlan::remove(std::uint32_t tag)\n+ * \\brief Remove the entry with given identifier from the plan\n+ */\n+\n+/**\n+ * \\fn MetadataListPlan::remove(const ControlId &ctrl)\n+ * \\brief Remove \\a ctrl from the plan\n+ */\n+\n+/**\n+ * \\class MetadataList\n+ * \\brief Class to hold metadata items\n+ */\n+\n+/**\n+ * \\fn MetadataList::MetadataList(const MetadataListPlan &plan)\n+ * \\brief Construct a metadata list according to \\a plan\n+ *\n+ * Construct a metadata list according to the provided \\a plan.\n+ */\n+\n+/**\n+ * \\fn MetadataList::MetadataList(const MetadataList &other)\n+ * \\brief Copy constructor\n+ * \\context This function is \\threadsafe wrt. \\a other.\n+ */\n+\n+/**\n+ * \\fn MetadataList::size() const\n+ * \\brief Retrieve the number of controls\n+ * \\context This function is \\threadsafe.\n+ * \\note If the list is being modified, the return value may be out of\n+ * date by the time the function returns\n+ */\n+\n+/**\n+ * \\fn MetadataList::empty() const\n+ * \\brief Check if empty\n+ * \\context This function is \\threadsafe.\n+ * \\note If the list is being modified, the return value may be out of\n+ * date by the time the function returns\n+ */\n+\n+/**\n+ * \\internal\n+ * \\fn MetadataList::clear()\n+ * \\brief Remove all items from the list\n+ * \\note This function in effect resets the list to its original state. As a consequence it invalidates - among others -\n+ * all iterators, Checkpoint, and Diff objects that are associated with the list. No readers must exist\n+ * when this function is called.\n+ */\n+\n+/**\n+ * \\fn MetadataList::begin() const\n+ * \\brief Retrieve begin iterator\n+ * \\context This function is \\threadsafe.\n+ */\n+\n+/**\n+ * \\fn MetadataList::end() const\n+ * \\brief Retrieve end iterator\n+ * \\context This function is \\threadsafe.\n+ */\n+\n+/**\n+ * \\fn MetadataList::get(const Control<T> &ctrl) const\n+ * \\brief Get the value of control \\a ctrl\n+ * \\return A std::optional<T> containing the control value, or std::nullopt if\n+ * the control \\a ctrl is not present in the list\n+ * \\context This function is \\threadsafe.\n+ */\n+\n+/**\n+ * \\fn MetadataList::get(std::uint32_t tag) const\n+ * \\brief Get the value of pertaining to the numeric identifier \\a tag\n+ * \\return A std::optional<T> containing the control value, or std::nullopt if\n+ * the control is not present in the list\n+ * \\context This function is \\threadsafe.\n+ */\n+\n+/**\n+ * \\fn MetadataList::set(const Control<T> &ctrl, const details::cxx20::type_identity_t<T> &value)\n+ * \\brief Set the value of control \\a ctrl to \\a value\n+ */\n+\n+/**\n+ * \\fn MetadataList::set(std::uint32_t tag, ControlValueView v)\n+ * \\brief Set the value of pertaining to the numeric identifier \\a tag to \\a v\n+ */\n+\n+/**\n+ * \\internal\n+ * \\fn MetadataList::merge(const MetadataList &other)\n+ * \\brief Add all missing items from \\a other\n+ *\n+ * Add all items from \\a other that are not present in \\a this. If an item\n+ * has a numeric identifier that was not present in the MetadataListPlan\n+ * used to construct \\a this, then the item is ignored.\n+ *\n+ * \\context This function is \\threadsafe wrt. \\a other.\n+ */\n+\n+/**\n+ * \\internal\n+ * \\fn MetadataList::merge(const ControlList &other)\n+ * \\copydoc MetadataList::merge(const MetadataList &other)\n+ */\n+\n+/**\n+ * \\enum MetadataList::SetError\n+ * \\brief TODO\n+ *\n+ * \\var MetadataList::SetError::UnknownTag\n+ * \\brief The tag is not supported by the metadata list\n+ * \\var MetadataList::SetError::AlreadySet\n+ * \\brief A value has already been added with the given tag\n+ * \\var MetadataList::SetError::DataTooLarge\n+ * \\brief The data is too large for the given tag\n+ * \\var MetadataList::SetError::TypeMismatch\n+ * \\brief The type of the value does not match the expected type\n+ */\n+\n+/**\n+ * \\internal\n+ * \\fn MetadataList::checkpoint() const\n+ * \\brief Create a checkpoint\n+ * \\context This function is \\threadsafe.\n+ */\n+\n+/**\n+ * \\class MetadataList::iterator\n+ * \\brief Iterator\n+ */\n+\n+/**\n+ * \\typedef MetadataList::iterator::difference_type\n+ * \\brief iterator's difference type\n+ */\n+\n+/**\n+ * \\typedef MetadataList::iterator::value_type\n+ * \\brief iterator's value type\n+ */\n+\n+/**\n+ * \\typedef MetadataList::iterator::pointer\n+ * \\brief iterator's pointer type\n+ */\n+\n+/**\n+ * \\typedef MetadataList::iterator::reference\n+ * \\brief iterator's reference type\n+ */\n+\n+/**\n+ * \\typedef MetadataList::iterator::iterator_category\n+ * \\brief iterator's category\n+ */\n+\n+/**\n+ * \\fn MetadataList::iterator::operator*()\n+ * \\brief Retrieve value at iterator\n+ * \\return A \\a ControlListView representing the value\n+ */\n+\n+/**\n+ * \\fn MetadataList::iterator::operator==(const iterator &other) const\n+ * \\brief Check if two iterators are equal\n+ */\n+\n+/**\n+ * \\fn MetadataList::iterator::operator!=(const iterator &other) const\n+ * \\brief Check if two iterators are not equal\n+ */\n+\n+/**\n+ * \\fn MetadataList::iterator::operator++(int)\n+ * \\brief Advance the iterator\n+ */\n+\n+/**\n+ * \\fn MetadataList::iterator::operator++()\n+ * \\brief Advance the iterator\n+ */\n+\n+/**\n+ * \\class MetadataList::Diff\n+ * \\brief Designates a set of consecutively added metadata items from a particular MetadataList\n+ * \\sa Camera::metadataAvailable\n+ * \\internal\n+ * \\sa MetadataList::Checkpoint::diffSince()\n+ * \\endinternal\n+ */\n+\n+/**\n+ * \\fn MetadataList::Diff::list() const\n+ * \\brief Retrieve the associated MetadataList\n+ */\n+\n+/**\n+ * \\fn MetadataList::Diff::size() const\n+ * \\brief Retrieve the number of metadata items designated\n+ */\n+\n+/**\n+ * \\fn MetadataList::Diff::empty() const\n+ * \\brief Check if no metadata items are designated\n+ */\n+\n+/**\n+ * \\fn MetadataList::Diff::operator bool() const\n+ * \\copydoc MetadataList::Diff::empty() const\n+ */\n+\n+/**\n+ * \\fn MetadataList::Diff::get(const Control<T> &ctrl) const\n+ * \\copydoc MetadataList::get(const Control<T> &ctrl) const\n+ * \\note The value pertaining to \\a ctrl will only be returned if it is part of Diff,\n+ * meaning that even if \\a ctrl is part of the backing MetadataList, it will not\n+ * be returned if \\a ctrl is not in the set of controls designated by this Diff object.\n+ */\n+\n+/**\n+ * \\fn MetadataList::Diff::get(std::uint32_t tag) const\n+ * \\copydoc MetadataList::Diff::get(const Control<T>&ctrl) const\n+ */\n+\n+/**\n+ * \\fn MetadataList::Diff::begin() const\n+ * \\brief Retrieve the begin iterator\n+ */\n+\n+/**\n+ * \\fn MetadataList::Diff::end() const\n+ * \\brief Retrieve the end iterator\n+ */\n+\n+/**\n+ * \\internal\n+ * \\class MetadataList::Checkpoint\n+ * \\brief Designates a point in the stream of metadata items\n+ *\n+ * A Checkpoint object designates a point in the stream of metadata items in the associated\n+ * MetadataList. Its main use to be able to retrieve the set of metadata items that were\n+ * added to the list after the designated point using diffSince().\n+ */\n+\n+/**\n+ * \\internal\n+ * \\fn MetadataList::Checkpoint::list() const\n+ * \\brief Retrieve the associated \\ref MetadataList\n+ */\n+\n+/**\n+ * \\internal\n+ * \\fn MetadataList::Checkpoint::diffSince() const\n+ * \\brief Retrieve the set of metadata items added since the checkpoint was created\n+ */\n+\n+} /* namespace libcamera */\ndiff --git a/test/controls/meson.build b/test/controls/meson.build\nindex 763f8905e..b68a4fc53 100644\n--- a/test/controls/meson.build\n+++ b/test/controls/meson.build\n@@ -5,6 +5,7 @@ control_tests = [\n {'name': 'control_info_map', 'sources': ['control_info_map.cpp']},\n {'name': 'control_list', 'sources': ['control_list.cpp']},\n {'name': 'control_value', 'sources': ['control_value.cpp']},\n+ {'name': 'metadata_list', 'sources': ['metadata_list.cpp']},\n ]\n \n foreach test : control_tests\ndiff --git a/test/controls/metadata_list.cpp b/test/controls/metadata_list.cpp\nnew file mode 100644\nindex 000000000..e02b4e28e\n--- /dev/null\n+++ b/test/controls/metadata_list.cpp\n@@ -0,0 +1,171 @@\n+/* SPDX-License-Identifier: GPL-2.0-or-later */\n+/*\n+ * Copyright (C) 2025, Ideas On Board Oy\n+ *\n+ * MetadataList tests\n+ */\n+\n+#include <future>\n+#include <iostream>\n+#include <thread>\n+\n+#include <libcamera/control_ids.h>\n+#include <libcamera/metadata_list.h>\n+#include <libcamera/property_ids.h>\n+\n+#include \"test.h\"\n+\n+using namespace std;\n+using namespace libcamera;\n+\n+#define ASSERT(x) do { \\\n+\tif (!static_cast<bool>(x)) { \\\n+\t\tstd::cerr << '`' << #x << \"` failed\" << std::endl; \\\n+\t\treturn TestFail; \\\n+\t} \\\n+} while (false)\n+\n+class MetadataListTest : public Test\n+{\n+public:\n+\tMetadataListTest() = default;\n+\n+protected:\n+\tint run() override\n+\t{\n+\t\tMetadataListPlan mlp;\n+\t\tmlp.add(controls::ExposureTime);\n+\t\tmlp.add(controls::ExposureValue);\n+\t\tmlp.add(controls::ColourGains);\n+\t\tmlp.add(controls::AfWindows, 10);\n+\t\tmlp.add(controls::AeEnable);\n+\t\tmlp.add(controls::SensorTimestamp);\n+\n+\t\tMetadataList ml(mlp);\n+\n+\t\tstatic_assert(static_cast<unsigned int>(properties::LOCATION) == controls::AE_ENABLE);\n+\t\tASSERT(ml.set(properties::Location, properties::CameraLocationFront) == MetadataList::SetError::TypeMismatch);\n+\n+\t\tASSERT(ml.set(controls::AfWindows, std::array<Rectangle, 11>{}) == MetadataList::SetError::DataTooLarge);\n+\t\tASSERT(ml.set(controls::ColourTemperature, 123) == MetadataList::SetError::UnknownTag);\n+\n+\t\tauto f1 = std::async(std::launch::async, [&] {\n+\t\t\tusing namespace std::chrono_literals;\n+\n+\t\t\tstd::this_thread::sleep_for(500ms);\n+\t\t\tASSERT(ml.set(controls::ExposureTime, 0x1111) == MetadataList::SetError());\n+\n+\t\t\tstd::this_thread::sleep_for(500ms);\n+\t\t\tASSERT(ml.set(controls::ExposureValue, 1) == MetadataList::SetError());\n+\n+\t\t\tstd::this_thread::sleep_for(500ms);\n+\t\t\tASSERT(ml.set(controls::ColourGains, std::array{\n+\t\t\t\t123.f,\n+\t\t\t\t456.f\n+\t\t\t}) == MetadataList::SetError());\n+\n+\t\t\tstd::this_thread::sleep_for(500ms);\n+\t\t\tASSERT(ml.set(controls::AfWindows, std::array{\n+\t\t\t\tRectangle(),\n+\t\t\t\tRectangle(1, 2, 3, 4),\n+\t\t\t\tRectangle(0x1111, 0x2222, 0x3333, 0x4444),\n+\t\t\t}) == MetadataList::SetError());\n+\n+\t\t\treturn TestPass;\n+\t\t});\n+\n+\t\tauto f2 = std::async(std::launch::async, [&] {\n+\t\t\tfor (;;) {\n+\t\t\t\tconst auto x = ml.get(controls::ExposureTime);\n+\t\t\t\tconst auto y = ml.get(controls::ExposureValue);\n+\t\t\t\tconst auto z = ml.get(controls::ColourGains);\n+\t\t\t\tconst auto w = ml.get(controls::AfWindows);\n+\n+\t\t\t\tif (x)\n+\t\t\t\t\tASSERT(*x == 0x1111);\n+\n+\t\t\t\tif (y)\n+\t\t\t\t\tASSERT(*y == 1.0f);\n+\n+\t\t\t\tif (z) {\n+\t\t\t\t\tASSERT(z->size() == 2);\n+\t\t\t\t\tASSERT((*z)[0] == 123.f);\n+\t\t\t\t\tASSERT((*z)[1] == 456.f);\n+\t\t\t\t}\n+\n+\t\t\t\tif (w) {\n+\t\t\t\t\tASSERT(w->size() == 3);\n+\t\t\t\t\tASSERT((*w)[0].isNull());\n+\t\t\t\t\tASSERT((*w)[1] == Rectangle(1, 2, 3, 4));\n+\t\t\t\t\tASSERT((*w)[2] == Rectangle(0x1111, 0x2222, 0x3333, 0x4444));\n+\t\t\t\t}\n+\n+\t\t\t\tif (x && y && z && w)\n+\t\t\t\t\tbreak;\n+\t\t\t}\n+\n+\t\t\treturn TestPass;\n+\t\t});\n+\n+\t\tASSERT(f1.get() == TestPass);\n+\t\tASSERT(f2.get() == TestPass);\n+\n+\t\tASSERT(ml.set(controls::ExposureTime, 0x2222) == MetadataList::SetError::AlreadySet);\n+\t\tASSERT(ml.set(controls::ExposureValue, 2) == MetadataList::SetError::AlreadySet);\n+\n+\t\tASSERT(ml.get(controls::ExposureTime) == 0x1111);\n+\t\tASSERT(ml.get(controls::ExposureValue) == 1);\n+\n+\t\tfor (auto &&[tag, v] : ml)\n+\t\t\tstd::cout << \"[\" << tag << \"] -> \" << v << '\\n';\n+\n+\t\tstd::cout << std::endl;\n+\n+\t\tml.clear();\n+\t\tASSERT(ml.empty());\n+\t\tASSERT(ml.size() == 0);\n+\n+\t\tASSERT(ml.set(controls::ExposureTime, 0x2222) == MetadataList::SetError());\n+\t\tASSERT(ml.get(controls::ExposureTime) == 0x2222);\n+\n+\t\tauto c = ml.checkpoint();\n+\t\tASSERT(&c.list() == &ml);\n+\n+\t\tASSERT(ml.set(controls::ExposureValue, 2) == MetadataList::SetError());\n+\t\tASSERT(ml.set(controls::SensorTimestamp, 0x99999999) == MetadataList::SetError());\n+\n+\t\tauto d = c.diffSince();\n+\t\tASSERT(&d.list() == &ml);\n+\n+\t\tASSERT(ml.set(controls::ColourGains, std::array{ 1.f, 2.f }) == MetadataList::SetError());\n+\n+\t\tASSERT(d);\n+\t\tASSERT(!d.empty());\n+\t\tASSERT(d.size() == 2);\n+\t\tASSERT(!d.get(controls::ExposureTime));\n+\t\tASSERT(!d.get(controls::ColourGains));\n+\t\tASSERT(!d.get(controls::AfWindows));\n+\t\tASSERT(d.get(controls::ExposureValue) == 2);\n+\t\tASSERT(d.get(controls::SensorTimestamp) == 0x99999999);\n+\n+\t\tfor (auto &&[tag, v] : d)\n+\t\t\tstd::cout << \"[\" << tag << \"] -> \" << v << '\\n';\n+\n+\t\t/* Test if iterators work with algorithms. */\n+\t\tstd::ignore = std::find_if(d.begin(), d.end(), [](const auto &) {\n+\t\t\treturn false;\n+\t\t});\n+\n+#if 0\n+ {\n+ auto it = ml.begin();\n+ ml.clear();\n+ std::ignore = *it; /* Trigger ASAN. */\n+ }\n+#endif\n+\n+\t\treturn TestPass;\n+\t}\n+};\n+\n+TEST_REGISTER(MetadataListTest)\n", "prefixes": [ "RFC", "v1", "07/23" ] }