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:
@@ -4,14 +4,6 @@
|
|||||||
|
|
||||||
namespace lcamera_dev {
|
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)
|
void validateCameraModeRequest(const LcameraDevCameraModeRequest& request)
|
||||||
{
|
{
|
||||||
if (request.width == 0 || request.height == 0)
|
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(
|
bool cameraModeRequestsEqual(
|
||||||
const LcameraDevCameraModeRequest& left,
|
const LcameraDevCameraModeRequest& left,
|
||||||
const LcameraDevCameraModeRequest& right)
|
const LcameraDevCameraModeRequest& right)
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ struct LcameraDevCameraModeRequest
|
|||||||
LcameraDevColourSpace colourSpace = LcameraDevColourSpace::Yuv;
|
LcameraDevColourSpace colourSpace = LcameraDevColourSpace::Yuv;
|
||||||
/** EXPLANATION:
|
/** EXPLANATION:
|
||||||
* When false, configure must select a fully planar YUV pixel format.
|
* When false, configure must select a fully planar YUV pixel format.
|
||||||
* When true, relaxed non-planar formats are not honored at the configure
|
* When true, configure may accept semi-planar or packed YUV; lcameraBuff
|
||||||
* API yet — producer-side deinterleaving is not implemented (Stage 2).
|
* deinterleaves components in later capture stages.
|
||||||
*/
|
*/
|
||||||
bool fullPlanarIsOptional = false;
|
bool fullPlanarIsOptional = false;
|
||||||
};
|
};
|
||||||
@@ -34,11 +34,6 @@ struct LcameraDevConfiguredCameraMode
|
|||||||
|
|
||||||
void validateCameraModeRequest(const LcameraDevCameraModeRequest& request);
|
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(
|
bool cameraModeRequestsEqual(
|
||||||
const LcameraDevCameraModeRequest& left,
|
const LcameraDevCameraModeRequest& left,
|
||||||
const LcameraDevCameraModeRequest& right);
|
const LcameraDevCameraModeRequest& right);
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ CameraSession::configureSessionModeCReq(
|
|||||||
}
|
}
|
||||||
|
|
||||||
validateCameraModeRequest(request);
|
validateCameraModeRequest(request);
|
||||||
rejectFullPlanarOptionalAtConfigureApi(request);
|
|
||||||
|
|
||||||
if (s.rsrc.configuredMode.has_value()
|
if (s.rsrc.configuredMode.has_value()
|
||||||
&& cameraModeRequestsEqual(s.rsrc.configuredRequest, request))
|
&& cameraModeRequestsEqual(s.rsrc.configuredRequest, request))
|
||||||
|
|||||||
@@ -112,7 +112,9 @@ LcameraDevConfiguredCameraMode configureLibcameraSessionMode(
|
|||||||
const std::vector<libcamera::PixelFormat> pixelFormatCandidates =
|
const std::vector<libcamera::PixelFormat> pixelFormatCandidates =
|
||||||
streamConfig.formats().pixelformats();
|
streamConfig.formats().pixelformats();
|
||||||
const std::optional<libcamera::PixelFormat> selectedPixelFormat =
|
const std::optional<libcamera::PixelFormat> selectedPixelFormat =
|
||||||
selectYuvCaptureFormat(pixelFormatCandidates, false);
|
selectYuvCaptureFormat(
|
||||||
|
pixelFormatCandidates,
|
||||||
|
request.fullPlanarIsOptional);
|
||||||
|
|
||||||
if (!selectedPixelFormat.has_value())
|
if (!selectedPixelFormat.has_value())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -52,23 +52,19 @@ TEST(CameraModeRequestTest, UnsupportedColourSpaceThrows)
|
|||||||
std::runtime_error);
|
std::runtime_error);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(CameraModeRequestTest, FullPlanarOptionalRejectedAtConfigureApi)
|
TEST(CameraModeRequestTest, CameraModeRequestsEqualComparesOptionalPlanarFlag)
|
||||||
{
|
{
|
||||||
LcameraDevCameraModeRequest request;
|
LcameraDevCameraModeRequest left;
|
||||||
request.width = 640;
|
left.width = 640;
|
||||||
request.height = 480;
|
left.height = 480;
|
||||||
request.fullPlanarIsOptional = true;
|
left.colourSpace = LcameraDevColourSpace::Yuv;
|
||||||
|
left.fullPlanarIsOptional = false;
|
||||||
|
|
||||||
try {
|
LcameraDevCameraModeRequest right = left;
|
||||||
rejectFullPlanarOptionalAtConfigureApi(request);
|
EXPECT_TRUE(cameraModeRequestsEqual(left, right));
|
||||||
FAIL() << "Expected runtime_error";
|
|
||||||
}
|
right.fullPlanarIsOptional = true;
|
||||||
catch (const std::runtime_error& exception)
|
EXPECT_FALSE(cameraModeRequestsEqual(left, right));
|
||||||
{
|
|
||||||
sscl::tests::expectExceptionMessageContains(
|
|
||||||
exception,
|
|
||||||
"not honored yet");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(CameraModeRequestTest, CameraModeRequestsEqualComparesAllFields)
|
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
|
||||||
} // namespace lcamera_dev
|
} // namespace lcamera_dev
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
namespace lcamera_dev {
|
namespace lcamera_dev {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
using libcamera::formats::MJPEG;
|
||||||
using libcamera::formats::NV12;
|
using libcamera::formats::NV12;
|
||||||
using libcamera::formats::YUYV;
|
using libcamera::formats::YUYV;
|
||||||
using libcamera::formats::YUV420;
|
using libcamera::formats::YUV420;
|
||||||
@@ -67,5 +68,18 @@ TEST(PlanarYuvFormatPolicyTest, IsFullyPlanarYuvRecognizesYuv420)
|
|||||||
EXPECT_FALSE(isFullyPlanarYuv(NV12));
|
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
|
||||||
} // namespace lcamera_dev
|
} // namespace lcamera_dev
|
||||||
|
|||||||
Vendored
+2
-1
@@ -31,7 +31,8 @@ struct BakedCameraProfile
|
|||||||
// Configure HIL note (2026-06-10): both cameras expose MJPEG and packed YUYV
|
// Configure HIL note (2026-06-10): both cameras expose MJPEG and packed YUYV
|
||||||
// via libcamera VideoRecording/Viewfinder — no fully planar YUV420/NV12 at
|
// via libcamera VideoRecording/Viewfinder — no fully planar YUV420/NV12 at
|
||||||
// 640x480. configureSessionModeCReq correctly throws when fullPlanarIsOptional
|
// 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[] = {
|
inline constexpr BakedCameraProfile bakedCameraProfiles[] = {
|
||||||
{
|
{
|
||||||
"dell-laptop",
|
"dell-laptop",
|
||||||
|
|||||||
Reference in New Issue
Block a user