Show a patch.

GET /api/1.1/patches/19691/?format=api
HTTP 200 OK
Allow: GET, PUT, PATCH, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept

{
    "id": 19691,
    "url": "https://patchwork.libcamera.org/api/1.1/patches/19691/?format=api",
    "web_url": "https://patchwork.libcamera.org/patch/19691/",
    "project": {
        "id": 1,
        "url": "https://patchwork.libcamera.org/api/1.1/projects/1/?format=api",
        "name": "libcamera",
        "link_name": "libcamera",
        "list_id": "libcamera_core",
        "list_email": "libcamera-devel@lists.libcamera.org",
        "web_url": "",
        "scm_url": "",
        "webscm_url": ""
    },
    "msgid": "<20240313105645.120317-4-stefan.klug@ideasonboard.com>",
    "date": "2024-03-13T10:56:35",
    "name": "[03/12] libcamera: lc-compliance: Add initial set of per-frame-control tests",
    "commit_ref": null,
    "pull_url": null,
    "state": "superseded",
    "archived": false,
    "hash": "6c20dcd9adc00d9fedade2e57de537af9f6e6f01",
    "submitter": {
        "id": 184,
        "url": "https://patchwork.libcamera.org/api/1.1/people/184/?format=api",
        "name": "Stefan Klug",
        "email": "stefan.klug@ideasonboard.com"
    },
    "delegate": null,
    "mbox": "https://patchwork.libcamera.org/patch/19691/mbox/",
    "series": [
        {
            "id": 4219,
            "url": "https://patchwork.libcamera.org/api/1.1/series/4219/?format=api",
            "web_url": "https://patchwork.libcamera.org/project/libcamera/list/?series=4219",
            "date": "2024-03-13T10:56:32",
            "name": "Preparation for per-frame-controls and initial tests",
            "version": 1,
            "mbox": "https://patchwork.libcamera.org/series/4219/mbox/"
        }
    ],
    "comments": "https://patchwork.libcamera.org/api/patches/19691/comments/",
    "check": "pending",
    "checks": "https://patchwork.libcamera.org/api/patches/19691/checks/",
    "tags": {},
    "headers": {
        "Return-Path": "<libcamera-devel-bounces@lists.libcamera.org>",
        "X-Original-To": "parsemail@patchwork.libcamera.org",
        "Delivered-To": "parsemail@patchwork.libcamera.org",
        "Received": [
            "from lancelot.ideasonboard.com (lancelot.ideasonboard.com\n\t[92.243.16.209])\n\tby patchwork.libcamera.org (Postfix) with ESMTPS id 66111BD1F1\n\tfor <parsemail@patchwork.libcamera.org>;\n\tWed, 13 Mar 2024 10:57:05 +0000 (UTC)",
            "from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 10DE762C96;\n\tWed, 13 Mar 2024 11:56:59 +0100 (CET)",
            "from perceval.ideasonboard.com (perceval.ideasonboard.com\n\t[213.167.242.64])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 347E662C85\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tWed, 13 Mar 2024 11:56:53 +0100 (CET)",
            "from jasper.fritz.box (unknown\n\t[IPv6:2a00:6020:448c:6c00:9b07:31b5:38e1:e957])\n\tby perceval.ideasonboard.com (Postfix) with ESMTPSA id B8D43899;\n\tWed, 13 Mar 2024 11:56:30 +0100 (CET)"
        ],
        "Authentication-Results": "lancelot.ideasonboard.com;\n\tdkim=fail reason=\"signature verification failed\" (1024-bit key;\n\tunprotected) header.d=ideasonboard.com header.i=@ideasonboard.com\n\theader.b=\"FBf94LHd\"; dkim-atps=neutral",
        "DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1710327390;\n\tbh=MV5YHf3tkBrRjQWbq+rAGFKuDu/i1UsUbjI6bEIQhmg=;\n\th=From:To:Cc:Subject:Date:In-Reply-To:References:From;\n\tb=FBf94LHdjjm2nI5lXKinnPz3kOekcjr1LQmP5WQN4l9sF+p3ScF/wJqmwPbXT1aXm\n\tm91PWSZdPw8q+2Z8w6j3DMZsO93b+8gGnpbbyoyORfo4CFn/JghdeWoPORQH+AZIx+\n\tVQEd+aF8TzoUujHSVnoK8kUMRCz7QjHWkPI9llsU=",
        "From": "Stefan Klug <stefan.klug@ideasonboard.com>",
        "To": "libcamera-devel@lists.libcamera.org",
        "Subject": "[PATCH 03/12] libcamera: lc-compliance: Add initial set of\n\tper-frame-control tests",
        "Date": "Wed, 13 Mar 2024 11:56:35 +0100",
        "Message-Id": "<20240313105645.120317-4-stefan.klug@ideasonboard.com>",
        "X-Mailer": "git-send-email 2.40.1",
        "In-Reply-To": "<20240313105645.120317-1-stefan.klug@ideasonboard.com>",
        "References": "<20240313105645.120317-1-stefan.klug@ideasonboard.com>",
        "MIME-Version": "1.0",
        "Content-Transfer-Encoding": "8bit",
        "X-BeenThere": "libcamera-devel@lists.libcamera.org",
        "X-Mailman-Version": "2.1.29",
        "Precedence": "list",
        "List-Id": "<libcamera-devel.lists.libcamera.org>",
        "List-Unsubscribe": "<https://lists.libcamera.org/options/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=unsubscribe>",
        "List-Archive": "<https://lists.libcamera.org/pipermail/libcamera-devel/>",
        "List-Post": "<mailto:libcamera-devel@lists.libcamera.org>",
        "List-Help": "<mailto:libcamera-devel-request@lists.libcamera.org?subject=help>",
        "List-Subscribe": "<https://lists.libcamera.org/listinfo/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=subscribe>",
        "Errors-To": "libcamera-devel-bounces@lists.libcamera.org",
        "Sender": "\"libcamera-devel\" <libcamera-devel-bounces@lists.libcamera.org>"
    },
    "content": "These tests check if controls (only exposure time and analogue gain at\nthe moment) get applied on the frame they were requested for.\n\nThis is tested by looking at the metadata and the mean brightness\nof the image center.\n\nAt the moment these tests fail. Fixes for the pipelines will be delivered\nin later patches.\n\nTo run only the teste, one can run:\nlc-compliance -c <cam> -f \"SingleStream.*\"\n\nNote that the current implementation is a bit picky on what the camera\nactually sees. If it is too dark (or too bright), the tests will fail.\nLooking at a white wall in a normally lit office usually works.\n\nSigned-off-by: Stefan Klug <stefan.klug@ideasonboard.com>\n---\n src/apps/lc-compliance/capture_test.cpp       |  46 +++\n src/apps/lc-compliance/meson.build            |   1 +\n src/apps/lc-compliance/per_frame_controls.cpp | 316 ++++++++++++++++++\n src/apps/lc-compliance/per_frame_controls.h   |  43 +++\n 4 files changed, 406 insertions(+)\n create mode 100644 src/apps/lc-compliance/per_frame_controls.cpp\n create mode 100644 src/apps/lc-compliance/per_frame_controls.h",
    "diff": "diff --git a/src/apps/lc-compliance/capture_test.cpp b/src/apps/lc-compliance/capture_test.cpp\nindex 1dcfcf92..b19e8936 100644\n--- a/src/apps/lc-compliance/capture_test.cpp\n+++ b/src/apps/lc-compliance/capture_test.cpp\n@@ -11,6 +11,7 @@\n #include <gtest/gtest.h>\n \n #include \"environment.h\"\n+#include \"per_frame_controls.h\"\n #include \"simple_capture.h\"\n \n using namespace libcamera;\n@@ -133,3 +134,48 @@ INSTANTIATE_TEST_SUITE_P(CaptureTests,\n \t\t\t testing::Combine(testing::ValuesIn(ROLES),\n \t\t\t\t\t  testing::ValuesIn(NUMREQUESTS)),\n \t\t\t SingleStream::nameParameters);\n+\n+/*\n+ * Test Per frame controls\n+ */\n+TEST_F(SingleStream, testExposureGainChangeOnSameFrame)\n+{\n+\tPerFrameControls capture(camera_);\n+\tcapture.configure(StreamRole::VideoRecording);\n+\tcapture.testExposureGainChangeOnSameFrame();\n+}\n+\n+TEST_F(SingleStream, testFramePreciseExposureChange)\n+{\n+\tPerFrameControls capture(camera_);\n+\tcapture.configure(StreamRole::VideoRecording);\n+\tcapture.testFramePreciseExposureChange();\n+}\n+\n+TEST_F(SingleStream, testFramePreciseGainChange)\n+{\n+\tPerFrameControls capture(camera_);\n+\tcapture.configure(StreamRole::VideoRecording);\n+\tcapture.testFramePreciseGainChange();\n+}\n+\n+TEST_F(SingleStream, testExposureGainIsAppliedOnFirstFrame)\n+{\n+\tPerFrameControls capture(camera_);\n+\tcapture.configure(StreamRole::VideoRecording);\n+\tcapture.testExposureGainIsAppliedOnFirstFrame();\n+}\n+\n+TEST_F(SingleStream, testExposureGainFromFirstRequestGetsApplied)\n+{\n+\tPerFrameControls capture(camera_);\n+\tcapture.configure(StreamRole::VideoRecording);\n+\tcapture.testExposureGainFromFirstRequestGetsApplied();\n+}\n+\n+TEST_F(SingleStream, testExposureGainFromFirstAndSecondRequestGetsApplied)\n+{\n+\tPerFrameControls capture(camera_);\n+\tcapture.configure(StreamRole::VideoRecording);\n+\tcapture.testExposureGainFromFirstAndSecondRequestGetsApplied();\n+}\ndiff --git a/src/apps/lc-compliance/meson.build b/src/apps/lc-compliance/meson.build\nindex eb7b2d71..2a6f52af 100644\n--- a/src/apps/lc-compliance/meson.build\n+++ b/src/apps/lc-compliance/meson.build\n@@ -15,6 +15,7 @@ lc_compliance_sources = files([\n     'capture_test.cpp',\n     'environment.cpp',\n     'main.cpp',\n+    'per_frame_controls.cpp',\n     'simple_capture.cpp',\n     'time_sheet.cpp',\n ])\ndiff --git a/src/apps/lc-compliance/per_frame_controls.cpp b/src/apps/lc-compliance/per_frame_controls.cpp\nnew file mode 100644\nindex 00000000..eb7164e0\n--- /dev/null\n+++ b/src/apps/lc-compliance/per_frame_controls.cpp\n@@ -0,0 +1,316 @@\n+/* SPDX-License-Identifier: GPL-2.0-or-later */\n+/*\n+ * Copyright (C) 2024, Ideas on Board Oy\n+ *\n+ * per_frame_controls.cpp - Tests for per frame controls\n+ */\n+#include \"per_frame_controls.h\"\n+\n+#include <gtest/gtest.h>\n+\n+#include \"time_sheet.h\"\n+\n+using namespace libcamera;\n+\n+static const bool doImageTests = true;\n+\n+PerFrameControls::PerFrameControls(std::shared_ptr<Camera> camera)\n+\t: SimpleCapture(camera)\n+{\n+}\n+\n+std::shared_ptr<TimeSheet>\n+PerFrameControls::startCaptureWithTimeSheet(unsigned int framesToCapture, const ControlList *controls)\n+{\n+\tControlList ctrls(camera_->controls().idmap());\n+\t/* Ensure defined default values */\n+\tctrls.set(controls::AeEnable, false);\n+\tctrls.set(controls::AeExposureMode, controls::ExposureCustom);\n+\tctrls.set(controls::ExposureTime, 10000);\n+\tctrls.set(controls::AnalogueGain, 1.0);\n+\n+\tif (controls)\n+\t\tctrls.merge(*controls, ControlList::MergePolicy::OverwriteExisting);\n+\n+\tstart(&ctrls);\n+\n+\tqueueCount_ = 0;\n+\tcaptureCount_ = 0;\n+\tcaptureLimit_ = framesToCapture;\n+\n+\tauto timeSheet = std::make_shared<TimeSheet>(captureLimit_, camera_->controls().idmap());\n+\ttimeSheet_ = timeSheet;\n+\treturn timeSheet;\n+}\n+\n+int PerFrameControls::queueRequest(Request *request)\n+{\n+\tqueueCount_++;\n+\tif (queueCount_ > captureLimit_)\n+\t\treturn 0;\n+\n+\tauto ts = timeSheet_.lock();\n+\tif (ts)\n+\t\tts->prepareForQueue(request, queueCount_ - 1);\n+\n+\treturn camera_->queueRequest(request);\n+}\n+\n+void PerFrameControls::requestComplete(Request *request)\n+{\n+\tauto ts = timeSheet_.lock();\n+\tif (ts)\n+\t\tts->handleCompleteRequest(request);\n+\n+\tcaptureCount_++;\n+\tif (captureCount_ >= captureLimit_) {\n+\t\tloop_->exit(0);\n+\t\treturn;\n+\t}\n+\n+\trequest->reuse(Request::ReuseBuffers);\n+\tif (queueRequest(request))\n+\t\tloop_->exit(-EINVAL);\n+}\n+\n+void PerFrameControls::runCaptureSession()\n+{\n+\tStream *stream = config_->at(0).stream();\n+\tconst std::vector<std::unique_ptr<FrameBuffer>> &buffers = allocator_->buffers(stream);\n+\n+\t/* Queue the recommended number of reqeuests. */\n+\tfor (const std::unique_ptr<FrameBuffer> &buffer : buffers) {\n+\t\tstd::unique_ptr<Request> request = camera_->createRequest();\n+\t\trequest->addBuffer(stream, buffer.get());\n+\t\tqueueRequest(request.get());\n+\t\trequests_.push_back(std::move(request));\n+\t}\n+\n+\t/* Run capture session. */\n+\tloop_ = new EventLoop();\n+\tloop_->exec();\n+\tstop();\n+\tdelete loop_;\n+}\n+\n+void PerFrameControls::testExposureGainChangeOnSameFrame()\n+{\n+\tControlList startValues;\n+\tstartValues.set(controls::ExposureTime, 5000);\n+\tstartValues.set(controls::AnalogueGain, 1.0);\n+\n+\tauto timeSheet = startCaptureWithTimeSheet(10, &startValues);\n+\tauto &ts = *timeSheet;\n+\n+\t/* wait a few frames to settle */\n+\tts[7].controls().set(controls::ExposureTime, 10000);\n+\tts[7].controls().set(controls::AnalogueGain, 4.0);\n+\n+\trunCaptureSession();\n+\n+\t/* Uncomment this to debug the test */\n+\t/* ts.printAllInfos(); */\n+\n+\tASSERT_TRUE(ts[5].metadata().contains(controls::ExposureTime.id())) << \"Required metadata entry is missing\";\n+\tASSERT_TRUE(ts[5].metadata().contains(controls::AnalogueGain.id())) << \"Required metadata entry is missing\";\n+\n+\tEXPECT_NEAR(ts[3].metadata().get(controls::ExposureTime).value(), 5000, 20);\n+\tEXPECT_NEAR(ts[3].metadata().get(controls::AnalogueGain).value(), 1.0, 0.05);\n+\n+\t//find the frame with the changes\n+\tint exposureChangeIndex = 0;\n+\tfor (unsigned i = 3; i < ts.size(); i++) {\n+\t\tif (ts[i].metadata().get(controls::ExposureTime).value() > 7500) {\n+\t\t\texposureChangeIndex = i;\n+\t\t\tbreak;\n+\t\t}\n+\t}\n+\n+\tint gainChangeIndex = 0;\n+\tfor (unsigned i = 3; i < ts.size(); i++) {\n+\t\tif (ts[i].metadata().get(controls::AnalogueGain).value() > 2.0) {\n+\t\t\tgainChangeIndex = i;\n+\t\t\tbreak;\n+\t\t}\n+\t}\n+\n+\tEXPECT_NE(exposureChangeIndex, 0) << \"Exposure change not found in metadata\";\n+\tEXPECT_NE(gainChangeIndex, 0) << \"Gain change not found in metadata\";\n+\tEXPECT_EQ(exposureChangeIndex, gainChangeIndex)\n+\t\t<< \"Metadata contained gain and exposure changes on different frames\";\n+\n+\tif (doImageTests) {\n+\t\tint brightnessChangeIndex = 0;\n+\t\tfor (unsigned i = 3; i < ts.size(); i++) {\n+\t\t\tif (ts[i].getBrightnessChange() > 1.3) {\n+\t\t\t\tEXPECT_EQ(brightnessChangeIndex, 0)\n+\t\t\t\t\t<< \"Detected multiple frames with brightness increase (Wrong control delays?)\";\n+\n+\t\t\t\tif (!brightnessChangeIndex)\n+\t\t\t\t\tbrightnessChangeIndex = i;\n+\t\t\t}\n+\t\t}\n+\n+\t\tEXPECT_EQ(exposureChangeIndex, brightnessChangeIndex)\n+\t\t\t<< \"Exposure change and mesaured brightness change were not on same frame. \"\n+\t\t\t<< \"(Wrong control delay?, Start frame event too late?)\";\n+\t\tEXPECT_EQ(exposureChangeIndex, gainChangeIndex)\n+\t\t\t<< \"Gain change and mesaured brightness change were not on same frame. \"\n+\t\t\t<< \"(Wrong control delay?, Start frame event too late?)\";\n+\t}\n+}\n+\n+void PerFrameControls::testFramePreciseExposureChange()\n+{\n+\tauto timeSheet = startCaptureWithTimeSheet(10);\n+\tauto &ts = *timeSheet;\n+\n+\tts[3].controls().set(controls::ExposureTime, 5000);\n+\t/* wait a few frames to settle */\n+\tts[6].controls().set(controls::ExposureTime, 20000);\n+\n+\trunCaptureSession();\n+\n+\t/* Uncomment this to debug the test */\n+\t/* ts.printAllInfos(); */\n+\n+\tASSERT_TRUE(ts[5].metadata().contains(controls::ExposureTime.id())) << \"Required metadata entry is missing\";\n+\n+\tEXPECT_NEAR(ts[5].metadata().get(controls::ExposureTime).value(), 5000, 20);\n+\tEXPECT_NEAR(ts[6].metadata().get(controls::ExposureTime).value(), 20000, 20);\n+\n+\tif (doImageTests) {\n+\t\t/* No increase just before setting exposure */\n+\t\tEXPECT_NEAR(ts[5].getBrightnessChange(), 1.0, 0.05)\n+\t\t\t<< \"Brightness changed too much before the expected time of change (control delay too high?).\";\n+\t\t/*\n+\t\t* Todo: The change is brightness was a bit low\n+\t\t* (Exposure time increase by 4x resulted in a brightness increase of < 2).\n+\t\t* This should be investigated.\n+\t\t*/\n+\t\tEXPECT_GT(ts[6].getBrightnessChange(), 1.3)\n+\t\t\t<< \"Brightness in frame \" << 6 << \" did not increase as expected (reference: \"\n+\t\t\t<< ts[3].getSpotBrightness() << \" current: \" << ts[6].getSpotBrightness() << \" )\" << std::endl;\n+\n+\t\t/* No increase just after setting exposure */\n+\t\tEXPECT_NEAR(ts[7].getBrightnessChange(), 1.0, 0.05)\n+\t\t\t<< \"Brightness changed too much after the expected time of change (control delay too low?).\";\n+\n+\t\t/* No increase just after setting exposure */\n+\t\tEXPECT_NEAR(ts[8].getBrightnessChange(), 1.0, 0.05)\n+\t\t\t<< \"Brightness changed too much 2 frames after the expected time of change (control delay too low?).\";\n+\t}\n+}\n+\n+void PerFrameControls::testFramePreciseGainChange()\n+{\n+\tauto timeSheet = startCaptureWithTimeSheet(10);\n+\tauto &ts = *timeSheet;\n+\n+\tts[3].controls().set(controls::AnalogueGain, 1.0);\n+\t/* wait a few frames to settle */\n+\tts[6].controls().set(controls::AnalogueGain, 4.0);\n+\n+\trunCaptureSession();\n+\n+\t/* Uncomment this, to debug the test */\n+\t/* ts.printAllInfos(); */\n+\n+\tASSERT_TRUE(ts[5].metadata().contains(controls::AnalogueGain.id())) << \"Required metadata entry is missing\";\n+\n+\tEXPECT_NEAR(ts[5].metadata().get(controls::AnalogueGain).value(), 1.0, 0.1);\n+\tEXPECT_NEAR(ts[6].metadata().get(controls::AnalogueGain).value(), 4.0, 0.1);\n+\n+\tif (doImageTests) {\n+\t\t/* No increase just before setting gain */\n+\t\tEXPECT_NEAR(ts[5].getBrightnessChange(), 1.0, 0.05)\n+\t\t\t<< \"Brightness changed too much before the expected time of change (control delay too high?).\";\n+\t\t/*\n+\t\t* Todo: I see a brightness change of roughly half the expected one.\n+\t\t* This is not yet understood and needs investigation\n+\t\t*/\n+\t\tEXPECT_GT(ts[6].getBrightnessChange(), 1.7)\n+\t\t\t<< \"Brightness in frame \" << 6 << \" did not increase as expected (reference: \"\n+\t\t\t<< ts[5].getSpotBrightness() << \" current: \" << ts[6].getSpotBrightness() << \" )\" << std::endl;\n+\n+\t\t/* No increase just after setting gain */\n+\t\tEXPECT_NEAR(ts[7].getBrightnessChange(), 1.0, 0.05)\n+\t\t\t<< \"Brightness changed too much after the expected time of change (control delay too low?).\";\n+\n+\t\t/* No increase just after setting gain */\n+\t\tEXPECT_NEAR(ts[8].getBrightnessChange(), 1.0, 0.05)\n+\t\t\t<< \"Brightness changed too much after the expected time of change (control delay too low?).\";\n+\t}\n+}\n+\n+void PerFrameControls::testExposureGainFromFirstRequestGetsApplied()\n+{\n+\tauto timeSheet = startCaptureWithTimeSheet(5);\n+\tauto &ts = *timeSheet;\n+\n+\tts[0].controls().set(controls::ExposureTime, 10000);\n+\tts[0].controls().set(controls::AnalogueGain, 4.0);\n+\n+\trunCaptureSession();\n+\n+\tASSERT_TRUE(ts[4].metadata().contains(controls::ExposureTime.id())) << \"Required metadata entry is missing\";\n+\tASSERT_TRUE(ts[4].metadata().contains(controls::AnalogueGain.id())) << \"Required metadata entry is missing\";\n+\n+\t/* We expect it to be applied after 3 frames, the latest*/\n+\tEXPECT_NEAR(ts[4].metadata().get(controls::ExposureTime).value(), 10000, 20);\n+\tEXPECT_NEAR(ts[4].metadata().get(controls::AnalogueGain).value(), 4.0, 0.1);\n+}\n+\n+void PerFrameControls::testExposureGainFromFirstAndSecondRequestGetsApplied()\n+{\n+\tauto timeSheet = startCaptureWithTimeSheet(5);\n+\tauto &ts = *timeSheet;\n+\n+\tts[0].controls().set(controls::ExposureTime, 8000);\n+\tts[0].controls().set(controls::AnalogueGain, 2.0);\n+\tts[1].controls().set(controls::ExposureTime, 10000);\n+\tts[1].controls().set(controls::AnalogueGain, 4.0);\n+\n+\trunCaptureSession();\n+\n+\tASSERT_TRUE(ts[4].metadata().contains(controls::ExposureTime.id())) << \"Required metadata entry is missing\";\n+\tASSERT_TRUE(ts[4].metadata().contains(controls::AnalogueGain.id())) << \"Required metadata entry is missing\";\n+\n+\t/* We expect it to be applied after 3 frames, the latest */\n+\tEXPECT_NEAR(ts[4].metadata().get(controls::ExposureTime).value(), 10000, 20);\n+\tEXPECT_NEAR(ts[4].metadata().get(controls::AnalogueGain).value(), 4.0, 0.1);\n+}\n+\n+void PerFrameControls::testExposureGainIsAppliedOnFirstFrame()\n+{\n+\tControlList startValues;\n+\tstartValues.set(controls::ExposureTime, 5000);\n+\tstartValues.set(controls::AnalogueGain, 1.0);\n+\n+\tauto ts1 = startCaptureWithTimeSheet(3, &startValues);\n+\n+\trunCaptureSession();\n+\n+\tASSERT_TRUE((*ts1)[0].metadata().contains(controls::ExposureTime.id())) << \"Required metadata entry is missing\";\n+\tASSERT_TRUE((*ts1)[0].metadata().contains(controls::AnalogueGain.id())) << \"Required metadata entry is missing\";\n+\n+\tEXPECT_NEAR((*ts1)[0].metadata().get(controls::ExposureTime).value(), 5000, 20);\n+\tEXPECT_NEAR((*ts1)[0].metadata().get(controls::AnalogueGain).value(), 1.0, 0.02);\n+\n+\t/* Second capture with different values to ensure we don't hit default/old values */\n+\tstartValues.set(controls::ExposureTime, 15000);\n+\tstartValues.set(controls::AnalogueGain, 4.0);\n+\n+\tauto ts2 = startCaptureWithTimeSheet(3, &startValues);\n+\n+\trunCaptureSession();\n+\n+\tEXPECT_NEAR((*ts2)[0].metadata().get(controls::ExposureTime).value(), 15000, 20);\n+\tEXPECT_NEAR((*ts2)[0].metadata().get(controls::AnalogueGain).value(), 4.0, 0.02);\n+\n+\tif (doImageTests) {\n+\t\t/* With 3x exposure and 4x gain we could expect a brightness increase of 2x */\n+\t\tdouble brightnessChange = ts2->get(1).getSpotBrightness() / ts1->get(1).getSpotBrightness();\n+\t\tEXPECT_GT(brightnessChange, 2.0);\n+\t}\n+}\ndiff --git a/src/apps/lc-compliance/per_frame_controls.h b/src/apps/lc-compliance/per_frame_controls.h\nnew file mode 100644\nindex 00000000..a341c61f\n--- /dev/null\n+++ b/src/apps/lc-compliance/per_frame_controls.h\n@@ -0,0 +1,43 @@\n+/* SPDX-License-Identifier: GPL-2.0-or-later */\n+/*\n+ * Copyright (C) 2024, Ideas on Board Oy\n+ *\n+ * per_frame_controls.h - Tests for per frame controls\n+ */\n+\n+#pragma once\n+\n+#include <memory>\n+\n+#include <libcamera/libcamera.h>\n+\n+#include \"../common/event_loop.h\"\n+\n+#include \"simple_capture.h\"\n+#include \"time_sheet.h\"\n+\n+class PerFrameControls : public SimpleCapture\n+{\n+public:\n+\tPerFrameControls(std::shared_ptr<libcamera::Camera> camera);\n+\n+\tstd::shared_ptr<TimeSheet>\n+\tstartCaptureWithTimeSheet(unsigned int framesToCapture, const libcamera::ControlList *controls = nullptr);\n+\tvoid runCaptureSession();\n+\n+\tvoid testExposureGainChangeOnSameFrame();\n+\tvoid testFramePreciseExposureChange();\n+\tvoid testFramePreciseGainChange();\n+\tvoid testExposureGainIsAppliedOnFirstFrame();\n+\tvoid testExposureGainFromFirstRequestGetsApplied();\n+\tvoid testExposureGainFromFirstAndSecondRequestGetsApplied();\n+\n+\tint queueRequest(libcamera::Request *request);\n+\tvoid requestComplete(libcamera::Request *request) override;\n+\n+\tunsigned int queueCount_;\n+\tunsigned int captureCount_;\n+\tunsigned int captureLimit_;\n+\n+\tstd::weak_ptr<TimeSheet> timeSheet_;\n+};\n",
    "prefixes": [
        "03/12"
    ]
}