From patchwork Wed Mar 25 15:13:56 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Stefan Klug X-Patchwork-Id: 26366 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id 9D1DBC3306 for ; Wed, 25 Mar 2026 15:15:51 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 118BE62CC5; Wed, 25 Mar 2026 16:15:51 +0100 (CET) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="bpPzCe/a"; dkim-atps=neutral Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [IPv6:2001:4b98:dc2:55:216:3eff:fef7:d647]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 15F3462CB1 for ; Wed, 25 Mar 2026 16:15:49 +0100 (CET) Received: from ideasonboard.com (unknown [IPv6:2a00:6020:448c:6c00:b16a:5ed9:4ada:a95a]) by perceval.ideasonboard.com (Postfix) with UTF8SMTPSA id EB8301862; Wed, 25 Mar 2026 16:14:30 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1774451671; bh=UqvxXSxT4S9Qzmc+IYgd7Sf4uCo3mJ5q/3Ivvy/ssgo=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=bpPzCe/ar7MAnKLVC6HcrdDm2jW8wIokJY7eSV8HWGzlow6/8oqWJD2ckYzdQFb/O YxZMC4MGVpyaww+mtgSXGHaKMQH/7EoDRHvjqH12JD9yvurC0OQmP6P93Cygif2qkD IaAHvU1GDhWU2WY5nXG+8C6tr1Ssy0iEVzq9uYj0= From: Stefan Klug To: libcamera-devel@lists.libcamera.org Cc: Stefan Klug Subject: [PATCH v2 24/32] libcamera: internal: Add a BufferQueue class to handle buffer queues Date: Wed, 25 Mar 2026 16:13:56 +0100 Message-ID: <20260325151416.2114564-25-stefan.klug@ideasonboard.com> X-Mailer: git-send-email 2.51.0 In-Reply-To: <20260325151416.2114564-1-stefan.klug@ideasonboard.com> References: <20260325151416.2114564-1-stefan.klug@ideasonboard.com> MIME-Version: 1.0 X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" Add a class that encapsulates a queue of v4l2 buffers and the typical use-cases. This simplifies manual queue management and helps in cases where a pre or postprocessing stage is needed. Signed-off-by: Stefan Klug --- Changes in v2: - Added this patch --- include/libcamera/internal/buffer_queue.h | 131 +++++++ include/libcamera/internal/meson.build | 1 + src/libcamera/buffer_queue.cpp | 449 ++++++++++++++++++++++ src/libcamera/meson.build | 1 + 4 files changed, 582 insertions(+) create mode 100644 include/libcamera/internal/buffer_queue.h create mode 100644 src/libcamera/buffer_queue.cpp diff --git a/include/libcamera/internal/buffer_queue.h b/include/libcamera/internal/buffer_queue.h new file mode 100644 index 000000000000..d5904baeed90 --- /dev/null +++ b/include/libcamera/internal/buffer_queue.h @@ -0,0 +1,131 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2025, Ideas on Board + * + * Sequence sync helper + */ + +#pragma once + +#include +#include + +#include +#include + +#include + +#include "sequence_sync_helper.h" + +namespace libcamera { + +LOG_DECLARE_CATEGORY(RkISP1Schedule) + +struct BufferQueueDelegateBase { + virtual ~BufferQueueDelegateBase() = default; + virtual int allocateBuffers(unsigned int count, + std::vector> *buffers) = 0; + virtual int importBuffers(unsigned int count) = 0; + virtual int releaseBuffers() = 0; + + virtual int queueBuffer(FrameBuffer *buffer) = 0; + + Signal bufferReady; +}; + +template +struct BufferQueueDelegate : public BufferQueueDelegateBase { + BufferQueueDelegate(T *video) : video_(video) + { + video_->bufferReady.connect(this, [this](FrameBuffer *buffer) { + this->bufferReady.emit(buffer); + }); + } + + int allocateBuffers(unsigned int count, + std::vector> *buffers) override + { + return video_->allocateBuffers(count, buffers); + } + + int importBuffers(unsigned int count) override + { + return video_->importBuffers(count); + } + + int queueBuffer(FrameBuffer *buffer) override + { + return video_->queueBuffer(buffer); + } + + int releaseBuffers() override + { + return video_->releaseBuffers(); + } + +private: + T *video_; +}; + +class BufferQueue +{ +public: + enum State { + Idle = 0, + Preparing, + Capturing, + Postprocessing + }; + + enum Flags { + PrepareStage = 1, + PostprocessStage = 2 + }; + + BufferQueue(std::unique_ptr &&delegate, int flags = 0, std::string name = {}); + + int allocateBuffers(unsigned int count); + int importBuffers(unsigned int count); + int releaseBuffers(); + + int sequenceCorrection(); + uint32_t nextSequence(); + + int prepareBuffer(uint32_t *sequence = nullptr); + int prepareBuffer(FrameBuffer *buffer, uint32_t *sequence = nullptr); + int preparedBuffer(); + + int queueBuffer(uint32_t *sequence = nullptr); + int queueBuffer(FrameBuffer *buffer, uint32_t *sequence = nullptr); + + void postprocessedBuffer(); + + bool empty(State state); + + FrameBuffer *front(State state); + + unsigned int expectedSequence(FrameBuffer *buffer) const; + const std::vector> &buffers() const; + + Signal bufferReady; + +protected: + void onBufferReady(FrameBuffer *buffer); + + int internalPrepareBuffer(FrameBuffer *buffer, uint32_t *sequence = nullptr); + int internalPreparedBuffer(); + void internalPostprocessedBuffer(); + + std::map> bufferState_; + std::map expectedSequence_; + std::vector> buffers_; + SequenceSyncHelper syncHelper_; + uint32_t nextSequence_; + std::string name_; + bool ownsBuffers_; + bool hasBuffers_; + int flags_; + std::unique_ptr delegate_; +}; + +} /* namespace libcamera */ diff --git a/include/libcamera/internal/meson.build b/include/libcamera/internal/meson.build index 80c425e13ccd..8d25629daa24 100644 --- a/include/libcamera/internal/meson.build +++ b/include/libcamera/internal/meson.build @@ -4,6 +4,7 @@ subdir('tracepoints') libcamera_internal_headers = files([ 'bayer_format.h', + 'buffer_queue.h', 'byte_stream_buffer.h', 'camera.h', 'camera_controls.h', diff --git a/src/libcamera/buffer_queue.cpp b/src/libcamera/buffer_queue.cpp new file mode 100644 index 000000000000..46dbdd809d08 --- /dev/null +++ b/src/libcamera/buffer_queue.cpp @@ -0,0 +1,449 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2025, Ideas on Board + * + * BufferQueue implementation + */ + +#include "libcamera/internal/buffer_queue.h" + +#include + +namespace libcamera { + +LOG_DEFINE_CATEGORY(BufferQueue) + +/** + * \class BufferQueue + * \brief Helper class to handle buffer queues + * + * Handling buffer queues is a common task when dealing with V4L2 video device. + * Depending on the specific use case, additional functionalities are required: + * - Supporting internally allocated and imported buffers + * - Estimate the sequence number that a buffer will have after dequeuing + * - Correct for errors in that sequence scheme due to frames beeing dropped in + * the kernel. + * - Optionally adding a preparation stage for a buffer before it get's queued + * to the device + * - Optionally adding a postprocessing stage after dequeueing + * + * This class encapsulates these functionalities in one component. To support + * arbitrary V4L2VideoDevice like classes, the actual access to the buffer + * related functions is handled through the BufferQueueDelegateBase interface + * with BufferQueueDelegate providing a default implementation that works with + * the V4L2VideDevice class. + * + * On construction time, it must be specified if a preparation stage and/or a + * prostprocessing stage shall be added. + * + * Internally there are 4 queues, a buffer can be in: + * Idle, Preparing, Capturing, Postprocessing. + * + * After creation of the BufferQueue it is important to route all buffer related + * actions through the queue. That is: + * 1. Allocation/import of buffers + * 2. Queueing buffers + * 3. Releasing buffers + * 4. Handling the bufferReady signal. + * + * The callable functions depend on the flags passed to the constructor and are + * shown on the following state diagram. + * + * *------* + * | Idle | + * *------* + * | +----------------+ + * +-- prepareBuffer() ---->| Preparing | + * | +----------------+ + * | | + * | preparedBuffer() + * | | + * | +----------------+ + * \-- queueBuffer() ------>| Capturing | + * +----------------+ + * | + * +----------------+ + * | Postprocessing | --> bufferReady signal + * +----------------+ + * / | + * /-------------------------------/ | postprocessedBuffer() + * | /--------------------------------/ + * *------* + * | Idle | + * *------* + * + * Notes: + * - If buffers are not allocated by the queue but imported they never end up in + * the idle queue but are passed in by prepareBuffer()/queueBuffer() and leave + * the Queue after postprocessing. + * - If a preparing stage is used, queueBuffer can not be called. + * - If the postprocessing stage is disabled, it will still be used while + * emitting the bufferReady signal but the transition to idle happens + * automatically afterwards. + */ + +/** + * \brief Construct a BufferQueue + * \param[in] delegate The delegate + * \param[in] flags Optional flags + * \param[in] name Optional name + * + * Construct a buffer queue using the given delegate to forward the buffer + * handling to. The default queue has only Idle and queued states. This can be + * changed using the \a flags parameter, to either add a prepare stage, a + * postprocess stage or both. + */ +BufferQueue::BufferQueue(std::unique_ptr &&delegate, + int flags, std::string name) + : nextSequence_(0), name_(std::move(name)), ownsBuffers_(false), + hasBuffers_(false), flags_(flags), delegate_(std::move(delegate)) +{ + delegate_->bufferReady.connect(this, &BufferQueue::onBufferReady); +} + +/** + * \brief Allocate buffers + * \param[in] count The number of buffers to allocate + * + * This function allocates \a count buffers by calling allocateBuffers() on the + * delegate and forwarding the return value. A non negative return code is + * treated as success. The buffers are owned by the BufferQueue. + * + * \return The value returned by the BufferQueueDelegateBase::allocateBuffers() + */ +int BufferQueue::allocateBuffers(unsigned int count) +{ + ASSERT(!hasBuffers_); + buffers_.clear(); + int ret = delegate_->allocateBuffers(count, &buffers_); + if (ret < 0) + return ret; + + for (const auto &buffer : buffers_) + bufferState_[Idle].push_back(buffer.get()); + + hasBuffers_ = true; + ownsBuffers_ = true; + return ret; +} + +/** + * \brief Import buffers + * \param[in] count The number of buffers to import + * + * This function imports \a count buffers by calling importBuffers() on the + * delegate and forwarding the return value. A non negative return code is + * treated as success. + * + * \return The value returned by the BufferQueueDelegateBase::importBuffers() + */ +int BufferQueue::importBuffers(unsigned int count) +{ + int ret = delegate_->importBuffers(count); + if (ret < 0) + return ret; + + hasBuffers_ = true; + ownsBuffers_ = false; + return 0; +} + +int BufferQueue::sequenceCorrection() +{ + return syncHelper_.correction(); +} + +uint32_t BufferQueue::nextSequence() +{ + return nextSequence_ + syncHelper_.correction(); +} + +/** + * \brief Move the next buffer to prepare state + * \param[out] sequence The expected sequence of the buffer + * + * This function moves the front buffer from the idle queue to prepare queue. If + * \a sequence is provided it is set to the expected sequence number of that + * buffer. + * + * \note This function must only be called if the queue has a prepare state and + * owns the buffers. + * + * \return 0 on success, a negative error code otherwise + */ +int BufferQueue::prepareBuffer(uint32_t *sequence) +{ + ASSERT(hasBuffers_); + ASSERT(ownsBuffers_); + ASSERT(flags_ & PrepareStage); + ASSERT(!bufferState_[Idle].empty()); + + FrameBuffer *buffer = bufferState_[Idle].front(); + return prepareBuffer(buffer, sequence); +} + +/** + * \brief Move a buffer to prepare state + * \param[in] buffer The buffer + * \param[out] sequence The expected sequence of the buffer + * + * This function moves \a buffer to prepare queue. If + * \a sequence is provided it is set to the expected sequence number of that + * buffer. + * + * \note This function must only be called if the queue has a prepare state. If + * the queue owns the buffer, \a buffer must point to the front buffer of the + * idle queue. + * + * \return 0 on success, a negative error code otherwise + */ +int BufferQueue::prepareBuffer(FrameBuffer *buffer, uint32_t *sequence) +{ + ASSERT(flags_ & PrepareStage); + + return internalPrepareBuffer(buffer, sequence); +} + +/** + * \brief Exit prepare state + * + * This function pops the frontmost buffer from the prepare queue and queues it + * on the underlying device by calling queueBuffer() on the delegate. + * + * \note This function must only be called if the queue has a prepare state. + * + * \return 0 on success, a negative error code otherwise + */ +int BufferQueue::preparedBuffer() +{ + ASSERT(flags_ & PrepareStage); + + return internalPreparedBuffer(); +} + +/** + * \brief Queue the next buffer + * \param[out] sequence The expected sequence of the buffer + * + * This function queues the front buffer from the idle queue to the underlying + * device ba calling queueBuffer() on the delegate. If \a sequence is provided + * it is set to the expected sequence number of that buffer. + * + * \note This function must only be called if the queue does not have a prepare + * state and owns the buffers. + * + * \return 0 on success, a negative error code otherwise + */ +int BufferQueue::queueBuffer(uint32_t *sequence) +{ + ASSERT(hasBuffers_); + ASSERT(ownsBuffers_); + ASSERT(!bufferState_[Idle].empty()); + + FrameBuffer *buffer = bufferState_[Idle].front(); + return queueBuffer(buffer, sequence); +} + +/** + * \brief Queue a buffer + * \param[in] buffer The buffer + * \param[out] sequence The expected sequence of the buffer + * + * This function queues \a buffer to the underlying device ba calling + * queueBuffer() on the delegate. If \a sequence is provided it is set to the + * expected sequence number of that buffer. + * + * \note This function must only be called if the queue does not have a prepare + * state. If the queue owns the buffers, \a buffer must point to the front + * buffer of the idle queue. + * + * \return 0 on success, a negative error code otherwise + */ +int BufferQueue::queueBuffer(FrameBuffer *buffer, uint32_t *sequence) +{ + ASSERT(!(flags_ & PrepareStage)); + return internalPrepareBuffer(buffer, sequence); +} + +/** + * \brief Exit postprocessed state + * + * This function pops the frontmost buffer from the postprocess queue and puts it + * back to the idle queue in case the buffers are owned by the queue + * + * \note This function must only be called if the queue has a prepare state. + */ +void BufferQueue::postprocessedBuffer() +{ + ASSERT(hasBuffers_); + ASSERT(flags_ & PostprocessStage); + return internalPostprocessedBuffer(); +} + +/** + * \brief Release buffers + * + * This function releases the allocated or imported buffers by calling + * releaseBuffers() on the delegate. + * + * \return 0 on success, a negative error code otherwise + */ +int BufferQueue::releaseBuffers() +{ + ASSERT(bufferState_[BufferQueue::Idle].size() == buffers_.size()); + + bufferState_[BufferQueue::Idle] = {}; + buffers_.clear(); + hasBuffers_ = false; + + return delegate_->releaseBuffers(); +} + +/** + * \brief Check if queue is empty + * \param[in] state The state + * + * \return True if the queue for the given state is empty, false otherwise + */ +bool BufferQueue::empty(BufferQueue::State state) +{ + return bufferState_[state].empty(); +} + +/** + * \brief Get the front buffer of a queue + * \param[in] state The state + * + * \return The front buffer of the queue, or null otherwise + */ +FrameBuffer *BufferQueue::front(BufferQueue::State state) +{ + if (empty(state)) + return nullptr; + return bufferState_[state].front(); +} + +/** + * \brief Get the expected sequence for a buffer + * \param[in] buffer The buffer + * + * \return The front buffer of the queue, or null otherwise + */ +unsigned int BufferQueue::expectedSequence(FrameBuffer *buffer) const +{ + auto it = expectedSequence_.find(buffer); + ASSERT(it != expectedSequence_.end()); + return it->second; +} + +/** + * \brief Get the allocated buffers + * + * \return The buffers owned by the queue + */ +const std::vector> &BufferQueue::buffers() const +{ + return buffers_; +} + +void BufferQueue::onBufferReady(FrameBuffer *buffer) +{ + ASSERT(!empty(Capturing)); + + auto &meta = buffer->metadata(); + auto &queue = bufferState_[Capturing]; + + /* + * V4L2 does not guarantee that buffers are dequeued in order. We expect + * drivers to usually do so, and therefore warn, if a buffer is returned + * out of order. After streamoff V4L2VideoDevice returns the buffers in + * arbitrary order so there is no warning needed in that case. + */ + auto it = std::find(queue.begin(), queue.end(), buffer); + ASSERT(it != queue.end()); + + if (it != queue.begin() && + meta.status != FrameMetadata::FrameCancelled) + LOG(BufferQueue, Warning) << name_ << ": Dequeued buffer out of order " << buffer; + + queue.erase(it); + if (meta.status == FrameMetadata::FrameCancelled) { + syncHelper_.cancelFrame(); + if (ownsBuffers_) + bufferState_[Idle].push_back(buffer); + } else { + syncHelper_.gotFrame(expectedSequence_[buffer], meta.sequence); + bufferState_[Postprocessing].push_back(buffer); + } + + bufferReady.emit(buffer); + + if (!(flags_ & PostprocessStage) && + meta.status != FrameMetadata::FrameCancelled) + internalPostprocessedBuffer(); +} + +int BufferQueue::internalPrepareBuffer(FrameBuffer *buffer, uint32_t *sequence) +{ + ASSERT(hasBuffers_); + + if (ownsBuffers_) { + ASSERT(!bufferState_[Idle].empty()); + ASSERT(bufferState_[Idle].front() == buffer); + } + + LOG(BufferQueue, Debug) << name_ << ":Buffer prepare: " + << buffer; + int correction = syncHelper_.correction(); + nextSequence_ += correction; + expectedSequence_[buffer] = nextSequence_; + if (ownsBuffers_) + bufferState_[Idle].pop_front(); + bufferState_[Preparing].push_back(buffer); + syncHelper_.pushCorrection(correction); + + if (sequence) + *sequence = nextSequence_; + + nextSequence_++; + + if (!(flags_ & PrepareStage)) + return internalPreparedBuffer(); + + return 0; +} + +int BufferQueue::internalPreparedBuffer() +{ + ASSERT(!bufferState_[Preparing].empty()); + + auto &srcQueue = bufferState_[Preparing]; + FrameBuffer *buffer = srcQueue.front(); + + srcQueue.pop_front(); + int ret = delegate_->queueBuffer(buffer); + if (ret < 0) { + LOG(BufferQueue, Error) << "Failed to queue buffer: " + << strerror(-ret); + if (ownsBuffers_) + bufferState_[Idle].push_back(buffer); + return ret; + } + + LOG(BufferQueue, Debug) << name_ << " Queued buffer: " << buffer; + + bufferState_[Capturing].push_back(buffer); + return 0; +} + +void BufferQueue::internalPostprocessedBuffer() +{ + ASSERT(!empty(Postprocessing)); + + FrameBuffer *buffer = bufferState_[Postprocessing].front(); + bufferState_[Postprocessing].pop_front(); + if (ownsBuffers_) + bufferState_[Idle].push_back(buffer); +} + +} /* namespace libcamera */ diff --git a/src/libcamera/meson.build b/src/libcamera/meson.build index 8b82555a5e81..55bdb895f6b4 100644 --- a/src/libcamera/meson.build +++ b/src/libcamera/meson.build @@ -18,6 +18,7 @@ libcamera_public_sources = files([ libcamera_internal_sources = files([ 'bayer_format.cpp', + 'buffer_queue.cpp', 'byte_stream_buffer.cpp', 'camera_controls.cpp', 'camera_lens.cpp',