[RFC,v1,46/54] py: Use camera buffer pool
diff mbox series

Message ID 20260629163017.863145-47-barnabas.pocze@ideasonboard.com
State New
Headers show
Series
  • libcamera: Split requests and buffers
Related show

Commit Message

Barnabás Pőcze June 29, 2026, 4:30 p.m. UTC
Port to using the camera's buffer pool.

Signed-off-by: Barnabás Pőcze <barnabas.pocze@ideasonboard.com>
---
 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(-)

Patch
diff mbox series

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<Request> {
+		void operator()(Request *request)
+		{
+			for (const auto &[stream, buffer] : request->buffers())
+				py::cast(buffer).dec_ref();
+
+			std::default_delete<Request>::operator()(request);
+		}
+	};
+
 	auto pyCameraManager = py::class_<PyCameraManager, std::shared_ptr<PyCameraManager>>(m, "CameraManager");
 	auto pyCamera = py::class_<Camera, PyCameraHolder>(m, "Camera");
 	auto pySensorConfiguration = py::class_<SensorConfiguration>(m, "SensorConfiguration");
@@ -123,7 +133,7 @@  PYBIND11_MODULE(_libcamera, m)
 	auto pyStream = py::class_<Stream>(m, "Stream");
 	auto pyControlId = py::class_<ControlId>(m, "ControlId");
 	auto pyControlInfo = py::class_<ControlInfo>(m, "ControlInfo");
-	auto pyRequest = py::class_<Request>(m, "Request");
+	auto pyRequest = py::class_<Request, std::unique_ptr<Request, RequestDeleter>>(m, "Request");
 	auto pyRequestStatus = py::enum_<Request::Status>(pyRequest, "Status");
 	auto pyRequestReuse = py::enum_<Request::ReuseFlag>(pyRequest, "Reuse");
 	auto pyFrameMetadata = py::class_<FrameMetadata>(m, "FrameMetadata");
@@ -195,10 +205,21 @@  PYBIND11_MODULE(_libcamera, m)
 		}, py::arg("controls") = std::unordered_map<const ControlId *, py::object>())
 
 		.def("stop", [](Camera &self) {
+			std::vector<FrameBuffer *> 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<Request> req = self.createRequest(cookie);
+			std::unique_ptr<Request, RequestDeleter> 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)