lcameraDev: honor opt-planar when selecting YUV capture format.

Pass fullPlanarIsOptional through session configure so optional planar
mode can succeed with packed YUYV; extend unit and configure HIL coverage.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-14 11:01:40 -04:00
parent 5f3d5c7818
commit 7a47f2bd49
8 changed files with 126 additions and 42 deletions
@@ -4,14 +4,6 @@
namespace lcamera_dev {
namespace {
constexpr const char *incompleteOptionalPlanarMessage =
"lcameraDev: fullPlanarIsOptional/opt-planar is not honored yet "
"(non-planar producer deinterleaving is not implemented)";
} // namespace
void validateCameraModeRequest(const LcameraDevCameraModeRequest& request)
{
if (request.width == 0 || request.height == 0)
@@ -28,15 +20,6 @@ void validateCameraModeRequest(const LcameraDevCameraModeRequest& request)
}
}
void rejectFullPlanarOptionalAtConfigureApi(
const LcameraDevCameraModeRequest& request)
{
if (request.fullPlanarIsOptional)
{
throw std::runtime_error(incompleteOptionalPlanarMessage);
}
}
bool cameraModeRequestsEqual(
const LcameraDevCameraModeRequest& left,
const LcameraDevCameraModeRequest& right)
+2 -7
View File
@@ -17,8 +17,8 @@ struct LcameraDevCameraModeRequest
LcameraDevColourSpace colourSpace = LcameraDevColourSpace::Yuv;
/** EXPLANATION:
* When false, configure must select a fully planar YUV pixel format.
* When true, relaxed non-planar formats are not honored at the configure
* API yet — producer-side deinterleaving is not implemented (Stage 2).
* When true, configure may accept semi-planar or packed YUV; lcameraBuff
* deinterleaves components in later capture stages.
*/
bool fullPlanarIsOptional = false;
};
@@ -34,11 +34,6 @@ struct LcameraDevConfiguredCameraMode
void validateCameraModeRequest(const LcameraDevCameraModeRequest& request);
/** Rejects fullPlanarIsOptional at the configure API until non-planar fallback
* paths exist in lcameraBuff. */
void rejectFullPlanarOptionalAtConfigureApi(
const LcameraDevCameraModeRequest& request);
bool cameraModeRequestsEqual(
const LcameraDevCameraModeRequest& left,
const LcameraDevCameraModeRequest& right);
-1
View File
@@ -43,7 +43,6 @@ CameraSession::configureSessionModeCReq(
}
validateCameraModeRequest(request);
rejectFullPlanarOptionalAtConfigureApi(request);
if (s.rsrc.configuredMode.has_value()
&& cameraModeRequestsEqual(s.rsrc.configuredRequest, request))
@@ -112,7 +112,9 @@ LcameraDevConfiguredCameraMode configureLibcameraSessionMode(
const std::vector<libcamera::PixelFormat> pixelFormatCandidates =
streamConfig.formats().pixelformats();
const std::optional<libcamera::PixelFormat> selectedPixelFormat =
selectYuvCaptureFormat(pixelFormatCandidates, false);
selectYuvCaptureFormat(
pixelFormatCandidates,
request.fullPlanarIsOptional);
if (!selectedPixelFormat.has_value())
{
@@ -52,23 +52,19 @@ TEST(CameraModeRequestTest, UnsupportedColourSpaceThrows)
std::runtime_error);
}
TEST(CameraModeRequestTest, FullPlanarOptionalRejectedAtConfigureApi)
TEST(CameraModeRequestTest, CameraModeRequestsEqualComparesOptionalPlanarFlag)
{
LcameraDevCameraModeRequest request;
request.width = 640;
request.height = 480;
request.fullPlanarIsOptional = true;
LcameraDevCameraModeRequest left;
left.width = 640;
left.height = 480;
left.colourSpace = LcameraDevColourSpace::Yuv;
left.fullPlanarIsOptional = false;
try {
rejectFullPlanarOptionalAtConfigureApi(request);
FAIL() << "Expected runtime_error";
}
catch (const std::runtime_error& exception)
{
sscl::tests::expectExceptionMessageContains(
exception,
"not honored yet");
}
LcameraDevCameraModeRequest right = left;
EXPECT_TRUE(cameraModeRequestsEqual(left, right));
right.fullPlanarIsOptional = true;
EXPECT_FALSE(cameraModeRequestsEqual(left, right));
}
TEST(CameraModeRequestTest, CameraModeRequestsEqualComparesAllFields)
@@ -537,5 +537,99 @@ TEST_F(LcameraDevConfigureHilTest, ConflictingReconfigureThrows)
}
}
void configureProfileWithOptPlanarExpectingYuyv(
const test_fixtures::BakedCameraProfile *profile,
const LcameraDevCameraModeRequest& request,
const std::shared_ptr<sscl::ComponentThread>& componentThread)
{
lcamera_dev::LcameraDevGetOrCreateResult createResult;
sscl::tests::runNonViralNurseryOnComponentThread(
componentThread,
[profile, &createResult](
sscl::co::NonViralTaskNursery::Slot::Lease& lease)
{
return getOrCreateCInd(
lease.getExceptionStorage(),
lease.getCallerLambda(),
profile->exampleSelector,
createResult);
});
EXPECT_TRUE(createResult.deviceSession != nullptr)
<< profile->profileTag;
LcameraDevConfiguredCameraMode configuredMode;
LcameraDevCameraModeRequest optPlanarRequest = request;
optPlanarRequest.fullPlanarIsOptional = true;
runNonViralNurseryRethrowingOnComponentThread(
componentThread,
[&createResult, &optPlanarRequest, &configuredMode](
sscl::co::NonViralTaskNursery::Slot::Lease& lease)
{
return configureCInd(
lease.getExceptionStorage(),
lease.getCallerLambda(),
createResult.deviceSession,
optPlanarRequest,
configuredMode);
});
EXPECT_FALSE(configuredMode.isFullyPlanar) << profile->profileTag;
EXPECT_EQ(configuredMode.planeCount, 1u) << profile->profileTag;
EXPECT_EQ(configuredMode.pixelFormatName, "YUYV") << profile->profileTag;
EXPECT_GE(configuredMode.width, 1u) << profile->profileTag;
EXPECT_GE(configuredMode.height, 1u) << profile->profileTag;
sscl::tests::runNonViralNurseryOnComponentThread(
componentThread,
[&createResult](sscl::co::NonViralTaskNursery::Slot::Lease& lease)
{
return releaseCInd(
lease.getExceptionStorage(),
lease.getCallerLambda(),
createResult.deviceSession);
});
}
TEST_F(LcameraDevConfigureHilTest, ConfigureUsbHdmiYuvWithOptPlanarSelectsYuyv)
{
const test_fixtures::BakedCameraProfile *profile =
findProfile("usb_hdmi_camera");
if (!profile) {
GTEST_SKIP() << "usb_hdmi_camera profile not available";
}
runLcameraDevMainAndNurseryTask(
[this, profile](
const std::shared_ptr<sscl::ComponentThread>& componentThread)
{
configureProfileWithOptPlanarExpectingYuyv(
profile,
configureRequest,
componentThread);
});
}
TEST_F(LcameraDevConfigureHilTest, ConfigureIntegratedWebcamYuvWithOptPlanarSelectsYuyv)
{
const test_fixtures::BakedCameraProfile *profile =
findProfile("integrated_webcam");
if (!profile) {
GTEST_SKIP() << "integrated_webcam profile not available";
}
runLcameraDevMainAndNurseryTask(
[this, profile](
const std::shared_ptr<sscl::ComponentThread>& componentThread)
{
configureProfileWithOptPlanarExpectingYuyv(
profile,
configureRequest,
componentThread);
});
}
} // namespace
} // namespace lcamera_dev
@@ -7,6 +7,7 @@
namespace lcamera_dev {
namespace {
using libcamera::formats::MJPEG;
using libcamera::formats::NV12;
using libcamera::formats::YUYV;
using libcamera::formats::YUV420;
@@ -67,5 +68,18 @@ TEST(PlanarYuvFormatPolicyTest, IsFullyPlanarYuvRecognizesYuv420)
EXPECT_FALSE(isFullyPlanarYuv(NV12));
}
TEST(PlanarYuvFormatPolicyTest, FullyPlanarOptionalPicksYuyvOverMjpeg)
{
const std::vector<libcamera::PixelFormat> candidates = {MJPEG, YUYV};
const std::optional<libcamera::PixelFormat> selected =
selectYuvCaptureFormat(candidates, true);
EXPECT_TRUE(selected.has_value());
EXPECT_EQ(*selected, YUYV);
EXPECT_FALSE(isFullyPlanarYuv(*selected));
EXPECT_EQ(yuvCapturePlaneCount(*selected), 1u);
}
} // namespace
} // namespace lcamera_dev
+2 -1
View File
@@ -31,7 +31,8 @@ struct BakedCameraProfile
// Configure HIL note (2026-06-10): both cameras expose MJPEG and packed YUYV
// via libcamera VideoRecording/Viewfinder — no fully planar YUV420/NV12 at
// 640x480. configureSessionModeCReq correctly throws when fullPlanarIsOptional
// is false; success-path HIL tests skip on this machine.
// is false; with opt-planar / fullPlanarIsOptional=true configure succeeds
// selecting YUYV (Yuv422). lcameraBuff Stage 2 attach HIL uses v-res=480p.
inline constexpr BakedCameraProfile bakedCameraProfiles[] = {
{
"dell-laptop",