[3/5] pipeline_handler: Add acquireDevice() method to mirror existing releaseDevice()
diff mbox series

Message ID 20240820195016.16028-4-hdegoede@redhat.com
State Superseded
Headers show
Series
  • Fix uvcvideo pipelinehandler keeping /dev/video# open
Related show

Commit Message

Hans de Goede Aug. 20, 2024, 7:50 p.m. UTC
ATM libcamera always keeps all /dev/video# and /dev/v4l2_subdev# nodes
for a pipeline open after enumerating the camera.

This is a problem for uvcvideo and atomisp /dev/video# nodes as well as
for VCM /dev/v4l2_subdev# nodes. Keeping these open stops the underlying
hw-devices from being able to enter runtime-suspend causing significant
unnecessary power-usage.

Add a stub acquireDevice() method to the PipelineHandler class which
pipeline handlers can override to delay opening /dev nodes until
the camera is acquired.

Note pipeline handlers typically also will need access to /dev nodes
for their CameraConfig::validate() implementation for tryFormat() calls.
Making sure that /dev nodes are opened as necessary from validate() is
left up to the pipeline handler implementation.

Signed-off-by: Hans de Goede <hdegoede@redhat.com>
---
 include/libcamera/internal/pipeline_handler.h |  3 +-
 src/libcamera/camera.cpp                      |  2 +-
 src/libcamera/pipeline_handler.cpp            | 43 +++++++++++++++----
 3 files changed, 37 insertions(+), 11 deletions(-)

Comments

Cheng-Hao Yang Aug. 21, 2024, 4:55 p.m. UTC | #1
Hi Hans,

Thanks for the patch. Actually in mtkisp7, we did something very similar
[1].
(And I just caught a bug by checking your changes. Thanks :)

[1]:
https://chromium-review.googlesource.com/c/chromiumos/third_party/libcamera/+/5525900

On Tue, Aug 20, 2024 at 9:50 PM Hans de Goede <hdegoede@redhat.com> wrote:

> ATM libcamera always keeps all /dev/video# and /dev/v4l2_subdev# nodes
> for a pipeline open after enumerating the camera.
>
> This is a problem for uvcvideo and atomisp /dev/video# nodes as well as
> for VCM /dev/v4l2_subdev# nodes. Keeping these open stops the underlying
> hw-devices from being able to enter runtime-suspend causing significant
> unnecessary power-usage.
>
> Add a stub acquireDevice() method to the PipelineHandler class which
> pipeline handlers can override to delay opening /dev nodes until
> the camera is acquired.
>
> Note pipeline handlers typically also will need access to /dev nodes
> for their CameraConfig::validate() implementation for tryFormat() calls.
> Making sure that /dev nodes are opened as necessary from validate() is
> left up to the pipeline handler implementation.
>
> Signed-off-by: Hans de Goede <hdegoede@redhat.com>
> ---
>  include/libcamera/internal/pipeline_handler.h |  3 +-
>  src/libcamera/camera.cpp                      |  2 +-
>  src/libcamera/pipeline_handler.cpp            | 43 +++++++++++++++----
>  3 files changed, 37 insertions(+), 11 deletions(-)
>
> diff --git a/include/libcamera/internal/pipeline_handler.h
> b/include/libcamera/internal/pipeline_handler.h
> index cad5812f..597f7c3e 100644
> --- a/include/libcamera/internal/pipeline_handler.h
> +++ b/include/libcamera/internal/pipeline_handler.h
> @@ -45,7 +45,7 @@ public:
>         MediaDevice *acquireMediaDevice(DeviceEnumerator *enumerator,
>                                         const DeviceMatch &dm);
>
> -       bool acquire();
> +       bool acquire(Camera *camera);
>         void release(Camera *camera);
>
>         virtual std::unique_ptr<CameraConfiguration>
> generateConfiguration(Camera *camera,
> @@ -79,6 +79,7 @@ protected:
>         virtual int queueRequestDevice(Camera *camera, Request *request) =
> 0;
>         virtual void stopDevice(Camera *camera) = 0;
>
> +       virtual bool acquireDevice(Camera *camera);
>         virtual void releaseDevice(Camera *camera);
>
>         CameraManager *manager_;
> diff --git a/src/libcamera/camera.cpp b/src/libcamera/camera.cpp
> index 382a68f7..4e393f89 100644
> --- a/src/libcamera/camera.cpp
> +++ b/src/libcamera/camera.cpp
> @@ -995,7 +995,7 @@ int Camera::acquire()
>         if (ret < 0)
>                 return ret == -EACCES ? -EBUSY : ret;
>
> -       if (!d->pipe_->acquire()) {
> +       if (!d->pipe_->acquire(this)) {
>                 LOG(Camera, Info)
>                         << "Pipeline handler in use by another process";
>                 return -EBUSY;
> diff --git a/src/libcamera/pipeline_handler.cpp
> b/src/libcamera/pipeline_handler.cpp
> index 1fc22d6a..e1342306 100644
> --- a/src/libcamera/pipeline_handler.cpp
> +++ b/src/libcamera/pipeline_handler.cpp
> @@ -163,20 +163,22 @@ MediaDevice
> *PipelineHandler::acquireMediaDevice(DeviceEnumerator *enumerator,
>   * has already acquired it
>   * \sa release()
>   */
> -bool PipelineHandler::acquire()
> +bool PipelineHandler::acquire(Camera *camera)
>  {
>         MutexLocker locker(lock_);
>
> -       if (useCount_) {
> -               ++useCount_;
> -               return true;
> +       if (useCount_ == 0) {
> +               for (std::shared_ptr<MediaDevice> &media : mediaDevices_) {
> +                       if (!media->lock()) {
> +                               unlockMediaDevices();
> +                               return false;
> +                       }
> +               }
>         }
>
> -       for (std::shared_ptr<MediaDevice> &media : mediaDevices_) {
> -               if (!media->lock()) {
> -                       unlockMediaDevices();
> -                       return false;
> -               }
> +       if (!acquireDevice(camera)) {
> +               unlockMediaDevices();
>

I think we should only unlock media devices if it's the first acquire
being called, as there might be other ongoing usages on other
cameras [2]. Therefore:
```
                    if (useCount_ == 0)
                            unlockMediaDevices();
```

[2]:
https://chromium-review.googlesource.com/c/chromiumos/third_party/libcamera/+/5525900/7/src/libcamera/pipeline_handler.cpp#181

+               return false;
>         }
>
>         ++useCount_;
> @@ -213,12 +215,35 @@ void PipelineHandler::release(Camera *camera)
>         --useCount_;
>  }
>
> +/**
> + * \brief Acquire resources associated with this camera
> + * \param[in] camera The camera for which to acquire resources
> + *
> + * Pipeline handlers may override this in order to get resources
> + * such as open /dev nodes are allocate buffers when a camera is
> + * acquired.
> + *
> + * Note this is called once for every camera which is acquired,
> + * so if there are shared resources the pipeline handler must take
> + * care to not release them until releaseDevice() has been called for
> + * all previously acquired cameras.
> + *
> + * \return True on success, false on failure.
> + * \sa releaseDevice()
> + */
> +bool PipelineHandler::acquireDevice([[maybe_unused]] Camera *camera)
> +{
> +       return true;
> +}
> +
>  /**
>   * \brief Release resources associated with this camera
>   * \param[in] camera The camera for which to release resources
>   *
>   * Pipeline handlers may override this in order to perform cleanup
> operations
>   * when a camera is released, such as freeing memory.
> + *
> + * \sa acquireDevice()
>   */
>  void PipelineHandler::releaseDevice([[maybe_unused]] Camera *camera)
>  {
> --
> 2.46.0
>
>
BR,
Harvey
Laurent Pinchart Aug. 25, 2024, 12:49 a.m. UTC | #2
On Wed, Aug 21, 2024 at 06:55:45PM +0200, Cheng-Hao Yang wrote:
> Hi Hans,
> 
> Thanks for the patch. Actually in mtkisp7, we did something very similar
> [1].
> (And I just caught a bug by checking your changes. Thanks :)
> 
> [1]:
> https://chromium-review.googlesource.com/c/chromiumos/third_party/libcamera/+/5525900
> 
> On Tue, Aug 20, 2024 at 9:50 PM Hans de Goede <hdegoede@redhat.com> wrote:
> 
> > ATM libcamera always keeps all /dev/video# and /dev/v4l2_subdev# nodes
> > for a pipeline open after enumerating the camera.
> >
> > This is a problem for uvcvideo and atomisp /dev/video# nodes as well as
> > for VCM /dev/v4l2_subdev# nodes. Keeping these open stops the underlying
> > hw-devices from being able to enter runtime-suspend causing significant
> > unnecessary power-usage.
> >
> > Add a stub acquireDevice() method to the PipelineHandler class which
> > pipeline handlers can override to delay opening /dev nodes until
> > the camera is acquired.
> >
> > Note pipeline handlers typically also will need access to /dev nodes
> > for their CameraConfig::validate() implementation for tryFormat() calls.
> > Making sure that /dev nodes are opened as necessary from validate() is
> > left up to the pipeline handler implementation.
> >
> > Signed-off-by: Hans de Goede <hdegoede@redhat.com>
> > ---
> >  include/libcamera/internal/pipeline_handler.h |  3 +-
> >  src/libcamera/camera.cpp                      |  2 +-
> >  src/libcamera/pipeline_handler.cpp            | 43 +++++++++++++++----
> >  3 files changed, 37 insertions(+), 11 deletions(-)
> >
> > diff --git a/include/libcamera/internal/pipeline_handler.h
> > b/include/libcamera/internal/pipeline_handler.h
> > index cad5812f..597f7c3e 100644
> > --- a/include/libcamera/internal/pipeline_handler.h
> > +++ b/include/libcamera/internal/pipeline_handler.h
> > @@ -45,7 +45,7 @@ public:
> >         MediaDevice *acquireMediaDevice(DeviceEnumerator *enumerator,
> >                                         const DeviceMatch &dm);
> >
> > -       bool acquire();
> > +       bool acquire(Camera *camera);
> >         void release(Camera *camera);
> >
> >         virtual std::unique_ptr<CameraConfiguration>
> > generateConfiguration(Camera *camera,
> > @@ -79,6 +79,7 @@ protected:
> >         virtual int queueRequestDevice(Camera *camera, Request *request) =
> > 0;
> >         virtual void stopDevice(Camera *camera) = 0;
> >
> > +       virtual bool acquireDevice(Camera *camera);
> >         virtual void releaseDevice(Camera *camera);
> >
> >         CameraManager *manager_;
> > diff --git a/src/libcamera/camera.cpp b/src/libcamera/camera.cpp
> > index 382a68f7..4e393f89 100644
> > --- a/src/libcamera/camera.cpp
> > +++ b/src/libcamera/camera.cpp
> > @@ -995,7 +995,7 @@ int Camera::acquire()
> >         if (ret < 0)
> >                 return ret == -EACCES ? -EBUSY : ret;
> >
> > -       if (!d->pipe_->acquire()) {
> > +       if (!d->pipe_->acquire(this)) {
> >                 LOG(Camera, Info)
> >                         << "Pipeline handler in use by another process";
> >                 return -EBUSY;
> > diff --git a/src/libcamera/pipeline_handler.cpp
> > b/src/libcamera/pipeline_handler.cpp
> > index 1fc22d6a..e1342306 100644
> > --- a/src/libcamera/pipeline_handler.cpp
> > +++ b/src/libcamera/pipeline_handler.cpp
> > @@ -163,20 +163,22 @@ MediaDevice
> > *PipelineHandler::acquireMediaDevice(DeviceEnumerator *enumerator,
> >   * has already acquired it
> >   * \sa release()
> >   */
> > -bool PipelineHandler::acquire()
> > +bool PipelineHandler::acquire(Camera *camera)
> >  {
> >         MutexLocker locker(lock_);
> >
> > -       if (useCount_) {
> > -               ++useCount_;
> > -               return true;
> > +       if (useCount_ == 0) {
> > +               for (std::shared_ptr<MediaDevice> &media : mediaDevices_) {
> > +                       if (!media->lock()) {
> > +                               unlockMediaDevices();
> > +                               return false;
> > +                       }
> > +               }
> >         }
> >
> > -       for (std::shared_ptr<MediaDevice> &media : mediaDevices_) {
> > -               if (!media->lock()) {
> > -                       unlockMediaDevices();
> > -                       return false;
> > -               }
> > +       if (!acquireDevice(camera)) {
> > +               unlockMediaDevices();
> >
> 
> I think we should only unlock media devices if it's the first acquire
> being called, as there might be other ongoing usages on other
> cameras [2]. Therefore:
> ```
>                     if (useCount_ == 0)
>                             unlockMediaDevices();
> ```
> 
> [2]:
> https://chromium-review.googlesource.com/c/chromiumos/third_party/libcamera/+/5525900/7/src/libcamera/pipeline_handler.cpp#181
> 
> +               return false;
> >         }
> >
> >         ++useCount_;
> > @@ -213,12 +215,35 @@ void PipelineHandler::release(Camera *camera)
> >         --useCount_;
> >  }
> >
> > +/**
> > + * \brief Acquire resources associated with this camera
> > + * \param[in] camera The camera for which to acquire resources
> > + *
> > + * Pipeline handlers may override this in order to get resources
> > + * such as open /dev nodes are allocate buffers when a camera is

s@open /dev nodes@opening devices/
s/are allocate/and allocating/

As discussed previously, I think we need to improve the
application-facing API to make resource and power management more
explicit. Camera::acquire() was never meant for this purpose. I
understand we need a short term solution to the power consumption issue
for UVC cameras without an enormous amount of yak shaving, and I'm fine
with this patch series as an interim measure, but I don't want to see
acquireDevice() being used in random ways by pipeline handlers other
than UVC before we address the more general problem. This should be
documented here.

> > + * acquired.

Please reflow these paragraphs to 80 columns.

> > + *
> > + * Note this is called once for every camera which is acquired,

s/which/that/

> > + * so if there are shared resources the pipeline handler must take

s/shared resources/resources shared by multiple cameras/

> > + * care to not release them until releaseDevice() has been called for
> > + * all previously acquired cameras.

Doesn't this paragraph belong to releaseDevice() ?

> > + *
> > + * \return True on success, false on failure.
> > + * \sa releaseDevice()
> > + */
> > +bool PipelineHandler::acquireDevice([[maybe_unused]] Camera *camera)
> > +{
> > +       return true;
> > +}
> > +
> >  /**
> >   * \brief Release resources associated with this camera
> >   * \param[in] camera The camera for which to release resources
> >   *
> >   * Pipeline handlers may override this in order to perform cleanup
> > operations
> >   * when a camera is released, such as freeing memory.
> > + *
> > + * \sa acquireDevice()
> >   */
> >  void PipelineHandler::releaseDevice([[maybe_unused]] Camera *camera)
> >  {
Hans de Goede Aug. 27, 2024, 2:11 p.m. UTC | #3
Hi,

On 8/21/24 6:55 PM, Cheng-Hao Yang wrote:
> Hi Hans,
> 
> Thanks for the patch. Actually in mtkisp7, we did something very similar [1].
> (And I just caught a bug by checking your changes. Thanks :)
> 
> [1]: https://chromium-review.googlesource.com/c/chromiumos/third_party/libcamera/+/5525900 <https://chromium-review.googlesource.com/c/chromiumos/third_party/libcamera/+/5525900>
> 
> On Tue, Aug 20, 2024 at 9:50 PM Hans de Goede <hdegoede@redhat.com <mailto:hdegoede@redhat.com>> wrote:
> 
>     ATM libcamera always keeps all /dev/video# and /dev/v4l2_subdev# nodes
>     for a pipeline open after enumerating the camera.
> 
>     This is a problem for uvcvideo and atomisp /dev/video# nodes as well as
>     for VCM /dev/v4l2_subdev# nodes. Keeping these open stops the underlying
>     hw-devices from being able to enter runtime-suspend causing significant
>     unnecessary power-usage.
> 
>     Add a stub acquireDevice() method to the PipelineHandler class which
>     pipeline handlers can override to delay opening /dev nodes until
>     the camera is acquired.
> 
>     Note pipeline handlers typically also will need access to /dev nodes
>     for their CameraConfig::validate() implementation for tryFormat() calls.
>     Making sure that /dev nodes are opened as necessary from validate() is
>     left up to the pipeline handler implementation.
> 
>     Signed-off-by: Hans de Goede <hdegoede@redhat.com <mailto:hdegoede@redhat.com>>
>     ---
>      include/libcamera/internal/pipeline_handler.h |  3 +-
>      src/libcamera/camera.cpp                      |  2 +-
>      src/libcamera/pipeline_handler.cpp            | 43 +++++++++++++++----
>      3 files changed, 37 insertions(+), 11 deletions(-)
> 
>     diff --git a/include/libcamera/internal/pipeline_handler.h b/include/libcamera/internal/pipeline_handler.h
>     index cad5812f..597f7c3e 100644
>     --- a/include/libcamera/internal/pipeline_handler.h
>     +++ b/include/libcamera/internal/pipeline_handler.h
>     @@ -45,7 +45,7 @@ public:
>             MediaDevice *acquireMediaDevice(DeviceEnumerator *enumerator,
>                                             const DeviceMatch &dm);
> 
>     -       bool acquire();
>     +       bool acquire(Camera *camera);
>             void release(Camera *camera);
> 
>             virtual std::unique_ptr<CameraConfiguration> generateConfiguration(Camera *camera,
>     @@ -79,6 +79,7 @@ protected:
>             virtual int queueRequestDevice(Camera *camera, Request *request) = 0;
>             virtual void stopDevice(Camera *camera) = 0;
> 
>     +       virtual bool acquireDevice(Camera *camera);
>             virtual void releaseDevice(Camera *camera);
> 
>             CameraManager *manager_;
>     diff --git a/src/libcamera/camera.cpp b/src/libcamera/camera.cpp
>     index 382a68f7..4e393f89 100644
>     --- a/src/libcamera/camera.cpp
>     +++ b/src/libcamera/camera.cpp
>     @@ -995,7 +995,7 @@ int Camera::acquire()
>             if (ret < 0)
>                     return ret == -EACCES ? -EBUSY : ret;
> 
>     -       if (!d->pipe_->acquire()) {
>     +       if (!d->pipe_->acquire(this)) {
>                     LOG(Camera, Info)
>                             << "Pipeline handler in use by another process";
>                     return -EBUSY;
>     diff --git a/src/libcamera/pipeline_handler.cpp b/src/libcamera/pipeline_handler.cpp
>     index 1fc22d6a..e1342306 100644
>     --- a/src/libcamera/pipeline_handler.cpp
>     +++ b/src/libcamera/pipeline_handler.cpp
>     @@ -163,20 +163,22 @@ MediaDevice *PipelineHandler::acquireMediaDevice(DeviceEnumerator *enumerator,
>       * has already acquired it
>       * \sa release()
>       */
>     -bool PipelineHandler::acquire()
>     +bool PipelineHandler::acquire(Camera *camera)
>      {
>             MutexLocker locker(lock_);
> 
>     -       if (useCount_) {
>     -               ++useCount_;
>     -               return true;
>     +       if (useCount_ == 0) {
>     +               for (std::shared_ptr<MediaDevice> &media : mediaDevices_) {
>     +                       if (!media->lock()) {
>     +                               unlockMediaDevices();
>     +                               return false;
>     +                       }
>     +               }
>             }
> 
>     -       for (std::shared_ptr<MediaDevice> &media : mediaDevices_) {
>     -               if (!media->lock()) {
>     -                       unlockMediaDevices();
>     -                       return false;
>     -               }
>     +       if (!acquireDevice(camera)) {
>     +               unlockMediaDevices();
> 
>  
> I think we should only unlock media devices if it's the first acquire
> being called, as there might be other ongoing usages on other
> cameras [2]. Therefore:
> ```
>                     if (useCount_ == 0)
>                             unlockMediaDevices();
> ```

Good point, fixed for v2.

Regards,

Hans
Hans de Goede Aug. 27, 2024, 2:25 p.m. UTC | #4
Hi,

On 8/25/24 2:49 AM, Laurent Pinchart wrote:

<snip>

>>> @@ -213,12 +215,35 @@ void PipelineHandler::release(Camera *camera)
>>>         --useCount_;
>>>  }
>>>
>>> +/**
>>> + * \brief Acquire resources associated with this camera
>>> + * \param[in] camera The camera for which to acquire resources
>>> + *
>>> + * Pipeline handlers may override this in order to get resources
>>> + * such as open /dev nodes are allocate buffers when a camera is
> 
> s@open /dev nodes@opening devices/
> s/are allocate/and allocating/

Ack, fixed for v2.

> As discussed previously, I think we need to improve the
> application-facing API to make resource and power management more
> explicit. Camera::acquire() was never meant for this purpose. I
> understand we need a short term solution to the power consumption issue
> for UVC cameras without an enormous amount of yak shaving

Ack, I have posted a RFC with the application-facing API changes we discussed
for this:

https://lists.libcamera.org/pipermail/libcamera-devel/2024-August/044384.html

> and I'm fine
> with this patch series as an interim measure, but I don't want to see
> acquireDevice() being used in random ways by pipeline handlers other
> than UVC before we address the more general problem. This should be
> documented here.

Ok I have added the following block to the docbook comment to address
this for v2:

 * This is used by the uvcvideo pipeline handler to delay opening /dev/video#
 * until the camera is acquired to avoid excess power consumption. The delayed
 * opening of /dev/video# is a special case because the kernel uvcvideo driver
 * powers on the USB device as soon as /dev/video# is opened. This behavior
 * must *not* be copied by other drivers.

>>> + * acquired.
> 
> Please reflow these paragraphs to 80 columns.

Ack, done for v2.

> 
>>> + *
>>> + * Note this is called once for every camera which is acquired,
> 
> s/which/that/
> 
>>> + * so if there are shared resources the pipeline handler must take
> 
> s/shared resources/resources shared by multiple cameras/
> 
>>> + * care to not release them until releaseDevice() has been called for
>>> + * all previously acquired cameras.
> 
> Doesn't this paragraph belong to releaseDevice() ?

that is a better place yes, I have moved it for v2.

Regards,

Hans
Laurent Pinchart Aug. 29, 2024, 9:06 p.m. UTC | #5
Hi Hans,

On Tue, Aug 27, 2024 at 04:25:00PM +0200, Hans de Goede wrote:
> On 8/25/24 2:49 AM, Laurent Pinchart wrote:
> 
> <snip>
> 
> >>> @@ -213,12 +215,35 @@ void PipelineHandler::release(Camera *camera)
> >>>         --useCount_;
> >>>  }
> >>>
> >>> +/**
> >>> + * \brief Acquire resources associated with this camera
> >>> + * \param[in] camera The camera for which to acquire resources
> >>> + *
> >>> + * Pipeline handlers may override this in order to get resources
> >>> + * such as open /dev nodes are allocate buffers when a camera is
> > 
> > s@open /dev nodes@opening devices/
> > s/are allocate/and allocating/
> 
> Ack, fixed for v2.
> 
> > As discussed previously, I think we need to improve the
> > application-facing API to make resource and power management more
> > explicit. Camera::acquire() was never meant for this purpose. I
> > understand we need a short term solution to the power consumption issue
> > for UVC cameras without an enormous amount of yak shaving
> 
> Ack, I have posted a RFC with the application-facing API changes we discussed
> for this:
> 
> https://lists.libcamera.org/pipermail/libcamera-devel/2024-August/044384.html

Thank you for that. I really appreciate your efforts. I'll reply to the
RFC after reviewing v2 of this series.

> > and I'm fine
> > with this patch series as an interim measure, but I don't want to see
> > acquireDevice() being used in random ways by pipeline handlers other
> > than UVC before we address the more general problem. This should be
> > documented here.
> 
> Ok I have added the following block to the docbook comment to address
> this for v2:
> 
>  * This is used by the uvcvideo pipeline handler to delay opening /dev/video#
>  * until the camera is acquired to avoid excess power consumption. The delayed
>  * opening of /dev/video# is a special case because the kernel uvcvideo driver
>  * powers on the USB device as soon as /dev/video# is opened. This behavior
>  * must *not* be copied by other drivers.
> 
> >>> + * acquired.
> > 
> > Please reflow these paragraphs to 80 columns.
> 
> Ack, done for v2.
> 
> >>> + *
> >>> + * Note this is called once for every camera which is acquired,
> > 
> > s/which/that/
> > 
> >>> + * so if there are shared resources the pipeline handler must take
> > 
> > s/shared resources/resources shared by multiple cameras/
> > 
> >>> + * care to not release them until releaseDevice() has been called for
> >>> + * all previously acquired cameras.
> > 
> > Doesn't this paragraph belong to releaseDevice() ?
> 
> that is a better place yes, I have moved it for v2.

Patch
diff mbox series

diff --git a/include/libcamera/internal/pipeline_handler.h b/include/libcamera/internal/pipeline_handler.h
index cad5812f..597f7c3e 100644
--- a/include/libcamera/internal/pipeline_handler.h
+++ b/include/libcamera/internal/pipeline_handler.h
@@ -45,7 +45,7 @@  public:
 	MediaDevice *acquireMediaDevice(DeviceEnumerator *enumerator,
 					const DeviceMatch &dm);
 
-	bool acquire();
+	bool acquire(Camera *camera);
 	void release(Camera *camera);
 
 	virtual std::unique_ptr<CameraConfiguration> generateConfiguration(Camera *camera,
@@ -79,6 +79,7 @@  protected:
 	virtual int queueRequestDevice(Camera *camera, Request *request) = 0;
 	virtual void stopDevice(Camera *camera) = 0;
 
+	virtual bool acquireDevice(Camera *camera);
 	virtual void releaseDevice(Camera *camera);
 
 	CameraManager *manager_;
diff --git a/src/libcamera/camera.cpp b/src/libcamera/camera.cpp
index 382a68f7..4e393f89 100644
--- a/src/libcamera/camera.cpp
+++ b/src/libcamera/camera.cpp
@@ -995,7 +995,7 @@  int Camera::acquire()
 	if (ret < 0)
 		return ret == -EACCES ? -EBUSY : ret;
 
-	if (!d->pipe_->acquire()) {
+	if (!d->pipe_->acquire(this)) {
 		LOG(Camera, Info)
 			<< "Pipeline handler in use by another process";
 		return -EBUSY;
diff --git a/src/libcamera/pipeline_handler.cpp b/src/libcamera/pipeline_handler.cpp
index 1fc22d6a..e1342306 100644
--- a/src/libcamera/pipeline_handler.cpp
+++ b/src/libcamera/pipeline_handler.cpp
@@ -163,20 +163,22 @@  MediaDevice *PipelineHandler::acquireMediaDevice(DeviceEnumerator *enumerator,
  * has already acquired it
  * \sa release()
  */
-bool PipelineHandler::acquire()
+bool PipelineHandler::acquire(Camera *camera)
 {
 	MutexLocker locker(lock_);
 
-	if (useCount_) {
-		++useCount_;
-		return true;
+	if (useCount_ == 0) {
+		for (std::shared_ptr<MediaDevice> &media : mediaDevices_) {
+			if (!media->lock()) {
+				unlockMediaDevices();
+				return false;
+			}
+		}
 	}
 
-	for (std::shared_ptr<MediaDevice> &media : mediaDevices_) {
-		if (!media->lock()) {
-			unlockMediaDevices();
-			return false;
-		}
+	if (!acquireDevice(camera)) {
+		unlockMediaDevices();
+		return false;
 	}
 
 	++useCount_;
@@ -213,12 +215,35 @@  void PipelineHandler::release(Camera *camera)
 	--useCount_;
 }
 
+/**
+ * \brief Acquire resources associated with this camera
+ * \param[in] camera The camera for which to acquire resources
+ *
+ * Pipeline handlers may override this in order to get resources
+ * such as open /dev nodes are allocate buffers when a camera is
+ * acquired.
+ *
+ * Note this is called once for every camera which is acquired,
+ * so if there are shared resources the pipeline handler must take
+ * care to not release them until releaseDevice() has been called for
+ * all previously acquired cameras.
+ *
+ * \return True on success, false on failure.
+ * \sa releaseDevice()
+ */
+bool PipelineHandler::acquireDevice([[maybe_unused]] Camera *camera)
+{
+	return true;
+}
+
 /**
  * \brief Release resources associated with this camera
  * \param[in] camera The camera for which to release resources
  *
  * Pipeline handlers may override this in order to perform cleanup operations
  * when a camera is released, such as freeing memory.
+ *
+ * \sa acquireDevice()
  */
 void PipelineHandler::releaseDevice([[maybe_unused]] Camera *camera)
 {