From patchwork Mon Jun 29 16:30:09 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= X-Patchwork-Id: 27126 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 36E1AC3304 for ; Mon, 29 Jun 2026 16:31:39 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id C7FE665F9B; Mon, 29 Jun 2026 18:31:38 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="oFjDtmbe"; dkim-atps=neutral Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [213.167.242.64]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 90E3465F6B for ; Mon, 29 Jun 2026 18:30:31 +0200 (CEST) Received: from pb-laptop.local (185.221.140.128.nat.pool.zt.hu [185.221.140.128]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id 624BD1044 for ; Mon, 29 Jun 2026 18:29:48 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1782750588; bh=VXWpR7gmQO/R6CcvpP28AdLvMQoFvogtaR38wHx23MI=; h=From:To:Subject:Date:In-Reply-To:References:From; b=oFjDtmbeBX6oVHTzLyvET4pjZ96GsW12AFIJ1hvKWfjaoJvEisgi1rD63WBARmSBY APCX9sPl0O51J5tw3iJznfe17FDA4aCvCW8GnmfBM5Qzx1gzp0UgwHre65nuZH2z8+ cGKodFxt7IRoDvQtm3nxqyVktAnJvBiDqSO+SeVk= From: =?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= To: libcamera-devel@lists.libcamera.org Subject: [RFC PATCH v1 46/54] py: Use camera buffer pool Date: Mon, 29 Jun 2026 18:30:09 +0200 Message-ID: <20260629163017.863145-47-barnabas.pocze@ideasonboard.com> X-Mailer: git-send-email 2.54.0 In-Reply-To: <20260629163017.863145-1-barnabas.pocze@ideasonboard.com> References: <20260629163017.863145-1-barnabas.pocze@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" Port to using the camera's buffer pool. Signed-off-by: Barnabás Pőcze --- src/py/cam/cam.py | 20 +++--- src/py/examples/simple-cam.py | 13 +++- src/py/examples/simple-capture.py | 20 +++--- src/py/examples/simple-continuous-capture.py | 32 ++++++---- src/py/libcamera/py_main.cpp | 64 ++++++++++++++------ 5 files changed, 101 insertions(+), 48 deletions(-) diff --git a/src/py/cam/cam.py b/src/py/cam/cam.py index 73764a8af7..1752960030 100755 --- a/src/py/cam/cam.py +++ b/src/py/cam/cam.py @@ -205,13 +205,6 @@ class CameraContext: for buf_num in range(num_bufs): request = self.camera.create_request(self.idx) - - for stream in self.streams: - buffers = self.allocator.buffers(stream) - buffer = buffers[buf_num] - - request.add_buffer(stream, buffer) - requests.append(request) self.requests = requests @@ -223,7 +216,14 @@ class CameraContext: self.camera.stop() def queue_requests(self): + for stream in self.streams: + for buffer in self.allocator.buffers(stream): + self.camera.add_buffer(stream, buffer) + for request in self.requests: + for stream in self.streams: + request.enable_stream(stream, True) + self.camera.queue_request(request) self.reqs_queued += 1 @@ -305,7 +305,13 @@ class CaptureState: # Called from renderer when it has finished with a request def request_processed(self, ctx, req): if ctx.reqs_queued < ctx.opt_capture: + for (stream, buffer) in req.buffers.items(): + ctx.camera.add_buffer(stream, buffer) + req.reuse() + for stream in ctx.streams: + req.enable_stream(stream, True) + ctx.camera.queue_request(req) ctx.reqs_queued += 1 diff --git a/src/py/examples/simple-cam.py b/src/py/examples/simple-cam.py index 1cd1019da9..f2e28a94ef 100755 --- a/src/py/examples/simple-cam.py +++ b/src/py/examples/simple-cam.py @@ -73,7 +73,12 @@ def process_request(request): # must be mapped by the application # Re-queue the Request to the camera. + for (stream, buffer) in request.buffers.items(): + camera.add_buffer(stream, buffer) + request.reuse() + request.enable_stream(stream, True) + camera.queue_request(request) @@ -285,9 +290,6 @@ def main(): for i in range(len(buffers)): request = camera.create_request() - buffer = buffers[i] - request.add_buffer(stream, buffer) - # Controls can be added to a request on a per frame basis. request.set_control(libcam.controls.Brightness, 0.5) @@ -311,7 +313,12 @@ def main(): # be waited upon using e.g. Python's selectors. camera.start() + + for buffer in buffers: + camera.add_buffer(stream, buffer) + for request in requests: + request.enable_stream(stream, True) camera.queue_request(request) sel = selectors.DefaultSelector() diff --git a/src/py/examples/simple-capture.py b/src/py/examples/simple-capture.py index 4b85408fac..00657f78ad 100755 --- a/src/py/examples/simple-capture.py +++ b/src/py/examples/simple-capture.py @@ -69,20 +69,15 @@ def main(): allocator = libcam.FrameBufferAllocator(cam) ret = allocator.allocate(stream) - assert ret > 0 - - num_bufs = len(allocator.buffers(stream)) + buffers = allocator.buffers(stream) + assert ret > 0 and len(buffers) > 0 # Create the requests and assign a buffer for each request reqs = [] - for i in range(num_bufs): + for i in range(len(buffers)): # Use the buffer index as the cookie req = cam.create_request(i) - - buffer = allocator.buffers(stream)[i] - req.add_buffer(stream, buffer) - reqs.append(req) # Start the camera @@ -96,7 +91,11 @@ def main(): # Queue the requests to the camera + for buffer in buffers: + cam.add_buffer(stream, buffer) + for req in reqs: + req.enable_stream(stream, True) cam.queue_request(req) frames_queued += 1 @@ -144,7 +143,12 @@ def main(): # We could create a totally new Request, but it is more efficient # to reuse the existing one that we just received. if frames_queued < TOTAL_FRAMES: + for (stream, buffer) in req.buffers.items(): + cam.add_buffer(stream, buffer) + req.reuse() + req.enable_stream(stream, True) + cam.queue_request(req) frames_queued += 1 diff --git a/src/py/examples/simple-continuous-capture.py b/src/py/examples/simple-continuous-capture.py index 5ae841519e..96cb79b43f 100755 --- a/src/py/examples/simple-continuous-capture.py +++ b/src/py/examples/simple-continuous-capture.py @@ -19,8 +19,10 @@ import sys class CameraCaptureContext: idx: int cam: libcam.Camera + cam_config: libcam.CameraConfiguration reqs: list[libcam.Request] mfbs: dict[libcam.FrameBuffer, libcamera.utils.MappedFrameBuffer] + buffers: list[libcam.FrameBuffer] def __init__(self, cam, idx): self.idx = idx @@ -32,11 +34,11 @@ class CameraCaptureContext: # Configure the camera - cam_config = cam.generate_configuration([libcam.StreamRole.Viewfinder]) + self.cam_config = cam.generate_configuration([libcam.StreamRole.Viewfinder]) - stream_config = cam_config.at(0) + stream_config = self.cam_config.at(0) - cam.configure(cam_config) + cam.configure(self.cam_config) stream = stream_config.stream @@ -44,24 +46,19 @@ class CameraCaptureContext: allocator = libcam.FrameBufferAllocator(cam) ret = allocator.allocate(stream) - assert ret > 0 + self.buffers = allocator.buffers(stream) + assert ret > 0 and len(self.buffers) > 0 - num_bufs = len(allocator.buffers(stream)) - - print(f'cam{idx} ({cam.id}): capturing {num_bufs} buffers with {stream_config}') + print(f'cam{idx} ({cam.id}): capturing {len(self.buffers)} buffers with {stream_config}') # Create the requests and assign a buffer for each request self.reqs = [] self.mfbs = {} - for i in range(num_bufs): + for buffer in self.buffers: # Use the camera index as the "cookie" req = cam.create_request(idx) - - buffer = allocator.buffers(stream)[i] - req.add_buffer(stream, buffer) - self.reqs.append(req) # Save a mmapped buffer so we can calculate the CRC later @@ -127,7 +124,12 @@ class CaptureContext: # a new Request, we re-use the old one. We need to call req.reuse() # to re-initialize the Request before queuing. + for (stream, buffer) in req.buffers.items(): + cam_ctx.cam.add_buffer(stream, buffer) + req.reuse() + req.enable_stream(stream, True) + cam_ctx.cam.queue_request(req) def handle_key_event(self): @@ -139,7 +141,13 @@ class CaptureContext: # Queue the requests to the camera for cam_ctx in self.camera_contexts: + stream = cam_ctx.cam_config.at(0).stream + + for buffer in cam_ctx.buffers: + cam_ctx.cam.add_buffer(stream, buffer) + for req in cam_ctx.reqs: + req.enable_stream(stream, True) cam_ctx.cam.queue_request(req) # Use Selector to wait for events from the camera and from the keyboard diff --git a/src/py/libcamera/py_main.cpp b/src/py/libcamera/py_main.cpp index 3b101f9961..26b348ef0e 100644 --- a/src/py/libcamera/py_main.cpp +++ b/src/py/libcamera/py_main.cpp @@ -110,6 +110,16 @@ PYBIND11_MODULE(_libcamera, m) * https://pybind11.readthedocs.io/en/latest/advanced/misc.html#avoiding-c-types-in-docstrings */ + struct RequestDeleter : std::default_delete { + void operator()(Request *request) + { + for (const auto &[stream, buffer] : request->buffers()) + py::cast(buffer).dec_ref(); + + std::default_delete::operator()(request); + } + }; + auto pyCameraManager = py::class_>(m, "CameraManager"); auto pyCamera = py::class_(m, "Camera"); auto pySensorConfiguration = py::class_(m, "SensorConfiguration"); @@ -123,7 +133,7 @@ PYBIND11_MODULE(_libcamera, m) auto pyStream = py::class_(m, "Stream"); auto pyControlId = py::class_(m, "ControlId"); auto pyControlInfo = py::class_(m, "ControlInfo"); - auto pyRequest = py::class_(m, "Request"); + auto pyRequest = py::class_>(m, "Request"); auto pyRequestStatus = py::enum_(pyRequest, "Status"); auto pyRequestReuse = py::enum_(pyRequest, "Reuse"); auto pyFrameMetadata = py::class_(m, "FrameMetadata"); @@ -195,10 +205,21 @@ PYBIND11_MODULE(_libcamera, m) }, py::arg("controls") = std::unordered_map()) .def("stop", [](Camera &self) { + std::vector queued; + + self.bufferCompleted.connect(&queued, [&](Request *request, const Stream *, FrameBuffer *buffer) { + if (!request) + queued.push_back(buffer); + }); + int ret = self.stop(); + self.bufferCompleted.disconnect(&queued); self.requestCompleted.disconnect(); + for (auto *buffer : queued) + py::cast(buffer).dec_ref(); + if (ret) throw std::system_error(-ret, std::generic_category(), "Failed to stop camera"); @@ -221,7 +242,7 @@ PYBIND11_MODULE(_libcamera, m) }) .def("create_request", [](Camera &self, uint64_t cookie) { - std::unique_ptr req = self.createRequest(cookie); + std::unique_ptr req(self.createRequest(cookie).release()); if (!req) throw std::system_error(ENOMEM, std::generic_category(), "Failed to create request"); @@ -246,6 +267,20 @@ PYBIND11_MODULE(_libcamera, m) } }) + /* \todo Fence is not supported */ + .def("add_buffer", [](Camera &self, const Stream *stream, FrameBuffer *buffer) { + py::object py_buf = py::cast(buffer); + + py_buf.inc_ref(); + + int ret = self.addBuffer(stream, buffer); + if (ret) { + py_buf.dec_ref(); + throw std::system_error(-ret, std::generic_category(), + "Failed to add buffer"); + } + }) + .def_property_readonly("streams", [](Camera &self) { py::set set; for (auto &s : self.streams()) { @@ -457,13 +492,6 @@ PYBIND11_MODULE(_libcamera, m) }); pyRequest - /* \todo Fence is not supported, so we cannot expose addBuffer() directly */ - .def("add_buffer", [](Request &self, const Stream *stream, FrameBuffer *buffer) { - int ret = self.addBuffer(stream, buffer); - if (ret) - throw std::system_error(-ret, std::generic_category(), - "Failed to add buffer"); - }, py::keep_alive<1, 3>()) /* Request keeps Framebuffer alive */ .def_property_readonly("status", &Request::status) .def_property_readonly("buffers", &Request::buffers) .def_property_readonly("cookie", &Request::cookie) @@ -485,11 +513,15 @@ PYBIND11_MODULE(_libcamera, m) return ret; }) - /* - * \todo As we add a keep_alive to the fb in addBuffers(), we - * can only allow reuse with ReuseBuffers. - */ - .def("reuse", [](Request &self) { self.reuse(Request::ReuseFlag::ReuseBuffers); }) + .def("reuse", [](Request &self) { + for (const auto &[stream, buffer] : self.buffers()) + py::cast(buffer).dec_ref(); + + self.reuse(); + }) + .def("enable_stream", [](Request &self, const Stream *stream, bool enabled) { + self.enableStream(stream, enabled); + }) .def("__str__", &Request::toString); pyRequestStatus @@ -497,10 +529,6 @@ PYBIND11_MODULE(_libcamera, m) .value("Complete", Request::RequestComplete) .value("Cancelled", Request::RequestCancelled); - pyRequestReuse - .value("Default", Request::ReuseFlag::Default) - .value("ReuseBuffers", Request::ReuseFlag::ReuseBuffers); - pyFrameMetadata .def_readonly("status", &FrameMetadata::status) .def_readonly("sequence", &FrameMetadata::sequence)