summaryrefslogtreecommitdiff
path: root/test
AgeCommit message (Collapse)Author
2024-07-31libcamera: Avoid variable-length arraysLaurent Pinchart
Unlike in C where they have been standardized since C99, variable-length arrays in C++ are an extension supported by gcc and clang. Clang started warning about this with -Wall in version 18: src/libcamera/ipc_unixsocket.cpp:250:11: error: variable length arrays in C++ are a Clang extension [-Werror,-Wvla-cxx-extension] 250 | char buf[CMSG_SPACE(num * sizeof(uint32_t))]; | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ One simple option is to disable the warning. However, usage of VLAs in C++ is discouraged by some, usually due to security reasons, based on the rationale that developers are often unaware of unintentional use of VLAs and how they may affect the security of the code when the array size is not properly validated. This rationale may sound dubious, as the most commonly proposed fix is to replace VLAs with vectors (or just arrays dynamically allocated with new() wrapped in unique pointers), without adding any size validation. This will not produce much better results. However, keeping the VLA warning and converting the code to dynamic allocation may still be slightly better, as it can prompt developers to notice VLAs and check if size validation is required. For these reasons, convert all VLAs to std::vector. Most of the VLAs don't need extra size validation, as the size is bound through different constraints (e.g. image width for line buffers). An arguable exception may be the buffers in IPCUnixSocket::sendData() and IPCUnixSocket::recvData() as the number of fds is not bound-checked locally, but we will run out of file descriptors before we could overflow the buffer size calculation. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Milan Zamazal <mzamazal@redhat.com> Acked-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
2024-07-25gstreamer: allocator: Ensure camera manager stay aliveNicolas Dufresne
Without the camera manager, it is not possible to cleanly delete the FrameBufferAllocator object. Keep the camera manager alive until all the memory object have been released. A shared_ptr to the CameraManager is introduced which is itself stored as a plain pointer and allocated and released explicitly. When more than one C++ member is required, this can be refactored to use a new C++ class, but the struct _GstLibcameraAllocator is allocated and freed by glib, so it does not have automatic destruction presently. Bug: https://bugs.libcamera.org/show_bug.cgi?id=211 [Kieran: Update test framework to remove expected test fail] Signed-off-by: Nicolas Dufresne <nicolas.dufresne@collabora.com> Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com> Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Signed-off-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
2024-07-25test: gstreamer: Test memory lifetimeNicolas Dufresne
Test that everything works fine if a buffer outlives the pipeline. [Kieran: Update test path with comments and clarify test case] Tested-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com> Signed-off-by: Nicolas Dufresne <nicolas.dufresne@collabora.com> Signed-off-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
2024-07-04test: utils: Extend utils::hex() test to 8-bit and 16-bit valuesLaurent Pinchart
Now that the utils::hex() function supports 8-bit and 16-bit integers, extend the unit test to cover them. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Umang Jain <umang.jain@ideasonboard.com> Reviewed-by: Stefan Klug <stefan.klug@ideasonboard.com>
2024-06-26test: gstreamer: Include missing sanitizer/asan_interface.h headerLaurent Pinchart
The GStreamer tests define a __asan_default_options() function to influence the behaviour of ASan. The function is declared in sanitizer/asan_interface.h, but we don't include the header. This will cause missing declaration warnings when we enable the -Wmissing-declarations option. Include the header to fix the issue. It can't be done unconditionally as not all toolchains provide ASan, so check for its availability at configuration time. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
2024-06-26test: ipc: unixsocket: Define local function in anonymous namespaceLaurent Pinchart
A local function in the unixsocket test is defined in the global namespace without the static keyword. This compiles fine for now, but will cause a missing declaration warning when we enable them. To prepare for that, enclose the function declaration in an anonymous namespace. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
2024-06-11meson: Group libipa and libipa_includes in a dependency objectLaurent Pinchart
Many build targets link with libipa and need libipa_includes. Group them in a libipa_dep dependency object to simplify the users. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com> Reviewed-by: Paul Elder <paul.elder@ideasonboard.com>
2024-06-10test: ipa: rkisp1: utils: Fix floating and fixed point conversion testPaul Elder
There was an issue where using map to store the test cases meant that the test for ignoring unused bits was skipped because of clashing keys. Fix this by moving the offending test out of the loop. While at it, also change the arbitrary floating comparison precision to be more precise. Also fix a missing documentation brief. Fixes: 9d152e9c66c1 ("ipa: rkisp1: Add a helper to convert floating-point to fixed-point") Signed-off-by: Paul Elder <paul.elder@ideasonboard.com> Reviewed-by: Stefan Klug <stefan.klug@ideasonboard.com> Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
2024-06-03test: ipa: rkisp1-utils: Fix coding style for template argumentsLaurent Pinchart
The coding style names template arguments using CamelCase with an uppercase initial letter. Fix the template arguments in the rkisp1-utils test. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Paul Elder <paul.elder@ideasonboard.com> Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
2024-06-03test: v4l2_videodevice: Increase timeout for vimc capture testsLaurent Pinchart
On slower machines, a 10s timeout to capture frames with vimc can be too short and cause test failures. Make the timeout proportional to the number of frames expected to be captured, using a conservative low estimate of the frame rate at 2fps. This does not increase the test time if the vimc driver is fast enough to produce frames. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com> Reviewed-by: Daniel Scally <dan.scally@ideasonboard.com>
2024-06-03test: fence: Increase timeout for fence testLaurent Pinchart
On slower machines, a 1s timeout to capture frames with vimc can be too short and cause test failures. Make the timeout proportional to the number of frames expected to be captured, using a conservative low estimate of the frame rate at 2fps. By itself, that change could increase the test time quite substantially on fast platforms, so break from the capture loop as soon as we capture enough frames. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Daniel Scally <dan.scally@ideasonboard.com> Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
2024-06-03test: fence: Fix race conditionLaurent Pinchart
The fence test is racy, as it relies on the main loop being executed between completion of request signalledRequestId_ and signalledRequestId_ + 1. This usually happens, but is not guaranteed. To fix the race condition, change the request identification logic by replacing usage of the cookie value, which is zero-based and wraps around at nbuffers_ - 1, with a completed request counter that is one-based and doesn't wrap. The completedRequestId_, expiredRequestId_ and signalledRequestId_ variables now track the identifier of the last request that has completed, the request whose fence will time out, and the request whose fence will be signalled. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Daniel Scally <dan.scally@ideasonboard.com> Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
2024-06-03test: fence: Turn class member variable into local variableLaurent Pinchart
The fence_ class member variable is only used locally in the FenceTest::run() function. Make it a local variable. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com> Reviewed-by: Daniel Scally <dan.scally@ideasonboard.com>
2024-06-03test: camera: Increase timeout for vimc capture testsLaurent Pinchart
On slower machines, a 1s timeout to capture frames with vimc can be too short and cause test failures. Make the timeout proportional to the number of frames expected to be captured, using a conservative low estimate of the frame rate at 2fps. By itself, that change could increase the test time quite substantially on fast platforms, so break from the capture loop as soon as we capture enough frames. To do so, interrupt the dispatcher at every request completion, or it will only get interrupted after the timer times out. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com> Reviewed-by: Daniel Scally <dan.scally@ideasonboard.com>
2024-05-31test: ipa: rkisp1-utils: Fix capitalization of hex numbersPaul Elder
Fix capitalization of the hexdecimal numbers in the test for conversion between floating point and fixed point numbers. Signed-off-by: Paul Elder <paul.elder@ideasonboard.com> Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com> Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
2024-05-31ipa: rkisp1: Add a helper to convert floating-point to fixed-pointPaul Elder
Add helper functions for converting between floating point and fixed point numbers. Also add tests for them. Signed-off-by: Paul Elder <paul.elder@ideasonboard.com> Reviewed-by: Stefan Klug <stefan.klug@ideasonboard.com> Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
2024-05-14libcamera: pipeline: Rename pipelines to a shorter nameJulien Vuillaumier
The PipelineHandlerFactoryBase class has a name that is propagated to the PipelineHandler instance it creates. In present implementation, this name comes from the REGISTER_PIPELINE_HANDLER registration macro. It corresponds to the stringified name of the PipelineHandler derived class. Therefore, PipelineHandler factories and instances names can be quite long such as "PipelineHandlerRkISP1". A libcamera user may have to explicitly refer to a PipelineHandler name for configuration purpose: one usage of the name can be to define a pipeline handlers match list and their priorities. It is desired, for user convenience, to use a short name to designate a pipeline handler. Reusing the short pipeline names already defined in the meson option files is an existing and consistent way of naming pipelines. This change adds an explicit name parameter to the REGISTER_PIPELINE_HANDLER registration macro. That parameter is used to define the name of a pipeline handler factory, instead of the current pipeline handler class name. Each pipeline registration is updated accordingly. The short name assigned corresponds to the pipeline directory name in the source tree. It is consistent with pipelines names used in meson. Changing the pipeline name has an impact on the IPA modules: each module defines a IPAModuleInfo structure. This structure has a pipelineName member defining the pipeline handler name it shall match with. Therefore, each internal IPA module definition has to be changed to have its IPAModuleInfo pipelineName name updated with the short pipeline handler name. In addition to this pipelineName member, the IPAModuleInfo structure also has a name member, associated to the IPA module name. Having renamed the pipelines to a short name, the pipeline name and the IPA module names of the IPAModuleInfo structure are the same: for in-tree IPA, they correspond to the respective pipeline and IPA subdirectories in the source tree. However the IPA name could be different, for instance with a close source IPA implementation built out-of-tree. Thus, it makes sense to keep the IPA name in that structure, as the 2 definitions may not always be redundant. Signed-off-by: Julien Vuillaumier <julien.vuillaumier@nxp.com> Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com> [Kieran: Adjust for clang-format style fix, reformat commitmsg] Signed-off-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
2024-05-13test: gstreamer: Simplify single stream testNicolas Dufresne
The single stream test for the GStreamer component has a simple pipeline construction using only a fakesink. The implementation currently supports connecting to a more complex stream construction defined by the streamDescription, but this is over engineered for the simple need to start a stream to capture and discard the frames. Convert the use of gst_parse_bin_from_description_full() which uses only a single element 'fakesink' to construct the fakesink directly and link it to the libcamerasrc. Signed-off-by: Nicolas Dufresne <nicolas.dufresne@collabora.com> Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com> Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
2024-05-09libcamera: Drop remaining file name from header comment blocksLaurent Pinchart
Source files in libcamera start by a comment block header, which includes the file name and a one-line description of the file contents. While the latter is useful to get a quick overview of the file contents at a glance, the former is mostly a source of inconvenience. The name in the comments can easily get out of sync with the file name when files are renamed, and copy & paste during development have often lead to incorrect names being used to start with. Readers of the source code are expected to know which file they're looking it. Drop the file name from the header comment blocks in all remaining locations that were not caught by the automated script as they are out of sync with the file name. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Daniel Scally <dan.scally@ideasonboard.com>
2024-05-08libcamera: Drop file name from header comment blocksLaurent Pinchart
Source files in libcamera start by a comment block header, which includes the file name and a one-line description of the file contents. While the latter is useful to get a quick overview of the file contents at a glance, the former is mostly a source of inconvenience. The name in the comments can easily get out of sync with the file name when files are renamed, and copy & paste during development have often lead to incorrect names being used to start with. Readers of the source code are expected to know which file they're looking it. Drop the file name from the header comment block. The change was generated with the following script: ---------------------------------------- dirs="include/libcamera src test utils" declare -rA patterns=( ['c']=' \* ' ['cpp']=' \* ' ['h']=' \* ' ['py']='# ' ['sh']='# ' ) for ext in ${!patterns[@]} ; do files=$(for dir in $dirs ; do find $dir -name "*.${ext}" ; done) pattern=${patterns[${ext}]} for file in $files ; do name=$(basename ${file}) sed -i "s/^\(${pattern}\)${name} - /\1/" "$file" done done ---------------------------------------- This misses several files that are out of sync with the comment block header. Those will be addressed separately and manually. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Daniel Scally <dan.scally@ideasonboard.com>
2024-05-07test: Don't add current build directory to include pathLaurent Pinchart
Meson adds the current source and build directory to the include path by default. This causes a namespace clash in tests when using C++20, as the Span class test is compiled into a binary named 'span', which then gets included by source code through indirect '#include <span>' directives. Unsurprisingly, the compiler doesn't react happily when fed binary data. We could work around the problem by renaming the test executable, but disabling the implicit inclusion of the local directory is a more generic solution that will avoid similar issues in the future. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com> Reviewed-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
2024-03-15libcamera: v4l2_subdevice: Rename V4L2SubdeviceFormat::mbus_code to codeLaurent Pinchart
The V4L2SubdeviceFormat::mbus_code member doesn't follow the libcamera coding style as it should use camelCase. Fix it by renaming it to just 'code', to shorten lines in addition to fixing the coding style. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com> Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
2024-03-12libcamera: controls: Add policy parameter to ControlList::merge()Stefan Klug
This is useful in many cases although not included in the stl. Note: This is an ABI incompatible change. Reviewed-by: Umang Jain <umang.jain@ideasonboard.com> Reviewed-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com> Signed-off-by: Stefan Klug <stefan.klug@ideasonboard.com>
2024-01-25test: timer-thread: Destroy Object from correct thread contextLaurent Pinchart
The TimeoutHandler used in the test is destroyed from the main thread, which is invalid for a thread-bound object bound to a different thread. Fix it by destroying it with deleteLater(). Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Milan Zamazal <mzamazal@redhat.com>
2024-01-25test: timer-thread: Move timer start from wrong thread to separate testLaurent Pinchart
Starting a timer from the wrong thread is expected to fail, and we test this in the timer-thread unit test. This is however not something that a caller is allowed to do, and libcamera will get assertion failures to catch this invalid usage. The unit test will then fail. To prepare for this, split the unit test in two, with a test that is expected by meson to succeed, and one that is expected to fail. The assertion will then cause an expected failure, making the test suite succeed. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Milan Zamazal <mzamazal@redhat.com>
2024-01-25test: signal-threads: Destroy Object from correct thread contextLaurent Pinchart
The SignalReceiver used in the test is destroyed from the main thread, which is invalid for a thread-bound object bound to a different thread. Fix it by destroying it with deleteLater(). Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Milan Zamazal <mzamazal@redhat.com>
2024-01-25test: message: Destroy Object from correct thread contextLaurent Pinchart
The MessageReceiver and RecursiveMessageReceiver used in the test are destroyed from the main thread, which is invalid for a thread-bound object bound to a different thread. Fix it by destroying them with deleteLater(). Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Milan Zamazal <mzamazal@redhat.com>
2024-01-25test: message: Remove incorrect slow receiver testLaurent Pinchart
The slow receiver test verifies there's no race condition between concurrent message delivery and object deletion. This is not a valid use case in the first place, as objects are not allowed to be deleted from a different thread than the one they are bound to. Remove the incorrect test. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Milan Zamazal <mzamazal@redhat.com>
2024-01-25test: event-thread: Destroy Object from correct thread contextLaurent Pinchart
The EventHandler used in the test is destroyed from the main thread, which is invalid for a thread-bound object bound to a different thread. Fix it by destroying it with deleteLater(). Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Milan Zamazal <mzamazal@redhat.com>
2024-01-25test: object-delete: Test deferred delete just before thread stopsLaurent Pinchart
The Object::deleteLater() function is expected to not race with stopping the thread the object is bound to. Add a test for this. The test currently fails, demonstrating a bug in libcamera. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Milan Zamazal <mzamazal@redhat.com>
2024-01-25libcamera: signal: Replace object.h inclusion with forward declatationLaurent Pinchart
The signal.h header doesn't need to include object.h. Replace it with a forward declaration, and instead include object.h in source files that require it. It can speed up compilation a little bit, but more importantly avoids unintended dependencies from the Signal class to the Object class to be added later as the compiler will catch them. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Milan Zamazal <mzamazal@redhat.com>
2024-01-23test: gstreamer: Use env instead of registry editNicolas Dufresne
Instead of editing the registry, use gst_env variable provided by the plugin and already used as part of the devenv shell. This reduces the complexity of the C++ test code. Signed-off-by: Nicolas Dufresne <nicolas.dufresne@collabora.com> Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
2023-12-07test: gstreamer: Remove videoconvert element from pipelineLaurent Pinchart
The GStreamer single stream test uses the following pipeline: libcamerasrc ! videoconvert ! fakesink The videoconvert element isn't useful as the data is thrown away by the fakesink anyway. We can shorten the pipeline to libcamerasrc ! fakesink to save CPU time and to avoid depending on the gstreamer1.0-plugins-base package to run the unit tests. The test could be further simplified by replacing gst_parse_bin_from_description_full() with gst_element_factory_make(), now that we only add one element to the bin. The extra cost incurred by the bin only impacts initialization time, and using a bin will make it easier to add other elements in the future if needed. Keep the bin, and only drop the videoconvert element. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Nicolas Dufresne <nicolas.dufresne@collabora.com> Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
2023-12-07test: ipc: unixsocket: Increase process exit timeoutLaurent Pinchart
When running tests on slower devices, 200ms is too low to wait for the process to exit. Increase the timeout to 2s. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Nicolas Dufresne <nicolas.dufresne@collabora.com> Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
2023-12-07test: log: log_process: Increase process exit timeoutLaurent Pinchart
When running tests on slower devices, 200ms is too low to wait for the process to exit. Increase the timeout to 2s. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
2023-12-07test: log: log_process: Improve debugging on process exit failuresLaurent Pinchart
When the process fails to run and exit normally, the test prints an error message that provides little information: process did not exit normally Expand the error message to print the exit status to make debugging easier. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
2023-12-07test: log: log_process: Fix uninitialized variable on process exit failureLaurent Pinchart
If the process fails to exit before the timeout, the LogProcessTest::exitStatus_ variable gets used uninitialized. Fix it by initializating to Process::NotExited. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
2023-12-07test: log: log_process: Log an error when failing due to incorrect messageLaurent Pinchart
One of the error paths in the test returns without logging a message, which makes failures difficult to debug. Fix it by adding an error message. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
2023-12-06test: gstreamer: Fix indentation in commentsLaurent Pinchart
A couple of comments are mis-indented in the gstreamer unit test. Fix them, and reflow the text while at it. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
2023-10-23test: Add unit test for Transform and OrientationJacopo Mondi
Add a unit test for Transform and Orientation to validate the implementation of the operations between the two types. In particular, test that: o1 / o2 = t o2 * t = o1 Signed-off-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com> Reviewed-by: David Plowman <david.plowman@raspberrypi.com> Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
2023-07-11tests: gstreamer: Fix compiler error with gcc 8.4.0Laurent Pinchart
The provider g_autoptr variable introduced by commit adb1bbb748a1 ("tests: gstreamer: Test cameras' enumeration from GstDeviceProvider") is left uninitialized when declared. The cleanup function could thus get called on an unitialized variable if the scope was exited before the variable gets initialized. This can't occur here, but gcc 8.4.0 still complains about it: /usr/include/glib-2.0/glib/gmacros.h: In member function ‘virtual int GstreamerDeviceProviderTest::run()’: /usr/include/glib-2.0/glib/gmacros.h:1049:27: error: ‘provider’ may be used uninitialized in this function [-Werror=maybe-uninitialized] { if (_ptr) (cleanup) ((ParentName *) _ptr); } \ ^ ../test/gstreamer/gstreamer_device_provider_test.cpp:37:32: note: ‘provider’ was declared here g_autoptr(GstDeviceProvider) provider; Silence the error by initializing the variable to NULL at declaration time. This is a good practice in any case, as later refactoring could otherwise introduce a scope exit before initialization. Fixes: adb1bbb748a1 ("tests: gstreamer: Test cameras' enumeration from GstDeviceProvider") Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Umang Jain <umang.jain@ideasonboard.com> Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com> Signed-off-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
2023-07-07meson: Fix space around colon issuesLaurent Pinchart
The meson style, which libcamera follows, recommends a space before colons in function parameters. Fix the style violations through the project. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Umang Jain <umang.jain@ideasonboard.com>
2023-07-06tests: gstreamer: Test cameras' enumeration from GstDeviceProviderUmang Jain
Test the enumeration of the cameras through GstDeviceProvider against the libcamera camera manager. Signed-off-by: Umang Jain <umang.jain@ideasonboard.com> Reviewed-by: Nicolas Dufresne <nicolas.dufresne@collabora.com>
2023-06-04py: unittests.py: Fix type checker warningsTomi Valkeinen
Fix type checker warnings by dropping unused imports and using _ for variable names that are not used. Signed-off-by: Tomi Valkeinen <tomi.valkeinen@ideasonboard.com> Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
2023-05-30py: unittests.py: Add weakref helpers and use delTomi Valkeinen
Add helpers to check if a weakref or a list of weakrefs is alive or dead. Also use 'del' for local variables instead of setting the variable to None. This makes debugging the test easier as the locals will be gone from locals() dict. Signed-off-by: Tomi Valkeinen <tomi.valkeinen@ideasonboard.com> Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
2023-05-30py: Use exceptions instead of returning error codesTomi Valkeinen
We have multiple methods which return an error code, mimicking the C++ API. Using exceptions is more natural in the Python API, so change all those methods to raise an Exception instead. Signed-off-by: Tomi Valkeinen <tomi.valkeinen@ideasonboard.com> Reviewed-by: Jacopo Mondi <jacopo@jmondi.org> Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
2023-05-30py: Fix CameraManager.version propertyTomi Valkeinen
The current CameraManager.version doesn't work at all (raises a TypeError), as that's not how you use expose C++ static methods as Python class methods. Fix it. Signed-off-by: Tomi Valkeinen <tomi.valkeinen@ideasonboard.com> Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
2023-04-20test: controls: control_info_map: Test default constructorMattijs Korpershoek
ControlInfoMap can be default-constructed. In that case, some of its members (like idmap_) can be a nullptr, and ControlInfoMap.find() will segfault. Add a test with a default constructed ControlInfoMap to cover this. Signed-off-by: Mattijs Korpershoek <mkorpershoek@baylibre.com> Reviewed-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com> Reviewed-by: Umang Jain <umang.jain@ideasonboard.com> Signed-off-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
2022-12-24test: Drop pipeline testLaurent Pinchart
The two pipeline test (for ipu3 and rkisp1) are meant to perform very basic validation of the corresponding pipeline handlers, limited to verifying camera enumeration. They are not unit tests as such, they are superseded by the lc-compliance tool, and they are never used in practice (and always skipped). Drop them. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Umang Jain <umang.jain@ideasonboard.com> Reviewed-by: Paul Elder <paul.elder@ideasonboard.com>
2022-12-24test: py: Fix test failure when ASan is enabledLaurent Pinchart
When the address sanitizer is enabled, the Python unit tests fail due to the link order runtime check as the Python interpreter is (generally) not linked to ASan. Fix this by LD_PRELOAD'ing the ASan runtime. We have to disable the leak detector as the Python interpreter itself leaks memory, which would result in test failures. To LD_PRELOAD the ASan runtime, the path to the binary needs to be known. gcc gives us a generic way to get the path, but that doesn't work with clang as the ASan runtime file name depends on the clang version and target architecture. We thus have to keep the Python test disabled when ASan is enabled and libcamera is compiled with clang. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Acked-by: Umang Jain <umang.jain@ideasonboard.com> Reviewed-by: Paul Elder <paul.elder@ideasonboard.com>
='n1354' href='#n1354'>1354 1355 1356 1357 1358 1359 1360 1361
/* SPDX-License-Identifier: BSD-2-Clause */
/*
 * Copyright (C) 2019-2021, Raspberry Pi (Trading) Ltd.
 *
 * rpi.cpp - Raspberry Pi Image Processing Algorithms
 */

#include <algorithm>
#include <array>
#include <fcntl.h>
#include <math.h>
#include <stdint.h>
#include <string.h>
#include <sys/mman.h>

#include <libcamera/buffer.h>
#include <libcamera/control_ids.h>
#include <libcamera/controls.h>
#include <libcamera/file_descriptor.h>
#include <libcamera/ipa/ipa_interface.h>
#include <libcamera/ipa/ipa_module_info.h>
#include <libcamera/ipa/raspberrypi.h>
#include <libcamera/ipa/raspberrypi_ipa_interface.h>
#include <libcamera/request.h>
#include <libcamera/span.h>

#include "libcamera/internal/buffer.h"
#include "libcamera/internal/camera_sensor.h"
#include "libcamera/internal/log.h"

#include <linux/bcm2835-isp.h>

#include "agc_algorithm.hpp"
#include "agc_status.h"
#include "alsc_status.h"
#include "awb_algorithm.hpp"
#include "awb_status.h"
#include "black_level_status.h"
#include "cam_helper.hpp"
#include "ccm_algorithm.hpp"
#include "ccm_status.h"
#include "contrast_algorithm.hpp"
#include "contrast_status.h"
#include "controller.hpp"
#include "denoise_algorithm.hpp"
#include "denoise_status.h"
#include "dpc_status.h"
#include "focus_status.h"
#include "geq_status.h"
#include "lux_status.h"
#include "metadata.hpp"
#include "noise_status.h"
#include "sharpen_algorithm.hpp"
#include "sharpen_status.h"

namespace libcamera {

/* Configure the sensor with these values initially. */
constexpr double DefaultAnalogueGain = 1.0;
constexpr unsigned int DefaultExposureTime = 20000;
constexpr double defaultMinFrameDuration = 1e6 / 30.0;
constexpr double defaultMaxFrameDuration = 1e6 / 0.01;

LOG_DEFINE_CATEGORY(IPARPI)

class IPARPi : public ipa::RPi::IPARPiInterface
{
public:
	IPARPi()
		: controller_(), frameCount_(0), checkCount_(0), mistrustCount_(0),
		  lsTable_(nullptr), firstStart_(true)
	{
	}

	~IPARPi()
	{
		if (lsTable_)
			munmap(lsTable_, ipa::RPi::MaxLsGridSize);
	}

	int init(const IPASettings &settings, ipa::RPi::SensorConfig *sensorConfig) override;
	void start(const ControlList &controls, ipa::RPi::StartConfig *startConfig) override;
	void stop() override {}

	int configure(const CameraSensorInfo &sensorInfo,
		      const std::map<unsigned int, IPAStream> &streamConfig,
		      const std::map<unsigned int, ControlInfoMap> &entityControls,
		      const ipa::RPi::IPAConfig &data,
		      ControlList *controls) override;
	void mapBuffers(const std::vector<IPABuffer> &buffers) override;
	void unmapBuffers(const std::vector<unsigned int> &ids) override;
	void signalStatReady(const uint32_t bufferId) override;
	void signalQueueRequest(const ControlList &controls) override;
	void signalIspPrepare(const ipa::RPi::ISPConfig &data) override;

private:
	void setMode(const CameraSensorInfo &sensorInfo);
	bool validateSensorControls();
	bool validateIspControls();
	void queueRequest(const ControlList &controls);
	void returnEmbeddedBuffer(unsigned int bufferId);
	void prepareISP(const ipa::RPi::ISPConfig &data);
	void reportMetadata();
	bool parseEmbeddedData(unsigned int bufferId, struct DeviceStatus &deviceStatus);
	void fillDeviceStatus(uint32_t exposureLines, uint32_t gainCode,
			      struct DeviceStatus &deviceStatus);
	void processStats(unsigned int bufferId);
	void applyFrameDurations(double minFrameDuration, double maxFrameDuration);
	void applyAGC(const struct AgcStatus *agcStatus, ControlList &ctrls);
	void applyAWB(const struct AwbStatus *awbStatus, ControlList &ctrls);
	void applyDG(const struct AgcStatus *dgStatus, ControlList &ctrls);
	void applyCCM(const struct CcmStatus *ccmStatus, ControlList &ctrls);
	void applyBlackLevel(const struct BlackLevelStatus *blackLevelStatus, ControlList &ctrls);
	void applyGamma(const struct ContrastStatus *contrastStatus, ControlList &ctrls);
	void applyGEQ(const struct GeqStatus *geqStatus, ControlList &ctrls);
	void applyDenoise(const struct DenoiseStatus *denoiseStatus, ControlList &ctrls);
	void applySharpen(const struct SharpenStatus *sharpenStatus, ControlList &ctrls);
	void applyDPC(const struct DpcStatus *dpcStatus, ControlList &ctrls);
	void applyLS(const struct AlscStatus *lsStatus, ControlList &ctrls);
	void resampleTable(uint16_t dest[], double const src[12][16], int destW, int destH);

	std::map<unsigned int, MappedFrameBuffer> buffers_;

	ControlInfoMap sensorCtrls_;
	ControlInfoMap ispCtrls_;
	ControlList libcameraMetadata_;

	/* Camera sensor params. */
	CameraMode mode_;

	/* Raspberry Pi controller specific defines. */
	std::unique_ptr<RPiController::CamHelper> helper_;
	RPiController::Controller controller_;
	RPiController::Metadata rpiMetadata_;

	/*
	 * We count frames to decide if the frame must be hidden (e.g. from
	 * display) or mistrusted (i.e. not given to the control algos).
	 */
	uint64_t frameCount_;

	/* For checking the sequencing of Prepare/Process calls. */
	uint64_t checkCount_;

	/* How many frames we should avoid running control algos on. */
	unsigned int mistrustCount_;

	/* LS table allocation passed in from the pipeline handler. */
	FileDescriptor lsTableHandle_;
	void *lsTable_;

	/* Distinguish the first camera start from others. */
	bool firstStart_;

	/* Frame duration (1/fps) limits, given in microseconds. */
	double minFrameDuration_;
	double maxFrameDuration_;
};

int IPARPi::init(const IPASettings &settings, ipa::RPi::SensorConfig *sensorConfig)
{
	/*
	 * Load the "helper" for this sensor. This tells us all the device specific stuff
	 * that the kernel driver doesn't. We only do this the first time; we don't need
	 * to re-parse the metadata after a simple mode-switch for no reason.
	 */
	helper_ = std::unique_ptr<RPiController::CamHelper>(RPiController::CamHelper::Create(settings.sensorModel));
	if (!helper_) {
		LOG(IPARPI, Error) << "Could not create camera helper for "
				   << settings.sensorModel;
		return -EINVAL;
	}

	/*
	 * Pass out the sensor config to the pipeline handler in order
	 * to setup the staggered writer class.
	 */
	int gainDelay, exposureDelay, vblankDelay, sensorMetadata;
	helper_->GetDelays(exposureDelay, gainDelay, vblankDelay);
	sensorMetadata = helper_->SensorEmbeddedDataPresent();

	sensorConfig->gainDelay = gainDelay;
	sensorConfig->exposureDelay = exposureDelay;
	sensorConfig->vblankDelay = vblankDelay;
	sensorConfig->sensorMetadata = sensorMetadata;

	/* Load the tuning file for this sensor. */
	controller_.Read(settings.configurationFile.c_str());
	controller_.Initialise();

	return 0;
}

void IPARPi::start(const ControlList &controls, ipa::RPi::StartConfig *startConfig)
{
	RPiController::Metadata metadata;

	ASSERT(startConfig);
	if (!controls.empty()) {
		/* We have been given some controls to action before start. */
		queueRequest(controls);
	}

	controller_.SwitchMode(mode_, &metadata);

	/* SwitchMode may supply updated exposure/gain values to use. */
	AgcStatus agcStatus;
	agcStatus.shutter_time = 0.0;
	agcStatus.analogue_gain = 0.0;

	metadata.Get("agc.status", agcStatus);
	if (agcStatus.shutter_time != 0.0 && agcStatus.analogue_gain != 0.0) {
		ControlList ctrls(sensorCtrls_);
		applyAGC(&agcStatus, ctrls);
		startConfig->controls = std::move(ctrls);
	}

	/*
	 * Initialise frame counts, and decide how many frames must be hidden or
	 * "mistrusted", which depends on whether this is a startup from cold,
	 * or merely a mode switch in a running system.
	 */
	frameCount_ = 0;
	checkCount_ = 0;
	unsigned int dropFrame = 0;
	if (firstStart_) {
		dropFrame = helper_->HideFramesStartup();
		mistrustCount_ = helper_->MistrustFramesStartup();

		/*
		 * Query the AGC/AWB for how many frames they may take to
		 * converge sufficiently. Where these numbers are non-zero
		 * we must allow for the frames with bad statistics
		 * (mistrustCount_) that they won't see. But if zero (i.e.
		 * no convergence necessary), no frames need to be dropped.
		 */
		unsigned int agcConvergenceFrames = 0;
		RPiController::AgcAlgorithm *agc = dynamic_cast<RPiController::AgcAlgorithm *>(
			controller_.GetAlgorithm("agc"));
		if (agc) {
			agcConvergenceFrames = agc->GetConvergenceFrames();
			if (agcConvergenceFrames)
				agcConvergenceFrames += mistrustCount_;
		}

		unsigned int awbConvergenceFrames = 0;
		RPiController::AwbAlgorithm *awb = dynamic_cast<RPiController::AwbAlgorithm *>(
			controller_.GetAlgorithm("awb"));
		if (awb) {
			awbConvergenceFrames = awb->GetConvergenceFrames();
			if (awbConvergenceFrames)
				awbConvergenceFrames += mistrustCount_;
		}

		dropFrame = std::max({ dropFrame, agcConvergenceFrames, awbConvergenceFrames });
		LOG(IPARPI, Debug) << "Drop " << dropFrame << " frames on startup";
	} else {
		dropFrame = helper_->HideFramesModeSwitch();
		mistrustCount_ = helper_->MistrustFramesModeSwitch();
	}

	startConfig->dropFrameCount = dropFrame;

	firstStart_ = false;
}

void IPARPi::setMode(const CameraSensorInfo &sensorInfo)
{
	mode_.bitdepth = sensorInfo.bitsPerPixel;
	mode_.width = sensorInfo.outputSize.width;
	mode_.height = sensorInfo.outputSize.height;
	mode_.sensor_width = sensorInfo.activeAreaSize.width;
	mode_.sensor_height = sensorInfo.activeAreaSize.height;
	mode_.crop_x = sensorInfo.analogCrop.x;
	mode_.crop_y = sensorInfo.analogCrop.y;

	/*
	 * Calculate scaling parameters. The scale_[xy] factors are determined
	 * by the ratio between the crop rectangle size and the output size.
	 */
	mode_.scale_x = sensorInfo.analogCrop.width / sensorInfo.outputSize.width;
	mode_.scale_y = sensorInfo.analogCrop.height / sensorInfo.outputSize.height;

	/*
	 * We're not told by the pipeline handler how scaling is split between
	 * binning and digital scaling. For now, as a heuristic, assume that
	 * downscaling up to 2 is achieved through binning, and that any
	 * additional scaling is achieved through digital scaling.
	 *
	 * \todo Get the pipeline handle to provide the full data
	 */
	mode_.bin_x = std::min(2, static_cast<int>(mode_.scale_x));
	mode_.bin_y = std::min(2, static_cast<int>(mode_.scale_y));

	/* The noise factor is the square root of the total binning factor. */
	mode_.noise_factor = sqrt(mode_.bin_x * mode_.bin_y);

	/*
	 * Calculate the line length in nanoseconds as the ratio between
	 * the line length in pixels and the pixel rate.
	 */
	mode_.line_length = 1e9 * sensorInfo.lineLength / sensorInfo.pixelRate;

	/*
	 * Set the frame length limits for the mode to ensure exposure and
	 * framerate calculations are clipped appropriately.
	 */
	mode_.min_frame_length = sensorInfo.minFrameLength;
	mode_.max_frame_length = sensorInfo.maxFrameLength;
}

int IPARPi::configure(const CameraSensorInfo &sensorInfo,
		      [[maybe_unused]] const std::map<unsigned int, IPAStream> &streamConfig,
		      const std::map<unsigned int, ControlInfoMap> &entityControls,
		      const ipa::RPi::IPAConfig &ipaConfig,
		      ControlList *controls)
{
	if (entityControls.size() != 2) {
		LOG(IPARPI, Error) << "No ISP or sensor controls found.";
		return -1;
	}

	sensorCtrls_ = entityControls.at(0);
	ispCtrls_ = entityControls.at(1);

	if (!validateSensorControls()) {
		LOG(IPARPI, Error) << "Sensor control validation failed.";
		return -1;
	}

	if (!validateIspControls()) {
		LOG(IPARPI, Error) << "ISP control validation failed.";
		return -1;
	}

	/* Setup a metadata ControlList to output metadata. */
	libcameraMetadata_ = ControlList(controls::controls);

	/* Re-assemble camera mode using the sensor info. */
	setMode(sensorInfo);

	mode_.transform = static_cast<libcamera::Transform>(ipaConfig.transform);

	/* Store the lens shading table pointer and handle if available. */
	if (ipaConfig.lsTableHandle.isValid()) {
		/* Remove any previous table, if there was one. */
		if (lsTable_) {
			munmap(lsTable_, ipa::RPi::MaxLsGridSize);
			lsTable_ = nullptr;
		}

		/* Map the LS table buffer into user space. */
		lsTableHandle_ = std::move(ipaConfig.lsTableHandle);
		if (lsTableHandle_.isValid()) {
			lsTable_ = mmap(nullptr, ipa::RPi::MaxLsGridSize, PROT_READ | PROT_WRITE,
					MAP_SHARED, lsTableHandle_.fd(), 0);

			if (lsTable_ == MAP_FAILED) {
				LOG(IPARPI, Error) << "dmaHeap mmap failure for LS table.";
				lsTable_ = nullptr;
			}
		}
	}

	/* Pass the camera mode to the CamHelper to setup algorithms. */
	helper_->SetCameraMode(mode_);

	if (firstStart_) {
		/* Supply initial values for frame durations. */
		applyFrameDurations(defaultMinFrameDuration, defaultMaxFrameDuration);

		/* Supply initial values for gain and exposure. */
		ControlList ctrls(sensorCtrls_);
		AgcStatus agcStatus;
		agcStatus.shutter_time = DefaultExposureTime;
		agcStatus.analogue_gain = DefaultAnalogueGain;
		applyAGC(&agcStatus, ctrls);

		ASSERT(controls);
		*controls = std::move(ctrls);
	}

	return 0;
}

void IPARPi::mapBuffers(const std::vector<IPABuffer> &buffers)
{
	for (const IPABuffer &buffer : buffers) {
		const FrameBuffer fb(buffer.planes);
		buffers_.emplace(buffer.id, MappedFrameBuffer(&fb, PROT_READ | PROT_WRITE));
	}
}

void IPARPi::unmapBuffers(const std::vector<unsigned int> &ids)
{
	for (unsigned int id : ids) {
		auto it = buffers_.find(id);
		if (it == buffers_.end())
			continue;

		buffers_.erase(id);
	}
}

void IPARPi::signalStatReady(uint32_t bufferId)
{
	if (++checkCount_ != frameCount_) /* assert here? */
		LOG(IPARPI, Error) << "WARNING: Prepare/Process mismatch!!!";
	if (frameCount_ > mistrustCount_)
		processStats(bufferId);

	reportMetadata();

	statsMetadataComplete.emit(bufferId & ipa::RPi::MaskID, libcameraMetadata_);
}

void IPARPi::signalQueueRequest(const ControlList &controls)
{
	queueRequest(controls);
}

void IPARPi::signalIspPrepare(const ipa::RPi::ISPConfig &data)
{
	/*
	 * At start-up, or after a mode-switch, we may want to
	 * avoid running the control algos for a few frames in case
	 * they are "unreliable".
	 */
	prepareISP(data);
	frameCount_++;

	/* Ready to push the input buffer into the ISP. */
	runIsp.emit(data.bayerBufferId & ipa::RPi::MaskID);
}

void IPARPi::reportMetadata()
{
	std::unique_lock<RPiController::Metadata> lock(rpiMetadata_);

	/*
	 * Certain information about the current frame and how it will be
	 * processed can be extracted and placed into the libcamera metadata
	 * buffer, where an application could query it.
	 */
	DeviceStatus *deviceStatus = rpiMetadata_.GetLocked<DeviceStatus>("device.status");
	if (deviceStatus) {
		libcameraMetadata_.set(controls::ExposureTime, deviceStatus->shutter_speed);
		libcameraMetadata_.set(controls::AnalogueGain, deviceStatus->analogue_gain);
	}

	AgcStatus *agcStatus = rpiMetadata_.GetLocked<AgcStatus>("agc.status");
	if (agcStatus) {
		libcameraMetadata_.set(controls::AeLocked, agcStatus->locked);
		libcameraMetadata_.set(controls::DigitalGain, agcStatus->digital_gain);
	}

	LuxStatus *luxStatus = rpiMetadata_.GetLocked<LuxStatus>("lux.status");
	if (luxStatus)
		libcameraMetadata_.set(controls::Lux, luxStatus->lux);

	AwbStatus *awbStatus = rpiMetadata_.GetLocked<AwbStatus>("awb.status");
	if (awbStatus) {
		libcameraMetadata_.set(controls::ColourGains, { static_cast<float>(awbStatus->gain_r),
								static_cast<float>(awbStatus->gain_b) });
		libcameraMetadata_.set(controls::ColourTemperature, awbStatus->temperature_K);
	}

	BlackLevelStatus *blackLevelStatus = rpiMetadata_.GetLocked<BlackLevelStatus>("black_level.status");
	if (blackLevelStatus)
		libcameraMetadata_.set(controls::SensorBlackLevels,
				       { static_cast<int32_t>(blackLevelStatus->black_level_r),
					 static_cast<int32_t>(blackLevelStatus->black_level_g),
					 static_cast<int32_t>(blackLevelStatus->black_level_g),
					 static_cast<int32_t>(blackLevelStatus->black_level_b) });

	FocusStatus *focusStatus = rpiMetadata_.GetLocked<FocusStatus>("focus.status");
	if (focusStatus && focusStatus->num == 12) {
		/*
		 * We get a 4x3 grid of regions by default. Calculate the average
		 * FoM over the central two positions to give an overall scene FoM.
		 * This can change later if it is not deemed suitable.
		 */
		int32_t focusFoM = (focusStatus->focus_measures[5] + focusStatus->focus_measures[6]) / 2;
		libcameraMetadata_.set(controls::FocusFoM, focusFoM);
	}

	CcmStatus *ccmStatus = rpiMetadata_.GetLocked<CcmStatus>("ccm.status");
	if (ccmStatus) {
		float m[9];
		for (unsigned int i = 0; i < 9; i++)
			m[i] = ccmStatus->matrix[i];
		libcameraMetadata_.set(controls::ColourCorrectionMatrix, m);
	}
}

bool IPARPi::validateSensorControls()
{
	static const uint32_t ctrls[] = {
		V4L2_CID_ANALOGUE_GAIN,
		V4L2_CID_EXPOSURE,
		V4L2_CID_VBLANK,
	};

	for (auto c : ctrls) {
		if (sensorCtrls_.find(c) == sensorCtrls_.end()) {
			LOG(IPARPI, Error) << "Unable to find sensor control "
					   << utils::hex(c);
			return false;
		}
	}

	return true;
}

bool IPARPi::validateIspControls()
{
	static const uint32_t ctrls[] = {
		V4L2_CID_RED_BALANCE,
		V4L2_CID_BLUE_BALANCE,
		V4L2_CID_DIGITAL_GAIN,
		V4L2_CID_USER_BCM2835_ISP_CC_MATRIX,
		V4L2_CID_USER_BCM2835_ISP_GAMMA,
		V4L2_CID_USER_BCM2835_ISP_BLACK_LEVEL,
		V4L2_CID_USER_BCM2835_ISP_GEQ,
		V4L2_CID_USER_BCM2835_ISP_DENOISE,
		V4L2_CID_USER_BCM2835_ISP_SHARPEN,
		V4L2_CID_USER_BCM2835_ISP_DPC,
		V4L2_CID_USER_BCM2835_ISP_LENS_SHADING,
		V4L2_CID_USER_BCM2835_ISP_CDN,
	};

	for (auto c : ctrls) {
		if (ispCtrls_.find(c) == ispCtrls_.end()) {
			LOG(IPARPI, Error) << "Unable to find ISP control "
					   << utils::hex(c);
			return false;
		}
	}

	return true;
}

/*
 * Converting between enums (used in the libcamera API) and the names that
 * we use to identify different modes. Unfortunately, the conversion tables
 * must be kept up-to-date by hand.
 */
static const std::map<int32_t, std::string> MeteringModeTable = {
	{ controls::MeteringCentreWeighted, "centre-weighted" },
	{ controls::MeteringSpot, "spot" },
	{ controls::MeteringMatrix, "matrix" },
	{ controls::MeteringCustom, "custom" },
};

static const std::map<int32_t, std::string> ConstraintModeTable = {
	{ controls::ConstraintNormal, "normal" },
	{ controls::ConstraintHighlight, "highlight" },
	{ controls::ConstraintCustom, "custom" },
};

static const std::map<int32_t, std::string> ExposureModeTable = {
	{ controls::ExposureNormal, "normal" },
	{ controls::ExposureShort, "short" },
	{ controls::ExposureLong, "long" },
	{ controls::ExposureCustom, "custom" },
};

static const std::map<int32_t, std::string> AwbModeTable = {
	{ controls::AwbAuto, "normal" },
	{ controls::AwbIncandescent, "incandescent" },
	{ controls::AwbTungsten, "tungsten" },
	{ controls::AwbFluorescent, "fluorescent" },
	{ controls::AwbIndoor, "indoor" },
	{ controls::AwbDaylight, "daylight" },
	{ controls::AwbCloudy, "cloudy" },
	{ controls::AwbCustom, "custom" },
};

static const std::map<int32_t, RPiController::DenoiseMode> DenoiseModeTable = {
	{ controls::draft::NoiseReductionModeOff, RPiController::DenoiseMode::Off },
	{ controls::draft::NoiseReductionModeFast, RPiController::DenoiseMode::ColourFast },
	{ controls::draft::NoiseReductionModeHighQuality, RPiController::DenoiseMode::ColourHighQuality },
	{ controls::draft::NoiseReductionModeMinimal, RPiController::DenoiseMode::ColourOff },
	{ controls::draft::NoiseReductionModeZSL, RPiController::DenoiseMode::ColourHighQuality },
};

void IPARPi::queueRequest(const ControlList &controls)
{
	/* Clear the return metadata buffer. */
	libcameraMetadata_.clear();

	for (auto const &ctrl : controls) {
		LOG(IPARPI, Info) << "Request ctrl: "
				  << controls::controls.at(ctrl.first)->name()
				  << " = " << ctrl.second.toString();

		switch (ctrl.first) {
		case controls::AE_ENABLE: {
			RPiController::Algorithm *agc = controller_.GetAlgorithm("agc");
			if (!agc) {
				LOG(IPARPI, Warning)
					<< "Could not set AE_ENABLE - no AGC algorithm";
				break;
			}

			if (ctrl.second.get<bool>() == false)
				agc->Pause();
			else
				agc->Resume();

			libcameraMetadata_.set(controls::AeEnable, ctrl.second.get<bool>());
			break;
		}

		case controls::EXPOSURE_TIME: {
			RPiController::AgcAlgorithm *agc = dynamic_cast<RPiController::AgcAlgorithm *>(
				controller_.GetAlgorithm("agc"));
			if (!agc) {
				LOG(IPARPI, Warning)
					<< "Could not set EXPOSURE_TIME - no AGC algorithm";
				break;
			}

			/* This expects units of micro-seconds. */
			agc->SetFixedShutter(ctrl.second.get<int32_t>());

			libcameraMetadata_.set(controls::ExposureTime, ctrl.second.get<int32_t>());
			break;
		}

		case controls::ANALOGUE_GAIN: {
			RPiController::AgcAlgorithm *agc = dynamic_cast<RPiController::AgcAlgorithm *>(
				controller_.GetAlgorithm("agc"));
			if (!agc) {
				LOG(IPARPI, Warning)
					<< "Could not set ANALOGUE_GAIN - no AGC algorithm";
				break;
			}

			agc->SetFixedAnalogueGain(ctrl.second.get<float>());

			libcameraMetadata_.set(controls::AnalogueGain,
					       ctrl.second.get<float>());
			break;
		}

		case controls::AE_METERING_MODE: {
			RPiController::AgcAlgorithm *agc = dynamic_cast<RPiController::AgcAlgorithm *>(
				controller_.GetAlgorithm("agc"));
			if (!agc) {
				LOG(IPARPI, Warning)
					<< "Could not set AE_METERING_MODE - no AGC algorithm";
				break;
			}

			int32_t idx = ctrl.second.get<int32_t>();
			if (MeteringModeTable.count(idx)) {
				agc->SetMeteringMode(MeteringModeTable.at(idx));
				libcameraMetadata_.set(controls::AeMeteringMode, idx);
			} else {
				LOG(IPARPI, Error) << "Metering mode " << idx
						   << " not recognised";
			}
			break;
		}

		case controls::AE_CONSTRAINT_MODE: {
			RPiController::AgcAlgorithm *agc = dynamic_cast<RPiController::AgcAlgorithm *>(
				controller_.GetAlgorithm("agc"));
			if (!agc) {
				LOG(IPARPI, Warning)
					<< "Could not set AE_CONSTRAINT_MODE - no AGC algorithm";
				break;
			}

			int32_t idx = ctrl.second.get<int32_t>();
			if (ConstraintModeTable.count(idx)) {
				agc->SetConstraintMode(ConstraintModeTable.at(idx));
				libcameraMetadata_.set(controls::AeConstraintMode, idx);
			} else {
				LOG(IPARPI, Error) << "Constraint mode " << idx
						   << " not recognised";
			}
			break;
		}

		case controls::AE_EXPOSURE_MODE: {
			RPiController::AgcAlgorithm *agc = dynamic_cast<RPiController::AgcAlgorithm *>(
				controller_.GetAlgorithm("agc"));
			if (!agc) {
				LOG(IPARPI, Warning)
					<< "Could not set AE_EXPOSURE_MODE - no AGC algorithm";
				break;
			}

			int32_t idx = ctrl.second.get<int32_t>();
			if (ExposureModeTable.count(idx)) {
				agc->SetExposureMode(ExposureModeTable.at(idx));
				libcameraMetadata_.set(controls::AeExposureMode, idx);
			} else {
				LOG(IPARPI, Error) << "Exposure mode " << idx
						   << " not recognised";
			}
			break;
		}

		case controls::EXPOSURE_VALUE: {
			RPiController::AgcAlgorithm *agc = dynamic_cast<RPiController::AgcAlgorithm *>(
				controller_.GetAlgorithm("agc"));
			if (!agc) {
				LOG(IPARPI, Warning)
					<< "Could not set EXPOSURE_VALUE - no AGC algorithm";
				break;
			}

			/*
			 * The SetEv() method takes in a direct exposure multiplier.
			 * So convert to 2^EV
			 */
			double ev = pow(2.0, ctrl.second.get<float>());
			agc->SetEv(ev);
			libcameraMetadata_.set(controls::ExposureValue,
					       ctrl.second.get<float>());
			break;
		}

		case controls::AWB_ENABLE: {
			RPiController::Algorithm *awb = controller_.GetAlgorithm("awb");
			if (!awb) {
				LOG(IPARPI, Warning)
					<< "Could not set AWB_ENABLE - no AWB algorithm";
				break;
			}

			if (ctrl.second.get<bool>() == false)
				awb->Pause();
			else
				awb->Resume();

			libcameraMetadata_.set(controls::AwbEnable,
					       ctrl.second.get<bool>());
			break;
		}

		case controls::AWB_MODE: {
			RPiController::AwbAlgorithm *awb = dynamic_cast<RPiController::AwbAlgorithm *>(
				controller_.GetAlgorithm("awb"));
			if (!awb) {
				LOG(IPARPI, Warning)
					<< "Could not set AWB_MODE - no AWB algorithm";
				break;
			}

			int32_t idx = ctrl.second.get<int32_t>();
			if (AwbModeTable.count(idx)) {
				awb->SetMode(AwbModeTable.at(idx));
				libcameraMetadata_.set(controls::AwbMode, idx);
			} else {
				LOG(IPARPI, Error) << "AWB mode " << idx
						   << " not recognised";
			}
			break;
		}

		case controls::COLOUR_GAINS: {
			auto gains = ctrl.second.get<Span<const float>>();
			RPiController::AwbAlgorithm *awb = dynamic_cast<RPiController::AwbAlgorithm *>(
				controller_.GetAlgorithm("awb"));
			if (!awb) {
				LOG(IPARPI, Warning)
					<< "Could not set COLOUR_GAINS - no AWB algorithm";
				break;
			}

			awb->SetManualGains(gains[0], gains[1]);
			if (gains[0] != 0.0f && gains[1] != 0.0f)
				/* A gain of 0.0f will switch back to auto mode. */
				libcameraMetadata_.set(controls::ColourGains,
						       { gains[0], gains[1] });
			break;
		}

		case controls::BRIGHTNESS: {
			RPiController::ContrastAlgorithm *contrast = dynamic_cast<RPiController::ContrastAlgorithm *>(
				controller_.GetAlgorithm("contrast"));
			if (!contrast) {
				LOG(IPARPI, Warning)
					<< "Could not set BRIGHTNESS - no contrast algorithm";
				break;
			}

			contrast->SetBrightness(ctrl.second.get<float>() * 65536);
			libcameraMetadata_.set(controls::Brightness,
					       ctrl.second.get<float>());
			break;
		}

		case controls::CONTRAST: {
			RPiController::ContrastAlgorithm *contrast = dynamic_cast<RPiController::ContrastAlgorithm *>(
				controller_.GetAlgorithm("contrast"));
			if (!contrast) {
				LOG(IPARPI, Warning)
					<< "Could not set CONTRAST - no contrast algorithm";
				break;
			}

			contrast->SetContrast(ctrl.second.get<float>());
			libcameraMetadata_.set(controls::Contrast,
					       ctrl.second.get<float>());
			break;
		}

		case controls::SATURATION: {
			RPiController::CcmAlgorithm *ccm = dynamic_cast<RPiController::CcmAlgorithm *>(
				controller_.GetAlgorithm("ccm"));
			if (!ccm) {
				LOG(IPARPI, Warning)
					<< "Could not set SATURATION - no ccm algorithm";
				break;
			}

			ccm->SetSaturation(ctrl.second.get<float>());
			libcameraMetadata_.set(controls::Saturation,
					       ctrl.second.get<float>());
			break;
		}

		case controls::SHARPNESS: {
			RPiController::SharpenAlgorithm *sharpen = dynamic_cast<RPiController::SharpenAlgorithm *>(
				controller_.GetAlgorithm("sharpen"));
			if (!sharpen) {
				LOG(IPARPI, Warning)
					<< "Could not set SHARPNESS - no sharpen algorithm";
				break;
			}

			sharpen->SetStrength(ctrl.second.get<float>());
			libcameraMetadata_.set(controls::Sharpness,
					       ctrl.second.get<float>());
			break;
		}

		case controls::SCALER_CROP: {
			/* We do nothing with this, but should avoid the warning below. */
			break;
		}

		case controls::FRAME_DURATIONS: {
			auto frameDurations = ctrl.second.get<Span<const int64_t>>();
			applyFrameDurations(frameDurations[0], frameDurations[1]);
			break;
		}

		case controls::NOISE_REDUCTION_MODE: {
			RPiController::DenoiseAlgorithm *sdn = dynamic_cast<RPiController::DenoiseAlgorithm *>(
				controller_.GetAlgorithm("SDN"));
			if (!sdn) {
				LOG(IPARPI, Warning)
					<< "Could not set NOISE_REDUCTION_MODE - no SDN algorithm";
				break;
			}

			int32_t idx = ctrl.second.get<int32_t>();
			auto mode = DenoiseModeTable.find(idx);
			if (mode != DenoiseModeTable.end()) {
				sdn->SetMode(mode->second);

				/*
				 * \todo If the colour denoise is not going to run due to an
				 * analysis image resolution or format mismatch, we should
				 * report the status correctly in the metadata.
				 */
				libcameraMetadata_.set(controls::draft::NoiseReductionMode, idx);
			} else {
				LOG(IPARPI, Error) << "Noise reduction mode " << idx
						   << " not recognised";
			}
			break;
		}

		default:
			LOG(IPARPI, Warning)
				<< "Ctrl " << controls::controls.at(ctrl.first)->name()
				<< " is not handled.";
			break;
		}
	}
}

void IPARPi::returnEmbeddedBuffer(unsigned int bufferId)
{
	embeddedComplete.emit(bufferId & ipa::RPi::MaskID);
}

void IPARPi::prepareISP(const ipa::RPi::ISPConfig &data)
{
	struct DeviceStatus deviceStatus = {};
	bool success = false;

	if (data.embeddedBufferPresent) {
		/*
		 * Pipeline handler has supplied us with an embedded data buffer,
		 * so parse it and extract the exposure and gain.
		 */
		success = parseEmbeddedData(data.embeddedBufferId, deviceStatus);

		/* Done with embedded data now, return to pipeline handler asap. */
		returnEmbeddedBuffer(data.embeddedBufferId);
	}

	if (!success) {
		/*
		 * Pipeline handler has not supplied an embedded data buffer,
		 * or embedded data buffer parsing has failed for some reason,
		 * so pull the exposure and gain values from the control list.
		 */
		int32_t exposureLines = data.controls.get(V4L2_CID_EXPOSURE).get<int32_t>();
		int32_t gainCode = data.controls.get(V4L2_CID_ANALOGUE_GAIN).get<int32_t>();
		fillDeviceStatus(exposureLines, gainCode, deviceStatus);
	}

	ControlList ctrls(ispCtrls_);

	rpiMetadata_.Clear();
	rpiMetadata_.Set("device.status", deviceStatus);
	controller_.Prepare(&rpiMetadata_);

	/* Lock the metadata buffer to avoid constant locks/unlocks. */
	std::unique_lock<RPiController::Metadata> lock(rpiMetadata_);

	AwbStatus *awbStatus = rpiMetadata_.GetLocked<AwbStatus>("awb.status");
	if (awbStatus)
		applyAWB(awbStatus, ctrls);

	CcmStatus *ccmStatus = rpiMetadata_.GetLocked<CcmStatus>("ccm.status");
	if (ccmStatus)
		applyCCM(ccmStatus, ctrls);

	AgcStatus *dgStatus = rpiMetadata_.GetLocked<AgcStatus>("agc.status");
	if (dgStatus)
		applyDG(dgStatus, ctrls);

	AlscStatus *lsStatus = rpiMetadata_.GetLocked<AlscStatus>("alsc.status");
	if (lsStatus)
		applyLS(lsStatus, ctrls);

	ContrastStatus *contrastStatus = rpiMetadata_.GetLocked<ContrastStatus>("contrast.status");
	if (contrastStatus)
		applyGamma(contrastStatus, ctrls);

	BlackLevelStatus *blackLevelStatus = rpiMetadata_.GetLocked<BlackLevelStatus>("black_level.status");
	if (blackLevelStatus)
		applyBlackLevel(blackLevelStatus, ctrls);

	GeqStatus *geqStatus = rpiMetadata_.GetLocked<GeqStatus>("geq.status");
	if (geqStatus)
		applyGEQ(geqStatus, ctrls);

	DenoiseStatus *denoiseStatus = rpiMetadata_.GetLocked<DenoiseStatus>("denoise.status");
	if (denoiseStatus)
		applyDenoise(denoiseStatus, ctrls);

	SharpenStatus *sharpenStatus = rpiMetadata_.GetLocked<SharpenStatus>("sharpen.status");
	if (sharpenStatus)
		applySharpen(sharpenStatus, ctrls);

	DpcStatus *dpcStatus = rpiMetadata_.GetLocked<DpcStatus>("dpc.status");
	if (dpcStatus)
		applyDPC(dpcStatus, ctrls);

	if (!ctrls.empty())
		setIspControls.emit(ctrls);
}

bool IPARPi::parseEmbeddedData(unsigned int bufferId, struct DeviceStatus &deviceStatus)
{
	auto it = buffers_.find(bufferId);
	if (it == buffers_.end()) {
		LOG(IPARPI, Error) << "Could not find embedded buffer!";
		return false;
	}

	Span<uint8_t> mem = it->second.maps()[0];
	helper_->Parser().SetBufferSize(mem.size());
	RPiController::MdParser::Status status = helper_->Parser().Parse(mem.data());
	if (status != RPiController::MdParser::Status::OK) {
		LOG(IPARPI, Error) << "Embedded Buffer parsing failed, error " << status;
		return false;
	} else {
		uint32_t exposureLines, gainCode;
		if (helper_->Parser().GetExposureLines(exposureLines) != RPiController::MdParser::Status::OK) {
			LOG(IPARPI, Error) << "Exposure time failed";
			return false;
		}

		if (helper_->Parser().GetGainCode(gainCode) != RPiController::MdParser::Status::OK) {
			LOG(IPARPI, Error) << "Gain failed";
			return false;
		}

		fillDeviceStatus(exposureLines, gainCode, deviceStatus);
	}

	return true;
}

void IPARPi::fillDeviceStatus(uint32_t exposureLines, uint32_t gainCode,
			      struct DeviceStatus &deviceStatus)
{
	deviceStatus.shutter_speed = helper_->Exposure(exposureLines);
	deviceStatus.analogue_gain = helper_->Gain(gainCode);

	LOG(IPARPI, Debug) << "Metadata - Exposure : "
			   << deviceStatus.shutter_speed
			   << " Gain : "
			   << deviceStatus.analogue_gain;
}

void IPARPi::processStats(unsigned int bufferId)
{
	auto it = buffers_.find(bufferId);
	if (it == buffers_.end()) {
		LOG(IPARPI, Error) << "Could not find stats buffer!";
		return;
	}

	Span<uint8_t> mem = it->second.maps()[0];
	bcm2835_isp_stats *stats = reinterpret_cast<bcm2835_isp_stats *>(mem.data());
	RPiController::StatisticsPtr statistics = std::make_shared<bcm2835_isp_stats>(*stats);
	controller_.Process(statistics, &rpiMetadata_);

	struct AgcStatus agcStatus;
	if (rpiMetadata_.Get("agc.status", agcStatus) == 0) {
		ControlList ctrls(sensorCtrls_);
		applyAGC(&agcStatus, ctrls);

		setDelayedControls.emit(ctrls);
	}
}

void IPARPi::applyAWB(const struct AwbStatus *awbStatus, ControlList &ctrls)
{
	LOG(IPARPI, Debug) << "Applying WB R: " << awbStatus->gain_r << " B: "
			   << awbStatus->gain_b;

	ctrls.set(V4L2_CID_RED_BALANCE,
		  static_cast<int32_t>(awbStatus->gain_r * 1000));
	ctrls.set(V4L2_CID_BLUE_BALANCE,
		  static_cast<int32_t>(awbStatus->gain_b * 1000));
}

void IPARPi::applyFrameDurations(double minFrameDuration, double maxFrameDuration)
{
	const double minSensorFrameDuration = 1e-3 * mode_.min_frame_length * mode_.line_length;
	const double maxSensorFrameDuration = 1e-3 * mode_.max_frame_length * mode_.line_length;

	/*
	 * This will only be applied once AGC recalculations occur.
	 * The values may be clamped based on the sensor mode capabilities as well.
	 */
	minFrameDuration_ = minFrameDuration ? minFrameDuration : defaultMaxFrameDuration;
	maxFrameDuration_ = maxFrameDuration ? maxFrameDuration : defaultMinFrameDuration;
	minFrameDuration_ = std::clamp(minFrameDuration_,
				       minSensorFrameDuration, maxSensorFrameDuration);
	maxFrameDuration_ = std::clamp(maxFrameDuration_,
				       minSensorFrameDuration, maxSensorFrameDuration);
	maxFrameDuration_ = std::max(maxFrameDuration_, minFrameDuration_);

	/* Return the validated limits via metadata. */
	libcameraMetadata_.set(controls::FrameDurations,
			       { static_cast<int64_t>(minFrameDuration_),
				 static_cast<int64_t>(maxFrameDuration_) });

	/*
	 * Calculate the maximum exposure time possible for the AGC to use.
	 * GetVBlanking() will update maxShutter with the largest exposure
	 * value possible.
	 */
	double maxShutter = std::numeric_limits<double>::max();
	helper_->GetVBlanking(maxShutter, minFrameDuration_, maxFrameDuration_);

	RPiController::AgcAlgorithm *agc = dynamic_cast<RPiController::AgcAlgorithm *>(
		controller_.GetAlgorithm("agc"));
	agc->SetMaxShutter(maxShutter);
}

void IPARPi::applyAGC(const struct AgcStatus *agcStatus, ControlList &ctrls)
{
	int32_t gainCode = helper_->GainCode(agcStatus->analogue_gain);

	/* GetVBlanking might clip exposure time to the fps limits. */
	double exposure = agcStatus->shutter_time;
	int32_t vblanking = helper_->GetVBlanking(exposure, minFrameDuration_,
						  maxFrameDuration_);
	int32_t exposureLines = helper_->ExposureLines(exposure);

	LOG(IPARPI, Debug) << "Applying AGC Exposure: " << exposure
			   << " (Shutter lines: " << exposureLines << ", AGC requested "
			   << agcStatus->shutter_time << ") Gain: "
			   << agcStatus->analogue_gain << " (Gain Code: "
			   << gainCode << ")";

	/*
	 * Due to the behavior of V4L2, the current value of VBLANK could clip the
	 * exposure time without us knowing. The next time though this function should
	 * clip exposure correctly.
	 */
	ctrls.set(V4L2_CID_VBLANK, vblanking);
	ctrls.set(V4L2_CID_EXPOSURE, exposureLines);
	ctrls.set(V4L2_CID_ANALOGUE_GAIN, gainCode);
}

void IPARPi::applyDG(const struct AgcStatus *dgStatus, ControlList &ctrls)
{
	ctrls.set(V4L2_CID_DIGITAL_GAIN,
		  static_cast<int32_t>(dgStatus->digital_gain * 1000));
}

void IPARPi::applyCCM(const struct CcmStatus *ccmStatus, ControlList &ctrls)
{
	bcm2835_isp_custom_ccm ccm;

	for (int i = 0; i < 9; i++) {
		ccm.ccm.ccm[i / 3][i % 3].den = 1000;
		ccm.ccm.ccm[i / 3][i % 3].num = 1000 * ccmStatus->matrix[i];
	}

	ccm.enabled = 1;
	ccm.ccm.offsets[0] = ccm.ccm.offsets[1] = ccm.ccm.offsets[2] = 0;

	ControlValue c(Span<const uint8_t>{ reinterpret_cast<uint8_t *>(&ccm),
					    sizeof(ccm) });
	ctrls.set(V4L2_CID_USER_BCM2835_ISP_CC_MATRIX, c);
}

void IPARPi::applyGamma(const struct ContrastStatus *contrastStatus, ControlList &ctrls)
{
	struct bcm2835_isp_gamma gamma;

	gamma.enabled = 1;
	for (int i = 0; i < CONTRAST_NUM_POINTS; i++) {
		gamma.x[i] = contrastStatus->points[i].x;
		gamma.y[i] = contrastStatus->points[i].y;
	}

	ControlValue c(Span<const uint8_t>{ reinterpret_cast<uint8_t *>(&gamma),
					    sizeof(gamma) });
	ctrls.set(V4L2_CID_USER_BCM2835_ISP_GAMMA, c);
}

void IPARPi::applyBlackLevel(const struct BlackLevelStatus *blackLevelStatus, ControlList &ctrls)
{
	bcm2835_isp_black_level blackLevel;

	blackLevel.enabled = 1;
	blackLevel.black_level_r = blackLevelStatus->black_level_r;
	blackLevel.black_level_g = blackLevelStatus->black_level_g;
	blackLevel.black_level_b = blackLevelStatus->black_level_b;

	ControlValue c(Span<const uint8_t>{ reinterpret_cast<uint8_t *>(&blackLevel),
					    sizeof(blackLevel) });
	ctrls.set(V4L2_CID_USER_BCM2835_ISP_BLACK_LEVEL, c);
}

void IPARPi::applyGEQ(const struct GeqStatus *geqStatus, ControlList &ctrls)
{
	bcm2835_isp_geq geq;

	geq.enabled = 1;
	geq.offset = geqStatus->offset;
	geq.slope.den = 1000;
	geq.slope.num = 1000 * geqStatus->slope;

	ControlValue c(Span<const uint8_t>{ reinterpret_cast<uint8_t *>(&geq),
					    sizeof(geq) });
	ctrls.set(V4L2_CID_USER_BCM2835_ISP_GEQ, c);
}

void IPARPi::applyDenoise(const struct DenoiseStatus *denoiseStatus, ControlList &ctrls)
{
	using RPiController::DenoiseMode;

	bcm2835_isp_denoise denoise;
	DenoiseMode mode = static_cast<DenoiseMode>(denoiseStatus->mode);

	denoise.enabled = mode != DenoiseMode::Off;
	denoise.constant = denoiseStatus->noise_constant;
	denoise.slope.num = 1000 * denoiseStatus->noise_slope;
	denoise.slope.den = 1000;
	denoise.strength.num = 1000 * denoiseStatus->strength;
	denoise.strength.den = 1000;

	/* Set the CDN mode to match the SDN operating mode. */
	bcm2835_isp_cdn cdn;
	switch (mode) {
	case DenoiseMode::ColourFast:
		cdn.enabled = 1;
		cdn.mode = CDN_MODE_FAST;
		break;
	case DenoiseMode::ColourHighQuality:
		cdn.enabled = 1;
		cdn.mode = CDN_MODE_HIGH_QUALITY;
		break;
	default:
		cdn.enabled = 0;
	}

	ControlValue c(Span<const uint8_t>{ reinterpret_cast<uint8_t *>(&denoise),
					    sizeof(denoise) });
	ctrls.set(V4L2_CID_USER_BCM2835_ISP_DENOISE, c);

	c = ControlValue(Span<const uint8_t>{ reinterpret_cast<uint8_t *>(&cdn),
					      sizeof(cdn) });
	ctrls.set(V4L2_CID_USER_BCM2835_ISP_CDN, c);
}

void IPARPi::applySharpen(const struct SharpenStatus *sharpenStatus, ControlList &ctrls)
{
	bcm2835_isp_sharpen sharpen;

	sharpen.enabled = 1;
	sharpen.threshold.num = 1000 * sharpenStatus->threshold;
	sharpen.threshold.den = 1000;
	sharpen.strength.num = 1000 * sharpenStatus->strength;
	sharpen.strength.den = 1000;
	sharpen.limit.num = 1000 * sharpenStatus->limit;
	sharpen.limit.den = 1000;

	ControlValue c(Span<const uint8_t>{ reinterpret_cast<uint8_t *>(&sharpen),
					    sizeof(sharpen) });
	ctrls.set(V4L2_CID_USER_BCM2835_ISP_SHARPEN, c);
}

void IPARPi::applyDPC(const struct DpcStatus *dpcStatus, ControlList &ctrls)
{
	bcm2835_isp_dpc dpc;

	dpc.enabled = 1;
	dpc.strength = dpcStatus->strength;

	ControlValue c(Span<const uint8_t>{ reinterpret_cast<uint8_t *>(&dpc),
					    sizeof(dpc) });
	ctrls.set(V4L2_CID_USER_BCM2835_ISP_DPC, c);
}

void IPARPi::applyLS(const struct AlscStatus *lsStatus, ControlList &ctrls)
{
	/*
	 * Program lens shading tables into pipeline.
	 * Choose smallest cell size that won't exceed 63x48 cells.
	 */
	const int cellSizes[] = { 16, 32, 64, 128, 256 };
	unsigned int numCells = std::size(cellSizes);
	unsigned int i, w, h, cellSize;
	for (i = 0; i < numCells; i++) {
		cellSize = cellSizes[i];
		w = (mode_.width + cellSize - 1) / cellSize;
		h = (mode_.height + cellSize - 1) / cellSize;
		if (w < 64 && h <= 48)
			break;
	}

	if (i == numCells) {
		LOG(IPARPI, Error) << "Cannot find cell size";
		return;
	}

	/* We're going to supply corner sampled tables, 16 bit samples. */
	w++, h++;
	bcm2835_isp_lens_shading ls = {
		.enabled = 1,
		.grid_cell_size = cellSize,
		.grid_width = w,
		.grid_stride = w,
		.grid_height = h,
		/* .dmabuf will be filled in by pipeline handler. */
		.dmabuf = 0,
		.ref_transform = 0,
		.corner_sampled = 1,
		.gain_format = GAIN_FORMAT_U4P10
	};

	if (!lsTable_ || w * h * 4 * sizeof(uint16_t) > ipa::RPi::MaxLsGridSize) {
		LOG(IPARPI, Error) << "Do not have a correctly allocate lens shading table!";
		return;
	}

	if (lsStatus) {
		/* Format will be u4.10 */
		uint16_t *grid = static_cast<uint16_t *>(lsTable_);

		resampleTable(grid, lsStatus->r, w, h);
		resampleTable(grid + w * h, lsStatus->g, w, h);
		std::memcpy(grid + 2 * w * h, grid + w * h, w * h * sizeof(uint16_t));
		resampleTable(grid + 3 * w * h, lsStatus->b, w, h);
	}

	ControlValue c(Span<const uint8_t>{ reinterpret_cast<uint8_t *>(&ls),
					    sizeof(ls) });
	ctrls.set(V4L2_CID_USER_BCM2835_ISP_LENS_SHADING, c);
}

/*
 * Resamples a 16x12 table with central sampling to destW x destH with corner
 * sampling.
 */
void IPARPi::resampleTable(uint16_t dest[], double const src[12][16],
			   int destW, int destH)
{
	/*
	 * Precalculate and cache the x sampling locations and phases to
	 * save recomputing them on every row.
	 */
	assert(destW > 1 && destH > 1 && destW <= 64);
	int xLo[64], xHi[64];
	double xf[64];
	double x = -0.5, xInc = 16.0 / (destW - 1);
	for (int i = 0; i < destW; i++, x += xInc) {
		xLo[i] = floor(x);
		xf[i] = x - xLo[i];
		xHi[i] = xLo[i] < 15 ? xLo[i] + 1 : 15;
		xLo[i] = xLo[i] > 0 ? xLo[i] : 0;
	}

	/* Now march over the output table generating the new values. */
	double y = -0.5, yInc = 12.0 / (destH - 1);
	for (int j = 0; j < destH; j++, y += yInc) {
		int yLo = floor(y);
		double yf = y - yLo;
		int yHi = yLo < 11 ? yLo + 1 : 11;
		yLo = yLo > 0 ? yLo : 0;
		double const *rowAbove = src[yLo];
		double const *rowBelow = src[yHi];
		for (int i = 0; i < destW; i++) {