summaryrefslogtreecommitdiff
path: root/utils
AgeCommit message (Collapse)Author
2021-01-21utils: checkstyle.py: Fix "protected" members in Commit classLaurent Pinchart
The Commit class and subclasses were reworked in commit 4f5d17f3a4f5 ("utils: checkstyle.py: Make title and files properties of commit class") with the introduction of members of the base class that were meant to be protected (not used externally, but accessible by subclasses). They have been named with a '__' prefix for this purpose, which was a bad choice as Python effectively replaces a leading '__' with a literal '__classname__' prefix to make them private (https://docs.python.org/3/tutorial/classes.html#private-variables). The members accessed in the derived classes are thus different from the ones in the base class. Fix this by replacing the double underscore prefix with a single underscore, which is a "weak internal use indicator" (as specified in https://www.python.org/dev/peps/pep-0008/), closer to the protected access specifier of C++. Reported-by: Umang Jain <email@uajain.com> Reported-by: Naushir Patuck <naush@raspberrypi.com> Fixes: 4f5d17f3a4f5 ("utils: checkstyle.py: Make title and files properties of commit class") Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Niklas Söderlund <niklas.soderlund@ragnatech.se> Tested-by: Naushir Patuck <naush@raspberrypi.com> Reviewed-by: Naushir Patuck <naush@raspberrypi.com>
2020-12-29utils: checkstyle.py: Drop astyle supportLaurent Pinchart
Formatting code using astyle doesn't lead to results as good as with clang-format, and doesn't receive much test coverage as most developers use clang-format. The code is thus bitrotting. Drop it. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Niklas Söderlund <niklas.soderlund@ragnatech.se> Reviewed-by: Paul Elder <paul.elder@ideasonboard.com>
2020-12-29utils: checkstyle.py: Add header add checkerLaurent Pinchart
Add a commit checker that ensures that all header files added to the libcamera includes (public or internal) are accompanied by a corresponding update of the meson.build file in the same directory. Here's the output of the new checker when run against a commit that forgot to update meson.build. $ ./utils/checkstyle.py b3383da79f1d --------------------------------------------------------------------------------- b3383da79f1d513b0d76db220a7104e1c1035e30 libcamera: buffer: Create a MappedBuffer --------------------------------------------------------------------------------- Header include/libcamera/internal/buffer.h added without corresponding update to include/libcamera/internal/meson.build --- 1 potential issue detected, please review In theory we could extend the checker to cover .cpp files too, but the issue will be quite noticeable as meson won't build the file if meson.build isn't updated. Header files are more tricky as problems would only occur at when installing the headers (for public headers), or would result in race conditions in the build. Both of those issues are harder to catch. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Niklas Söderlund <niklas.soderlund@ragnatech.se>
2020-12-29utils: checkstyle.py: Add commit checkersLaurent Pinchart
Add a new category of checkers that operate on a whole commit. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Niklas Söderlund <niklas.soderlund@ragnatech.se>
2020-12-29utils: checkstyle.py: Move diff parsing to Commit classLaurent Pinchart
To avoid duplicating diff parsing in commit checkers, move it to the Commit class. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Niklas Söderlund <niklas.soderlund@ragnatech.se>
2020-12-29utils: checkstyle.py: Add ability to filter files by status in a commitLaurent Pinchart
A commit can perform different operations on a file. Record the file status (added, modified, renamed, deleted, ...) and add the ability to filter files by status when listing the files touched by a commit. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Niklas Söderlund <niklas.soderlund@ragnatech.se>
2020-12-29utils: checkstyle.py: Make title and files properties of commit classLaurent Pinchart
Make the API of the Commit class more explicit by exposing the title and files as properties instead of through a get_info() method. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Niklas Söderlund <niklas.soderlund@ragnatech.se> Reviewed-by: Paul Elder <paul.elder@ideasonboard.com>
2020-12-29utils: checkstyle.py: Move commit handling to a separate sectionLaurent Pinchart
To prepare for checkers that operate directly on commits, move the related classes to a separate section. No functional change is included. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Niklas Söderlund <niklas.soderlund@ragnatech.se> Reviewed-by: Paul Elder <paul.elder@ideasonboard.com>
2020-12-29utils: checkstyle.py: Factor out automatic class registryLaurent Pinchart
The style checkers and formatters duplicate automatic class registry code. Factor it out to a common ClassRegistry helper class. The list of subclasses is moved to a class member variable of the auto-registered base class type. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Niklas Söderlund <niklas.soderlund@ragnatech.se>
2020-12-29utils: checkstyle.py: Drop arguments to super() when possibleLaurent Pinchart
The super() call is a shortcut syntax for super(__class__, <first arg>). Drop the arguments when they match the default. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Niklas Söderlund <niklas.soderlund@ragnatech.se> Reviewed-by: Paul Elder <paul.elder@ideasonboard.com>
2020-12-27utils: gen-controls: Fix enumerators documentationLaurent Pinchart
The Doxygen documentation for enumerators prefixes the enumerator name with the enumeration name. For unscoped enumerations, this is incorrect. Drop the scope. This fixes warnings produced by Doxygen when multiple enumerators with identical names are defined in different scopes. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Tested-by: Jacopo Mondi <jacopo@jmondi.org> Reviewed-by: Niklas Söderlund <niklas.soderlund@ragnatech.se>
2020-11-20src: ipa: raspberrypi: Change 'sport' exposure mode name to 'short'David Plowman
The names have to match for the setting to work. Use the libcamera terminology for consistency (even though it touches more files). Signed-off-by: David Plowman <david.plowman@raspberrypi.com> Reviewed-by: Naushir Patuck <naush@raspberrypi.com> Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com> Signed-off-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
2020-11-11README, meson: Add dependency on ply and jinja2 for IPA interface generationPaul Elder
Specify in the readme and meson file that we depend on python3-ply and python3-jinja2 for generating the IPA interface. Signed-off-by: Paul Elder <paul.elder@ideasonboard.com> Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Niklas Söderlund <niklas.soderlund@ragnatech.se> Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
2020-11-11utils: ipc: import mojoPaul Elder
Import mojo from the Chromium repository, so that we can use it for generating code for the IPC mechanism. The commit from which this was taken is: a079161ec8c6907b883f9cb84fc8c4e7896cb1d0 "Add PPAPI constructs for sending focus object to PdfAccessibilityTree" This tree has been pruned to remove directories that didn't have any necessary code: - mojo/* except for mojo/public - mojo core, docs, and misc files - mojo/public/* except for mojo/public/{tools,LICENSE} - language bindings for IPC, tests, and some mojo internals - mojo/public/tools/{fuzzers,chrome_ipc} - mojo/public/tools/bindings/generators - code generation for other languages No files were modified. Signed-off-by: Paul Elder <paul.elder@ideasonboard.com> Acked-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Acked-by: Niklas Söderlund <niklas.soderlund@ragnatech.se> Acked-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
2020-11-08utils: checkstyle.py: Add d-pointer formatterLaurent Pinchart
Add a formatter to ensure consistent naming of 'd' and 'o' variables related to the d-pointer design pattern, as implemented by the Extensible class. The formatter also ensures that the pointer is always const. const-correctness issues related to the data pointed to will be caught by the compiler, and thus don't need to be checked here. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Niklas Söderlund <niklas.soderlund@ragnatech.se>
2020-11-03utils: tracepoints: Add simple statistics scriptPaul Elder
Add a script that scans a trace for IPA call tracepoints, and returns statistics on the time taken for IPA calls. Signed-off-by: Paul Elder <paul.elder@ideasonboard.com> Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
2020-11-03libcamera: tracing: Implement tracing infrastructurePaul Elder
Implement tracing infrastructure in libcamera. It takes .tp files, as required by lttng, and generates a tracepoint header and C file, as lttng requires. meson is updated accordingly to get it to compile with the rest of libcamera. Update the documentation accordingly. Signed-off-by: Paul Elder <paul.elder@ideasonboard.com> Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
2020-11-02utils: hooks: pre-push: Reject commits with a Change-IdLaurent Pinchart
We're not using gerrit, so let's prevent Change-Id tags creeping in unadvertently. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Niklas Söderlund <niklas.soderlund@ragnatech.se> Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
2020-10-26libcamera: controls: Fix rogue whitespaceJacopo Mondi
Remove an extra whitespace in the declaration of a dictionary entry in gen-controls.py script. Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Signed-off-by: Jacopo Mondi <jacopo@jmondi.org>
2020-10-26libcamera: controls: Generate an array of valid valuesJacopo Mondi
For each Control that supports enumerated values generate an array of ControlValue which contains the full list of valid values. At the expense of a slight increase in memory occupation this change allows the construction of the ControlInfo associated with a Control from the values list, defaulting the minimum and maximum values reported by the ControlInfo. Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com> Signed-off-by: Jacopo Mondi <jacopo@jmondi.org>
2020-10-26libcamera: controls: Rename enumerate valuesJacopo Mondi
Rename the enumeration of supported values with the suffix "Enum" in place of "Values" to prepare to re-use the suffix "Values" for the vector of Control's value introduced by the next patch. Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com> Signed-off-by: Jacopo Mondi <jacopo@jmondi.org>
2020-10-26libcamera: Support draft controls and propertiesKieran Bingham
Extend the control and property framework to support exposing draft controls and properties in a scoped namespace. The controls/properties themselves will retain the same ordering in the relevant enum/id maps - but the access to any draft control will require explicitly referencing through its' draft:: namespace prefix. 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> [Added missing hunk in control_ids.cpp.in and changed subject] Signed-off-by: Jacopo Mondi <jacopo@jmondi.org>
2020-10-02utils: hooks: pre-push: Accept Acked-by in addition to Reviewed-byLaurent Pinchart
Allow pushing commits that have no Reviewed-by tag but have at least one Acked-by tag. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com> Reviewed-by: Niklas Söderlund <niklas.soderlund@ragnatech.se>
2020-09-24meson: Define python3 and python3-yaml required dependenciesEzequiel Garcia
With this change, meson will complain specifically about missing python3 and missing python3-yaml. As specified by meson documentation: https://mesonbuild.com/Python-module.html, this change requires meson v0.51. Signed-off-by: Ezequiel Garcia <ezequiel@collabora.com> Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
2020-09-24libcamera: ipa: Move key generation to utilsKieran Bingham
Move the GPLv2 utilities used for generating public and private keys to the utilities subtree. Signed-off-by: Kieran Bingham <kieran.bingham@ideasonboard.com> Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Paul Elder <paul.elder@ideasonboard.com>
2020-09-24libcamera: Move Header generation utilities to utilsKieran Bingham
Move the GPL2 utilities which handle generation of controls, formats and the top level libcamera header to the utils subtree. Signed-off-by: Kieran Bingham <kieran.bingham@ideasonboard.com> Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Paul Elder <paul.elder@ideasonboard.com>
2020-09-21Documentation: Adjust guidelines regarding math.h headerLaurent Pinchart
While libcamera prefers usage of the C standard library headers (xxx.h) over the C++ version (cxxx), we make an exception for cmath as the overloaded versions of the math functions are convenient. Document this, and adjust checkstyle.py accordingly. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Jacopo Mondi <jacopo@jmondi.org> Reviewed-by: Niklas Söderlund <niklas.soderlund@ragnatech.se>
2020-08-06libcamera: Add build time to version string for dirty buildsNiklas Söderlund
Having the build time in the version string is useful when building from a dirty worktree and deploying to targets as a quick way to identify the binary has been deployed successfully. Before this change the version string is reported as libcamera v0.0.0+1692-aaff196a-dirty While with this change the version string is reported as libcamera v0.0.0+1692-aaff196a-dirty (2020-08-05T22:42:18+02:00) Signed-off-by: Niklas Söderlund <niklas.soderlund@ragnatech.se> Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
2020-07-03utils: raspberrypi: ctt: json_pretty_print: Add newline at end of outputLaurent Pinchart
Make sure the output ends with a newline. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: David Plowman <david.plowman@raspberrypi.com> Tested-by: David Plowman <david.plowman@raspberrypi.com>
2020-07-03utils: raspberrypi: ctt: json_pretty_print: Avoid spaces at end of linesLaurent Pinchart
Avoid outputting spaces at end of lines by recording the need for a space and outputting it before the next character only if not a newline. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: David Plowman <david.plowman@raspberrypi.com> Tested-by: David Plowman <david.plowman@raspberrypi.com>
2020-07-03utils: raspberrypi: ctt: json_pretty_print: Collapse newlinesLaurent Pinchart
Simplify the newline skipping logic by simply collapsing newlines. If a newline has been output, all subsequent newlines will be skipped until the next non-newline character is output. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: David Plowman <david.plowman@raspberrypi.com> Tested-by: David Plowman <david.plowman@raspberrypi.com>
2020-07-03utils: raspberrypi: ctt: json_pretty_print: Fix indentation handlingLaurent Pinchart
Indentation is handled by outputting spaces right after outputting a newline character. That works in most cases, but would result in the input '{}' being printed as { } instead of { } Fix it by outputting the indentation before outputting the next character after a newline. The indentation value will be updated by then. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: David Plowman <david.plowman@raspberrypi.com> Tested-by: David Plowman <david.plowman@raspberrypi.com>
2020-07-03utils: raspberrypi: ctt: json_pretty_print: Add character write methodLaurent Pinchart
Add a write method to the JSONPrettyPrinter class to output a character. This will be used to handle state updates when outputting individual characters. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: David Plowman <david.plowman@raspberrypi.com> Tested-by: David Plowman <david.plowman@raspberrypi.com>
2020-07-03utils: raspberrypi: ctt: json_pretty_print: Skip all spacesLaurent Pinchart
Skip all white space characters, not just ' '. This makes a difference if the input JSON data is already formatted. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: David Plowman <david.plowman@raspberrypi.com> Tested-by: David Plowman <david.plowman@raspberrypi.com>
2020-07-03utils: raspberrypi: ctt: json_pretty_print: Make test output to stdoutLaurent Pinchart
The standalone test mode output to a file name "pretty.json". To make the test mode more versatile, output to stdout instead. The user can then decide how to use the output. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: David Plowman <david.plowman@raspberrypi.com> Tested-by: David Plowman <david.plowman@raspberrypi.com>
2020-07-03utils: raspberrypi: ctt: json_pretty_print: Make output file a class memberLaurent Pinchart
Instead of passing the output file to every method of the printer class, make it a class member. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: David Plowman <david.plowman@raspberrypi.com> Tested-by: David Plowman <david.plowman@raspberrypi.com>
2020-07-03utils: raspberrypi: ctt: json_pretty_print: Turn printer into a classLaurent Pinchart
Instead of passing a state dictionary to every method, turn the printer into a class and store the state internally. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: David Plowman <david.plowman@raspberrypi.com> Tested-by: David Plowman <david.plowman@raspberrypi.com>
2020-07-03utils: raspberrypi: ctt: json_pretty_print: Fix printer testLaurent Pinchart
The ctt_pretty_print_json.py file supports being run standalone to test the code. It however suffers from multiple issues: - The same input file name is hardcoded, and doesn't exist in the repository - The input file name is used instead of JSON data Fix both issues and make the input file selectable on the command line. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com> Reviewed-by: David Plowman <david.plowman@raspberrypi.com> Tested-by: David Plowman <david.plowman@raspberrypi.com>
2020-06-29utils: raspberrypi: ctt: Fix pycodestyle W605Laurent Pinchart
W605 invalid escape sequence '\.' Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Niklas Söderlund <niklas.soderlund@ragnatech.se> Reviewed-by: David Plowman <david.plowman@raspberrypi.com>
2020-05-13utils: raspberrypi: ctt: Fix pycodestyle E302Laurent Pinchart
E302 expected 2 blank lines, found 0 Note that issues are still flagged, due to the use of docstrings as multi-lines comments. This will be addressed separately. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com> Reviewed-by: David Plowman <david.plowman@raspberrypi.com>
2020-05-13utils: raspberrypi: ctt: Fix pycodestyle E305Laurent Pinchart
E305 expected 2 blank lines after class or function definition Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com> Reviewed-by: David Plowman <david.plowman@raspberrypi.com>
2020-05-13utils: raspberrypi: ctt: Fix pycodestyle E741Laurent Pinchart
E741 ambiguous variable name 'l' Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com> Reviewed-by: David Plowman <david.plowman@raspberrypi.com>
2020-05-13utils: raspberrypi: ctt: Fix pycodestyle W504Laurent Pinchart
W504 line break after binary operator Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com> Reviewed-by: David Plowman <david.plowman@raspberrypi.com>
2020-05-13utils: raspberrypi: ctt: Fix pycodestyle E722Laurent Pinchart
E722 do not use bare 'except' Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com> Reviewed-by: David Plowman <david.plowman@raspberrypi.com>
2020-05-13utils: raspberrypi: ctt: Fix pycodestyle E721Laurent Pinchart
E721 do not compare types, use 'isinstance()' Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com> Reviewed-by: David Plowman <david.plowman@raspberrypi.com>
2020-05-13utils: raspberrypi: ctt: Fix pycodestyle E713Laurent Pinchart
E713 test for membership should be 'not in' Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com> Reviewed-by: David Plowman <david.plowman@raspberrypi.com>
2020-05-13utils: raspberrypi: ctt: Fix pycodestyle E116 and E117Laurent Pinchart
E116 unexpected indentation (comment) E117 over-indented (comment) Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com> Reviewed-by: David Plowman <david.plowman@raspberrypi.com>
2020-05-13utils: raspberrypi: ctt: Fix pycodestyle E123 and E126Laurent Pinchart
E123 closing bracket does not match indentation of opening bracket's line E126 continuation line over-indented for hanging indent Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com> Reviewed-by: David Plowman <david.plowman@raspberrypi.com>
2020-05-13utils: raspberrypi: ctt: Fix pycodestyle E711 and E712Laurent Pinchart
E711 comparison to None should be 'if cond is None:' E711 comparison to None should be 'if cond is not None:' E712 comparison to False should be 'if cond is False:' or 'if not cond:' E712 comparison to True should be 'if cond is True:' or 'if cond:' Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com> Reviewed-by: David Plowman <david.plowman@raspberrypi.com>
2020-05-13utils: raspberrypi: ctt: Fix pycodestyle E222Laurent Pinchart
E222 multiple spaces after operator Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com> Reviewed-by: David Plowman <david.plowman@raspberrypi.com>
5'>1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515
/* SPDX-License-Identifier: BSD-2-Clause */
/*
 * Copyright (C) 2019-2023, Raspberry Pi Ltd
 *
 * Raspberry Pi IPA base class
 */

#include "ipa_base.h"

#include <cmath>

#include <libcamera/base/log.h>
#include <libcamera/base/span.h>
#include <libcamera/control_ids.h>
#include <libcamera/property_ids.h>

#include "controller/af_algorithm.h"
#include "controller/af_status.h"
#include "controller/agc_algorithm.h"
#include "controller/awb_algorithm.h"
#include "controller/awb_status.h"
#include "controller/black_level_status.h"
#include "controller/ccm_algorithm.h"
#include "controller/ccm_status.h"
#include "controller/contrast_algorithm.h"
#include "controller/denoise_algorithm.h"
#include "controller/hdr_algorithm.h"
#include "controller/lux_status.h"
#include "controller/sharpen_algorithm.h"
#include "controller/statistics.h"

namespace libcamera {

using namespace std::literals::chrono_literals;
using utils::Duration;

namespace {

/* Number of frame length times to hold in the queue. */
constexpr unsigned int FrameLengthsQueueSize = 10;

/* Configure the sensor with these values initially. */
constexpr double defaultAnalogueGain = 1.0;
constexpr Duration defaultExposureTime = 20.0ms;
constexpr Duration defaultMinFrameDuration = 1.0s / 30.0;
constexpr Duration defaultMaxFrameDuration = 250.0s;

/*
 * Determine the minimum allowable inter-frame duration to run the controller
 * algorithms. If the pipeline handler provider frames at a rate higher than this,
 * we rate-limit the controller Prepare() and Process() calls to lower than or
 * equal to this rate.
 */
constexpr Duration controllerMinFrameDuration = 1.0s / 30.0;

/* List of controls handled by the Raspberry Pi IPA */
const ControlInfoMap::Map ipaControls{
	{ &controls::AeEnable, ControlInfo(false, true) },
	{ &controls::ExposureTime, ControlInfo(0, 66666) },
	{ &controls::AnalogueGain, ControlInfo(1.0f, 16.0f) },
	{ &controls::AeMeteringMode, ControlInfo(controls::AeMeteringModeValues) },
	{ &controls::AeConstraintMode, ControlInfo(controls::AeConstraintModeValues) },
	{ &controls::AeExposureMode, ControlInfo(controls::AeExposureModeValues) },
	{ &controls::ExposureValue, ControlInfo(-8.0f, 8.0f, 0.0f) },
	{ &controls::AeFlickerMode, ControlInfo(static_cast<int>(controls::FlickerOff),
						static_cast<int>(controls::FlickerManual),
						static_cast<int>(controls::FlickerOff)) },
	{ &controls::AeFlickerPeriod, ControlInfo(100, 1000000) },
	{ &controls::Brightness, ControlInfo(-1.0f, 1.0f, 0.0f) },
	{ &controls::Contrast, ControlInfo(0.0f, 32.0f, 1.0f) },
	{ &controls::HdrMode, ControlInfo(controls::HdrModeValues) },
	{ &controls::Sharpness, ControlInfo(0.0f, 16.0f, 1.0f) },
	{ &controls::ScalerCrop, ControlInfo(Rectangle{}, Rectangle(65535, 65535, 65535, 65535), Rectangle{}) },
	{ &controls::FrameDurationLimits, ControlInfo(INT64_C(33333), INT64_C(120000)) },
	{ &controls::draft::NoiseReductionMode, ControlInfo(controls::draft::NoiseReductionModeValues) },
	{ &controls::rpi::StatsOutputEnable, ControlInfo(false, true, false) },
};

/* IPA controls handled conditionally, if the sensor is not mono */
const ControlInfoMap::Map ipaColourControls{
	{ &controls::AwbEnable, ControlInfo(false, true) },
	{ &controls::AwbMode, ControlInfo(controls::AwbModeValues) },
	{ &controls::ColourGains, ControlInfo(0.0f, 32.0f) },
	{ &controls::Saturation, ControlInfo(0.0f, 32.0f, 1.0f) },
};

/* IPA controls handled conditionally, if the lens has a focus control */
const ControlInfoMap::Map ipaAfControls{
	{ &controls::AfMode, ControlInfo(controls::AfModeValues) },
	{ &controls::AfRange, ControlInfo(controls::AfRangeValues) },
	{ &controls::AfSpeed, ControlInfo(controls::AfSpeedValues) },
	{ &controls::AfMetering, ControlInfo(controls::AfMeteringValues) },
	{ &controls::AfWindows, ControlInfo(Rectangle{}, Rectangle(65535, 65535, 65535, 65535), Rectangle{}) },
	{ &controls::AfTrigger, ControlInfo(controls::AfTriggerValues) },
	{ &controls::AfPause, ControlInfo(controls::AfPauseValues) },
	{ &controls::LensPosition, ControlInfo(0.0f, 32.0f, 1.0f) }
};

} /* namespace */

LOG_DEFINE_CATEGORY(IPARPI)

namespace ipa::RPi {

IpaBase::IpaBase()
	: controller_(), frameLengths_(FrameLengthsQueueSize, 0s), statsMetadataOutput_(false),
	  stitchSwapBuffers_(false), frameCount_(0), mistrustCount_(0), lastRunTimestamp_(0),
	  firstStart_(true), flickerState_({ 0, 0s })
{
}

IpaBase::~IpaBase()
{
}

int32_t IpaBase::init(const IPASettings &settings, const InitParams &params, InitResult *result)
{
	/*
	 * 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, hblankDelay, sensorMetadata;
	helper_->getDelays(exposureDelay, gainDelay, vblankDelay, hblankDelay);
	sensorMetadata = helper_->sensorEmbeddedDataPresent();

	result->sensorConfig.gainDelay = gainDelay;
	result->sensorConfig.exposureDelay = exposureDelay;
	result->sensorConfig.vblankDelay = vblankDelay;
	result->sensorConfig.hblankDelay = hblankDelay;
	result->sensorConfig.sensorMetadata = sensorMetadata;

	/* Load the tuning file for this sensor. */
	int ret = controller_.read(settings.configurationFile.c_str());
	if (ret) {
		LOG(IPARPI, Error)
			<< "Failed to load tuning data file "
			<< settings.configurationFile;
		return ret;
	}

	lensPresent_ = params.lensPresent;

	controller_.initialise();

	/* Return the controls handled by the IPA */
	ControlInfoMap::Map ctrlMap = ipaControls;
	if (lensPresent_)
		ctrlMap.merge(ControlInfoMap::Map(ipaAfControls));

	monoSensor_ = params.sensorInfo.cfaPattern == properties::draft::ColorFilterArrangementEnum::MONO;
	if (!monoSensor_)
		ctrlMap.merge(ControlInfoMap::Map(ipaColourControls));

	result->controlInfo = ControlInfoMap(std::move(ctrlMap), controls::controls);

	return platformInit(params, result);
}

int32_t IpaBase::configure(const IPACameraSensorInfo &sensorInfo, const ConfigParams &params,
			   ConfigResult *result)
{
	sensorCtrls_ = params.sensorControls;

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

	if (lensPresent_) {
		lensCtrls_ = params.lensControls;
		if (!validateLensControls()) {
			LOG(IPARPI, Warning) << "Lens validation failed, "
					     << "no lens control will be available.";
			lensPresent_ = false;
		}
	}

	/* 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>(params.transform);

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

	/*
	 * Initialise this ControlList correctly, even if empty, in case the IPA is
	 * running is isolation mode (passing the ControlList through the IPC layer).
	 */
	ControlList ctrls(sensorCtrls_);

	/* The pipeline handler passes out the mode's sensitivity. */
	result->modeSensitivity = mode_.sensitivity;

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

		/* Supply initial values for gain and exposure. */
		AgcStatus agcStatus;
		agcStatus.shutterTime = defaultExposureTime;
		agcStatus.analogueGain = defaultAnalogueGain;
		applyAGC(&agcStatus, ctrls);

		/*
		 * Set the lens to the default (typically hyperfocal) position
		 * on first start.
		 */
		if (lensPresent_) {
			RPiController::AfAlgorithm *af =
				dynamic_cast<RPiController::AfAlgorithm *>(controller_.getAlgorithm("af"));

			if (af) {
				float defaultPos =
					ipaAfControls.at(&controls::LensPosition).def().get<float>();
				ControlList lensCtrl(lensCtrls_);
				int32_t hwpos;

				af->setLensPosition(defaultPos, &hwpos);
				lensCtrl.set(V4L2_CID_FOCUS_ABSOLUTE, hwpos);
				result->lensControls = std::move(lensCtrl);
			}
		}
	}

	result->sensorControls = std::move(ctrls);

	/*
	 * Apply the correct limits to the exposure, gain and frame duration controls
	 * based on the current sensor mode.
	 */
	ControlInfoMap::Map ctrlMap = ipaControls;
	ctrlMap[&controls::FrameDurationLimits] =
		ControlInfo(static_cast<int64_t>(mode_.minFrameDuration.get<std::micro>()),
			    static_cast<int64_t>(mode_.maxFrameDuration.get<std::micro>()));

	ctrlMap[&controls::AnalogueGain] =
		ControlInfo(static_cast<float>(mode_.minAnalogueGain),
			    static_cast<float>(mode_.maxAnalogueGain));

	ctrlMap[&controls::ExposureTime] =
		ControlInfo(static_cast<int32_t>(mode_.minShutter.get<std::micro>()),
			    static_cast<int32_t>(mode_.maxShutter.get<std::micro>()));

	/* Declare colour processing related controls for non-mono sensors. */
	if (!monoSensor_)
		ctrlMap.merge(ControlInfoMap::Map(ipaColourControls));

	/* Declare Autofocus controls, only if we have a controllable lens */
	if (lensPresent_)
		ctrlMap.merge(ControlInfoMap::Map(ipaAfControls));

	result->controlInfo = ControlInfoMap(std::move(ctrlMap), controls::controls);

	return platformConfigure(params, result);
}

void IpaBase::start(const ControlList &controls, StartResult *result)
{
	RPiController::Metadata metadata;

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

	controller_.switchMode(mode_, &metadata);

	/* Reset the frame lengths queue state. */
	lastTimeout_ = 0s;
	frameLengths_.clear();
	frameLengths_.resize(FrameLengthsQueueSize, 0s);

	/* SwitchMode may supply updated exposure/gain values to use. */
	AgcStatus agcStatus;
	agcStatus.shutterTime = 0.0s;
	agcStatus.analogueGain = 0.0;

	metadata.get("agc.status", agcStatus);
	if (agcStatus.shutterTime && agcStatus.analogueGain) {
		ControlList ctrls(sensorCtrls_);
		applyAGC(&agcStatus, ctrls);
		result->controls = std::move(ctrls);
		setCameraTimeoutValue();
	}
	/* Make a note of this as it tells us the HDR status of the first few frames. */
	hdrStatus_ = agcStatus.hdr;

	/*
	 * 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;
	if (firstStart_) {
		dropFrameCount_ = 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_;
		}

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

	result->dropFrameCount = dropFrameCount_;

	firstStart_ = false;
	lastRunTimestamp_ = 0;

	platformStart(controls, result);
}

void IpaBase::mapBuffers(const std::vector<IPABuffer> &buffers)
{
	for (const IPABuffer &buffer : buffers) {
		const FrameBuffer fb(buffer.planes);
		buffers_.emplace(buffer.id,
				 MappedFrameBuffer(&fb, MappedFrameBuffer::MapFlag::ReadWrite));
	}
}

void IpaBase::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 IpaBase::prepareIsp(const PrepareParams &params)
{
	applyControls(params.requestControls);

	/*
	 * 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".
	 */
	int64_t frameTimestamp = params.sensorControls.get(controls::SensorTimestamp).value_or(0);
	unsigned int ipaContext = params.ipaContext % rpiMetadata_.size();
	RPiController::Metadata &rpiMetadata = rpiMetadata_[ipaContext];
	Span<uint8_t> embeddedBuffer;

	rpiMetadata.clear();
	fillDeviceStatus(params.sensorControls, ipaContext);

	if (params.buffers.embedded) {
		/*
		 * Pipeline handler has supplied us with an embedded data buffer,
		 * we must pass it to the CamHelper for parsing.
		 */
		auto it = buffers_.find(params.buffers.embedded);
		ASSERT(it != buffers_.end());
		embeddedBuffer = it->second.planes()[0];
	}

	/*
	 * AGC wants to know the algorithm status from the time it actioned the
	 * sensor exposure/gain changes. So fetch it from the metadata list
	 * indexed by the IPA cookie returned, and put it in the current frame
	 * metadata.
	 *
	 * Note if the HDR mode has changed, as things like tonemaps may need updating.
	 */
	AgcStatus agcStatus;
	bool hdrChange = false;
	RPiController::Metadata &delayedMetadata = rpiMetadata_[params.delayContext];
	if (!delayedMetadata.get<AgcStatus>("agc.status", agcStatus)) {
		rpiMetadata.set("agc.delayed_status", agcStatus);
		hdrChange = agcStatus.hdr.mode != hdrStatus_.mode;
		hdrStatus_ = agcStatus.hdr;
	}

	/*
	 * This may overwrite the DeviceStatus using values from the sensor
	 * metadata, and may also do additional custom processing.
	 */
	helper_->prepare(embeddedBuffer, rpiMetadata);

	/* Allow a 10% margin on the comparison below. */
	Duration delta = (frameTimestamp - lastRunTimestamp_) * 1.0ns;
	if (lastRunTimestamp_ && frameCount_ > dropFrameCount_ &&
	    delta < controllerMinFrameDuration * 0.9 && !hdrChange) {
		/*
		 * Ensure we merge the previous frame's metadata with the current
		 * frame. This will not overwrite exposure/gain values for the
		 * current frame, or any other bits of metadata that were added
		 * in helper_->Prepare().
		 */
		RPiController::Metadata &lastMetadata =
			rpiMetadata_[(ipaContext ? ipaContext : rpiMetadata_.size()) - 1];
		rpiMetadata.mergeCopy(lastMetadata);
		processPending_ = false;
	} else {
		processPending_ = true;
		lastRunTimestamp_ = frameTimestamp;
	}

	/*
	 * If the statistics are inline (i.e. already available with the Bayer
	 * frame), call processStats() now before prepare().
	 */
	if (controller_.getHardwareConfig().statsInline)
		processStats({ params.buffers, params.ipaContext });

	/* Do we need/want to call prepare? */
	if (processPending_) {
		controller_.prepare(&rpiMetadata);
		/* Actually prepare the ISP parameters for the frame. */
		platformPrepareIsp(params, rpiMetadata);
	}

	frameCount_++;

	/* If the statistics are inline the metadata can be returned early. */
	if (controller_.getHardwareConfig().statsInline)
		reportMetadata(ipaContext);

	/* Ready to push the input buffer into the ISP. */
	prepareIspComplete.emit(params.buffers, stitchSwapBuffers_);
}

void IpaBase::processStats(const ProcessParams &params)
{
	unsigned int ipaContext = params.ipaContext % rpiMetadata_.size();

	if (processPending_ && frameCount_ >= mistrustCount_) {
		RPiController::Metadata &rpiMetadata = rpiMetadata_[ipaContext];

		auto it = buffers_.find(params.buffers.stats);
		if (it == buffers_.end()) {
			LOG(IPARPI, Error) << "Could not find stats buffer!";
			return;
		}

		RPiController::StatisticsPtr statistics = platformProcessStats(it->second.planes()[0]);

		/* reportMetadata() will pick this up and set the FocusFoM metadata */
		rpiMetadata.set("focus.status", statistics->focusRegions);

		helper_->process(statistics, rpiMetadata);
		controller_.process(statistics, &rpiMetadata);

		struct AgcStatus agcStatus;
		if (rpiMetadata.get("agc.status", agcStatus) == 0) {
			ControlList ctrls(sensorCtrls_);
			applyAGC(&agcStatus, ctrls);
			setDelayedControls.emit(ctrls, ipaContext);
			setCameraTimeoutValue();
		}
	}

	/*
	 * If the statistics are not inline the metadata must be returned now,
	 * before the processStatsComplete signal.
	 */
	if (!controller_.getHardwareConfig().statsInline)
		reportMetadata(ipaContext);

	processStatsComplete.emit(params.buffers);
}

void IpaBase::setMode(const IPACameraSensorInfo &sensorInfo)
{
	mode_.bitdepth = sensorInfo.bitsPerPixel;
	mode_.width = sensorInfo.outputSize.width;
	mode_.height = sensorInfo.outputSize.height;
	mode_.sensorWidth = sensorInfo.activeAreaSize.width;
	mode_.sensorHeight = sensorInfo.activeAreaSize.height;
	mode_.cropX = sensorInfo.analogCrop.x;
	mode_.cropY = sensorInfo.analogCrop.y;
	mode_.pixelRate = sensorInfo.pixelRate;

	/*
	 * Calculate scaling parameters. The scale_[xy] factors are determined
	 * by the ratio between the crop rectangle size and the output size.
	 */
	mode_.scaleX = sensorInfo.analogCrop.width / sensorInfo.outputSize.width;
	mode_.scaleY = 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_.binX = std::min(2, static_cast<int>(mode_.scaleX));
	mode_.binY = std::min(2, static_cast<int>(mode_.scaleY));

	/* The noise factor is the square root of the total binning factor. */
	mode_.noiseFactor = std::sqrt(mode_.binX * mode_.binY);

	/*
	 * Calculate the line length as the ratio between the line length in
	 * pixels and the pixel rate.
	 */
	mode_.minLineLength = sensorInfo.minLineLength * (1.0s / sensorInfo.pixelRate);
	mode_.maxLineLength = sensorInfo.maxLineLength * (1.0s / sensorInfo.pixelRate);

	/*
	 * Ensure that the maximum pixel processing rate does not exceed the ISP
	 * hardware capabilities. If it does, try adjusting the minimum line
	 * length to compensate if possible.
	 */
	Duration minPixelTime = controller_.getHardwareConfig().minPixelProcessingTime;
	Duration pixelTime = mode_.minLineLength / mode_.width;
	if (minPixelTime && pixelTime < minPixelTime) {
		Duration adjustedLineLength = minPixelTime * mode_.width;
		if (adjustedLineLength <= mode_.maxLineLength) {
			LOG(IPARPI, Info)
				<< "Adjusting mode minimum line length from " << mode_.minLineLength
				<< " to " << adjustedLineLength << " because of ISP constraints.";
			mode_.minLineLength = adjustedLineLength;
		} else {
			LOG(IPARPI, Error)
				<< "Sensor minimum line length of " << pixelTime * mode_.width
				<< " (" << 1us / pixelTime << " MPix/s)"
				<< " is below the minimum allowable ISP limit of "
				<< adjustedLineLength
				<< " (" << 1us / minPixelTime << " MPix/s) ";
			LOG(IPARPI, Error)
				<< "THIS WILL CAUSE IMAGE CORRUPTION!!! "
				<< "Please update the camera sensor driver to allow more horizontal blanking control.";
		}
	}

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

	/* Store these for convenience. */
	mode_.minFrameDuration = mode_.minFrameLength * mode_.minLineLength;
	mode_.maxFrameDuration = mode_.maxFrameLength * mode_.maxLineLength;

	/*
	 * Some sensors may have different sensitivities in different modes;
	 * the CamHelper will know the correct value.
	 */
	mode_.sensitivity = helper_->getModeSensitivity(mode_);

	const ControlInfo &gainCtrl = sensorCtrls_.at(V4L2_CID_ANALOGUE_GAIN);
	const ControlInfo &shutterCtrl = sensorCtrls_.at(V4L2_CID_EXPOSURE);

	mode_.minAnalogueGain = helper_->gain(gainCtrl.min().get<int32_t>());
	mode_.maxAnalogueGain = helper_->gain(gainCtrl.max().get<int32_t>());

	/*
	 * We need to give the helper the min/max frame durations so it can calculate
	 * the correct exposure limits below.
	 */
	helper_->setCameraMode(mode_);

	/* Shutter speed is calculated based on the limits of the frame durations. */
	mode_.minShutter = helper_->exposure(shutterCtrl.min().get<int32_t>(), mode_.minLineLength);
	mode_.maxShutter = Duration::max();
	helper_->getBlanking(mode_.maxShutter,
			     mode_.minFrameDuration, mode_.maxFrameDuration);
}

void IpaBase::setCameraTimeoutValue()
{
	/*
	 * Take the maximum value of the exposure queue as the camera timeout
	 * value to pass back to the pipeline handler. Only signal if it has changed
	 * from the last set value.
	 */
	auto max = std::max_element(frameLengths_.begin(), frameLengths_.end());

	if (*max != lastTimeout_) {
		setCameraTimeout.emit(max->get<std::milli>());
		lastTimeout_ = *max;
	}
}

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

	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 IpaBase::validateLensControls()
{
	if (lensCtrls_.find(V4L2_CID_FOCUS_ABSOLUTE) == lensCtrls_.end()) {
		LOG(IPARPI, Error) << "Unable to find Lens control V4L2_CID_FOCUS_ABSOLUTE";
		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::ConstraintShadows, "shadows" },
	{ 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, "auto" },
	{ 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::AfAlgorithm::AfMode> AfModeTable = {
	{ controls::AfModeManual, RPiController::AfAlgorithm::AfModeManual },
	{ controls::AfModeAuto, RPiController::AfAlgorithm::AfModeAuto },
	{ controls::AfModeContinuous, RPiController::AfAlgorithm::AfModeContinuous },
};

static const std::map<int32_t, RPiController::AfAlgorithm::AfRange> AfRangeTable = {
	{ controls::AfRangeNormal, RPiController::AfAlgorithm::AfRangeNormal },
	{ controls::AfRangeMacro, RPiController::AfAlgorithm::AfRangeMacro },
	{ controls::AfRangeFull, RPiController::AfAlgorithm::AfRangeFull },
};

static const std::map<int32_t, RPiController::AfAlgorithm::AfPause> AfPauseTable = {
	{ controls::AfPauseImmediate, RPiController::AfAlgorithm::AfPauseImmediate },
	{ controls::AfPauseDeferred, RPiController::AfAlgorithm::AfPauseDeferred },
	{ controls::AfPauseResume, RPiController::AfAlgorithm::AfPauseResume },
};

static const std::map<int32_t, std::string> HdrModeTable = {
	{ controls::HdrModeOff, "Off" },
	{ controls::HdrModeMultiExposureUnmerged, "MultiExposureUnmerged" },
	{ controls::HdrModeMultiExposure, "MultiExposure" },
	{ controls::HdrModeSingleExposure, "SingleExposure" },
	{ controls::HdrModeNight, "Night" },
};

void IpaBase::applyControls(const ControlList &controls)
{
	using RPiController::AgcAlgorithm;
	using RPiController::AfAlgorithm;
	using RPiController::ContrastAlgorithm;
	using RPiController::DenoiseAlgorithm;
	using RPiController::HdrAlgorithm;

	/* Clear the return metadata buffer. */
	libcameraMetadata_.clear();

	/* Because some AF controls are mode-specific, handle AF mode change first. */
	if (controls.contains(controls::AF_MODE)) {
		AfAlgorithm *af = dynamic_cast<AfAlgorithm *>(controller_.getAlgorithm("af"));
		if (!af) {
			LOG(IPARPI, Warning)
				<< "Could not set AF_MODE - no AF algorithm";
		}

		int32_t idx = controls.get(controls::AF_MODE).get<int32_t>();
		auto mode = AfModeTable.find(idx);
		if (mode == AfModeTable.end()) {
			LOG(IPARPI, Error) << "AF mode " << idx
					   << " not recognised";
		} else if (af)
			af->setMode(mode->second);
	}

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

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

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

			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;
			}

			/* The control provides units of microseconds. */
			agc->setFixedShutter(0, ctrl.second.get<int32_t>() * 1.0us);

			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(0, 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() function takes in a direct exposure multiplier.
			 * So convert to 2^EV
			 */
			double ev = pow(2.0, ctrl.second.get<float>());
			agc->setEv(0, ev);
			libcameraMetadata_.set(controls::ExposureValue,
					       ctrl.second.get<float>());
			break;
		}

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

			int32_t mode = ctrl.second.get<int32_t>();
			bool modeValid = true;

			switch (mode) {
			case controls::FlickerOff:
				agc->setFlickerPeriod(0us);

				break;

			case controls::FlickerManual:
				agc->setFlickerPeriod(flickerState_.manualPeriod);

				break;

			default:
				LOG(IPARPI, Error) << "Flicker mode " << mode << " is not supported";
				modeValid = false;

				break;
			}

			if (modeValid)
				flickerState_.mode = mode;

			break;
		}

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

			uint32_t manualPeriod = ctrl.second.get<int32_t>();
			flickerState_.manualPeriod = manualPeriod * 1.0us;

			/*
			 * We note that it makes no difference if the mode gets set to "manual"
			 * first, and the period updated after, or vice versa.
			 */
			if (flickerState_.mode == controls::FlickerManual)
				agc->setFlickerPeriod(flickerState_.manualPeriod);

			break;
		}

		case controls::AWB_ENABLE: {
			/* Silently ignore this control for a mono sensor. */
			if (monoSensor_)
				break;

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

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

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

		case controls::AWB_MODE: {
			/* Silently ignore this control for a mono sensor. */
			if (monoSensor_)
				break;

			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: {
			/* Silently ignore this control for a mono sensor. */
			if (monoSensor_)
				break;

			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: {
			/* Silently ignore this control for a mono sensor. */
			if (monoSensor_)
				break;

			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_DURATION_LIMITS: {
			auto frameDurations = ctrl.second.get<Span<const int64_t>>();
			applyFrameDurations(frameDurations[0] * 1.0us, frameDurations[1] * 1.0us);
			break;
		}

		case controls::draft::NOISE_REDUCTION_MODE:
			/* Handled below in handleControls() */
			libcameraMetadata_.set(controls::draft::NoiseReductionMode,
					       ctrl.second.get<int32_t>());
			break;

		case controls::AF_MODE:
			break; /* We already handled this one above */

		case controls::AF_RANGE: {
			AfAlgorithm *af = dynamic_cast<AfAlgorithm *>(controller_.getAlgorithm("af"));
			if (!af) {
				LOG(IPARPI, Warning)
					<< "Could not set AF_RANGE - no focus algorithm";
				break;
			}

			auto range = AfRangeTable.find(ctrl.second.get<int32_t>());
			if (range == AfRangeTable.end()) {
				LOG(IPARPI, Error) << "AF range " << ctrl.second.get<int32_t>()
						   << " not recognised";
				break;
			}
			af->setRange(range->second);
			break;
		}

		case controls::AF_SPEED: {
			AfAlgorithm *af = dynamic_cast<AfAlgorithm *>(controller_.getAlgorithm("af"));
			if (!af) {
				LOG(IPARPI, Warning)
					<< "Could not set AF_SPEED - no focus algorithm";
				break;
			}

			AfAlgorithm::AfSpeed speed = ctrl.second.get<int32_t>() == controls::AfSpeedFast ?
						      AfAlgorithm::AfSpeedFast : AfAlgorithm::AfSpeedNormal;
			af->setSpeed(speed);
			break;
		}

		case controls::AF_METERING: {
			AfAlgorithm *af = dynamic_cast<AfAlgorithm *>(controller_.getAlgorithm("af"));
			if (!af) {
				LOG(IPARPI, Warning)
					<< "Could not set AF_METERING - no AF algorithm";
				break;
			}
			af->setMetering(ctrl.second.get<int32_t>() == controls::AfMeteringWindows);
			break;
		}

		case controls::AF_WINDOWS: {
			AfAlgorithm *af = dynamic_cast<AfAlgorithm *>(controller_.getAlgorithm("af"));
			if (!af) {
				LOG(IPARPI, Warning)
					<< "Could not set AF_WINDOWS - no AF algorithm";
				break;
			}
			af->setWindows(ctrl.second.get<Span<const Rectangle>>());
			break;
		}

		case controls::AF_PAUSE: {
			AfAlgorithm *af = dynamic_cast<AfAlgorithm *>(controller_.getAlgorithm("af"));
			if (!af || af->getMode() != AfAlgorithm::AfModeContinuous) {
				LOG(IPARPI, Warning)
					<< "Could not set AF_PAUSE - no AF algorithm or not Continuous";
				break;
			}
			auto pause = AfPauseTable.find(ctrl.second.get<int32_t>());
			if (pause == AfPauseTable.end()) {
				LOG(IPARPI, Error) << "AF pause " << ctrl.second.get<int32_t>()
						   << " not recognised";
				break;
			}
			af->pause(pause->second);
			break;
		}

		case controls::AF_TRIGGER: {
			AfAlgorithm *af = dynamic_cast<AfAlgorithm *>(controller_.getAlgorithm("af"));
			if (!af || af->getMode() != AfAlgorithm::AfModeAuto) {
				LOG(IPARPI, Warning)
					<< "Could not set AF_TRIGGER - no AF algorithm or not Auto";
				break;
			} else {
				if (ctrl.second.get<int32_t>() == controls::AfTriggerStart)
					af->triggerScan();
				else
					af->cancelScan();
			}
			break;
		}

		case controls::LENS_POSITION: {
			AfAlgorithm *af = dynamic_cast<AfAlgorithm *>(controller_.getAlgorithm("af"));
			if (af) {
				int32_t hwpos;