Resolve device selector on detach and add YuvStimProducer state tests.

Detach finds the shared producer via lcameraDev_resolveDeviceSelectorCReq
then removes buffers by attach identity; unit tests cover quale guards.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-14 11:04:22 -04:00
parent e7b7a311f7
commit 63532a6ee2
6 changed files with 374 additions and 18 deletions
+12 -18
View File
@@ -28,6 +28,7 @@ LcameraDevDllState::LcameraDevDllState()
lcameraDev_main(nullptr),
lcameraDev_exit(nullptr),
lcameraDev_getOrCreateDeviceCReq(nullptr),
lcameraDev_resolveDeviceSelectorCReq(nullptr),
lcameraDev_releaseDeviceCReq(nullptr),
lcameraDev_configureSessionModeCReq(nullptr)
{}
@@ -47,23 +48,6 @@ std::shared_ptr<YuvStimProducer> findStimProducerByCameraId(
return nullptr;
}
std::shared_ptr<YuvStimProducer> findStimProducerWithAttachedBuffer(
const std::shared_ptr<device::DeviceAttachmentSpec>& desc)
{
for (const std::shared_ptr<YuvStimProducer>& producer :
attachedStimulusProducers)
{
assert(producer != nullptr);
if (producer->getAttachedStimulusBufferByAttachIdentity(
desc->deviceIdentifier, desc->qualeIfaceApi))
{
return producer;
}
}
return nullptr;
}
bool validateAttachRequest(
const std::shared_ptr<device::DeviceAttachmentSpec>& spec)
{
@@ -110,6 +94,11 @@ void loadLcameraDevSymbols()
dlsym(
lcameraDevDll.dlopenHandle.get(),
"lcameraDev_getOrCreateDeviceCReq"));
lcameraDevDll.lcameraDev_resolveDeviceSelectorCReq = reinterpret_cast<
lcameraDev_resolveDeviceSelectorCReqFn *>(
dlsym(
lcameraDevDll.dlopenHandle.get(),
"lcameraDev_resolveDeviceSelectorCReq"));
lcameraDevDll.lcameraDev_releaseDeviceCReq = reinterpret_cast<
lcameraDev_releaseDeviceCReqFn *>(
dlsym(
@@ -124,6 +113,7 @@ void loadLcameraDevSymbols()
if (!lcameraDevDll.lcameraDev_main
|| !lcameraDevDll.lcameraDev_exit
|| !lcameraDevDll.lcameraDev_getOrCreateDeviceCReq
|| !lcameraDevDll.lcameraDev_resolveDeviceSelectorCReq
|| !lcameraDevDll.lcameraDev_releaseDeviceCReq
|| !lcameraDevDll.lcameraDev_configureSessionModeCReq)
{
@@ -276,7 +266,11 @@ lcameraBuff_detachDeviceCReq(
co_return StimBuffDeviceOpResult{false, desc};
}
auto producer = findStimProducerWithAttachedBuffer(desc);
const lcamera_dev::CameraIdentityRecord resolvedIdentity =
co_await (*lcameraDevDll.lcameraDev_resolveDeviceSelectorCReq)(
desc->deviceSelector);
auto producer = findStimProducerByCameraId(resolvedIdentity.id);
if (!producer) {
co_return StimBuffDeviceOpResult{true, desc};
}
@@ -25,6 +25,7 @@ struct LcameraDevDllState
lcameraDev_mainFn *lcameraDev_main;
lcameraDev_exitFn *lcameraDev_exit;
lcameraDev_getOrCreateDeviceCReqFn *lcameraDev_getOrCreateDeviceCReq;
lcameraDev_resolveDeviceSelectorCReqFn *lcameraDev_resolveDeviceSelectorCReq;
lcameraDev_releaseDeviceCReqFn *lcameraDev_releaseDeviceCReq;
lcameraDev_configureSessionModeCReqFn *lcameraDev_configureSessionModeCReq;
};
@@ -2,6 +2,8 @@ add_executable(lcameraBuff_unit_tests
lcameraBuffParams_tests.cpp
pixelAndColorFormatDecisions_tests.cpp
yuvLayoutClassification_tests.cpp
yuvStimProducer_state_tests.cpp
hilSmoCallbacksStub.cpp
)
target_include_directories(lcameraBuff_unit_tests PRIVATE
@@ -0,0 +1,95 @@
#include <hilSmoCallbacksStub.h>
#include <opts.h>
#include <user/comparatorApiDesc.h>
#include <spinscale/componentThread.h>
namespace lcamera_buff_tests {
namespace {
std::optional<std::string> searchForLibInSmoSearchPathsStub(
const std::string& libraryPath)
{
(void)libraryPath;
const char *overridePath = std::getenv("LCAMERADEV_LIB_PATH");
if (overridePath != nullptr && std::string(overridePath).size() > 0) {
return overridePath;
}
return std::nullopt;
}
std::shared_ptr<sscl::ComponentThread> componentThreadGetSelfStub()
{
return sscl::ComponentThread::getSelf();
}
OptionParser& optionParserGetOptionsStub()
{
return OptionParser::getOptions();
}
void releaseUseHostPtrBufferStub(
std::shared_ptr<smo::compute::ClBuffer> buffer)
{
(void)buffer;
}
std::shared_ptr<smo::compute::ComputeDevice> computeManagerGetDeviceStub()
{
return nullptr;
}
void computeManagerReleaseDeviceStub(
std::shared_ptr<smo::compute::ComputeDevice> device)
{
(void)device;
}
std::shared_ptr<smo::cologex::ExportedComparatorTypeDesc>
comparatorManagerGetComparatorTypeStub(smo::cologex::ComparatorTypeId typeId)
{
(void)typeId;
return nullptr;
}
std::unique_ptr<smo::cologex::Comparator> comparatorGetNewInstanceStub(
const std::shared_ptr<smo::cologex::ExportedComparatorTypeDesc>& comparatorType)
{
(void)comparatorType;
return nullptr;
}
const smo::stim_buff::SmoCallbacks hilCallbacks = {
.searchForLibInSmoSearchPaths = searchForLibInSmoSearchPathsStub,
.ComponentThread_getSelf = componentThreadGetSelfStub,
.OptionParser_getOptions = optionParserGetOptionsStub,
.ComputeManager_createUseHostPtrBuffer = createUseHostPtrBufferStub,
.ComputeManager_releaseUseHostPtrBuffer = releaseUseHostPtrBufferStub,
.ComputeManager_getDevice = computeManagerGetDeviceStub,
.ComputeManager_releaseDevice = computeManagerReleaseDeviceStub,
.ComparatorManager_getComparatorType =
comparatorManagerGetComparatorTypeStub,
.Comparator_getNewInstance = comparatorGetNewInstanceStub,
};
} // namespace
std::shared_ptr<smo::compute::ClBuffer> createUseHostPtrBufferStub(
void* hostPtr,
size_t size,
cl_mem_flags flags)
{
static const std::vector<std::shared_ptr<smo::compute::ComputeDevice>>
emptyDevices;
return std::make_shared<smo::compute::ClBuffer>(
hostPtr, size, flags, emptyDevices);
}
const smo::stim_buff::SmoCallbacks& hilSmoCallbacksStub()
{
return hilCallbacks;
}
} // namespace lcamera_buff_tests
@@ -0,0 +1,20 @@
#ifndef LCAMERA_BUFF_TESTS_HIL_SMO_CALLBACKS_STUB_H
#define LCAMERA_BUFF_TESTS_HIL_SMO_CALLBACKS_STUB_H
#include <user/compute.h>
#include <user/smoHooks.h>
#include <memory>
#include <vector>
namespace lcamera_buff_tests {
const smo::stim_buff::SmoCallbacks& hilSmoCallbacksStub();
std::shared_ptr<smo::compute::ClBuffer> createUseHostPtrBufferStub(
void* hostPtr,
size_t size,
cl_mem_flags flags);
} // namespace lcamera_buff_tests
#endif // LCAMERA_BUFF_TESTS_HIL_SMO_CALLBACKS_STUB_H
@@ -0,0 +1,244 @@
#include <boost/asio/io_context.hpp>
#include <cameraIdentity.h>
#include <gtest/gtest.h>
#include <hilSmoCallbacksStub.h>
#include <lcameraBuffInternal.h>
#include <support/exceptionAssertions.h>
#include <yuvChannelStimulusBuffer.h>
#include <yuvStimProducer.h>
#include <memory>
#include <string>
namespace smo {
namespace stim_buff {
namespace lcamera_buff {
namespace {
constexpr const char *usbExternalSelector =
"model-substr:USB;location:external";
constexpr const char *usbAliasSelector =
"model-substr:USB Video Device;location:external";
std::shared_ptr<device::DeviceAttachmentSpec> makeLcameraAttachSpec(
const std::string& deviceSelector,
const std::string& qualeIfaceApi,
const std::string& deviceIdentifier = "cam0")
{
auto spec = std::make_shared<device::DeviceAttachmentSpec>();
spec->deviceIdentifier = deviceIdentifier;
spec->sensorType = 'e';
spec->qualeIfaceApi = qualeIfaceApi;
spec->stimBuffApi = "lcameraBuff";
spec->provider = "lcameraDev";
spec->deviceSelector = deviceSelector;
spec->stimBuffApiParams = {
{"v-res", "480p"},
{"colour-space", "yuv"},
{"opt-planar", ""},
};
return spec;
}
LcameraBuffParsedParams makeDefaultParsedParams()
{
LcameraBuffParsedParams parsedParams;
parsedParams.width = 640;
parsedParams.height = 480;
parsedParams.colourSpace = lcamera_dev::LcameraDevColourSpace::Yuv;
parsedParams.fullPlanarIsOptional = true;
return parsedParams;
}
lcamera_dev::LcameraDevConfiguredCameraMode makeYuyv480ConfiguredMode()
{
lcamera_dev::LcameraDevConfiguredCameraMode configuredMode;
configuredMode.width = 640;
configuredMode.height = 480;
configuredMode.colourSpace = lcamera_dev::LcameraDevColourSpace::Yuv;
configuredMode.pixelFormatName = "YUYV";
configuredMode.isFullyPlanar = false;
configuredMode.planeCount = 1;
return configuredMode;
}
lcamera_dev::CameraIdentityRecord makeTestCameraIdentity()
{
lcamera_dev::CameraIdentityRecord identity;
identity.id = "unit-test-camera-1";
identity.model = "UnitTestCam";
return identity;
}
class YuvStimProducerQualeIfaceTest
: public ::testing::Test
{
protected:
void SetUp() override
{
lcameraBuffSmoHooksPtr = &lcamera_buff_tests::hilSmoCallbacksStub();
}
void TearDown() override
{
lcameraBuffSmoHooksPtr = nullptr;
}
boost::asio::io_context ioContext;
const LcameraBuffParsedParams parsedParams = makeDefaultParsedParams();
const lcamera_dev::LcameraDevConfiguredCameraMode configuredMode =
makeYuyv480ConfiguredMode();
const lcamera_dev::CameraIdentityRecord cameraIdentity =
makeTestCameraIdentity();
YuvStimProducer makeProducer(
const std::shared_ptr<device::DeviceAttachmentSpec>& producerSpec)
{
return YuvStimProducer(
producerSpec,
ioContext,
nullptr,
cameraIdentity,
parsedParams,
configuredMode);
}
};
TEST_F(YuvStimProducerQualeIfaceTest, GetOrCreateSameSpecReturnsExistingBuffer)
{
auto producerSpec = makeLcameraAttachSpec(
usbExternalSelector, "colour-yuv-y");
YuvStimProducer producer = makeProducer(producerSpec);
const auto firstBuffer =
producer.getOrCreateAttachedStimulusBuffer(producerSpec);
ASSERT_NE(firstBuffer, nullptr);
const auto secondBuffer =
producer.getOrCreateAttachedStimulusBuffer(producerSpec);
EXPECT_EQ(firstBuffer, secondBuffer);
EXPECT_EQ(producer.attachedStimulusBuffers.size(), 1u);
}
TEST_F(YuvStimProducerQualeIfaceTest,
RejectDuplicateQualeWhenAlternateSelectorResolvesToSameSession)
{
/* One YuvStimProducer == one resolved libcamera camera id / session.
* A second DAP line may spell a different deviceSelector yet resolve to
* that same session via attachToExistingProducer; it must not attach the
* same qualeIface API twice.
*/
auto producerSpec = makeLcameraAttachSpec(
usbExternalSelector, "colour-yuv-y");
YuvStimProducer producer = makeProducer(producerSpec);
const auto firstSpec = makeLcameraAttachSpec(
usbExternalSelector, "colour-yuv-y", "cam-y-line0");
ASSERT_NE(producer.getOrCreateAttachedStimulusBuffer(firstSpec), nullptr);
const auto duplicateQualeAlternateSelectorSpec = makeLcameraAttachSpec(
usbAliasSelector, "colour-yuv-y", "cam-y-line1");
try {
producer.getOrCreateAttachedStimulusBuffer(
duplicateQualeAlternateSelectorSpec);
FAIL() << "Expected std::runtime_error";
}
catch (const std::runtime_error& exception)
{
sscl::tests::expectExceptionMessageContains(
exception,
"already attached for camera session");
sscl::tests::expectExceptionMessageContains(
exception,
"colour-yuv-y");
}
EXPECT_EQ(producer.attachedStimulusBuffers.size(), 1u);
EXPECT_NE(
firstSpec->deviceSelector,
duplicateQualeAlternateSelectorSpec->deviceSelector);
}
TEST_F(YuvStimProducerQualeIfaceTest,
AllowDistinctQualeIfacesForAlternateSelectorsOnSameSession)
{
auto producerSpec = makeLcameraAttachSpec(
usbExternalSelector, "colour-yuv-y");
YuvStimProducer producer = makeProducer(producerSpec);
const auto ySpec = makeLcameraAttachSpec(
usbExternalSelector, "colour-yuv-y", "cam-y");
const auto uSpec = makeLcameraAttachSpec(
usbAliasSelector, "colour-yuv-u", "cam-u");
const auto vSpec = makeLcameraAttachSpec(
usbExternalSelector, "colour-yuv-v", "cam-v");
ASSERT_NE(producer.getOrCreateAttachedStimulusBuffer(ySpec), nullptr);
ASSERT_NE(producer.getOrCreateAttachedStimulusBuffer(uSpec), nullptr);
ASSERT_NE(producer.getOrCreateAttachedStimulusBuffer(vSpec), nullptr);
EXPECT_EQ(producer.attachedStimulusBuffers.size(), 3u);
EXPECT_TRUE(producer.hasBufferWithQualeIfaceApi("colour-yuv-y"));
EXPECT_TRUE(producer.hasBufferWithQualeIfaceApi("colour-yuv-u"));
EXPECT_TRUE(producer.hasBufferWithQualeIfaceApi("colour-yuv-v"));
}
TEST_F(YuvStimProducerQualeIfaceTest,
RejectDuplicateQualeAfterYViaSelectorAAndUViaSelectorB)
{
auto producerSpec = makeLcameraAttachSpec(
usbExternalSelector, "colour-yuv-y");
YuvStimProducer producer = makeProducer(producerSpec);
ASSERT_NE(producer.getOrCreateAttachedStimulusBuffer(
makeLcameraAttachSpec(usbExternalSelector, "colour-yuv-y", "cam-y")),
nullptr);
ASSERT_NE(producer.getOrCreateAttachedStimulusBuffer(
makeLcameraAttachSpec(usbAliasSelector, "colour-yuv-u", "cam-u")),
nullptr);
const auto duplicateYViaAlternateSelector = makeLcameraAttachSpec(
usbAliasSelector, "colour-yuv-y", "cam-y-second-dap-line");
EXPECT_THROW(
producer.getOrCreateAttachedStimulusBuffer(
duplicateYViaAlternateSelector),
std::runtime_error);
EXPECT_EQ(producer.attachedStimulusBuffers.size(), 2u);
}
TEST_F(YuvStimProducerQualeIfaceTest,
FindAttachedBufferByAttachIdentityWithAlternateSelector)
{
auto producerSpec = makeLcameraAttachSpec(
usbExternalSelector, "colour-yuv-y");
YuvStimProducer producer = makeProducer(producerSpec);
const auto uSpec = makeLcameraAttachSpec(
usbExternalSelector, "colour-yuv-u", "cam-u");
ASSERT_NE(producer.getOrCreateAttachedStimulusBuffer(uSpec), nullptr);
const auto foundBuffer =
producer.getAttachedStimulusBufferByAttachIdentity(
"cam-u", "colour-yuv-u");
ASSERT_NE(foundBuffer, nullptr);
EXPECT_EQ(
foundBuffer->deviceAttachmentSpec->deviceSelector,
usbExternalSelector);
EXPECT_EQ(
producer.getAttachedStimulusBuffer(
makeLcameraAttachSpec(
usbAliasSelector, "colour-yuv-u", "cam-u")),
nullptr);
EXPECT_NE(
producer.getAttachedStimulusBufferByAttachIdentity(
"cam-u", "colour-yuv-u"),
nullptr);
}
} // namespace
} // namespace lcamera_buff
} // namespace stim_buff
} // namespace smo