{"id":23875,"url":"https://patchwork.libcamera.org/api/patches/23875/?format=json","web_url":"https://patchwork.libcamera.org/patch/23875/","project":{"id":1,"url":"https://patchwork.libcamera.org/api/projects/1/?format=json","name":"libcamera","link_name":"libcamera","list_id":"libcamera_core","list_email":"libcamera-devel@lists.libcamera.org","web_url":"","scm_url":"","webscm_url":""},"msgid":"<20250721104622.1550908-8-barnabas.pocze@ideasonboard.com>","date":"2025-07-21T10:46:07","name":"[RFC,v2,07/22] libcamera: Add `MetadataList`","commit_ref":null,"pull_url":null,"state":"superseded","archived":false,"hash":"722932a98c2cb0adb65047fb92e6cb62f44cf461","submitter":{"id":216,"url":"https://patchwork.libcamera.org/api/people/216/?format=json","name":"Barnabás Pőcze","email":"barnabas.pocze@ideasonboard.com"},"delegate":null,"mbox":"https://patchwork.libcamera.org/patch/23875/mbox/","series":[{"id":5305,"url":"https://patchwork.libcamera.org/api/series/5305/?format=json","web_url":"https://patchwork.libcamera.org/project/libcamera/list/?series=5305","date":"2025-07-21T10:46:00","name":"libcamera: Add `MetadataList`","version":2,"mbox":"https://patchwork.libcamera.org/series/5305/mbox/"}],"comments":"https://patchwork.libcamera.org/api/patches/23875/comments/","check":"pending","checks":"https://patchwork.libcamera.org/api/patches/23875/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 A436FC3323\n\tfor <parsemail@patchwork.libcamera.org>;\n\tMon, 21 Jul 2025 10:46:43 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 0997068FE7;\n\tMon, 21 Jul 2025 12:46:43 +0200 (CEST)","from perceval.ideasonboard.com (perceval.ideasonboard.com\n\t[IPv6:2001:4b98:dc2:55:216:3eff:fef7:d647])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 11F7F68FE5\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tMon, 21 Jul 2025 12:46:31 +0200 (CEST)","from pb-laptop.local (185.221.140.39.nat.pool.zt.hu\n\t[185.221.140.39])\n\tby perceval.ideasonboard.com (Postfix) with ESMTPSA id 9080D7950\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tMon, 21 Jul 2025 12:45:52 +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=\"k/LEBd8O\"; dkim-atps=neutral","DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1753094754;\n\tbh=XNTRm/k0w5WTU6yoYtlaQ8Q+FeGl0rZuWc8wF6JU9q0=;\n\th=From:To:Subject:Date:In-Reply-To:References:From;\n\tb=k/LEBd8OlFU/pUMAIqUl2Q4xfyV+RtibTluoq2RKVT39WxOSgJFSynV8Gs6XeMeK+\n\tGaK5b6PGkebFbvKOrfMIFeBZtk1P1B/tBMs0tDEw4ROOoqanU2m3eej9rZD9Uq6yzC\n\t5cqGW6+0xXRxbRWG0c/Oc+JwV0f6HR8VSwK7I4nw=","From":"=?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= <barnabas.pocze@ideasonboard.com>","To":"libcamera-devel@lists.libcamera.org","Subject":"[RFC PATCH v2 07/22] libcamera: Add `MetadataList`","Date":"Mon, 21 Jul 2025 12:46:07 +0200","Message-ID":"<20250721104622.1550908-8-barnabas.pocze@ideasonboard.com>","X-Mailer":"git-send-email 2.50.1","In-Reply-To":"<20250721104622.1550908-1-barnabas.pocze@ideasonboard.com>","References":"<20250721104622.1550908-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---\nchanges in v2:\n  * remove multiple not strictly necessary functions\n---\n include/libcamera/meson.build          |   2 +\n include/libcamera/metadata_list.h      | 547 +++++++++++++++++++++++++\n include/libcamera/metadata_list_plan.h | 130 ++++++\n src/libcamera/meson.build              |   1 +\n src/libcamera/metadata_list.cpp        | 344 ++++++++++++++++\n test/controls/meson.build              |   1 +\n test/controls/metadata_list.cpp        | 170 ++++++++\n 7 files changed, 1195 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..7fe3dbbab\n--- /dev/null\n+++ b/include/libcamera/metadata_list.h\n@@ -0,0 +1,547 @@\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\talloc_ += sizeof(ValueHeader);\n+\t\t\talloc_ += e.alignment - 1; // XXX: this is the maximum\n+\t\t\talloc_ += e.size * e.numElements;\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 * e.numElements,\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 &) = delete;\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 state_.load(std::memory_order_relaxed).fill == 0; }\n+\n+\tenum class SetError {\n+\t\tUnknownTag = 1,\n+\t\tAlreadySet,\n+\t\tSizeMismatch,\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 ControlList &other)\n+\t{\n+\t\t// TODO: check id map of `other`?\n+\n+\t\tconst auto c = checkpoint();\n+\n+\t\tfor (const 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[[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::partition_point(entries.begin(), entries.end(), [&](const auto &e) {\n+\t\t\treturn e.tag < tag;\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::SizeMismatch;\n+\t\t} else {\n+\t\t\tif (src.size_bytes() != e.capacity)\n+\t\t\t\treturn SetError::SizeMismatch;\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..2ed35c54f\n--- /dev/null\n+++ b/include/libcamera/metadata_list_plan.h\n@@ -0,0 +1,130 @@\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+\tstruct Entry {\n+\t\tstd::uint32_t size;\n+\t\tstd::uint32_t alignment; // TODO: is this necessary?\n+\t\tstd::uint32_t numElements;\n+\t\tControlType type;\n+\t\tbool isArray;\n+\t};\n+\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) set(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 set<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 set<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) set(const Control<T> &ctrl, std::size_t numElements)\n+\t{\n+\t\treturn set<typename T::value_type>(ctrl.id(), numElements, true);\n+\t}\n+\n+\t[[nodiscard]] bool set(std::uint32_t tag,\n+\t\t\t       std::size_t size, std::size_t alignment,\n+\t\t\t       std::size_t numElements, ControlType type, bool isArray)\n+\t{\n+\t\tif (size == 0 || size > std::numeric_limits<std::uint32_t>::max())\n+\t\t\treturn false;\n+\t\tif (alignment > std::numeric_limits<std::uint32_t>::max())\n+\t\t\treturn false;\n+\t\tif (!details::cxx20::has_single_bit(alignment))\n+\t\t\treturn false;\n+\t\tif (numElements > std::numeric_limits<std::uint32_t>::max() / size)\n+\t\t\treturn false;\n+\t\tif (!isArray && numElements != 1)\n+\t\t\treturn false;\n+\n+\t\titems_.insert_or_assign(tag, Entry{\n+\t\t\t.size = std::uint32_t(size),\n+\t\t\t.alignment = std::uint32_t(alignment),\n+\t\t\t.numElements = std::uint32_t(numElements),\n+\t\t\t.type = type,\n+\t\t\t.isArray = isArray,\n+\t\t});\n+\n+\t\treturn true;\n+\t}\n+\n+\t[[nodiscard]] const Entry *get(std::uint32_t tag) const\n+\t{\n+\t\tauto it = items_.find(tag);\n+\t\tif (it == items_.end())\n+\t\t\treturn nullptr;\n+\n+\t\treturn &it->second;\n+\t}\n+\n+\t[[nodiscard]] const Entry *get(const ControlId &cid) const\n+\t{\n+\t\tconst auto *e = get(cid.id());\n+\t\tif (!e)\n+\t\t\treturn nullptr;\n+\n+\t\tif (e->type != cid.type() || e->isArray != cid.isArray())\n+\t\t\treturn nullptr;\n+\n+\t\treturn e;\n+\t}\n+\n+private:\n+\tstd::map<std::uint32_t, Entry> items_;\n+\n+\ttemplate<typename T>\n+\tdecltype(auto) set(std::uint32_t tag, std::size_t numElements, bool isArray)\n+\t{\n+\t\tstatic_assert(std::is_trivially_copyable_v<T>);\n+\n+\t\t[[maybe_unused]] bool ok = set(tag,\n+\t\t\t\t\t       sizeof(T), alignof(T),\n+\t\t\t\t\t       numElements, details::control_type<T>::value, isArray);\n+\t\tassert(ok);\n+\n+\t\treturn *this;\n+\t}\n+};\n+\n+} /* namespace libcamera */\ndiff --git a/src/libcamera/meson.build b/src/libcamera/meson.build\nindex de1eb99b2..8c5ce4503 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..ebefdfdad\n--- /dev/null\n+++ b/src/libcamera/metadata_list.cpp\n@@ -0,0 +1,344 @@\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 MetadataList\n+ */\n+\n+/**\n+ * \\class MetadataListPlan::Entry\n+ * \\brief Details of a metadata item\n+ */\n+\n+/**\n+ * \\internal\n+ * \\var MetadataListPlan::Entry::size\n+ * \\brief Number of bytes in a single element\n+ *\n+ * \\var MetadataListPlan::Entry::alignment\n+ * \\brief Required alignment of the elements\n+ * \\endinternal\n+ *\n+ * \\var MetadataListPlan::Entry::numElements\n+ * \\brief Number of elements in the value\n+ * \\sa ControlValueView::numElements()\n+ *\n+ * \\var MetadataListPlan::Entry::type\n+ * \\brief The type of the value\n+ * \\sa ControlValueView::type()\n+ *\n+ * \\var MetadataListPlan::Entry::isArray\n+ * \\brief Whether or not the value is array-like\n+ * \\sa ControlValueView::isArray()\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 entries\n+ */\n+\n+/**\n+ * \\fn MetadataListPlan::empty() const\n+ * \\brief Check if empty\n+ */\n+\n+/**\n+ * \\internal\n+ * \\fn MetadataListPlan::clear()\n+ * \\brief Remove all controls\n+ */\n+\n+/**\n+ * \\internal\n+ * \\fn MetadataListPlan::set(const Control<T> &ctrl)\n+ * \\brief Add an entry for the given control to the metadata list plan\n+ * \\param[in] ctrl The control\n+ */\n+\n+/**\n+ * \\internal\n+ * \\fn MetadataListPlan::set(const Control<T> &ctrl, std::size_t count)\n+ * \\brief Add an entry for the given 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+ * \\internal\n+ * \\fn MetadataListPlan::set(std::uint32_t tag,\n+ *\t\t\t     std::size_t size, std::size_t alignment,\n+ *\t\t\t     std::size_t count, ControlType type, bool isArray)\n+ * \\brief Add an entry to the metadata list plan\n+ * \\return \\a true if the entry has been added, or \\a false if the given parameters\n+ *         would result in an invalid entry\n+ *\n+ * This functions adds an entry with essentially arbitrary parameters, without deriving\n+ * them from a given ControlId instance. This is mainly used when deserializing.\n+ */\n+\n+/**\n+ * \\fn MetadataListPlan::get(std::uint32_t tag) const\n+ * \\brief Find the \\ref Entry \"entry\" with the given identifier\n+ */\n+\n+/**\n+ * \\fn MetadataListPlan::get(const ControlId &cid) const\n+ * \\brief Find the \\ref Entry \"entry\" for the given ControlId\n+ *\n+ * The \\ref Entry \"entry\" is only returned if ControlId::type() and ControlId::isArray()\n+ * of \\a cid matches Entry::type and Entry::isArray, respectively.\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::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+ * \\internal\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+ * \\internal\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 ControlList &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.\n+ */\n+\n+/**\n+ * \\internal\n+ * \\enum MetadataList::SetError\n+ * \\brief Error code returned by a set operation\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::SizeMismatch\n+ * \\brief The size of the data is not appropriate 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 any 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 lookup will fail if the metadata item is not designated by this Diff object,\n+ *       even if it is otherwise present in the backing MetadataList.\n+ */\n+\n+/**\n+ * \\fn MetadataList::Diff::get(std::uint32_t tag) const\n+ * \\copydoc MetadataList::get(std::uint32_t tag) const\n+ * \\note The lookup will fail if the metadata item is not designated by this Diff object,\n+ *       even if it is otherwise present in the backing MetadataList.\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::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..f0872acd9\n--- /dev/null\n+++ b/test/controls/metadata_list.cpp\n@@ -0,0 +1,170 @@\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.set(controls::ExposureTime);\n+\t\tmlp.set(controls::ExposureValue);\n+\t\tmlp.set(controls::ColourGains);\n+\t\tmlp.set(controls::AfWindows, 10);\n+\t\tmlp.set(controls::AeEnable);\n+\t\tmlp.set(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::SizeMismatch);\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+\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","v2","07/22"]}