[RFC PATCH v1 07/23] libcamera: Add `MetadataList`
Barnabás Pőcze
barnabas.pocze at ideasonboard.com
Fri Jun 6 18:41:40 CEST 2025
Add a dedicated `MetadataList` type, whose purpose is to store the metadata
reported by a camera for a given request. Previously, a `ControlList` was
used for this purpose. The reason for introducing a separate type is to
simplify the access to the returned metadata during the entire lifetime
of a request.
Specifically, for early metadata completion to be easily usable it should be
guaranteed that any completed metadata item can be accessed and looked up
at least until the associated requested is reused with `Request::reuse()`.
However, when a metadata item is completed early, the pipeline handler
might still work on the request in the `CameraManager`'s private thread,
therefore there is an inherent synchronization issue when an application
accesses early metadata.
Restricting the user to only access the metadata items of a not yet completed
request in the early metadata availability signal handler by ways of
documenting or enforcing it at runtime could be an option, but it is not
too convenient for the user.
The current `ControlList` implementation employs an `std::unordered_map`,
so pointers remain stable when the container is modified, so an application
could keep accessing particular metadata items outside the signal handler,
but this fact is far from obvious, and the user would still not be able
to make a copy of all metadata or do lookups based on the numeric ids or
the usual `libcamera::Control<>` objects, thus some type safety is lost.
The above also requires that each metadata item is only completed once for
a given request, but this does not appear to be serious limitation,
and in fact, this restriction is enforced by `MetadataList`.
The introduced `MetadataList` supports single writer - multiple reader
scenarios, and it can be set, looked-up, and copied in a wait-free fashion
without introducing data races or other synchronization issues. This is
achieved by requiring the possible set of metadata items to be known
(such set is stored in a `MetadataListPlan` object). Based on the this
plan, a single contiguous allocation is made to accommodate all potential
metadata items. Due to this single contiguous allocation that is not modified
during the lifetime of a `MetadataList` and atomic modifications, it is
possible to easily gaurantee thread-safe set, lookup, and copy; assuming
there is only ever a single writer.
Signed-off-by: Barnabás Pőcze <barnabas.pocze at ideasonboard.com>
---
include/libcamera/meson.build | 2 +
include/libcamera/metadata_list.h | 619 +++++++++++++++++++++++++
include/libcamera/metadata_list_plan.h | 109 +++++
src/libcamera/meson.build | 1 +
src/libcamera/metadata_list.cpp | 315 +++++++++++++
test/controls/meson.build | 1 +
test/controls/metadata_list.cpp | 171 +++++++
7 files changed, 1218 insertions(+)
create mode 100644 include/libcamera/metadata_list.h
create mode 100644 include/libcamera/metadata_list_plan.h
create mode 100644 src/libcamera/metadata_list.cpp
create mode 100644 test/controls/metadata_list.cpp
diff --git a/include/libcamera/meson.build b/include/libcamera/meson.build
index 30ea76f94..410b548dd 100644
--- a/include/libcamera/meson.build
+++ b/include/libcamera/meson.build
@@ -12,6 +12,8 @@ libcamera_public_headers = files([
'framebuffer_allocator.h',
'geometry.h',
'logging.h',
+ 'metadata_list.h',
+ 'metadata_list_plan.h',
'orientation.h',
'pixel_format.h',
'request.h',
diff --git a/include/libcamera/metadata_list.h b/include/libcamera/metadata_list.h
new file mode 100644
index 000000000..7514bd2ad
--- /dev/null
+++ b/include/libcamera/metadata_list.h
@@ -0,0 +1,619 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2025, Ideas On Board Oy
+ *
+ * Metadata list
+ */
+
+#pragma once
+
+#include <algorithm>
+#include <atomic>
+#include <cassert>
+#include <cstdint>
+#include <cstring>
+#include <new>
+#include <optional>
+#include <type_traits>
+
+#include <libcamera/base/details/align.h>
+#include <libcamera/base/details/cxx20.h>
+#include <libcamera/base/span.h>
+
+#include <libcamera/controls.h>
+#include <libcamera/metadata_list_plan.h>
+
+// TODO: want this?
+#if __has_include(<sanitizer/asan_interface.h>)
+#if __SANITIZE_ADDRESS__ /* gcc */
+#include <sanitizer/asan_interface.h>
+#define HAS_ASAN 1
+#elif defined(__has_feature)
+#if __has_feature(address_sanitizer) /* clang */
+#include <sanitizer/asan_interface.h>
+#define HAS_ASAN 1
+#endif
+#endif
+#endif
+
+namespace libcamera {
+
+class MetadataList
+{
+private:
+ struct ValueParams {
+ ControlType type;
+ bool isArray;
+ std::uint32_t numElements;
+ };
+
+ struct Entry {
+ const std::uint32_t tag;
+ const std::uint32_t capacity;
+ const std::uint32_t alignment;
+ const ControlType type;
+ bool isArray;
+
+ static constexpr std::uint32_t invalidOffset = -1;
+ /*
+ * Offset from the beginning of the allocation, and
+ * and _not_ relative to `contentOffset_`.
+ */
+ std::atomic_uint32_t headerOffset = invalidOffset;
+
+ [[nodiscard]] std::optional<std::uint32_t> hasValue() const
+ {
+ auto offset = headerOffset.load(std::memory_order_relaxed);
+ if (offset == invalidOffset)
+ return {};
+
+ return offset;
+ }
+
+ [[nodiscard]] std::optional<std::uint32_t> acquireData() const
+ {
+ auto offset = hasValue();
+ if (offset) {
+ /* sync with release-store on `headerOffset` in `MetadataList::set()` */
+ std::atomic_thread_fence(std::memory_order_acquire);
+ }
+
+ return offset;
+ }
+ };
+
+ struct ValueHeader {
+ std::uint32_t tag;
+ std::uint32_t size;
+ std::uint32_t alignment;
+ ValueParams params;
+ };
+
+ struct State {
+ std::uint32_t count;
+ std::uint32_t fill;
+ };
+
+public:
+ explicit MetadataList(const MetadataListPlan &plan)
+ : capacity_(plan.size()),
+ contentOffset_(MetadataList::contentOffset(capacity_)),
+ alloc_(contentOffset_)
+ {
+ for (const auto &[tag, e] : plan) {
+ assert(details::cxx20::has_single_bit(e.alignment));
+
+ alloc_ += sizeof(ValueHeader);
+ alloc_ += e.alignment - 1; // XXX: this is the maximum
+ alloc_ += e.size;
+ alloc_ += alignof(ValueHeader) - 1; // XXX: this is the maximum
+ }
+
+ p_ = static_cast<std::byte *>(::operator new(alloc_));
+
+ auto *entries = reinterpret_cast<Entry *>(p_ + entriesOffset());
+ auto it = plan.begin();
+
+ for (std::size_t i = 0; i < capacity_; i++, ++it) {
+ const auto &[tag, e] = *it;
+
+ new (&entries[i]) Entry{
+ .tag = tag,
+ .capacity = e.size,
+ .alignment = e.alignment,
+ .type = e.type,
+ .isArray = e.isArray,
+ };
+ }
+
+#if HAS_ASAN
+ ::__sanitizer_annotate_contiguous_container(
+ p_ + contentOffset_, p_ + alloc_,
+ p_ + alloc_, p_ + contentOffset_
+ );
+#endif
+ }
+
+ MetadataList(const MetadataList &other)
+ : capacity_(other.capacity_),
+ contentOffset_(other.contentOffset_),
+ alloc_(other.alloc_),
+ p_(static_cast<std::byte *>(::operator new(alloc_)))
+ {
+ auto *entries = reinterpret_cast<Entry *>(p_ + entriesOffset());
+ const auto otherEntries = other.entries();
+
+ for (std::size_t i = 0; i < capacity_; i++) {
+ auto *e = new (&entries[i]) Entry{
+ .tag = otherEntries[i].tag,
+ .capacity = otherEntries[i].capacity,
+ .alignment = otherEntries[i].alignment,
+ .type = otherEntries[i].type,
+ .isArray = otherEntries[i].isArray,
+ };
+
+ auto v = other.data_of(otherEntries[i]);
+ if (!v)
+ continue;
+
+ [[maybe_unused]] auto r = set(*e, v);
+ assert(r == SetError());
+ }
+
+#if HAS_ASAN
+ ::__sanitizer_annotate_contiguous_container(
+ p_ + contentOffset_, p_ + alloc_,
+ p_ + alloc_, p_ + contentOffset_
+ );
+#endif
+ }
+
+ MetadataList(MetadataList &&) = delete;
+
+ MetadataList &operator=(const MetadataList &) = delete;
+ MetadataList &operator=(MetadataList &&) = delete;
+
+ ~MetadataList()
+ {
+#if HAS_ASAN
+ /*
+ * The documentation says the range apparently has to be
+ * restored to its initial state before it is deallocated.
+ */
+ ::__sanitizer_annotate_contiguous_container(
+ p_ + contentOffset_, p_ + alloc_,
+ p_ + contentOffset_ + state_.load(std::memory_order_relaxed).fill, p_ + alloc_
+ );
+#endif
+
+ ::operator delete(p_, alloc_);
+ }
+
+ // TODO: want these?
+ [[nodiscard]] std::size_t size() const { return state_.load(std::memory_order_relaxed).count; }
+ [[nodiscard]] bool empty() const { return size() == 0; }
+
+ enum class SetError {
+ UnknownTag = 1,
+ AlreadySet,
+ DataTooLarge,
+ TypeMismatch,
+ };
+
+ [[nodiscard]] SetError set(std::uint32_t tag, ControlValueView v)
+ {
+ auto *e = find(tag);
+ if (!e)
+ return SetError::UnknownTag;
+
+ return set(*e, v);
+ }
+
+ template<typename T>
+ /* TODO: [[nodiscard]] */ SetError set(const Control<T> &ctrl, const details::cxx20::type_identity_t<T> &value)
+ {
+ using TypeInfo = libcamera::details::control_type<T>;
+
+ if constexpr (TypeInfo::size > 0) {
+ static_assert(std::is_trivially_copyable_v<typename T::value_type>);
+
+ return set(ctrl.id(), {
+ TypeInfo::value,
+ true,
+ value.size(),
+ reinterpret_cast<const std::byte *>(value.data()),
+ });
+ } else {
+ static_assert(std::is_trivially_copyable_v<T>);
+
+ return set(ctrl.id(), {
+ TypeInfo::value,
+ false,
+ 1,
+ reinterpret_cast<const std::byte *>(&value),
+ });
+ }
+ }
+
+ template<typename T>
+ [[nodiscard]] decltype(auto) get(const Control<T> &ctrl) const
+ {
+ ControlValueView v = get(ctrl.id());
+
+ return v ? std::optional(v.get<T>()) : std::nullopt;
+ }
+
+ // TODO: operator ControlListView() const ?
+ // TODO: explicit operator ControlList() const ?
+
+ [[nodiscard]] ControlValueView get(std::uint32_t tag) const
+ {
+ const auto *e = find(tag);
+ if (!e)
+ return {};
+
+ return data_of(*e);
+ }
+
+ void clear()
+ {
+ for (auto &e : entries())
+ e.headerOffset.store(Entry::invalidOffset, std::memory_order_relaxed);
+
+ [[maybe_unused]] auto s = state_.exchange({}, std::memory_order_relaxed);
+
+#if HAS_ASAN
+ ::__sanitizer_annotate_contiguous_container(
+ p_ + contentOffset_, p_ + alloc_,
+ p_ + contentOffset_ + s.fill, p_ + contentOffset_
+ );
+#endif
+ }
+
+ class iterator
+ {
+ public:
+ using difference_type = std::ptrdiff_t;
+ using value_type = std::pair<std::uint32_t, ControlValueView>;
+ using pointer = void;
+ using reference = value_type;
+ using iterator_category = std::forward_iterator_tag;
+
+ iterator() = default;
+
+ iterator& operator++()
+ {
+ const auto &h = header();
+
+ p_ += sizeof(h);
+ p_ = details::align::up(p_, h.alignment);
+ p_ += h.size;
+ p_ = details::align::up(p_, alignof(decltype(h)));
+
+ return *this;
+ }
+
+ iterator operator++(int)
+ {
+ auto copy = *this;
+ ++*this;
+ return copy;
+ }
+
+ [[nodiscard]] reference operator*() const
+ {
+ const auto &h = header();
+ const auto *data = details::align::up(p_ + sizeof(h), h.alignment);
+
+ return { h.tag, { h.params.type, h.params.isArray, h.params.numElements, data } };
+ }
+
+ [[nodiscard]] bool operator==(const iterator &other) const
+ {
+ return p_ == other.p_;
+ }
+
+ [[nodiscard]] bool operator!=(const iterator &other) const
+ {
+ return !(*this == other);
+ }
+
+ private:
+ iterator(const std::byte *p)
+ : p_(p)
+ {
+ }
+
+ [[nodiscard]] const ValueHeader &header() const
+ {
+ return *reinterpret_cast<const ValueHeader *>(p_);
+ }
+
+ friend MetadataList;
+
+ const std::byte *p_ = nullptr;
+ };
+
+ [[nodiscard]] iterator begin() const
+ {
+ return { p_ + contentOffset_ };
+ }
+
+ [[nodiscard]] iterator end() const
+ {
+ return { p_ + contentOffset_ + state_.load(std::memory_order_acquire).fill };
+ }
+
+ class Diff
+ {
+ public:
+ // TODO: want these?
+ [[nodiscard]] explicit operator bool() const { return !empty(); }
+ [[nodiscard]] bool empty() const { return start_ == stop_; }
+ [[nodiscard]] std::size_t size() const { return changed_; }
+ [[nodiscard]] const MetadataList &list() const { return *l_; }
+
+ [[nodiscard]] ControlValueView get(std::uint32_t tag) const
+ {
+ const auto *e = l_->find(tag);
+ if (!e)
+ return {};
+
+ auto o = e->acquireData();
+ if (!o)
+ return {};
+
+ if (!(start_ <= *o && *o < stop_))
+ return {};
+
+ return l_->data_of(*o);
+ }
+
+ template<typename T>
+ [[nodiscard]] decltype(auto) get(const Control<T> &ctrl) const
+ {
+ ControlValueView v = get(ctrl.id());
+
+ return v ? std::optional(v.get<T>()) : std::nullopt;
+ }
+
+ [[nodiscard]] iterator begin() const
+ {
+ return { l_->p_ + start_ };
+ }
+
+ [[nodiscard]] iterator end() const
+ {
+ return { l_->p_ + stop_ };
+ }
+
+ private:
+ Diff(const MetadataList &l, std::size_t changed, std::size_t oldFill, std::size_t newFill)
+ : l_(&l),
+ changed_(changed),
+ start_(l.contentOffset_ + oldFill),
+ stop_(l.contentOffset_ + newFill)
+ {
+ }
+
+ friend MetadataList;
+ friend struct Checkpoint;
+
+ const MetadataList *l_ = nullptr;
+ std::size_t changed_;
+ std::size_t start_;
+ std::size_t stop_;
+ };
+
+ Diff merge(const MetadataList &other)
+ {
+ const auto entries = this->entries();
+ const auto otherEntries = other.entries();
+ const auto c = checkpoint();
+
+ for (std::size_t i = 0, j = 0; i < entries.size() && j < otherEntries.size(); ) {
+ if (entries[i].tag < otherEntries[j].tag) {
+ i += 1;
+ continue;
+ }
+
+ if (entries[i].tag > otherEntries[j].tag) {
+ j += 1;
+ continue;
+ }
+
+ assert(entries[i].alignment >= otherEntries[j].alignment);
+ assert(entries[i].capacity >= otherEntries[j].capacity);
+
+ if (!entries[i].hasValue()) {
+ auto v = other.data_of(otherEntries[j]);
+ if (v) {
+ [[maybe_unused]] auto r = set(entries[i], v);
+ assert(r == SetError());
+ }
+ }
+
+ i += 1;
+ j += 1;
+ }
+
+ return c.diffSince();
+ }
+
+ Diff merge(const ControlList &other)
+ {
+ // TODO: check id map of `other`?
+
+ const auto c = checkpoint();
+
+ for (auto &&[tag, value] : other) {
+ auto *e = find(tag);
+ if (e) {
+ [[maybe_unused]] auto r = set(*e, value);
+ assert(r == SetError() || r == SetError::AlreadySet); // TODO: ?
+ }
+ }
+
+ return c.diffSince();
+ }
+
+ class Checkpoint
+ {
+ public:
+ // TODO: want this?
+ [[nodiscard]] const MetadataList &list() const { return *l_; }
+
+ [[nodiscard]] Diff diffSince() const
+ {
+ /* sync with release-store on `state_` in `set()` */
+ const auto curr = l_->state_.load(std::memory_order_acquire);
+
+ assert(s_.count <= curr.count);
+ assert(s_.fill <= curr.fill);
+
+ return {
+ *l_,
+ curr.count - s_.count,
+ s_.fill,
+ curr.fill,
+ };
+ }
+
+ private:
+ Checkpoint(const MetadataList &l)
+ : l_(&l),
+ s_(l.state_.load(std::memory_order_relaxed))
+ {
+ }
+
+ friend MetadataList;
+
+ const MetadataList *l_ = nullptr;
+ State s_ = {};
+ };
+
+ [[nodiscard]] Checkpoint checkpoint() const
+ {
+ return { *this };
+ }
+
+private:
+ [[nodiscard]] static constexpr std::size_t entriesOffset()
+ {
+ return 0;
+ }
+
+ [[nodiscard]] static constexpr std::size_t contentOffset(std::size_t entries)
+ {
+ return details::align::up(entriesOffset() + entries * sizeof(Entry), alignof(ValueHeader));
+ }
+
+ [[nodiscard]] Span<Entry> entries() const
+ {
+ return { reinterpret_cast<Entry *>(p_ + entriesOffset()), capacity_ };
+ }
+
+ [[nodiscard]] Entry *find(std::uint32_t tag) const
+ {
+ const auto entries = this->entries();
+ auto it = std::lower_bound(entries.begin(), entries.end(), tag, [](const auto &e, const auto &t) {
+ return e.tag < t;
+ });
+
+ if (it == entries.end() || it->tag != tag)
+ return nullptr;
+
+ return &*it;
+ }
+
+ [[nodiscard]] ControlValueView data_of(const Entry &e) const
+ {
+ const auto o = e.acquireData();
+ return o ? data_of(*o) : ControlValueView{ };
+ }
+
+ [[nodiscard]] ControlValueView data_of(std::size_t headerOffset) const
+ {
+ assert(headerOffset <= alloc_ - sizeof(ValueHeader));
+ assert(details::align::is(p_ + headerOffset, alignof(ValueHeader)));
+
+ const auto *vh = reinterpret_cast<const ValueHeader *>(p_ + headerOffset);
+ const auto *p = reinterpret_cast<const std::byte *>(vh) + sizeof(*vh);
+ std::size_t avail = p_ + alloc_ - p;
+
+ const auto *data = details::align::up(vh->size, vh->alignment, p, &avail);
+ assert(data);
+
+ return { vh->params.type, vh->params.isArray, vh->params.numElements, data };
+ }
+
+ [[nodiscard]] SetError set(Entry &e, ControlValueView v)
+ {
+ if (e.hasValue())
+ return SetError::AlreadySet;
+ if (e.type != v.type() || e.isArray != v.isArray())
+ return SetError::TypeMismatch;
+
+ const auto src = v.data();
+ if (e.isArray) {
+ if (src.size_bytes() > e.capacity)
+ return SetError::DataTooLarge;
+ } else {
+ assert(src.size_bytes() == e.capacity);
+ }
+
+ auto s = state_.load(std::memory_order_relaxed);
+ std::byte *oldEnd = p_ + contentOffset_ + s.fill;
+ std::byte *p = oldEnd;
+
+ auto *headerPtr = details::align::up<ValueHeader>(p);
+ auto *dataPtr = details::align::up(src.size_bytes(), e.alignment, p);
+ details::align::up(0, alignof(ValueHeader), p);
+
+#if HAS_ASAN
+ ::__sanitizer_annotate_contiguous_container(
+ p_ + contentOffset_, p_ + alloc_,
+ oldEnd, p
+ );
+#endif
+
+ new (headerPtr) ValueHeader{
+ .tag = e.tag,
+ .size = std::uint32_t(src.size_bytes()),
+ .alignment = e.alignment,
+ .params = {
+ .type = v.type(),
+ .isArray = v.isArray(),
+ .numElements = std::uint32_t(v.numElements()),
+ },
+ };
+ std::memcpy(dataPtr, src.data(), src.size_bytes());
+ e.headerOffset.store(reinterpret_cast<std::byte *>(headerPtr) - p_, std::memory_order_release);
+
+ s.fill += p - oldEnd;
+ s.count += 1;
+
+ state_.store(s, std::memory_order_release);
+
+ return {};
+ }
+
+ std::size_t capacity_ = 0;
+ std::size_t contentOffset_ = -1;
+ std::size_t alloc_ = 0;
+ std::atomic<State> state_ = State{};
+ std::byte *p_ = nullptr;
+ // TODO: ControlIdMap in any way shape or form?
+
+ /*
+ * If this is problematic on a 32-bit architecture, then
+ * `count` can be stored in a separate atomic variable
+ * but then `Diff::changed_` must be removed since the fill
+ * level and item count cannot be retrieved atomically.
+ */
+ static_assert(decltype(state_)::is_always_lock_free);
+};
+
+} /* namespace libcamera */
+
+#undef HAS_ASAN
diff --git a/include/libcamera/metadata_list_plan.h b/include/libcamera/metadata_list_plan.h
new file mode 100644
index 000000000..9ad4ae87e
--- /dev/null
+++ b/include/libcamera/metadata_list_plan.h
@@ -0,0 +1,109 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2025, Ideas On Board Oy
+ */
+
+#pragma once
+
+#include <cassert>
+#include <cstddef>
+#include <cstdint>
+#include <limits>
+#include <map>
+#include <type_traits>
+
+#include <libcamera/base/details/cxx20.h>
+
+#include <libcamera/controls.h>
+
+namespace libcamera {
+
+class MetadataListPlan
+{
+public:
+ [[nodiscard]] bool empty() const { return items_.empty(); }
+ [[nodiscard]] std::size_t size() const { return items_.size(); }
+ [[nodiscard]] decltype(auto) begin() const { return items_.begin(); }
+ [[nodiscard]] decltype(auto) end() const { return items_.end(); }
+ void clear() { items_.clear(); }
+
+ template<
+ typename T,
+ std::enable_if_t<libcamera::details::control_type<T>::size != libcamera::dynamic_extent> * = nullptr
+ >
+ decltype(auto) add(const Control<T> &ctrl)
+ {
+ if constexpr (libcamera::details::control_type<T>::size > 0) {
+ static_assert(libcamera::details::control_type<T>::size != libcamera::dynamic_extent);
+
+ return add<typename T::value_type>(
+ ctrl.id(),
+ libcamera::details::control_type<T>::size,
+ true
+ );
+ } else {
+ return add<T>(ctrl.id(), 1, false);
+ }
+ }
+
+ template<
+ typename T,
+ std::enable_if_t<libcamera::details::control_type<T>::size == libcamera::dynamic_extent> * = nullptr
+ >
+ decltype(auto) add(const Control<T> &ctrl, std::size_t count)
+ {
+ return add<typename T::value_type>(ctrl.id(), count, true);
+ }
+
+#ifndef __DOXYGEN__
+ MetadataListPlan &add(std::uint32_t tag,
+ std::size_t size, std::size_t count, std::size_t alignment,
+ ControlType type, bool isArray)
+ {
+ assert(size > 0 && size <= std::numeric_limits<std::uint32_t>::max());
+ assert(count <= std::numeric_limits<std::uint32_t>::max() / size);
+ assert(alignment <= std::numeric_limits<std::uint32_t>::max());
+ assert(details::cxx20::has_single_bit(alignment));
+ assert(isArray || count == 1);
+
+ items_.insert_or_assign(tag, Entry{
+ .size = std::uint32_t(size * count),
+ .alignment = std::uint32_t(alignment),
+ .type = type,
+ .isArray = isArray,
+ });
+
+ return *this;
+ }
+#endif
+
+ bool remove(std::uint32_t tag)
+ {
+ return items_.erase(tag);
+ }
+
+ bool remove(const ControlId &ctrl)
+ {
+ return remove(ctrl.id());
+ }
+
+private:
+ struct Entry {
+ std::uint32_t size;
+ std::uint32_t alignment; // TODO: is this necessary?
+ ControlType type;
+ bool isArray;
+ };
+
+ std::map<std::uint32_t, Entry> items_;
+
+ template<typename T>
+ decltype(auto) add(std::uint32_t tag, std::size_t count, bool isArray)
+ {
+ static_assert(std::is_trivially_copyable_v<T>);
+
+ return add(tag, sizeof(T), count, alignof(T), details::control_type<T>::value, isArray);
+ }
+};
+
+} /* namespace libcamera */
diff --git a/src/libcamera/meson.build b/src/libcamera/meson.build
index 202db1efe..d7a850907 100644
--- a/src/libcamera/meson.build
+++ b/src/libcamera/meson.build
@@ -9,6 +9,7 @@ libcamera_public_sources = files([
'framebuffer.cpp',
'framebuffer_allocator.cpp',
'geometry.cpp',
+ 'metadata_list.cpp',
'orientation.cpp',
'pixel_format.cpp',
'request.cpp',
diff --git a/src/libcamera/metadata_list.cpp b/src/libcamera/metadata_list.cpp
new file mode 100644
index 000000000..ccfe37318
--- /dev/null
+++ b/src/libcamera/metadata_list.cpp
@@ -0,0 +1,315 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2025, Ideas On Board Oy
+ */
+
+#include <libcamera/metadata_list.h>
+
+namespace libcamera {
+
+/**
+ * \class MetadataListPlan
+ * \brief Class to hold the possible set of metadata items for a \ref MetadataList
+ */
+
+/**
+ * \fn MetadataListPlan::begin() const
+ * \brief Retrieve the begin iterator
+ */
+
+/**
+ * \fn MetadataListPlan::end() const
+ * \brief Retrieve the end iterator
+ */
+
+/**
+ * \fn MetadataListPlan::size() const
+ * \brief Retrieve the number of controls
+ */
+
+/**
+ * \fn MetadataListPlan::empty() const
+ * \brief Check if empty
+ */
+
+/**
+ * \fn MetadataListPlan::clear()
+ * \brief Remove all controls
+ */
+
+/**
+ * \fn MetadataListPlan::add(const Control<T> &ctrl)
+ * \brief Add a control to the metadata list plan
+ */
+
+/**
+ * \fn MetadataListPlan::add(const Control<T> &ctrl, std::size_t count)
+ * \brief Add a dynamically-sized control to the metadata list plan
+ * \param[in] ctrl The control
+ * \param[in] count The maximum number of elements
+ *
+ * Add the dynamically-sized control \a ctrl to the metadata list plan with a maximum
+ * capacity of \a count elements.
+ */
+
+/**
+ * \fn MetadataListPlan::remove(std::uint32_t tag)
+ * \brief Remove the entry with given identifier from the plan
+ */
+
+/**
+ * \fn MetadataListPlan::remove(const ControlId &ctrl)
+ * \brief Remove \a ctrl from the plan
+ */
+
+/**
+ * \class MetadataList
+ * \brief Class to hold metadata items
+ */
+
+/**
+ * \fn MetadataList::MetadataList(const MetadataListPlan &plan)
+ * \brief Construct a metadata list according to \a plan
+ *
+ * Construct a metadata list according to the provided \a plan.
+ */
+
+/**
+ * \fn MetadataList::MetadataList(const MetadataList &other)
+ * \brief Copy constructor
+ * \context This function is \threadsafe wrt. \a other.
+ */
+
+/**
+ * \fn MetadataList::size() const
+ * \brief Retrieve the number of controls
+ * \context This function is \threadsafe.
+ * \note If the list is being modified, the return value may be out of
+ * date by the time the function returns
+ */
+
+/**
+ * \fn MetadataList::empty() const
+ * \brief Check if empty
+ * \context This function is \threadsafe.
+ * \note If the list is being modified, the return value may be out of
+ * date by the time the function returns
+ */
+
+/**
+ * \internal
+ * \fn MetadataList::clear()
+ * \brief Remove all items from the list
+ * \note This function in effect resets the list to its original state. As a consequence it invalidates - among others -
+ * all iterators, Checkpoint, and Diff objects that are associated with the list. No readers must exist
+ * when this function is called.
+ */
+
+/**
+ * \fn MetadataList::begin() const
+ * \brief Retrieve begin iterator
+ * \context This function is \threadsafe.
+ */
+
+/**
+ * \fn MetadataList::end() const
+ * \brief Retrieve end iterator
+ * \context This function is \threadsafe.
+ */
+
+/**
+ * \fn MetadataList::get(const Control<T> &ctrl) const
+ * \brief Get the value of control \a ctrl
+ * \return A std::optional<T> containing the control value, or std::nullopt if
+ * the control \a ctrl is not present in the list
+ * \context This function is \threadsafe.
+ */
+
+/**
+ * \fn MetadataList::get(std::uint32_t tag) const
+ * \brief Get the value of pertaining to the numeric identifier \a tag
+ * \return A std::optional<T> containing the control value, or std::nullopt if
+ * the control is not present in the list
+ * \context This function is \threadsafe.
+ */
+
+/**
+ * \fn MetadataList::set(const Control<T> &ctrl, const details::cxx20::type_identity_t<T> &value)
+ * \brief Set the value of control \a ctrl to \a value
+ */
+
+/**
+ * \fn MetadataList::set(std::uint32_t tag, ControlValueView v)
+ * \brief Set the value of pertaining to the numeric identifier \a tag to \a v
+ */
+
+/**
+ * \internal
+ * \fn MetadataList::merge(const MetadataList &other)
+ * \brief Add all missing items from \a other
+ *
+ * Add all items from \a other that are not present in \a this. If an item
+ * has a numeric identifier that was not present in the MetadataListPlan
+ * used to construct \a this, then the item is ignored.
+ *
+ * \context This function is \threadsafe wrt. \a other.
+ */
+
+/**
+ * \internal
+ * \fn MetadataList::merge(const ControlList &other)
+ * \copydoc MetadataList::merge(const MetadataList &other)
+ */
+
+/**
+ * \enum MetadataList::SetError
+ * \brief TODO
+ *
+ * \var MetadataList::SetError::UnknownTag
+ * \brief The tag is not supported by the metadata list
+ * \var MetadataList::SetError::AlreadySet
+ * \brief A value has already been added with the given tag
+ * \var MetadataList::SetError::DataTooLarge
+ * \brief The data is too large for the given tag
+ * \var MetadataList::SetError::TypeMismatch
+ * \brief The type of the value does not match the expected type
+ */
+
+/**
+ * \internal
+ * \fn MetadataList::checkpoint() const
+ * \brief Create a checkpoint
+ * \context This function is \threadsafe.
+ */
+
+/**
+ * \class MetadataList::iterator
+ * \brief Iterator
+ */
+
+/**
+ * \typedef MetadataList::iterator::difference_type
+ * \brief iterator's difference type
+ */
+
+/**
+ * \typedef MetadataList::iterator::value_type
+ * \brief iterator's value type
+ */
+
+/**
+ * \typedef MetadataList::iterator::pointer
+ * \brief iterator's pointer type
+ */
+
+/**
+ * \typedef MetadataList::iterator::reference
+ * \brief iterator's reference type
+ */
+
+/**
+ * \typedef MetadataList::iterator::iterator_category
+ * \brief iterator's category
+ */
+
+/**
+ * \fn MetadataList::iterator::operator*()
+ * \brief Retrieve value at iterator
+ * \return A \a ControlListView representing the value
+ */
+
+/**
+ * \fn MetadataList::iterator::operator==(const iterator &other) const
+ * \brief Check if two iterators are equal
+ */
+
+/**
+ * \fn MetadataList::iterator::operator!=(const iterator &other) const
+ * \brief Check if two iterators are not equal
+ */
+
+/**
+ * \fn MetadataList::iterator::operator++(int)
+ * \brief Advance the iterator
+ */
+
+/**
+ * \fn MetadataList::iterator::operator++()
+ * \brief Advance the iterator
+ */
+
+/**
+ * \class MetadataList::Diff
+ * \brief Designates a set of consecutively added metadata items from a particular MetadataList
+ * \sa Camera::metadataAvailable
+ * \internal
+ * \sa MetadataList::Checkpoint::diffSince()
+ * \endinternal
+ */
+
+/**
+ * \fn MetadataList::Diff::list() const
+ * \brief Retrieve the associated MetadataList
+ */
+
+/**
+ * \fn MetadataList::Diff::size() const
+ * \brief Retrieve the number of metadata items designated
+ */
+
+/**
+ * \fn MetadataList::Diff::empty() const
+ * \brief Check if no metadata items are designated
+ */
+
+/**
+ * \fn MetadataList::Diff::operator bool() const
+ * \copydoc MetadataList::Diff::empty() const
+ */
+
+/**
+ * \fn MetadataList::Diff::get(const Control<T> &ctrl) const
+ * \copydoc MetadataList::get(const Control<T> &ctrl) const
+ * \note The value pertaining to \a ctrl will only be returned if it is part of Diff,
+ * meaning that even if \a ctrl is part of the backing MetadataList, it will not
+ * be returned if \a ctrl is not in the set of controls designated by this Diff object.
+ */
+
+/**
+ * \fn MetadataList::Diff::get(std::uint32_t tag) const
+ * \copydoc MetadataList::Diff::get(const Control<T>&ctrl) const
+ */
+
+/**
+ * \fn MetadataList::Diff::begin() const
+ * \brief Retrieve the begin iterator
+ */
+
+/**
+ * \fn MetadataList::Diff::end() const
+ * \brief Retrieve the end iterator
+ */
+
+/**
+ * \internal
+ * \class MetadataList::Checkpoint
+ * \brief Designates a point in the stream of metadata items
+ *
+ * A Checkpoint object designates a point in the stream of metadata items in the associated
+ * MetadataList. Its main use to be able to retrieve the set of metadata items that were
+ * added to the list after the designated point using diffSince().
+ */
+
+/**
+ * \internal
+ * \fn MetadataList::Checkpoint::list() const
+ * \brief Retrieve the associated \ref MetadataList
+ */
+
+/**
+ * \internal
+ * \fn MetadataList::Checkpoint::diffSince() const
+ * \brief Retrieve the set of metadata items added since the checkpoint was created
+ */
+
+} /* namespace libcamera */
diff --git a/test/controls/meson.build b/test/controls/meson.build
index 763f8905e..b68a4fc53 100644
--- a/test/controls/meson.build
+++ b/test/controls/meson.build
@@ -5,6 +5,7 @@ control_tests = [
{'name': 'control_info_map', 'sources': ['control_info_map.cpp']},
{'name': 'control_list', 'sources': ['control_list.cpp']},
{'name': 'control_value', 'sources': ['control_value.cpp']},
+ {'name': 'metadata_list', 'sources': ['metadata_list.cpp']},
]
foreach test : control_tests
diff --git a/test/controls/metadata_list.cpp b/test/controls/metadata_list.cpp
new file mode 100644
index 000000000..e02b4e28e
--- /dev/null
+++ b/test/controls/metadata_list.cpp
@@ -0,0 +1,171 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2025, Ideas On Board Oy
+ *
+ * MetadataList tests
+ */
+
+#include <future>
+#include <iostream>
+#include <thread>
+
+#include <libcamera/control_ids.h>
+#include <libcamera/metadata_list.h>
+#include <libcamera/property_ids.h>
+
+#include "test.h"
+
+using namespace std;
+using namespace libcamera;
+
+#define ASSERT(x) do { \
+ if (!static_cast<bool>(x)) { \
+ std::cerr << '`' << #x << "` failed" << std::endl; \
+ return TestFail; \
+ } \
+} while (false)
+
+class MetadataListTest : public Test
+{
+public:
+ MetadataListTest() = default;
+
+protected:
+ int run() override
+ {
+ MetadataListPlan mlp;
+ mlp.add(controls::ExposureTime);
+ mlp.add(controls::ExposureValue);
+ mlp.add(controls::ColourGains);
+ mlp.add(controls::AfWindows, 10);
+ mlp.add(controls::AeEnable);
+ mlp.add(controls::SensorTimestamp);
+
+ MetadataList ml(mlp);
+
+ static_assert(static_cast<unsigned int>(properties::LOCATION) == controls::AE_ENABLE);
+ ASSERT(ml.set(properties::Location, properties::CameraLocationFront) == MetadataList::SetError::TypeMismatch);
+
+ ASSERT(ml.set(controls::AfWindows, std::array<Rectangle, 11>{}) == MetadataList::SetError::DataTooLarge);
+ ASSERT(ml.set(controls::ColourTemperature, 123) == MetadataList::SetError::UnknownTag);
+
+ auto f1 = std::async(std::launch::async, [&] {
+ using namespace std::chrono_literals;
+
+ std::this_thread::sleep_for(500ms);
+ ASSERT(ml.set(controls::ExposureTime, 0x1111) == MetadataList::SetError());
+
+ std::this_thread::sleep_for(500ms);
+ ASSERT(ml.set(controls::ExposureValue, 1) == MetadataList::SetError());
+
+ std::this_thread::sleep_for(500ms);
+ ASSERT(ml.set(controls::ColourGains, std::array{
+ 123.f,
+ 456.f
+ }) == MetadataList::SetError());
+
+ std::this_thread::sleep_for(500ms);
+ ASSERT(ml.set(controls::AfWindows, std::array{
+ Rectangle(),
+ Rectangle(1, 2, 3, 4),
+ Rectangle(0x1111, 0x2222, 0x3333, 0x4444),
+ }) == MetadataList::SetError());
+
+ return TestPass;
+ });
+
+ auto f2 = std::async(std::launch::async, [&] {
+ for (;;) {
+ const auto x = ml.get(controls::ExposureTime);
+ const auto y = ml.get(controls::ExposureValue);
+ const auto z = ml.get(controls::ColourGains);
+ const auto w = ml.get(controls::AfWindows);
+
+ if (x)
+ ASSERT(*x == 0x1111);
+
+ if (y)
+ ASSERT(*y == 1.0f);
+
+ if (z) {
+ ASSERT(z->size() == 2);
+ ASSERT((*z)[0] == 123.f);
+ ASSERT((*z)[1] == 456.f);
+ }
+
+ if (w) {
+ ASSERT(w->size() == 3);
+ ASSERT((*w)[0].isNull());
+ ASSERT((*w)[1] == Rectangle(1, 2, 3, 4));
+ ASSERT((*w)[2] == Rectangle(0x1111, 0x2222, 0x3333, 0x4444));
+ }
+
+ if (x && y && z && w)
+ break;
+ }
+
+ return TestPass;
+ });
+
+ ASSERT(f1.get() == TestPass);
+ ASSERT(f2.get() == TestPass);
+
+ ASSERT(ml.set(controls::ExposureTime, 0x2222) == MetadataList::SetError::AlreadySet);
+ ASSERT(ml.set(controls::ExposureValue, 2) == MetadataList::SetError::AlreadySet);
+
+ ASSERT(ml.get(controls::ExposureTime) == 0x1111);
+ ASSERT(ml.get(controls::ExposureValue) == 1);
+
+ for (auto &&[tag, v] : ml)
+ std::cout << "[" << tag << "] -> " << v << '\n';
+
+ std::cout << std::endl;
+
+ ml.clear();
+ ASSERT(ml.empty());
+ ASSERT(ml.size() == 0);
+
+ ASSERT(ml.set(controls::ExposureTime, 0x2222) == MetadataList::SetError());
+ ASSERT(ml.get(controls::ExposureTime) == 0x2222);
+
+ auto c = ml.checkpoint();
+ ASSERT(&c.list() == &ml);
+
+ ASSERT(ml.set(controls::ExposureValue, 2) == MetadataList::SetError());
+ ASSERT(ml.set(controls::SensorTimestamp, 0x99999999) == MetadataList::SetError());
+
+ auto d = c.diffSince();
+ ASSERT(&d.list() == &ml);
+
+ ASSERT(ml.set(controls::ColourGains, std::array{ 1.f, 2.f }) == MetadataList::SetError());
+
+ ASSERT(d);
+ ASSERT(!d.empty());
+ ASSERT(d.size() == 2);
+ ASSERT(!d.get(controls::ExposureTime));
+ ASSERT(!d.get(controls::ColourGains));
+ ASSERT(!d.get(controls::AfWindows));
+ ASSERT(d.get(controls::ExposureValue) == 2);
+ ASSERT(d.get(controls::SensorTimestamp) == 0x99999999);
+
+ for (auto &&[tag, v] : d)
+ std::cout << "[" << tag << "] -> " << v << '\n';
+
+ /* Test if iterators work with algorithms. */
+ std::ignore = std::find_if(d.begin(), d.end(), [](const auto &) {
+ return false;
+ });
+
+#if 0
+ {
+ auto it = ml.begin();
+ ml.clear();
+ std::ignore = *it; /* Trigger ASAN. */
+ }
+#endif
+
+ return TestPass;
+ }
+};
+
+TEST_REGISTER(MetadataListTest)
--
2.49.0
More information about the libcamera-devel
mailing list