[v3] libcamera: software_isp: Handle signals in the proper thread
diff mbox series

Message ID 20250131195928.57070-1-mzamazal@redhat.com
State Accepted
Headers show
Series
  • [v3] libcamera: software_isp: Handle signals in the proper thread
Related show

Commit Message

Milan Zamazal Jan. 31, 2025, 7:59 p.m. UTC
inputBufferReady ready signal in the simple pipeline is handled in the
pipeline handler thread.  outputBufferReady and ispStatsReady signals
should be handled there too.

Rather than relying on the user of the SoftwareIsp instance, let
SoftwareIsp inherits Object.  SoftwareIsp serves as a signal proxy, the
signals above are emitted from signal handlers.  This means that if
SoftwareIsp inherits Object then the slots are invoked in SoftwareIsp
thread.  Which is the camera manager thread because the SoftwareIsp
instance is created there.

Signed-off-by: Milan Zamazal <mzamazal@redhat.com>
Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
---
 .../internal/software_isp/software_isp.h         |  3 ++-
 src/libcamera/pipeline/simple/simple.cpp         | 16 +---------------
 2 files changed, 3 insertions(+), 16 deletions(-)

Comments

Kieran Bingham Feb. 1, 2025, 10:05 a.m. UTC | #1
Quoting Milan Zamazal (2025-01-31 19:59:28)
> inputBufferReady ready signal in the simple pipeline is handled in the
> pipeline handler thread.  outputBufferReady and ispStatsReady signals
> should be handled there too.
> 
> Rather than relying on the user of the SoftwareIsp instance, let
> SoftwareIsp inherits Object.  SoftwareIsp serves as a signal proxy, the
> signals above are emitted from signal handlers.  This means that if
> SoftwareIsp inherits Object then the slots are invoked in SoftwareIsp
> thread.  Which is the camera manager thread because the SoftwareIsp
> instance is created there.
> 
> Signed-off-by: Milan Zamazal <mzamazal@redhat.com>
> Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
> ---
>  .../internal/software_isp/software_isp.h         |  3 ++-
>  src/libcamera/pipeline/simple/simple.cpp         | 16 +---------------
>  2 files changed, 3 insertions(+), 16 deletions(-)
> 
> diff --git a/include/libcamera/internal/software_isp/software_isp.h b/include/libcamera/internal/software_isp/software_isp.h
> index d51b03fd..440a296d 100644
> --- a/include/libcamera/internal/software_isp/software_isp.h
> +++ b/include/libcamera/internal/software_isp/software_isp.h
> @@ -18,6 +18,7 @@
>  
>  #include <libcamera/base/class.h>
>  #include <libcamera/base/log.h>
> +#include <libcamera/base/object.h>
>  #include <libcamera/base/signal.h>
>  #include <libcamera/base/thread.h>
>  
> @@ -43,7 +44,7 @@ struct StreamConfiguration;
>  
>  LOG_DECLARE_CATEGORY(SoftwareIsp)
>  
> -class SoftwareIsp
> +class SoftwareIsp : public Object
>  {
>  public:
>         SoftwareIsp(PipelineHandler *pipe, const CameraSensor *sensor,
> diff --git a/src/libcamera/pipeline/simple/simple.cpp b/src/libcamera/pipeline/simple/simple.cpp
> index 8ac24e6e..6e039bf3 100644
> --- a/src/libcamera/pipeline/simple/simple.cpp
> +++ b/src/libcamera/pipeline/simple/simple.cpp
> @@ -537,21 +537,7 @@ int SimpleCameraData::init()
>                                 << "Failed to create software ISP, disabling software debayering";
>                         swIsp_.reset();
>                 } else {
> -                       /*
> -                        * The inputBufferReady signal is emitted from the soft ISP thread,
> -                        * and needs to be handled in the pipeline handler thread. Signals
> -                        * implement queued delivery, but this works transparently only if
> -                        * the receiver is bound to the target thread. As the
> -                        * SimpleCameraData class doesn't inherit from the Object class, it
> -                        * is not bound to any thread, and the signal would be delivered
> -                        * synchronously. Instead, connect the signal to a lambda function
> -                        * bound explicitly to the pipe, which is bound to the pipeline
> -                        * handler thread. The function then simply forwards the call to
> -                        * conversionInputDone().
> -                        */
> -                       swIsp_->inputBufferReady.connect(pipe, [this](FrameBuffer *buffer) {
> -                               this->conversionInputDone(buffer);
> -                       });
> +                       swIsp_->inputBufferReady.connect(this, &SimpleCameraData::conversionInputDone);

This looks like it was easier than I thought it was going to be!


Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>

>                         swIsp_->outputBufferReady.connect(this, &SimpleCameraData::conversionOutputDone);
>                         swIsp_->ispStatsReady.connect(this, &SimpleCameraData::ispStatsReady);
>                         swIsp_->setSensorControls.connect(this, &SimpleCameraData::setSensorControls);
> -- 
> 2.48.1
>
Stanislaw Gruszka Feb. 14, 2025, 8:42 a.m. UTC | #2
Hi Milan,

thanks for working on this.

On Fri, Jan 31, 2025 at 08:59:28PM +0100, Milan Zamazal wrote:
> inputBufferReady ready signal in the simple pipeline is handled in the
> pipeline handler thread.  outputBufferReady and ispStatsReady signals
> should be handled there too.
> 
> Rather than relying on the user of the SoftwareIsp instance, let
> SoftwareIsp inherits Object.  SoftwareIsp serves as a signal proxy, the
> signals above are emitted from signal handlers.  This means that if
> SoftwareIsp inherits Object then the slots are invoked in SoftwareIsp
> thread.  Which is the camera manager thread because the SoftwareIsp
> instance is created there.
> 
> Signed-off-by: Milan Zamazal <mzamazal@redhat.com>
> Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>

Unfortunately I have assertion failures like below with the change.
Reproducible approximately 1 per 4 times when stopping qcam.

Could you please take a look? I can dig more into it,
but I think it could be easier for you :-) Can provide
more data if needed.

Thanks
Stanislaw

2:12:50.912442715] [11753] FATAL default soft_ipa_proxy.cpp:456 assertion "state_ == ProxyRunning" failed in processStatsThread()
Backtrace:
libcamera::ipa::soft::IPAProxySoft::processStatsThread(unsigned int, unsigned int, libcamera::ControlList const&)+0xd6 (src/libcamera/proxy/soft_ipa_proxy.cpp:457)
libcamera::ipa::soft::IPAProxySoft::processStats(unsigned int, unsigned int, libcamera::ControlList const&)+0x57 (src/libcamera/proxy/soft_ipa_proxy.cpp:449)
libcamera::SoftwareIsp::processStats(unsigned int, unsigned int, libcamera::ControlList const&)+0x117 (../src/libcamera/software_isp/software_isp.cpp:174)
libcamera::SimpleCameraData::ispStatsReady(unsigned int, unsigned int)+0x76 (../src/libcamera/pipeline/simple/simple.cpp:894)
libcamera::BoundMethodMember<libcamera::SimpleCameraData, void, unsigned int, unsigned int>::activate(unsigned int, unsigned int, bool)+0xa4 (../include/libcamera/base/bound_method.h:180)
libcamera::Signal<unsigned int, unsigned int>::emit(unsigned int, unsigned int)+0x99 (../include/libcamera/base/signal.h:152)
libcamera::SoftwareIsp::statsReady(unsigned int, unsigned int)+0x31 (../src/libcamera/software_isp/software_isp.cpp:360)
libcamera::BoundMethodMember<libcamera::SoftwareIsp, void, unsigned int, unsigned int>::invoke(unsigned int, unsigned int)+0x80 (../include/libcamera/base/bound_method.h:191)
std::enable_if<std::is_void<void>::value, void>::type libcamera::BoundMethodArgs<void, unsigned int, unsigned int>::invokePack<0ul, 1ul, void>(libcamera::BoundMethodPackBase*, std::integer_sequence<unsigned long, 0ul, 1ul>)+0x5f (../include/libcamera/base/bound_method.h:116)
libcamera::BoundMethodArgs<void, unsigned int, unsigned int>::invokePack(libcamera::BoundMethodPackBase*)+0x27 (../include/libcamera/base/bound_method.h:125)
libcamera::InvokeMessage::invoke()+0x46 (../src/libcamera/base/message.cpp:154)
libcamera::Object::message(libcamera::Message*)+0x7a (../src/libcamera/base/object.cpp:213)
libcamera::Thread::dispatchMessages(libcamera::Message::Type)+0x317 (../src/libcamera/base/thread.cpp:650)
libcamera::ipa::soft::IPAProxySoft::stopThread()+0x167 (src/libcamera/proxy/soft_ipa_proxy.cpp:285)
libcamera::ipa::soft::IPAProxySoft::stop()+0x39 (src/libcamera/proxy/soft_ipa_proxy.cpp:268)
libcamera::SoftwareIsp::stop()+0x64 (../src/libcamera/software_isp/software_isp.cpp:332)
libcamera::SimplePipelineHandler::stopDevice(libcamera::Camera*)+0xb6 (../src/libcamera/pipeline/simple/simple.cpp:1396)
libcamera::PipelineHandler::stop(libcamera::Camera*)+0x54 (../src/libcamera/pipeline_handler.cpp:367)
libcamera::BoundMethodMember<libcamera::PipelineHandler, void, libcamera::Camera*>::invoke(libcamera::Camera*)+0x7f (../include/libcamera/base/bound_method.h:191)
std::enable_if<std::is_void<void>::value, void>::type libcamera::BoundMethodArgs<void, libcamera::Camera*>::invokePack<0ul, void>(libcamera::BoundMethodPackBase*, std::integer_sequence<unsigned long, 0ul>)+0x4a (../include/libcamera/base/bound_method.h:116)
libcamera::BoundMethodArgs<void, libcamera::Camera*>::invokePack(libcamera::BoundMethodPackBase*)+0x27 (../include/libcamera/base/bound_method.h:125)
libcamera::InvokeMessage::invoke()+0x46 (../src/libcamera/base/message.cpp:154)
libcamera::Object::message(libcamera::Message*)+0x7a (../src/libcamera/base/object.cpp:213)
libcamera::Thread::dispatchMessages(libcamera::Message::Type)+0x317 (../src/libcamera/base/thread.cpp:650)
libcamera::EventDispatcherPoll::processEvents()+0x38 (../src/libcamera/base/event_dispatcher_poll.cpp:149)
libcamera::Thread::exec()+0x70 (../src/libcamera/base/thread.cpp:310)
libcamera::CameraManager::Private::run()+0x121 (../src/libcamera/camera_manager.cpp:91)
libcamera::Thread::startThread()+0xf2 (../src/libcamera/base/thread.cpp:290)
void std::__invoke_impl<void, void (libcamera::Thread::*)(), libcamera::Thread*>(std::__invoke_memfun_deref, void (libcamera::Thread::*&&)(), libcamera::Thread*&&)+0x6a (/usr/include/c++/13/bits/invoke.h:74)
std::__invoke_result<void (libcamera::Thread::*)(), libcamera::Thread*>::type std::__invoke<void (libcamera::Thread::*)(), libcamera::Thread*>(void (libcamera::Thread::*&&)(), libcamera::Thread*&&)+0x3b (/usr/include/c++/13/bits/invoke.h:97)
void std::thread::_Invoker<std::tuple<void (libcamera::Thread::*)(), libcamera::Thread*> >::_M_invoke<0ul, 1ul>(std::_Index_tuple<0ul, 1ul>)+0x47 (/usr/include/c++/13/bits/std_thread.h:292)
std::thread::_Invoker<std::tuple<void (libcamera::Thread::*)(), libcamera::Thread*> >::operator()()+0x1c (/usr/include/c++/13/bits/std_thread.h:299)
std::thread::_State_impl<std::thread::_Invoker<std::tuple<void (libcamera::Thread::*)(), libcamera::Thread*> > >::_M_run()+0x20 (/usr/include/c++/13/bits/std_thread.h:244)
execute_native_thread_routine+0x14 (/build/gcc-14-ig5ci0/gcc-14-14.2.0/build/x86_64-linux-gnu/libstdc++-v3/include/bits/unique_ptr.h:93)
start_thread+0x384 (./nptl/pthread_create.c:447)
__clone3+0x2c (../sysdeps/unix/sysv/linux/x86_64/clone3.S:80)
Aborted (core dumped)

> ---
>  .../internal/software_isp/software_isp.h         |  3 ++-
>  src/libcamera/pipeline/simple/simple.cpp         | 16 +---------------
>  2 files changed, 3 insertions(+), 16 deletions(-)
> 
> diff --git a/include/libcamera/internal/software_isp/software_isp.h b/include/libcamera/internal/software_isp/software_isp.h
> index d51b03fd..440a296d 100644
> --- a/include/libcamera/internal/software_isp/software_isp.h
> +++ b/include/libcamera/internal/software_isp/software_isp.h
> @@ -18,6 +18,7 @@
>  
>  #include <libcamera/base/class.h>
>  #include <libcamera/base/log.h>
> +#include <libcamera/base/object.h>
>  #include <libcamera/base/signal.h>
>  #include <libcamera/base/thread.h>
>  
> @@ -43,7 +44,7 @@ struct StreamConfiguration;
>  
>  LOG_DECLARE_CATEGORY(SoftwareIsp)
>  
> -class SoftwareIsp
> +class SoftwareIsp : public Object
>  {
>  public:
>  	SoftwareIsp(PipelineHandler *pipe, const CameraSensor *sensor,
> diff --git a/src/libcamera/pipeline/simple/simple.cpp b/src/libcamera/pipeline/simple/simple.cpp
> index 8ac24e6e..6e039bf3 100644
> --- a/src/libcamera/pipeline/simple/simple.cpp
> +++ b/src/libcamera/pipeline/simple/simple.cpp
> @@ -537,21 +537,7 @@ int SimpleCameraData::init()
>  				<< "Failed to create software ISP, disabling software debayering";
>  			swIsp_.reset();
>  		} else {
> -			/*
> -			 * The inputBufferReady signal is emitted from the soft ISP thread,
> -			 * and needs to be handled in the pipeline handler thread. Signals
> -			 * implement queued delivery, but this works transparently only if
> -			 * the receiver is bound to the target thread. As the
> -			 * SimpleCameraData class doesn't inherit from the Object class, it
> -			 * is not bound to any thread, and the signal would be delivered
> -			 * synchronously. Instead, connect the signal to a lambda function
> -			 * bound explicitly to the pipe, which is bound to the pipeline
> -			 * handler thread. The function then simply forwards the call to
> -			 * conversionInputDone().
> -			 */
> -			swIsp_->inputBufferReady.connect(pipe, [this](FrameBuffer *buffer) {
> -				this->conversionInputDone(buffer);
> -			});
> +			swIsp_->inputBufferReady.connect(this, &SimpleCameraData::conversionInputDone);
>  			swIsp_->outputBufferReady.connect(this, &SimpleCameraData::conversionOutputDone);
>  			swIsp_->ispStatsReady.connect(this, &SimpleCameraData::ispStatsReady);
>  			swIsp_->setSensorControls.connect(this, &SimpleCameraData::setSensorControls);
> -- 
> 2.48.1
>
Milan Zamazal Feb. 14, 2025, 8:57 p.m. UTC | #3
Hi Stanislaw,

Stanislaw Gruszka <stanislaw.gruszka@linux.intel.com> writes:

> Hi Milan,
>
> thanks for working on this.
>
> On Fri, Jan 31, 2025 at 08:59:28PM +0100, Milan Zamazal wrote:
>> inputBufferReady ready signal in the simple pipeline is handled in the
>> pipeline handler thread.  outputBufferReady and ispStatsReady signals
>> should be handled there too.
>> 
>> Rather than relying on the user of the SoftwareIsp instance, let
>> SoftwareIsp inherits Object.  SoftwareIsp serves as a signal proxy, the
>> signals above are emitted from signal handlers.  This means that if
>> SoftwareIsp inherits Object then the slots are invoked in SoftwareIsp
>> thread.  Which is the camera manager thread because the SoftwareIsp
>> instance is created there.
>> 
>> Signed-off-by: Milan Zamazal <mzamazal@redhat.com>
>> Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
>
> Unfortunately I have assertion failures like below with the change.
> Reproducible approximately 1 per 4 times when stopping qcam.

I can reproduce it occasionally with cam too.

> Could you please take a look? 

I think what happens there is:

- The ISP stats signal is emitted at the "right" moment to cause the
  race and gets queued.

- The pipeline is asked to stop and the IPA proxy state is set to
  ProxyStopping.

- As part of stopping the IPA, pending messages of the given thread are
  invoked.

- The ISP stats message causes a call to IPA to process the stats, which
  fails on the assertion due to ProxyStopping IPA state.  The stats
  related IPA call itself is not different from what hardware pipelines
  do but the circumstances when it happens are different, either
  preventing the race in hardware pipelines or making it much less
  likely (I don't know).

Now the question is what can be done about it.  Things should be already
run in the right thread as discussed previously.  The signal emitter
cannot do anything about the issue.  So the pending message should be
either discarded or the IPA call should be blocked if the IPA state is
not ProxyRunning.

The only way I can see to discard the messages is to destroy the
corresponding object, which is not an option in this case.

I can't see no public way to check the IPA state from outside, excluding
the option of blocking the IPA call too.  I'm not sure how to interpret
the fact that ProxyState enum is public while its only use is to declare
a protected class member; does it indicate that the option of dealing
with the state from outside is left open?

At the moment, what can help is tracking the stop action in SoftwareIsp
class itself and preventing forwarding the signal from there in such a
case.  This apparently fixes the issue.  I can post a patch next week if
there is no better suggestion.

> I can dig more into it, but I think it could be easier for you :-) Can
> provide more data if needed.
>
> Thanks
> Stanislaw
>
> 2:12:50.912442715] [11753] FATAL default soft_ipa_proxy.cpp:456 assertion "state_ == ProxyRunning" failed
> in processStatsThread()
> Backtrace:
> libcamera::ipa::soft::IPAProxySoft::processStatsThread(unsigned int, unsigned int, libcamera::ControlList
> const&)+0xd6 (src/libcamera/proxy/soft_ipa_proxy.cpp:457)
> libcamera::ipa::soft::IPAProxySoft::processStats(unsigned int, unsigned int, libcamera::ControlList
> const&)+0x57 (src/libcamera/proxy/soft_ipa_proxy.cpp:449)
> libcamera::SoftwareIsp::processStats(unsigned int, unsigned int, libcamera::ControlList const&)+0x117
> (../src/libcamera/software_isp/software_isp.cpp:174)
> libcamera::SimpleCameraData::ispStatsReady(unsigned int, unsigned int)+0x76
> (../src/libcamera/pipeline/simple/simple.cpp:894)
> libcamera::BoundMethodMember<libcamera::SimpleCameraData, void, unsigned int, unsigned
> int>::activate(unsigned int, unsigned int, bool)+0xa4 (../include/libcamera/base/bound_method.h:180)
> libcamera::Signal<unsigned int, unsigned int>::emit(unsigned int, unsigned int)+0x99
> (../include/libcamera/base/signal.h:152)
> libcamera::SoftwareIsp::statsReady(unsigned int, unsigned int)+0x31
> (../src/libcamera/software_isp/software_isp.cpp:360)
> libcamera::BoundMethodMember<libcamera::SoftwareIsp, void, unsigned int, unsigned int>::invoke(unsigned
> int, unsigned int)+0x80 (../include/libcamera/base/bound_method.h:191)
> std::enable_if<std::is_void<void>::value, void>::type libcamera::BoundMethodArgs<void, unsigned int,
> unsigned int>::invokePack<0ul, 1ul, void>(libcamera::BoundMethodPackBase*, std::integer_sequence<unsigned
> long, 0ul, 1ul>)+0x5f (../include/libcamera/base/bound_method.h:116)
> libcamera::BoundMethodArgs<void, unsigned int, unsigned
> int>::invokePack(libcamera::BoundMethodPackBase*)+0x27 (../include/libcamera/base/bound_method.h:125)
> libcamera::InvokeMessage::invoke()+0x46 (../src/libcamera/base/message.cpp:154)
> libcamera::Object::message(libcamera::Message*)+0x7a (../src/libcamera/base/object.cpp:213)
> libcamera::Thread::dispatchMessages(libcamera::Message::Type)+0x317
> (../src/libcamera/base/thread.cpp:650)
> libcamera::ipa::soft::IPAProxySoft::stopThread()+0x167 (src/libcamera/proxy/soft_ipa_proxy.cpp:285)
> libcamera::ipa::soft::IPAProxySoft::stop()+0x39 (src/libcamera/proxy/soft_ipa_proxy.cpp:268)
> libcamera::SoftwareIsp::stop()+0x64 (../src/libcamera/software_isp/software_isp.cpp:332)
> libcamera::SimplePipelineHandler::stopDevice(libcamera::Camera*)+0xb6
> (../src/libcamera/pipeline/simple/simple.cpp:1396)
> libcamera::PipelineHandler::stop(libcamera::Camera*)+0x54 (../src/libcamera/pipeline_handler.cpp:367)
> libcamera::BoundMethodMember<libcamera::PipelineHandler, void,
> libcamera::Camera*>::invoke(libcamera::Camera*)+0x7f (../include/libcamera/base/bound_method.h:191)
> std::enable_if<std::is_void<void>::value, void>::type libcamera::BoundMethodArgs<void,
> libcamera::Camera*>::invokePack<0ul, void>(libcamera::BoundMethodPackBase*,
> std::integer_sequence<unsigned long, 0ul>)+0x4a (../include/libcamera/base/bound_method.h:116)
> libcamera::BoundMethodArgs<void, libcamera::Camera*>::invokePack(libcamera::BoundMethodPackBase*)+0x27
> (../include/libcamera/base/bound_method.h:125)
> libcamera::InvokeMessage::invoke()+0x46 (../src/libcamera/base/message.cpp:154)
> libcamera::Object::message(libcamera::Message*)+0x7a (../src/libcamera/base/object.cpp:213)
> libcamera::Thread::dispatchMessages(libcamera::Message::Type)+0x317
> (../src/libcamera/base/thread.cpp:650)
> libcamera::EventDispatcherPoll::processEvents()+0x38
> (../src/libcamera/base/event_dispatcher_poll.cpp:149)
> libcamera::Thread::exec()+0x70 (../src/libcamera/base/thread.cpp:310)
> libcamera::CameraManager::Private::run()+0x121 (../src/libcamera/camera_manager.cpp:91)
> libcamera::Thread::startThread()+0xf2 (../src/libcamera/base/thread.cpp:290)
> void std::__invoke_impl<void, void (libcamera::Thread::*)(),
> libcamera::Thread*>(std::__invoke_memfun_deref, void (libcamera::Thread::*&&)(),
> libcamera::Thread*&&)+0x6a (/usr/include/c++/13/bits/invoke.h:74)
> std::__invoke_result<void (libcamera::Thread::*)(), libcamera::Thread*>::type std::__invoke<void
> (libcamera::Thread::*)(), libcamera::Thread*>(void (libcamera::Thread::*&&)(), libcamera::Thread*&&)+0x3b
> (/usr/include/c++/13/bits/invoke.h:97)
> void std::thread::_Invoker<std::tuple<void (libcamera::Thread::*)(), libcamera::Thread*>
>>::_M_invoke<0ul, 1ul>(std::_Index_tuple<0ul, 1ul>)+0x47 (/usr/include/c++/13/bits/std_thread.h:292)
> std::thread::_Invoker<std::tuple<void (libcamera::Thread::*)(), libcamera::Thread*> >::operator()()+0x1c
> (/usr/include/c++/13/bits/std_thread.h:299)
> std::thread::_State_impl<std::thread::_Invoker<std::tuple<void (libcamera::Thread::*)(),
> libcamera::Thread*> > >::_M_run()+0x20 (/usr/include/c++/13/bits/std_thread.h:244)
> execute_native_thread_routine+0x14
> (/build/gcc-14-ig5ci0/gcc-14-14.2.0/build/x86_64-linux-gnu/libstdc++-v3/include/bits/unique_ptr.h:93)
> start_thread+0x384 (./nptl/pthread_create.c:447)
> __clone3+0x2c (../sysdeps/unix/sysv/linux/x86_64/clone3.S:80)
> Aborted (core dumped)
>
>> ---
>>  .../internal/software_isp/software_isp.h         |  3 ++-
>>  src/libcamera/pipeline/simple/simple.cpp         | 16 +---------------
>>  2 files changed, 3 insertions(+), 16 deletions(-)
>> 
>> diff --git a/include/libcamera/internal/software_isp/software_isp.h b/include/libcamera/internal/software_isp/software_isp.h
>> index d51b03fd..440a296d 100644
>> --- a/include/libcamera/internal/software_isp/software_isp.h
>> +++ b/include/libcamera/internal/software_isp/software_isp.h
>> @@ -18,6 +18,7 @@
>>  
>>  #include <libcamera/base/class.h>
>>  #include <libcamera/base/log.h>
>> +#include <libcamera/base/object.h>
>>  #include <libcamera/base/signal.h>
>>  #include <libcamera/base/thread.h>
>>  
>> @@ -43,7 +44,7 @@ struct StreamConfiguration;
>>  
>>  LOG_DECLARE_CATEGORY(SoftwareIsp)
>>  
>> -class SoftwareIsp
>> +class SoftwareIsp : public Object
>>  {
>>  public:
>>  	SoftwareIsp(PipelineHandler *pipe, const CameraSensor *sensor,
>> diff --git a/src/libcamera/pipeline/simple/simple.cpp b/src/libcamera/pipeline/simple/simple.cpp
>> index 8ac24e6e..6e039bf3 100644
>> --- a/src/libcamera/pipeline/simple/simple.cpp
>> +++ b/src/libcamera/pipeline/simple/simple.cpp
>> @@ -537,21 +537,7 @@ int SimpleCameraData::init()
>>  				<< "Failed to create software ISP, disabling software debayering";
>>  			swIsp_.reset();
>>  		} else {
>> -			/*
>> -			 * The inputBufferReady signal is emitted from the soft ISP thread,
>> -			 * and needs to be handled in the pipeline handler thread. Signals
>> -			 * implement queued delivery, but this works transparently only if
>> -			 * the receiver is bound to the target thread. As the
>> -			 * SimpleCameraData class doesn't inherit from the Object class, it
>> -			 * is not bound to any thread, and the signal would be delivered
>> -			 * synchronously. Instead, connect the signal to a lambda function
>> -			 * bound explicitly to the pipe, which is bound to the pipeline
>> -			 * handler thread. The function then simply forwards the call to
>> -			 * conversionInputDone().
>> -			 */
>> -			swIsp_->inputBufferReady.connect(pipe, [this](FrameBuffer *buffer) {
>> -				this->conversionInputDone(buffer);
>> -			});
>> +			swIsp_->inputBufferReady.connect(this, &SimpleCameraData::conversionInputDone);
>>  			swIsp_->outputBufferReady.connect(this, &SimpleCameraData::conversionOutputDone);
>>  			swIsp_->ispStatsReady.connect(this, &SimpleCameraData::ispStatsReady);
>>  			swIsp_->setSensorControls.connect(this, &SimpleCameraData::setSensorControls);
>> -- 
>> 2.48.1
>>
Laurent Pinchart Feb. 16, 2025, 12:32 a.m. UTC | #4
Hi Milan,

On Fri, Feb 14, 2025 at 09:57:22PM +0100, Milan Zamazal wrote:
> Stanislaw Gruszka writes:
> > On Fri, Jan 31, 2025 at 08:59:28PM +0100, Milan Zamazal wrote:
> >> inputBufferReady ready signal in the simple pipeline is handled in the
> >> pipeline handler thread.  outputBufferReady and ispStatsReady signals
> >> should be handled there too.
> >> 
> >> Rather than relying on the user of the SoftwareIsp instance, let
> >> SoftwareIsp inherits Object.  SoftwareIsp serves as a signal proxy, the
> >> signals above are emitted from signal handlers.  This means that if
> >> SoftwareIsp inherits Object then the slots are invoked in SoftwareIsp
> >> thread.  Which is the camera manager thread because the SoftwareIsp
> >> instance is created there.
> >> 
> >> Signed-off-by: Milan Zamazal <mzamazal@redhat.com>
> >> Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
> >
> > Unfortunately I have assertion failures like below with the change.
> > Reproducible approximately 1 per 4 times when stopping qcam.
> 
> I can reproduce it occasionally with cam too.
> 
> > Could you please take a look? 
> 
> I think what happens there is:
> 
> - The ISP stats signal is emitted at the "right" moment to cause the
>   race and gets queued.
> 
> - The pipeline is asked to stop and the IPA proxy state is set to
>   ProxyStopping.
> 
> - As part of stopping the IPA, pending messages of the given thread are
>   invoked.
> 
> - The ISP stats message causes a call to IPA to process the stats, which
>   fails on the assertion due to ProxyStopping IPA state.  The stats
>   related IPA call itself is not different from what hardware pipelines
>   do but the circumstances when it happens are different, either
>   preventing the race in hardware pipelines or making it much less
>   likely (I don't know).

We indeed tried to model the software ISP similarly to hardware devices,
in that they would run separately from the pipeline handler thread (the
hardware obviously runs on its own separately from the CPU, and the
software ISP runs in a separate thread to mimick that), and emit a
signal received in the pipeline handler thread. There's a crucial
difference though, related to how the signal is delivered.

For hardware devices, the file descriptor of the kernel device is added
to the event loop's even sources (through an EventNotifier). When the
pipeline handler thread goes back to event loop processing, it sees an
event on the file descriptor, and calls the event notifier's signal
handler, in the same thread. For video devices used to capture
statistics, that's an instance of the V4L2VideoDevice class, which
decodes the event and emits the corresponding signal (e.g. bufferReady).
That signal is connected to the pipeline handler's buffer ready event,
which dequeues the statistics buffer. All of this happens synchronously
in the same thread. When the video device is stopped, the notifier is
disabled synchronously with the stop() call. As stop() is called in the
pipeline handler thread, this guarantees that no event will be received
by the pipeline handler when stop() returns. Races are not possible.

For the software ISP, the event is delivered by emitting a signal from
the ISP thread, in the DebayerCpu::process() function. The signal is
connected to a function of the SoftwareIsp class, using queued signal
delivery. The function then emits another signal, connected to the
pipeline handler. Queued signal delivery means that emitting a signal
queues a message to the receiver's message queue. That message is
processed when the receiver's thread runs the event loop, and calls the
signal handler synchronously from there. As for hardware devices, the
signal handler is guaranteed to be called in the pipeline handler
thread. However, because the signal message can be queued before the
software ISP thread is stopped with SoftwareIsp::stop(), its delivery
can occur after the stop() function returns.

> Now the question is what can be done about it.  Things should be already
> run in the right thread as discussed previously.  The signal emitter
> cannot do anything about the issue.  So the pending message should be
> either discarded or the IPA call should be blocked if the IPA state is
> not ProxyRunning.
> 
> The only way I can see to discard the messages is to destroy the
> corresponding object, which is not an option in this case.
> 
> I can't see no public way to check the IPA state from outside, excluding
> the option of blocking the IPA call too.  I'm not sure how to interpret
> the fact that ProxyState enum is public while its only use is to declare
> a protected class member; does it indicate that the option of dealing
> with the state from outside is left open?
> 
> At the moment, what can help is tracking the stop action in SoftwareIsp
> class itself and preventing forwarding the signal from there in such a
> case.  This apparently fixes the issue.  I can post a patch next week if
> there is no better suggestion.

Event handling for hardware device was designed to avoid race
conditions, and I think it has served us well. As seen here, handling
race conditions with threads is difficult. I think the best approach is
to design components and APIs to isolate the rest of libcamera from
handling those problems. Handling the race condition in pipeline
handlers is likely possible, but it will be more error-prone, especially
if we consider using the software ISP (or other CPU- or GPU-based image
processing) in other pipeline handlers in the future. We should
therefore avoid event delivery after SoftwareIsp::stop() returns.

Your proposed solution, of blocking the signal delivery in the
SoftwareIsp class when stop() is called, seems good to me. It's as close
as we can get to disabling the event notifier for hardware devices.

The stop() function is currently implemented as

void SoftwareIsp::stop()
{
	ispWorkerThread_.exit();
	ispWorkerThread_.wait();

	ipa_->stop();    
} 

After stopping the thread, we know that it will not touch any buffer
anymore, and won't send any new event. We may have messages queued at
that point, but they won't be processed asynchronously. Normally those
messages won't get delivered before we return to the event loop, but
ipa_->stop() calls dispatchMessages() to address the exact same issue as
the one we're dealing with here. This causes the queued messages from
the software ISP thread (if any) to be delivered immediately. Even if
that wasn't the case, we would still have an issue, as the messages
would get delivered when the pipeline handler thread returns to the
event loop. We can set a running state variable to false just before
calling ipa_->stop(), and block delivery of the signals when the
variable is false.

I think care also needs to be taken to return all queued buffers to
the pipeline handler, like we do with hardware devices. Before returning
to the caller from the stop() function, any buffer that has been queued
with SoftwareIsp::queueBuffers() should be returned to the pipeline
handler, the same way V4L2VideoDevice::streamOff() will return buffers
in the FrameCancelled state. The documentation of SoftwareIsp::stop()
should explain this.

There's one thing that makes me slightly uncomfortable with this though.
Blocking signal delivery based on the running state means that we leave
messages in the queue. ipa_->stop() will flush those, but if its
implementation changes later, then the messages would still be in the
queue when SoftwareIsp::stop() returns. That's mostly fine, control will
quickly return to the event loop, those messages will be delivered, and
the running state being set to false will block their delivery. However,
if the pipeline handler were to restart the software ISP right after
stopping it (I'm not sure why it would do so, but let's assume there
would be a use case), then the old messages will be delivered when
control returns to the event loop, as the running variable would be true
at that point. I'm wondering if it wouldn't be better to drop the
messages from the queue just before calling ipa_->stop(), with
Thread::dispatchMessages(). I think the function should be extended with
a 'Object *receiver' parameter, and only handle messages for that
receiver. The IPAProxy::stop() function could then limit message
dispatching to the appropriate receiver.

> > I can dig more into it, but I think it could be easier for you :-) Can
> > provide more data if needed.
> >
> > Thanks
> > Stanislaw
> >
> > 2:12:50.912442715] [11753] FATAL default soft_ipa_proxy.cpp:456 assertion "state_ == ProxyRunning" failed
> > in processStatsThread()
> > Backtrace:
> > libcamera::ipa::soft::IPAProxySoft::processStatsThread(unsigned int, unsigned int, libcamera::ControlList
> > const&)+0xd6 (src/libcamera/proxy/soft_ipa_proxy.cpp:457)
> > libcamera::ipa::soft::IPAProxySoft::processStats(unsigned int, unsigned int, libcamera::ControlList
> > const&)+0x57 (src/libcamera/proxy/soft_ipa_proxy.cpp:449)
> > libcamera::SoftwareIsp::processStats(unsigned int, unsigned int, libcamera::ControlList const&)+0x117
> > (../src/libcamera/software_isp/software_isp.cpp:174)
> > libcamera::SimpleCameraData::ispStatsReady(unsigned int, unsigned int)+0x76
> > (../src/libcamera/pipeline/simple/simple.cpp:894)
> > libcamera::BoundMethodMember<libcamera::SimpleCameraData, void, unsigned int, unsigned
> > int>::activate(unsigned int, unsigned int, bool)+0xa4 (../include/libcamera/base/bound_method.h:180)
> > libcamera::Signal<unsigned int, unsigned int>::emit(unsigned int, unsigned int)+0x99
> > (../include/libcamera/base/signal.h:152)
> > libcamera::SoftwareIsp::statsReady(unsigned int, unsigned int)+0x31
> > (../src/libcamera/software_isp/software_isp.cpp:360)
> > libcamera::BoundMethodMember<libcamera::SoftwareIsp, void, unsigned int, unsigned int>::invoke(unsigned
> > int, unsigned int)+0x80 (../include/libcamera/base/bound_method.h:191)
> > std::enable_if<std::is_void<void>::value, void>::type libcamera::BoundMethodArgs<void, unsigned int,
> > unsigned int>::invokePack<0ul, 1ul, void>(libcamera::BoundMethodPackBase*, std::integer_sequence<unsigned
> > long, 0ul, 1ul>)+0x5f (../include/libcamera/base/bound_method.h:116)
> > libcamera::BoundMethodArgs<void, unsigned int, unsigned
> > int>::invokePack(libcamera::BoundMethodPackBase*)+0x27 (../include/libcamera/base/bound_method.h:125)
> > libcamera::InvokeMessage::invoke()+0x46 (../src/libcamera/base/message.cpp:154)
> > libcamera::Object::message(libcamera::Message*)+0x7a (../src/libcamera/base/object.cpp:213)
> > libcamera::Thread::dispatchMessages(libcamera::Message::Type)+0x317
> > (../src/libcamera/base/thread.cpp:650)
> > libcamera::ipa::soft::IPAProxySoft::stopThread()+0x167 (src/libcamera/proxy/soft_ipa_proxy.cpp:285)
> > libcamera::ipa::soft::IPAProxySoft::stop()+0x39 (src/libcamera/proxy/soft_ipa_proxy.cpp:268)
> > libcamera::SoftwareIsp::stop()+0x64 (../src/libcamera/software_isp/software_isp.cpp:332)
> > libcamera::SimplePipelineHandler::stopDevice(libcamera::Camera*)+0xb6
> > (../src/libcamera/pipeline/simple/simple.cpp:1396)
> > libcamera::PipelineHandler::stop(libcamera::Camera*)+0x54 (../src/libcamera/pipeline_handler.cpp:367)
> > libcamera::BoundMethodMember<libcamera::PipelineHandler, void,
> > libcamera::Camera*>::invoke(libcamera::Camera*)+0x7f (../include/libcamera/base/bound_method.h:191)
> > std::enable_if<std::is_void<void>::value, void>::type libcamera::BoundMethodArgs<void,
> > libcamera::Camera*>::invokePack<0ul, void>(libcamera::BoundMethodPackBase*,
> > std::integer_sequence<unsigned long, 0ul>)+0x4a (../include/libcamera/base/bound_method.h:116)
> > libcamera::BoundMethodArgs<void, libcamera::Camera*>::invokePack(libcamera::BoundMethodPackBase*)+0x27
> > (../include/libcamera/base/bound_method.h:125)
> > libcamera::InvokeMessage::invoke()+0x46 (../src/libcamera/base/message.cpp:154)
> > libcamera::Object::message(libcamera::Message*)+0x7a (../src/libcamera/base/object.cpp:213)
> > libcamera::Thread::dispatchMessages(libcamera::Message::Type)+0x317
> > (../src/libcamera/base/thread.cpp:650)
> > libcamera::EventDispatcherPoll::processEvents()+0x38
> > (../src/libcamera/base/event_dispatcher_poll.cpp:149)
> > libcamera::Thread::exec()+0x70 (../src/libcamera/base/thread.cpp:310)
> > libcamera::CameraManager::Private::run()+0x121 (../src/libcamera/camera_manager.cpp:91)
> > libcamera::Thread::startThread()+0xf2 (../src/libcamera/base/thread.cpp:290)
> > void std::__invoke_impl<void, void (libcamera::Thread::*)(),
> > libcamera::Thread*>(std::__invoke_memfun_deref, void (libcamera::Thread::*&&)(),
> > libcamera::Thread*&&)+0x6a (/usr/include/c++/13/bits/invoke.h:74)
> > std::__invoke_result<void (libcamera::Thread::*)(), libcamera::Thread*>::type std::__invoke<void
> > (libcamera::Thread::*)(), libcamera::Thread*>(void (libcamera::Thread::*&&)(), libcamera::Thread*&&)+0x3b
> > (/usr/include/c++/13/bits/invoke.h:97)
> > void std::thread::_Invoker<std::tuple<void (libcamera::Thread::*)(), libcamera::Thread*>
> >>::_M_invoke<0ul, 1ul>(std::_Index_tuple<0ul, 1ul>)+0x47 (/usr/include/c++/13/bits/std_thread.h:292)
> > std::thread::_Invoker<std::tuple<void (libcamera::Thread::*)(), libcamera::Thread*> >::operator()()+0x1c
> > (/usr/include/c++/13/bits/std_thread.h:299)
> > std::thread::_State_impl<std::thread::_Invoker<std::tuple<void (libcamera::Thread::*)(),
> > libcamera::Thread*> > >::_M_run()+0x20 (/usr/include/c++/13/bits/std_thread.h:244)
> > execute_native_thread_routine+0x14
> > (/build/gcc-14-ig5ci0/gcc-14-14.2.0/build/x86_64-linux-gnu/libstdc++-v3/include/bits/unique_ptr.h:93)
> > start_thread+0x384 (./nptl/pthread_create.c:447)
> > __clone3+0x2c (../sysdeps/unix/sysv/linux/x86_64/clone3.S:80)
> > Aborted (core dumped)
> >
> >> ---
> >>  .../internal/software_isp/software_isp.h         |  3 ++-
> >>  src/libcamera/pipeline/simple/simple.cpp         | 16 +---------------
> >>  2 files changed, 3 insertions(+), 16 deletions(-)
> >> 
> >> diff --git a/include/libcamera/internal/software_isp/software_isp.h b/include/libcamera/internal/software_isp/software_isp.h
> >> index d51b03fd..440a296d 100644
> >> --- a/include/libcamera/internal/software_isp/software_isp.h
> >> +++ b/include/libcamera/internal/software_isp/software_isp.h
> >> @@ -18,6 +18,7 @@
> >>  
> >>  #include <libcamera/base/class.h>
> >>  #include <libcamera/base/log.h>
> >> +#include <libcamera/base/object.h>
> >>  #include <libcamera/base/signal.h>
> >>  #include <libcamera/base/thread.h>
> >>  
> >> @@ -43,7 +44,7 @@ struct StreamConfiguration;
> >>  
> >>  LOG_DECLARE_CATEGORY(SoftwareIsp)
> >>  
> >> -class SoftwareIsp
> >> +class SoftwareIsp : public Object
> >>  {
> >>  public:
> >>  	SoftwareIsp(PipelineHandler *pipe, const CameraSensor *sensor,
> >> diff --git a/src/libcamera/pipeline/simple/simple.cpp b/src/libcamera/pipeline/simple/simple.cpp
> >> index 8ac24e6e..6e039bf3 100644
> >> --- a/src/libcamera/pipeline/simple/simple.cpp
> >> +++ b/src/libcamera/pipeline/simple/simple.cpp
> >> @@ -537,21 +537,7 @@ int SimpleCameraData::init()
> >>  				<< "Failed to create software ISP, disabling software debayering";
> >>  			swIsp_.reset();
> >>  		} else {
> >> -			/*
> >> -			 * The inputBufferReady signal is emitted from the soft ISP thread,
> >> -			 * and needs to be handled in the pipeline handler thread. Signals
> >> -			 * implement queued delivery, but this works transparently only if
> >> -			 * the receiver is bound to the target thread. As the
> >> -			 * SimpleCameraData class doesn't inherit from the Object class, it
> >> -			 * is not bound to any thread, and the signal would be delivered
> >> -			 * synchronously. Instead, connect the signal to a lambda function
> >> -			 * bound explicitly to the pipe, which is bound to the pipeline
> >> -			 * handler thread. The function then simply forwards the call to
> >> -			 * conversionInputDone().
> >> -			 */
> >> -			swIsp_->inputBufferReady.connect(pipe, [this](FrameBuffer *buffer) {
> >> -				this->conversionInputDone(buffer);
> >> -			});
> >> +			swIsp_->inputBufferReady.connect(this, &SimpleCameraData::conversionInputDone);
> >>  			swIsp_->outputBufferReady.connect(this, &SimpleCameraData::conversionOutputDone);
> >>  			swIsp_->ispStatsReady.connect(this, &SimpleCameraData::ispStatsReady);
> >>  			swIsp_->setSensorControls.connect(this, &SimpleCameraData::setSensorControls);
Milan Zamazal Feb. 17, 2025, 1:26 p.m. UTC | #5
Hi Laurent,

thank you for thorough explanation and clarifications.

Laurent Pinchart <laurent.pinchart@ideasonboard.com> writes:

> Hi Milan,
>
> On Fri, Feb 14, 2025 at 09:57:22PM +0100, Milan Zamazal wrote:
>> Stanislaw Gruszka writes:
>> > On Fri, Jan 31, 2025 at 08:59:28PM +0100, Milan Zamazal wrote:
>> >> inputBufferReady ready signal in the simple pipeline is handled in the
>> >> pipeline handler thread.  outputBufferReady and ispStatsReady signals
>> >> should be handled there too.
>> >> 
>> >> Rather than relying on the user of the SoftwareIsp instance, let
>> >> SoftwareIsp inherits Object.  SoftwareIsp serves as a signal proxy, the
>> >> signals above are emitted from signal handlers.  This means that if
>> >> SoftwareIsp inherits Object then the slots are invoked in SoftwareIsp
>> >> thread.  Which is the camera manager thread because the SoftwareIsp
>> >> instance is created there.
>> >> 
>> >> Signed-off-by: Milan Zamazal <mzamazal@redhat.com>
>> >> Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
>> >
>> > Unfortunately I have assertion failures like below with the change.
>> > Reproducible approximately 1 per 4 times when stopping qcam.
>> 
>> I can reproduce it occasionally with cam too.
>> 
>> > Could you please take a look? 
>> 
>> I think what happens there is:
>> 
>> - The ISP stats signal is emitted at the "right" moment to cause the
>>   race and gets queued.
>> 
>> - The pipeline is asked to stop and the IPA proxy state is set to
>>   ProxyStopping.
>> 
>> - As part of stopping the IPA, pending messages of the given thread are
>>   invoked.
>> 
>> - The ISP stats message causes a call to IPA to process the stats, which
>>   fails on the assertion due to ProxyStopping IPA state.  The stats
>>   related IPA call itself is not different from what hardware pipelines
>>   do but the circumstances when it happens are different, either
>>   preventing the race in hardware pipelines or making it much less
>>   likely (I don't know).
>
> We indeed tried to model the software ISP similarly to hardware devices,
> in that they would run separately from the pipeline handler thread (the
> hardware obviously runs on its own separately from the CPU, and the
> software ISP runs in a separate thread to mimick that), and emit a
> signal received in the pipeline handler thread. There's a crucial
> difference though, related to how the signal is delivered.
>
> For hardware devices, the file descriptor of the kernel device is added
> to the event loop's even sources (through an EventNotifier). When the
> pipeline handler thread goes back to event loop processing, it sees an
> event on the file descriptor, and calls the event notifier's signal
> handler, in the same thread. For video devices used to capture
> statistics, that's an instance of the V4L2VideoDevice class, which
> decodes the event and emits the corresponding signal (e.g. bufferReady).
> That signal is connected to the pipeline handler's buffer ready event,
> which dequeues the statistics buffer. All of this happens synchronously
> in the same thread. When the video device is stopped, the notifier is
> disabled synchronously with the stop() call. As stop() is called in the
> pipeline handler thread, this guarantees that no event will be received
> by the pipeline handler when stop() returns. Races are not possible.
>
> For the software ISP, the event is delivered by emitting a signal from
> the ISP thread, in the DebayerCpu::process() function. The signal is
> connected to a function of the SoftwareIsp class, using queued signal
> delivery. The function then emits another signal, connected to the
> pipeline handler. Queued signal delivery means that emitting a signal
> queues a message to the receiver's message queue. That message is
> processed when the receiver's thread runs the event loop, and calls the
> signal handler synchronously from there. As for hardware devices, the
> signal handler is guaranteed to be called in the pipeline handler
> thread. However, because the signal message can be queued before the
> software ISP thread is stopped with SoftwareIsp::stop(), its delivery
> can occur after the stop() function returns.
>
>> Now the question is what can be done about it.  Things should be already
>> run in the right thread as discussed previously.  The signal emitter
>> cannot do anything about the issue.  So the pending message should be
>> either discarded or the IPA call should be blocked if the IPA state is
>> not ProxyRunning.
>> 
>> The only way I can see to discard the messages is to destroy the
>> corresponding object, which is not an option in this case.
>> 
>> I can't see no public way to check the IPA state from outside, excluding
>> the option of blocking the IPA call too.  I'm not sure how to interpret
>> the fact that ProxyState enum is public while its only use is to declare
>> a protected class member; does it indicate that the option of dealing
>> with the state from outside is left open?
>> 
>> At the moment, what can help is tracking the stop action in SoftwareIsp
>> class itself and preventing forwarding the signal from there in such a
>> case.  This apparently fixes the issue.  I can post a patch next week if
>> there is no better suggestion.
>
> Event handling for hardware device was designed to avoid race
> conditions, and I think it has served us well. As seen here, handling
> race conditions with threads is difficult. I think the best approach is
> to design components and APIs to isolate the rest of libcamera from
> handling those problems. Handling the race condition in pipeline
> handlers is likely possible, but it will be more error-prone, especially
> if we consider using the software ISP (or other CPU- or GPU-based image
> processing) in other pipeline handlers in the future. We should
> therefore avoid event delivery after SoftwareIsp::stop() returns.
>
> Your proposed solution, of blocking the signal delivery in the
> SoftwareIsp class when stop() is called, seems good to me. It's as close
> as we can get to disabling the event notifier for hardware devices.
>
> The stop() function is currently implemented as
>
> void SoftwareIsp::stop()
> {
> 	ispWorkerThread_.exit();
> 	ispWorkerThread_.wait();
>
> 	ipa_->stop();    
> } 
>
> After stopping the thread, we know that it will not touch any buffer
> anymore, and won't send any new event. We may have messages queued at
> that point, but they won't be processed asynchronously. Normally those
> messages won't get delivered before we return to the event loop, but
> ipa_->stop() calls dispatchMessages() to address the exact same issue as
> the one we're dealing with here. This causes the queued messages from
> the software ISP thread (if any) to be delivered immediately. Even if
> that wasn't the case, we would still have an issue, as the messages
> would get delivered when the pipeline handler thread returns to the
> event loop. We can set a running state variable to false just before
> calling ipa_->stop(), and block delivery of the signals when the
> variable is false.

Yes, this is what I attempted on Friday and it worked fine.

> I think care also needs to be taken to return all queued buffers to
> the pipeline handler, like we do with hardware devices. Before returning
> to the caller from the stop() function, any buffer that has been queued
> with SoftwareIsp::queueBuffers() should be returned to the pipeline
> handler, the same way V4L2VideoDevice::streamOff() will return buffers
> in the FrameCancelled state. The documentation of SoftwareIsp::stop()
> should explain this.

Good point.

> There's one thing that makes me slightly uncomfortable with this though.
> Blocking signal delivery based on the running state means that we leave
> messages in the queue. ipa_->stop() will flush those, but if its
> implementation changes later, then the messages would still be in the
> queue when SoftwareIsp::stop() returns. That's mostly fine, control will
> quickly return to the event loop, those messages will be delivered, and
> the running state being set to false will block their delivery. However,
> if the pipeline handler were to restart the software ISP right after
> stopping it (I'm not sure why it would do so, but let's assume there
> would be a use case), then the old messages will be delivered when
> control returns to the event loop, as the running variable would be true
> at that point. I'm wondering if it wouldn't be better to drop the
> messages from the queue just before calling ipa_->stop(), with
> Thread::dispatchMessages(). I think the function should be extended with
> a 'Object *receiver' parameter, and only handle messages for that
> receiver. The IPAProxy::stop() function could then limit message
> dispatching to the appropriate receiver.

Let's try and see.

>> > I can dig more into it, but I think it could be easier for you :-) Can
>> > provide more data if needed.
>> >
>> > Thanks
>> > Stanislaw
>> >
>> > 2:12:50.912442715] [11753] FATAL default soft_ipa_proxy.cpp:456 assertion "state_ == ProxyRunning" failed
>> > in processStatsThread()
>> > Backtrace:
>> > libcamera::ipa::soft::IPAProxySoft::processStatsThread(unsigned int, unsigned int, libcamera::ControlList
>> > const&)+0xd6 (src/libcamera/proxy/soft_ipa_proxy.cpp:457)
>> > libcamera::ipa::soft::IPAProxySoft::processStats(unsigned int, unsigned int, libcamera::ControlList
>> > const&)+0x57 (src/libcamera/proxy/soft_ipa_proxy.cpp:449)
>> > libcamera::SoftwareIsp::processStats(unsigned int, unsigned int, libcamera::ControlList const&)+0x117
>> > (../src/libcamera/software_isp/software_isp.cpp:174)
>> > libcamera::SimpleCameraData::ispStatsReady(unsigned int, unsigned int)+0x76
>> > (../src/libcamera/pipeline/simple/simple.cpp:894)
>> > libcamera::BoundMethodMember<libcamera::SimpleCameraData, void, unsigned int, unsigned
>> > int>::activate(unsigned int, unsigned int, bool)+0xa4 (../include/libcamera/base/bound_method.h:180)
>> > libcamera::Signal<unsigned int, unsigned int>::emit(unsigned int, unsigned int)+0x99
>> > (../include/libcamera/base/signal.h:152)
>> > libcamera::SoftwareIsp::statsReady(unsigned int, unsigned int)+0x31
>> > (../src/libcamera/software_isp/software_isp.cpp:360)
>> > libcamera::BoundMethodMember<libcamera::SoftwareIsp, void, unsigned int, unsigned int>::invoke(unsigned
>> > int, unsigned int)+0x80 (../include/libcamera/base/bound_method.h:191)
>> > std::enable_if<std::is_void<void>::value, void>::type libcamera::BoundMethodArgs<void, unsigned int,
>> > unsigned int>::invokePack<0ul, 1ul, void>(libcamera::BoundMethodPackBase*, std::integer_sequence<unsigned
>> > long, 0ul, 1ul>)+0x5f (../include/libcamera/base/bound_method.h:116)
>> > libcamera::BoundMethodArgs<void, unsigned int, unsigned
>> > int>::invokePack(libcamera::BoundMethodPackBase*)+0x27 (../include/libcamera/base/bound_method.h:125)
>> > libcamera::InvokeMessage::invoke()+0x46 (../src/libcamera/base/message.cpp:154)
>> > libcamera::Object::message(libcamera::Message*)+0x7a (../src/libcamera/base/object.cpp:213)
>> > libcamera::Thread::dispatchMessages(libcamera::Message::Type)+0x317
>> > (../src/libcamera/base/thread.cpp:650)
>> > libcamera::ipa::soft::IPAProxySoft::stopThread()+0x167 (src/libcamera/proxy/soft_ipa_proxy.cpp:285)
>> > libcamera::ipa::soft::IPAProxySoft::stop()+0x39 (src/libcamera/proxy/soft_ipa_proxy.cpp:268)
>> > libcamera::SoftwareIsp::stop()+0x64 (../src/libcamera/software_isp/software_isp.cpp:332)
>> > libcamera::SimplePipelineHandler::stopDevice(libcamera::Camera*)+0xb6
>> > (../src/libcamera/pipeline/simple/simple.cpp:1396)
>> > libcamera::PipelineHandler::stop(libcamera::Camera*)+0x54 (../src/libcamera/pipeline_handler.cpp:367)
>> > libcamera::BoundMethodMember<libcamera::PipelineHandler, void,
>> > libcamera::Camera*>::invoke(libcamera::Camera*)+0x7f (../include/libcamera/base/bound_method.h:191)
>> > std::enable_if<std::is_void<void>::value, void>::type libcamera::BoundMethodArgs<void,
>> > libcamera::Camera*>::invokePack<0ul, void>(libcamera::BoundMethodPackBase*,
>> > std::integer_sequence<unsigned long, 0ul>)+0x4a (../include/libcamera/base/bound_method.h:116)
>> > libcamera::BoundMethodArgs<void, libcamera::Camera*>::invokePack(libcamera::BoundMethodPackBase*)+0x27
>> > (../include/libcamera/base/bound_method.h:125)
>> > libcamera::InvokeMessage::invoke()+0x46 (../src/libcamera/base/message.cpp:154)
>> > libcamera::Object::message(libcamera::Message*)+0x7a (../src/libcamera/base/object.cpp:213)
>> > libcamera::Thread::dispatchMessages(libcamera::Message::Type)+0x317
>> > (../src/libcamera/base/thread.cpp:650)
>> > libcamera::EventDispatcherPoll::processEvents()+0x38
>> > (../src/libcamera/base/event_dispatcher_poll.cpp:149)
>> > libcamera::Thread::exec()+0x70 (../src/libcamera/base/thread.cpp:310)
>> > libcamera::CameraManager::Private::run()+0x121 (../src/libcamera/camera_manager.cpp:91)
>> > libcamera::Thread::startThread()+0xf2 (../src/libcamera/base/thread.cpp:290)
>> > void std::__invoke_impl<void, void (libcamera::Thread::*)(),
>> > libcamera::Thread*>(std::__invoke_memfun_deref, void (libcamera::Thread::*&&)(),
>> > libcamera::Thread*&&)+0x6a (/usr/include/c++/13/bits/invoke.h:74)
>> > std::__invoke_result<void (libcamera::Thread::*)(), libcamera::Thread*>::type std::__invoke<void
>> > (libcamera::Thread::*)(), libcamera::Thread*>(void (libcamera::Thread::*&&)(), libcamera::Thread*&&)+0x3b
>> > (/usr/include/c++/13/bits/invoke.h:97)
>> > void std::thread::_Invoker<std::tuple<void (libcamera::Thread::*)(), libcamera::Thread*>
>> >>::_M_invoke<0ul, 1ul>(std::_Index_tuple<0ul, 1ul>)+0x47 (/usr/include/c++/13/bits/std_thread.h:292)
>> > std::thread::_Invoker<std::tuple<void (libcamera::Thread::*)(), libcamera::Thread*> >::operator()()+0x1c
>> > (/usr/include/c++/13/bits/std_thread.h:299)
>> > std::thread::_State_impl<std::thread::_Invoker<std::tuple<void (libcamera::Thread::*)(),
>> > libcamera::Thread*> > >::_M_run()+0x20 (/usr/include/c++/13/bits/std_thread.h:244)
>> > execute_native_thread_routine+0x14
>> > (/build/gcc-14-ig5ci0/gcc-14-14.2.0/build/x86_64-linux-gnu/libstdc++-v3/include/bits/unique_ptr.h:93)
>> > start_thread+0x384 (./nptl/pthread_create.c:447)
>> > __clone3+0x2c (../sysdeps/unix/sysv/linux/x86_64/clone3.S:80)
>> > Aborted (core dumped)
>> >
>> >> ---
>> >>  .../internal/software_isp/software_isp.h         |  3 ++-
>> >>  src/libcamera/pipeline/simple/simple.cpp         | 16 +---------------
>> >>  2 files changed, 3 insertions(+), 16 deletions(-)
>> >> 
>> >> diff --git a/include/libcamera/internal/software_isp/software_isp.h b/include/libcamera/internal/software_isp/software_isp.h
>> >> index d51b03fd..440a296d 100644
>> >> --- a/include/libcamera/internal/software_isp/software_isp.h
>> >> +++ b/include/libcamera/internal/software_isp/software_isp.h
>> >> @@ -18,6 +18,7 @@
>> >>  
>> >>  #include <libcamera/base/class.h>
>> >>  #include <libcamera/base/log.h>
>> >> +#include <libcamera/base/object.h>
>> >>  #include <libcamera/base/signal.h>
>> >>  #include <libcamera/base/thread.h>
>> >>  
>> >> @@ -43,7 +44,7 @@ struct StreamConfiguration;
>> >>  
>> >>  LOG_DECLARE_CATEGORY(SoftwareIsp)
>> >>  
>> >> -class SoftwareIsp
>> >> +class SoftwareIsp : public Object
>> >>  {
>> >>  public:
>> >>  	SoftwareIsp(PipelineHandler *pipe, const CameraSensor *sensor,
>> >> diff --git a/src/libcamera/pipeline/simple/simple.cpp b/src/libcamera/pipeline/simple/simple.cpp
>> >> index 8ac24e6e..6e039bf3 100644
>> >> --- a/src/libcamera/pipeline/simple/simple.cpp
>> >> +++ b/src/libcamera/pipeline/simple/simple.cpp
>> >> @@ -537,21 +537,7 @@ int SimpleCameraData::init()
>> >>  				<< "Failed to create software ISP, disabling software debayering";
>> >>  			swIsp_.reset();
>> >>  		} else {
>> >> -			/*
>> >> -			 * The inputBufferReady signal is emitted from the soft ISP thread,
>> >> -			 * and needs to be handled in the pipeline handler thread. Signals
>> >> -			 * implement queued delivery, but this works transparently only if
>> >> -			 * the receiver is bound to the target thread. As the
>> >> -			 * SimpleCameraData class doesn't inherit from the Object class, it
>> >> -			 * is not bound to any thread, and the signal would be delivered
>> >> -			 * synchronously. Instead, connect the signal to a lambda function
>> >> -			 * bound explicitly to the pipe, which is bound to the pipeline
>> >> -			 * handler thread. The function then simply forwards the call to
>> >> -			 * conversionInputDone().
>> >> -			 */
>> >> -			swIsp_->inputBufferReady.connect(pipe, [this](FrameBuffer *buffer) {
>> >> -				this->conversionInputDone(buffer);
>> >> -			});
>> >> +			swIsp_->inputBufferReady.connect(this, &SimpleCameraData::conversionInputDone);
>> >>  			swIsp_->outputBufferReady.connect(this, &SimpleCameraData::conversionOutputDone);
>> >>  			swIsp_->ispStatsReady.connect(this, &SimpleCameraData::ispStatsReady);
>> >>  			swIsp_->setSensorControls.connect(this, &SimpleCameraData::setSensorControls);

Patch
diff mbox series

diff --git a/include/libcamera/internal/software_isp/software_isp.h b/include/libcamera/internal/software_isp/software_isp.h
index d51b03fd..440a296d 100644
--- a/include/libcamera/internal/software_isp/software_isp.h
+++ b/include/libcamera/internal/software_isp/software_isp.h
@@ -18,6 +18,7 @@ 
 
 #include <libcamera/base/class.h>
 #include <libcamera/base/log.h>
+#include <libcamera/base/object.h>
 #include <libcamera/base/signal.h>
 #include <libcamera/base/thread.h>
 
@@ -43,7 +44,7 @@  struct StreamConfiguration;
 
 LOG_DECLARE_CATEGORY(SoftwareIsp)
 
-class SoftwareIsp
+class SoftwareIsp : public Object
 {
 public:
 	SoftwareIsp(PipelineHandler *pipe, const CameraSensor *sensor,
diff --git a/src/libcamera/pipeline/simple/simple.cpp b/src/libcamera/pipeline/simple/simple.cpp
index 8ac24e6e..6e039bf3 100644
--- a/src/libcamera/pipeline/simple/simple.cpp
+++ b/src/libcamera/pipeline/simple/simple.cpp
@@ -537,21 +537,7 @@  int SimpleCameraData::init()
 				<< "Failed to create software ISP, disabling software debayering";
 			swIsp_.reset();
 		} else {
-			/*
-			 * The inputBufferReady signal is emitted from the soft ISP thread,
-			 * and needs to be handled in the pipeline handler thread. Signals
-			 * implement queued delivery, but this works transparently only if
-			 * the receiver is bound to the target thread. As the
-			 * SimpleCameraData class doesn't inherit from the Object class, it
-			 * is not bound to any thread, and the signal would be delivered
-			 * synchronously. Instead, connect the signal to a lambda function
-			 * bound explicitly to the pipe, which is bound to the pipeline
-			 * handler thread. The function then simply forwards the call to
-			 * conversionInputDone().
-			 */
-			swIsp_->inputBufferReady.connect(pipe, [this](FrameBuffer *buffer) {
-				this->conversionInputDone(buffer);
-			});
+			swIsp_->inputBufferReady.connect(this, &SimpleCameraData::conversionInputDone);
 			swIsp_->outputBufferReady.connect(this, &SimpleCameraData::conversionOutputDone);
 			swIsp_->ispStatsReady.connect(this, &SimpleCameraData::ispStatsReady);
 			swIsp_->setSensorControls.connect(this, &SimpleCameraData::setSensorControls);