diff --git a/stimBuffApis/CMakeLists.txt b/stimBuffApis/CMakeLists.txt index efe2191..aec82e1 100644 --- a/stimBuffApis/CMakeLists.txt +++ b/stimBuffApis/CMakeLists.txt @@ -1,2 +1,3 @@ add_subdirectory(xcbWindow) add_subdirectory(livoxGen1) +add_subdirectory(lcameraBuff) diff --git a/stimBuffApis/lcameraBuff/CMakeLists.txt b/stimBuffApis/lcameraBuff/CMakeLists.txt new file mode 100644 index 0000000..8fc9e72 --- /dev/null +++ b/stimBuffApis/lcameraBuff/CMakeLists.txt @@ -0,0 +1,54 @@ +cmake_dependent_option(ENABLE_STIMBUFFAPI_lcameraBuff + "Enable libcamera YUV stim buff API" ON + "ENABLE_LIB_lcameraDev" OFF) + +if(ENABLE_STIMBUFFAPI_lcameraBuff) + set(CONFIG_STIMBUFFAPI_LCAMERABUFF_ENABLED 1) + + pkg_check_modules(LIBCAMERA REQUIRED libcamera) + + add_library(lcameraBuff SHARED + lcameraBuff.cpp + lcameraBuffParams.cpp + pixelAndColorFormatDecisions.cpp + yuvCaptureLayout.cpp + yuvChannelStimulusBuffer.cpp + yuvStimProducer.cpp + ) + + set_target_properties(lcameraBuff PROPERTIES + VERSION ${PROJECT_VERSION} + SOVERSION ${PROJECT_VERSION_MAJOR} + ) + + target_include_directories(lcameraBuff PUBLIC + ${Boost_INCLUDE_DIRS} + ${CMAKE_SOURCE_DIR}/include + ${CMAKE_SOURCE_DIR}/smocore/include + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_SOURCE_DIR}/commonLibs + ${CMAKE_SOURCE_DIR}/commonLibs/lcameraDev + ${LIBCAMERA_INCLUDE_DIRS} + ) + + target_link_libraries(lcameraBuff PUBLIC + Boost::system + Boost::log + attachmentSupport + spinscale + ) + + add_custom_command(TARGET lcameraBuff POST_BUILD + COMMAND ${CMAKE_COMMAND} -DVERIFY_FILE="$" + -P ${CMAKE_SOURCE_DIR}/cmake/VerifyBoostDynamic.cmake + COMMENT "Verifying Boost dynamic dependencies for lcameraBuff" + ) + + install(TARGETS lcameraBuff + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} NAMELINK_SKIP + ) + + if(ENABLE_TESTS) + add_subdirectory(tests) + endif() +endif() diff --git a/stimBuffApis/lcameraBuff/lcameraBuff.cpp b/stimBuffApis/lcameraBuff/lcameraBuff.cpp new file mode 100644 index 0000000..a289ff5 --- /dev/null +++ b/stimBuffApis/lcameraBuff/lcameraBuff.cpp @@ -0,0 +1,354 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace smo::stim_buff::lcamera_buff { + +const SmoCallbacks *lcameraBuffSmoHooksPtr = nullptr; +SmoThreadingModelDesc lcameraBuffThreadingModelDesc; +LcameraDevDllState lcameraDevDll; +std::vector> attachedStimulusProducers; + +void LcameraDevDllState::DlCloser::operator()(void *handle) +{ + if (handle) { + dlclose(handle); + } +} + +LcameraDevDllState::LcameraDevDllState() +: dlopenHandle(nullptr, DlCloser{}), + lcameraDev_main(nullptr), + lcameraDev_exit(nullptr), + lcameraDev_getOrCreateDeviceCReq(nullptr), + lcameraDev_releaseDeviceCReq(nullptr), + lcameraDev_configureSessionModeCReq(nullptr) +{} + +std::shared_ptr findStimProducerByCameraId( + const std::string& resolvedCameraId) +{ + for (const std::shared_ptr& producer : + attachedStimulusProducers) + { + assert(producer != nullptr); + if (producer->resolvedCameraId == resolvedCameraId) { + return producer; + } + } + + return nullptr; +} + +std::shared_ptr findStimProducerWithAttachedBuffer( + const std::shared_ptr& desc) +{ + for (const std::shared_ptr& producer : + attachedStimulusProducers) + { + assert(producer != nullptr); + if (producer->getAttachedStimulusBufferByAttachIdentity( + desc->deviceIdentifier, desc->qualeIfaceApi)) + { + return producer; + } + } + + return nullptr; +} + +bool validateAttachRequest( + const std::shared_ptr& spec) +{ + if (!spec) + { + std::cerr << __func__ << ": null DeviceAttachmentSpec\n"; + return false; + } + + if (spec->stimBuffApi != "lcameraBuff") + { + std::cerr << __func__ << ": stimBuffApi must be lcameraBuff, got '" + << spec->stimBuffApi << "'\n"; + return false; + } + + if (spec->provider != "lcameraDev") + { + std::cerr << __func__ << ": provider must be lcameraDev(), got '" + << spec->provider << "'\n"; + return false; + } + + if (!YuvStimProducer::supportsQualeIfaceApi(spec->qualeIfaceApi)) + { + std::cerr << __func__ << ": unsupported qualeIfaceApi '" + << spec->qualeIfaceApi << "'\n"; + return false; + } + + return true; +} + +namespace { + +void loadLcameraDevSymbols() +{ + lcameraDevDll.lcameraDev_main = reinterpret_cast( + dlsym(lcameraDevDll.dlopenHandle.get(), "lcameraDev_main")); + lcameraDevDll.lcameraDev_exit = reinterpret_cast( + dlsym(lcameraDevDll.dlopenHandle.get(), "lcameraDev_exit")); + lcameraDevDll.lcameraDev_getOrCreateDeviceCReq = reinterpret_cast< + lcameraDev_getOrCreateDeviceCReqFn *>( + dlsym( + lcameraDevDll.dlopenHandle.get(), + "lcameraDev_getOrCreateDeviceCReq")); + lcameraDevDll.lcameraDev_releaseDeviceCReq = reinterpret_cast< + lcameraDev_releaseDeviceCReqFn *>( + dlsym( + lcameraDevDll.dlopenHandle.get(), + "lcameraDev_releaseDeviceCReq")); + lcameraDevDll.lcameraDev_configureSessionModeCReq = reinterpret_cast< + lcameraDev_configureSessionModeCReqFn *>( + dlsym( + lcameraDevDll.dlopenHandle.get(), + "lcameraDev_configureSessionModeCReq")); + + if (!lcameraDevDll.lcameraDev_main + || !lcameraDevDll.lcameraDev_exit + || !lcameraDevDll.lcameraDev_getOrCreateDeviceCReq + || !lcameraDevDll.lcameraDev_releaseDeviceCReq + || !lcameraDevDll.lcameraDev_configureSessionModeCReq) + { + throw std::runtime_error( + std::string(__func__) + + ": failed to resolve lcameraDev library symbols"); + } +} + +sscl::co::ViralNonPostingInvoker +attachChannelBufferToProducer( + const std::shared_ptr& desc, + const std::shared_ptr& producer) +{ + producer->getOrCreateAttachedStimulusBuffer(desc); + co_return StimBuffDeviceOpResult{true, desc}; +} + +sscl::co::ViralNonPostingInvoker +attachToExistingProducer( + const std::shared_ptr& desc, + const std::shared_ptr& producer) +{ + const LcameraBuffParsedParams parsedParams = parseLcameraBuffParams(*desc); + const lcamera_dev::LcameraDevCameraModeRequest modeRequest = + toCameraModeRequest(parsedParams); + + co_await (*lcameraDevDll.lcameraDev_configureSessionModeCReq)( + producer->deviceSession, + modeRequest); + + co_return co_await attachChannelBufferToProducer(desc, producer); +} + +sscl::co::ViralNonPostingInvoker +attachByCreatingProducer( + const std::shared_ptr& desc, + const std::shared_ptr& componentThread, + const lcamera_dev::LcameraDevGetOrCreateResult& createResult) +{ + const LcameraBuffParsedParams parsedParams = parseLcameraBuffParams(*desc); + const lcamera_dev::LcameraDevCameraModeRequest modeRequest = + toCameraModeRequest(parsedParams); + + const lcamera_dev::LcameraDevConfiguredCameraMode configuredMode = + co_await (*lcameraDevDll.lcameraDev_configureSessionModeCReq)( + createResult.deviceSession, + modeRequest); + + auto producer = std::make_shared( + desc, + componentThread->getIoContext(), + createResult.deviceSession, + createResult.resolvedIdentity, + parsedParams, + configuredMode); + + attachedStimulusProducers.push_back(producer); + + co_return co_await attachChannelBufferToProducer(desc, producer); +} + +} // namespace + +sscl::co::ViralNonPostingInvoker lcameraBuff_initializeCInd() +{ + if (!lcameraBuffSmoHooksPtr) + { + throw std::runtime_error( + std::string(__func__) + ": SMO hooks not initialized"); + } + + const std::optional libPath = + lcameraBuffSmoHooksPtr->searchForLibInSmoSearchPaths( + "liblcameraDev.so"); + + lcameraDevDll.dlopenHandle.reset( + dlopen( + libPath.value_or("liblcameraDev.so").c_str(), + RTLD_LAZY)); + + if (!lcameraDevDll.dlopenHandle) + { + throw std::runtime_error( + std::string(__func__) + ": failed to load lcameraDev library: " + + (dlerror() ? dlerror() : "unknown error")); + } + + loadLcameraDevSymbols(); + + (*lcameraDevDll.lcameraDev_main)( + lcameraBuffThreadingModelDesc.componentThread); + + co_return 0; +} + +sscl::co::ViralNonPostingInvoker lcameraBuff_finalizeCInd() +{ + for (const std::shared_ptr& producer : + attachedStimulusProducers) + { + assert(producer != nullptr); + producer->deviceSession.reset(); + } + + attachedStimulusProducers.clear(); + + if (lcameraDevDll.lcameraDev_exit) { + (*lcameraDevDll.lcameraDev_exit)(); + } + + lcameraDevDll.dlopenHandle.reset(nullptr); + lcameraDevDll = LcameraDevDllState(); + co_return 0; +} + +sscl::co::DynamicViralPostingInvoker +lcameraBuff_attachDeviceCReq( + sscl::co::ExplicitPostTarget, + const std::shared_ptr& desc, + const std::shared_ptr& componentThread) +{ + if (!validateAttachRequest(desc)) { + co_return StimBuffDeviceOpResult{false, desc}; + } + + const lcamera_dev::LcameraDevGetOrCreateResult createResult = + co_await (*lcameraDevDll.lcameraDev_getOrCreateDeviceCReq)( + desc->deviceSelector); + + const std::string resolvedCameraId = createResult.resolvedIdentity.id; + + auto existingProducer = findStimProducerByCameraId(resolvedCameraId); + if (existingProducer) { + co_return co_await attachToExistingProducer(desc, existingProducer); + } + + co_return co_await attachByCreatingProducer( + desc, componentThread, createResult); +} + +sscl::co::DynamicViralPostingInvoker +lcameraBuff_detachDeviceCReq( + [[maybe_unused]] sscl::co::ExplicitPostTarget postTarget, + const std::shared_ptr& desc) +{ + (void)postTarget; + + if (!validateAttachRequest(desc)) { + co_return StimBuffDeviceOpResult{false, desc}; + } + + auto producer = findStimProducerWithAttachedBuffer(desc); + if (!producer) { + co_return StimBuffDeviceOpResult{true, desc}; + } + + auto stimBuffer = producer->getAttachedStimulusBufferByAttachIdentity( + desc->deviceIdentifier, desc->qualeIfaceApi); + + if (!stimBuffer) { + co_return StimBuffDeviceOpResult{true, desc}; + } + + producer->destroyAttachedStimulusBuffer(stimBuffer); + + const std::shared_ptr deviceSession = + producer->deviceSession; + + co_await (*lcameraDevDll.lcameraDev_releaseDeviceCReq)(deviceSession); + + if (!producer->attachedStimulusBuffers.empty()) { + co_return StimBuffDeviceOpResult{true, desc}; + } + + attachedStimulusProducers.erase( + std::remove_if( + attachedStimulusProducers.begin(), + attachedStimulusProducers.end(), + [&producer](const std::shared_ptr& candidate) + { + return candidate == producer; + }), + attachedStimulusProducers.end()); + + producer->deviceSession.reset(); + + co_return StimBuffDeviceOpResult{true, desc}; +} + +} // namespace smo::stim_buff::lcamera_buff + +extern "C" smo::stim_buff::SMO_GET_STIM_BUFF_API_DESC_FN_TYPEDEF + SMO_GET_STIM_BUFF_API_DESC_FN_NAME; + +namespace { + +static const smo::stim_buff::StimBuffApiDesc lcameraBuffApiDesc = { + .name = "lcameraBuff", + .exportedQualeIfaceApis = + { + {.name = "colour-yuv-y"}, + {.name = "colour-yuv-u"}, + {.name = "colour-yuv-v"}, + }, + .sal_mgmt_libOps = + { + .initializeCInd = + smo::stim_buff::lcamera_buff::lcameraBuff_initializeCInd, + .finalizeCInd = + smo::stim_buff::lcamera_buff::lcameraBuff_finalizeCInd, + .attachDeviceCReq = + smo::stim_buff::lcamera_buff::lcameraBuff_attachDeviceCReq, + .detachDeviceCReq = + smo::stim_buff::lcamera_buff::lcameraBuff_detachDeviceCReq, + }, +}; + +} // namespace + +const smo::stim_buff::StimBuffApiDesc& SMO_GET_STIM_BUFF_API_DESC_FN_NAME( + const smo::stim_buff::SmoCallbacks& callbacks, + const smo::stim_buff::SmoThreadingModelDesc& threadingModel) +{ + smo::stim_buff::lcamera_buff::lcameraBuffSmoHooksPtr = &callbacks; + smo::stim_buff::lcamera_buff::lcameraBuffThreadingModelDesc = threadingModel; + return lcameraBuffApiDesc; +} diff --git a/stimBuffApis/lcameraBuff/lcameraBuffInternal.h b/stimBuffApis/lcameraBuff/lcameraBuffInternal.h new file mode 100644 index 0000000..84ca222 --- /dev/null +++ b/stimBuffApis/lcameraBuff/lcameraBuffInternal.h @@ -0,0 +1,59 @@ +#ifndef LCAMERA_BUFF_INTERNAL_H +#define LCAMERA_BUFF_INTERNAL_H + +#include +#include +#include +#include +#include +#include + +namespace smo::stim_buff::lcamera_buff { + +class YuvStimProducer; + +struct LcameraDevDllState +{ + struct DlCloser + { + void operator()(void *handle); + }; + + LcameraDevDllState(); + + std::unique_ptr dlopenHandle; + lcameraDev_mainFn *lcameraDev_main; + lcameraDev_exitFn *lcameraDev_exit; + lcameraDev_getOrCreateDeviceCReqFn *lcameraDev_getOrCreateDeviceCReq; + lcameraDev_releaseDeviceCReqFn *lcameraDev_releaseDeviceCReq; + lcameraDev_configureSessionModeCReqFn *lcameraDev_configureSessionModeCReq; +}; + +extern const SmoCallbacks *lcameraBuffSmoHooksPtr; +extern SmoThreadingModelDesc lcameraBuffThreadingModelDesc; +extern LcameraDevDllState lcameraDevDll; +extern std::vector> attachedStimulusProducers; + +std::shared_ptr findStimProducerByCameraId( + const std::string& resolvedCameraId); + +bool validateAttachRequest( + const std::shared_ptr& spec); + +sscl::co::ViralNonPostingInvoker lcameraBuff_initializeCInd(); +sscl::co::ViralNonPostingInvoker lcameraBuff_finalizeCInd(); + +sscl::co::DynamicViralPostingInvoker +lcameraBuff_attachDeviceCReq( + sscl::co::ExplicitPostTarget postTarget, + const std::shared_ptr& desc, + const std::shared_ptr& componentThread); + +sscl::co::DynamicViralPostingInvoker +lcameraBuff_detachDeviceCReq( + sscl::co::ExplicitPostTarget postTarget, + const std::shared_ptr& desc); + +} // namespace smo::stim_buff::lcamera_buff + +#endif // LCAMERA_BUFF_INTERNAL_H diff --git a/stimBuffApis/lcameraBuff/lcameraBuffParams.cpp b/stimBuffApis/lcameraBuff/lcameraBuffParams.cpp new file mode 100644 index 0000000..86d7f39 --- /dev/null +++ b/stimBuffApis/lcameraBuff/lcameraBuffParams.cpp @@ -0,0 +1,290 @@ +#include +#include +#include +#include +#include + +namespace smo { +namespace stim_buff { +namespace lcamera_buff { + +namespace { + +constexpr const char *mutuallyExclusiveResolutionMessage = + "lcameraBuff: explicit dimension width/height params cannot be combined " + "with vertical-resolution shorthand params"; + +constexpr const char *missingResolutionMessage = + "lcameraBuff: specify either explicit dimension width and height, or a " + "vertical-resolution shorthand (v-res, vres, etc.)"; + +constexpr const char *incompleteExplicitResolutionMessage = + "lcameraBuff: both dimension width and height are required when using " + "explicit dimension params"; + +const std::vector widthParamSynonyms = { + "frame-width", + "frame-w", + "dimension-width", + "dimension-w", + "dim-width", + "dim-w", +}; + +const std::vector heightParamSynonyms = { + "frame-height", + "frame-h", + "dimension-height", + "dimension-h", + "dim-height", + "dim-h", +}; + +const std::vector verticalResolutionParamSynonyms = { + "vertical-resolution", + "v-resolution", + "v-res", + "vres", +}; + +const std::vector colourSpaceParamSynonyms = { + "colorspace", + "color-space", + "colourspace", + "colour-space", +}; + +const std::vector optPlanarParamSynonyms = { + "full-planar-is-optional", + "opt-planar", +}; + +std::string normalizeVerticalResolutionToken(const std::string& token) +{ + std::string normalizedToken = + device::DeviceAttachmentSpec::normalizeParamToken(token); + + if (normalizedToken.empty()) + { + throw std::runtime_error( + "lcameraBuff: vertical-resolution value is empty"); + } + + if (normalizedToken.back() == 'p') + { + normalizedToken.pop_back(); + } + + if (normalizedToken.empty()) + { + throw std::runtime_error( + "lcameraBuff: vertical-resolution value is empty after " + "normalization"); + } + + return normalizedToken; +} + +struct VerticalResolutionPreset +{ + unsigned width; + unsigned height; +}; + +const std::unordered_map& +verticalResolutionPresetTable() +{ + static const std::unordered_map + presets = { + {"360", {640, 360}}, + {"480", {640, 480}}, + {"720", {1280, 720}}, + {"1080", {1920, 1080}}, + }; + + return presets; +} + +VerticalResolutionPreset resolveVerticalResolutionPreset( + const std::string& token) +{ + const std::string normalizedToken = + normalizeVerticalResolutionToken(token); + + const std::unordered_map& presets = + verticalResolutionPresetTable(); + + const auto presetIt = presets.find(normalizedToken); + if (presetIt == presets.end()) + { + std::ostringstream supportedStream; + bool first = true; + for (const auto& entry : presets) + { + if (!first) { + supportedStream << ", "; + } + + supportedStream << entry.first << "p"; + first = false; + } + + throw std::runtime_error( + "lcameraBuff: unsupported vertical-resolution '" + + token + "'; supported values: " + + supportedStream.str()); + } + + return presetIt->second; +} + +lcamera_dev::LcameraDevColourSpace parseColourSpaceValue( + const std::string& value) +{ + const std::string lowered = + device::DeviceAttachmentSpec::normalizeParamToken(value); + if (lowered == "yuv") { + return lcamera_dev::LcameraDevColourSpace::Yuv; + } + + throw std::runtime_error( + "lcameraBuff: unsupported colour-space '" + value + + "'; supported values: yuv"); +} + +void applyExplicitDimensions( + const std::vector>& params, + LcameraBuffParsedParams& parsedParams) +{ + const std::optional> widthParam = + device::DeviceAttachmentSpec::findOptionalParamWithSynonyms( + params, widthParamSynonyms); + const std::optional> heightParam = + device::DeviceAttachmentSpec::findOptionalParamWithSynonyms( + params, heightParamSynonyms); + + if (!widthParam && !heightParam) { return; } + + const std::optional> + verticalResolutionParam = + device::DeviceAttachmentSpec::findOptionalParamWithSynonyms( + params, verticalResolutionParamSynonyms); + + if (verticalResolutionParam) { + throw std::runtime_error(mutuallyExclusiveResolutionMessage); + } + + if (!widthParam || !heightParam) { + throw std::runtime_error(incompleteExplicitResolutionMessage); + } + + parsedParams.width = static_cast( + device::DeviceAttachmentSpec::parseParamValueAsPositiveInt( + widthParam->first, widthParam->second)); + parsedParams.height = static_cast( + device::DeviceAttachmentSpec::parseParamValueAsPositiveInt( + heightParam->first, heightParam->second)); +} + +void applyVerticalResolutionShorthand( + const std::vector>& params, + LcameraBuffParsedParams& parsedParams) +{ + const std::optional verticalResolutionValue = + device::DeviceAttachmentSpec::parseOptionalParamValueWithSynonyms( + params, + verticalResolutionParamSynonyms, + "lcameraBuff: vertical-resolution param requires a value " + "(e.g. v-res=480p)"); + + if (!verticalResolutionValue) { return; } + + const VerticalResolutionPreset preset = + resolveVerticalResolutionPreset(*verticalResolutionValue); + parsedParams.width = preset.width; + parsedParams.height = preset.height; +} + +void applyColourSpaceParam( + const std::vector>& params, + LcameraBuffParsedParams& parsedParams) +{ + const std::optional colourSpaceValue = + device::DeviceAttachmentSpec::parseOptionalParamValueWithSynonyms( + params, + colourSpaceParamSynonyms, + "lcameraBuff: colour-space param requires a value " + "(e.g. colour-space=yuv)"); + + if (!colourSpaceValue) { return; } + + parsedParams.colourSpace = parseColourSpaceValue(*colourSpaceValue); +} + +void finalizeCheckResolutionValues(const LcameraBuffParsedParams& parsedParams) +{ + if (parsedParams.width == 0 || parsedParams.height == 0) + { + throw std::runtime_error(missingResolutionMessage); + } +} + +} // namespace + +LcameraBuffParsedParams parseLcameraBuffParams( + const device::DeviceAttachmentSpec& spec) +{ + const auto& params = spec.stimBuffApiParams; + + device::DeviceAttachmentSpec::rejectUnknownParams( + params, + "lcameraBuff: unknown stimbuff-api param '", + widthParamSynonyms, + heightParamSynonyms, + verticalResolutionParamSynonyms, + colourSpaceParamSynonyms, + optPlanarParamSynonyms); + + LcameraBuffParsedParams parsedParams; + + applyExplicitDimensions(params, parsedParams); + applyVerticalResolutionShorthand(params, parsedParams); + applyColourSpaceParam(params, parsedParams); + + /* Presence flag: opt-planar or opt-planar= with no value means true. */ + parsedParams.fullPlanarIsOptional = + device::DeviceAttachmentSpec::parseOptionalParamAsBoolWithSynonyms( + params, + optPlanarParamSynonyms, + /*defaultValue=*/false, + /*emptyMeansTrue=*/true); + + finalizeCheckResolutionValues(parsedParams); + + return parsedParams; +} + +lcamera_dev::LcameraDevCameraModeRequest toCameraModeRequest( + const LcameraBuffParsedParams& parsedParams) +{ + lcamera_dev::LcameraDevCameraModeRequest request; + request.width = parsedParams.width; + request.height = parsedParams.height; + request.colourSpace = parsedParams.colourSpace; + request.fullPlanarIsOptional = parsedParams.fullPlanarIsOptional; + return request; +} + +bool lcameraBuffParamsEqual( + const LcameraBuffParsedParams& left, + const LcameraBuffParsedParams& right) +{ + return left.width == right.width + && left.height == right.height + && left.colourSpace == right.colourSpace + && left.fullPlanarIsOptional == right.fullPlanarIsOptional; +} + +} // namespace lcamera_buff +} // namespace stim_buff +} // namespace smo diff --git a/stimBuffApis/lcameraBuff/lcameraBuffParams.h b/stimBuffApis/lcameraBuff/lcameraBuffParams.h new file mode 100644 index 0000000..8f4940c --- /dev/null +++ b/stimBuffApis/lcameraBuff/lcameraBuffParams.h @@ -0,0 +1,34 @@ +#ifndef LCAMERA_BUFF_PARAMS_H +#define LCAMERA_BUFF_PARAMS_H + +#include +#include + +namespace smo { +namespace stim_buff { +namespace lcamera_buff { + +struct LcameraBuffParsedParams +{ + unsigned width = 0; + unsigned height = 0; + lcamera_dev::LcameraDevColourSpace colourSpace = + lcamera_dev::LcameraDevColourSpace::Yuv; + bool fullPlanarIsOptional = false; +}; + +LcameraBuffParsedParams parseLcameraBuffParams( + const device::DeviceAttachmentSpec& spec); + +lcamera_dev::LcameraDevCameraModeRequest toCameraModeRequest( + const LcameraBuffParsedParams& parsedParams); + +bool lcameraBuffParamsEqual( + const LcameraBuffParsedParams& left, + const LcameraBuffParsedParams& right); + +} // namespace lcamera_buff +} // namespace stim_buff +} // namespace smo + +#endif // LCAMERA_BUFF_PARAMS_H diff --git a/stimBuffApis/lcameraBuff/pixelAndColorFormatDecisions.cpp b/stimBuffApis/lcameraBuff/pixelAndColorFormatDecisions.cpp new file mode 100644 index 0000000..5eacbc8 --- /dev/null +++ b/stimBuffApis/lcameraBuff/pixelAndColorFormatDecisions.cpp @@ -0,0 +1,141 @@ +#include +#include +#include +#include +#include +#include + +namespace smo { +namespace stim_buff { +namespace lcamera_buff { + +namespace { + +std::string normalizeFourccToken(std::string token) +{ + for (char& character : token) + { + character = static_cast( + std::toupper(static_cast(character))); + } + + return token; +} + +bool fourccInSet( + const std::string& fourcc, + const std::unordered_set& candidates) +{ + return candidates.find(fourcc) != candidates.end(); +} + +const std::unordered_set& yuv420FourccSet() +{ + static const std::unordered_set fourccs = { + "YU12", "YV12", "YUV420", "YVU420", "NV12", "NV21", + }; + return fourccs; +} + +const std::unordered_set& yuv422FourccSet() +{ + static const std::unordered_set fourccs = { + "YUYV", "YVYU", "UYVY", "VYUY", "YUV422", "YVU422", + "NV16", "NV61", "YU16", "YV16", + }; + return fourccs; +} + +const std::unordered_set& yuv444FourccSet() +{ + static const std::unordered_set fourccs = { + "YUV444", "YVU444", + }; + return fourccs; +} + +unsigned chromaPlaneWidth(unsigned frameWidth, YuvChromaSubsampling subsampling) +{ + switch (subsampling) + { + case YuvChromaSubsampling::Yuv420: + case YuvChromaSubsampling::Yuv422: + return (frameWidth + 1u) / 2u; + + case YuvChromaSubsampling::Yuv444: + return frameWidth; + } + + throw std::logic_error( + "lcameraBuff: unhandled YuvChromaSubsampling in chromaPlaneWidth"); +} + +unsigned chromaPlaneHeight(unsigned frameHeight, YuvChromaSubsampling subsampling) +{ + switch (subsampling) + { + case YuvChromaSubsampling::Yuv420: + return (frameHeight + 1u) / 2u; + + case YuvChromaSubsampling::Yuv422: + case YuvChromaSubsampling::Yuv444: + return frameHeight; + } + + throw std::logic_error( + "lcameraBuff: unhandled YuvChromaSubsampling in chromaPlaneHeight"); +} + +} // namespace + +YuvChromaSubsampling classifyYuvChromaSubsampling( + const lcamera_dev::LcameraDevConfiguredCameraMode& configuredMode) +{ + const std::string fourcc = + normalizeFourccToken(configuredMode.pixelFormatName); + + if (fourccInSet(fourcc, yuv420FourccSet())) { + return YuvChromaSubsampling::Yuv420; + } + + if (fourccInSet(fourcc, yuv422FourccSet())) { + return YuvChromaSubsampling::Yuv422; + } + + if (fourccInSet(fourcc, yuv444FourccSet())) { + return YuvChromaSubsampling::Yuv444; + } + + throw std::runtime_error( + "lcameraBuff: unsupported YUV pixel format for chroma geometry: " + + configuredMode.pixelFormatName); +} + +size_t computeDeinterleavedChannelByteSize( + YuvChannelKind channel, + unsigned width, unsigned height, + YuvChromaSubsampling subsampling) +{ + switch (channel) + { + case YuvChannelKind::Y: + return static_cast(width) * static_cast(height); + + case YuvChannelKind::U: + case YuvChannelKind::V: + { + const unsigned chromaWidth = chromaPlaneWidth(width, subsampling); + const unsigned chromaHeight = + chromaPlaneHeight(height, subsampling); + return static_cast(chromaWidth) + * static_cast(chromaHeight); + } + } + + throw std::logic_error( + "lcameraBuff: unhandled YuvChannelKind in computeDeinterleavedChannelByteSize"); +} + +} // namespace lcamera_buff +} // namespace stim_buff +} // namespace smo diff --git a/stimBuffApis/lcameraBuff/pixelAndColorFormatDecisions.h b/stimBuffApis/lcameraBuff/pixelAndColorFormatDecisions.h new file mode 100644 index 0000000..faa401e --- /dev/null +++ b/stimBuffApis/lcameraBuff/pixelAndColorFormatDecisions.h @@ -0,0 +1,59 @@ +#ifndef LCAMERA_BUFF_PIXEL_AND_COLOR_FORMAT_DECISIONS_H +#define LCAMERA_BUFF_PIXEL_AND_COLOR_FORMAT_DECISIONS_H + +#include +#include + +/** + * Y/U/V channel sizing for lcameraBuff. + * + * lcameraBuff exports three quale ifaces (colour-yuv-y/u/v), each backed by a + * separate deinterleaved channel buffer. libcamera, however, negotiates a single + * configured pixel format (YUYV, NV12, YUV420, etc.) that may be packed or + * multi-plane. This module bridges that gap at attach time: it classifies the + * configured fourcc into a chroma subsampling family (420 / 422 / 444) and + * computes how many bytes each deinterleaved Y, U, or V plane needs for the + * negotiated width and height. + * + * What it provides to the stimbuff: + * - YuvChromaSubsampling on YuvStimProducer (derived from configured mode) + * - channelByteSize for each YuvChannelStimulusBuffer / StagingBuffer mmap + * + * Capture layout (packed vs planar, direct vs deinterleaved backing) is handled + * separately by yuvCaptureLayout.h; this module is only plane geometry and + * byte counts. Future capture/deinterleave stages must write into buffers sized + * by these functions. + */ + +namespace smo { +namespace stim_buff { +namespace lcamera_buff { + +enum class YuvChannelKind +{ + Y, + U, + V, +}; + +enum class YuvChromaSubsampling +{ + Yuv420, + Yuv422, + Yuv444, +}; + +YuvChromaSubsampling classifyYuvChromaSubsampling( + const lcamera_dev::LcameraDevConfiguredCameraMode& configuredMode); + +size_t computeDeinterleavedChannelByteSize( + YuvChannelKind channel, + unsigned width, + unsigned height, + YuvChromaSubsampling subsampling); + +} // namespace lcamera_buff +} // namespace stim_buff +} // namespace smo + +#endif // LCAMERA_BUFF_PIXEL_AND_COLOR_FORMAT_DECISIONS_H diff --git a/stimBuffApis/lcameraBuff/tests/CMakeLists.txt b/stimBuffApis/lcameraBuff/tests/CMakeLists.txt new file mode 100644 index 0000000..1a89dc7 --- /dev/null +++ b/stimBuffApis/lcameraBuff/tests/CMakeLists.txt @@ -0,0 +1,27 @@ +add_executable(lcameraBuff_unit_tests + lcameraBuffParams_tests.cpp + pixelAndColorFormatDecisions_tests.cpp + yuvLayoutClassification_tests.cpp +) + +target_include_directories(lcameraBuff_unit_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_SOURCE_DIR}/stimBuffApis/lcameraBuff + ${CMAKE_SOURCE_DIR}/commonLibs/lcameraDev + ${CMAKE_SOURCE_DIR}/libspinscale/tests + ${CMAKE_SOURCE_DIR}/tests/fixtures + ${CMAKE_SOURCE_DIR}/include + ${CMAKE_BINARY_DIR}/include +) + +target_link_libraries(lcameraBuff_unit_tests + gtest_main + lcameraBuff + spinscale_test_support + ${Boost_LIBRARIES} + ${OPENCL_LIBRARIES} +) + +add_dependencies(lcameraBuff_unit_tests gtest_main spinscale_test_support) + +add_test(NAME lcameraBuff_unit_tests COMMAND lcameraBuff_unit_tests) diff --git a/stimBuffApis/lcameraBuff/tests/lcameraBuffParams_tests.cpp b/stimBuffApis/lcameraBuff/tests/lcameraBuffParams_tests.cpp new file mode 100644 index 0000000..f692ff9 --- /dev/null +++ b/stimBuffApis/lcameraBuff/tests/lcameraBuffParams_tests.cpp @@ -0,0 +1,131 @@ +#include +#include +#include +#include + +namespace smo { +namespace stim_buff { +namespace lcamera_buff { +namespace { + +device::DeviceAttachmentSpec makeSpecWithParams( + std::vector> params) +{ + device::DeviceAttachmentSpec spec; + spec.stimBuffApi = "lcameraBuff"; + spec.provider = "lcameraDev"; + spec.stimBuffApiParams = std::move(params); + return spec; +} + +TEST(LcameraBuffParamsTest, DimWidthHeightSynonymsParseExplicitResolution) +{ + device::DeviceAttachmentSpec spec = makeSpecWithParams({ + {"dim-w", "640"}, + {"dimension-h", "480"}, + {"colour-space", "yuv"}, + }); + + const LcameraBuffParsedParams parsed = parseLcameraBuffParams(spec); + + EXPECT_EQ(parsed.width, 640u); + EXPECT_EQ(parsed.height, 480u); + EXPECT_EQ( + parsed.colourSpace, + lcamera_dev::LcameraDevColourSpace::Yuv); + EXPECT_FALSE(parsed.fullPlanarIsOptional); +} + +TEST(LcameraBuffParamsTest, VerticalResolutionShorthand480p) +{ + device::DeviceAttachmentSpec spec = makeSpecWithParams({ + {"v-res", "480p"}, + {"colour-space", "yuv"}, + {"opt-planar", ""}, + }); + + const LcameraBuffParsedParams parsed = parseLcameraBuffParams(spec); + + EXPECT_EQ(parsed.width, 640u); + EXPECT_EQ(parsed.height, 480u); + EXPECT_TRUE(parsed.fullPlanarIsOptional); +} + +TEST(LcameraBuffParamsTest, OptPlanarSynonymEmptyValueMeansTrue) +{ + device::DeviceAttachmentSpec spec = makeSpecWithParams({ + {"v-res", "480p"}, + {"full-planar-is-optional", ""}, + }); + + EXPECT_TRUE(parseLcameraBuffParams(spec).fullPlanarIsOptional); +} + +TEST(LcameraBuffParamsTest, OptPlanarExplicitFalseDisables) +{ + device::DeviceAttachmentSpec spec = makeSpecWithParams({ + {"dim-w", "640"}, + {"dim-h", "480"}, + {"opt-planar", "false"}, + }); + + EXPECT_FALSE(parseLcameraBuffParams(spec).fullPlanarIsOptional); +} + +TEST(LcameraBuffParamsTest, OptPlanarAbsentDefaultsFalse) +{ + device::DeviceAttachmentSpec spec = makeSpecWithParams({ + {"v-res", "480p"}, + }); + + EXPECT_FALSE(parseLcameraBuffParams(spec).fullPlanarIsOptional); +} + +TEST(LcameraBuffParamsTest, MixedResolutionGroupsThrow) +{ + device::DeviceAttachmentSpec spec = makeSpecWithParams({ + {"v-res", "480p"}, + {"dim-w", "640"}, + }); + + try { + parseLcameraBuffParams(spec); + FAIL() << "Expected runtime_error"; + } + catch (const std::runtime_error& exception) + { + sscl::tests::expectExceptionMessageContains( + exception, + "cannot be combined"); + } +} + +TEST(LcameraBuffParamsTest, UnsupportedColourSpaceThrows) +{ + device::DeviceAttachmentSpec spec = makeSpecWithParams({ + {"v-res", "480"}, + {"colour-space", "rgb"}, + }); + + EXPECT_THROW(parseLcameraBuffParams(spec), std::runtime_error); +} + +TEST(LcameraBuffParamsTest, ToCameraModeRequestCopiesFields) +{ + LcameraBuffParsedParams parsed; + parsed.width = 1280; + parsed.height = 720; + parsed.fullPlanarIsOptional = true; + + const lcamera_dev::LcameraDevCameraModeRequest request = + toCameraModeRequest(parsed); + + EXPECT_EQ(request.width, 1280u); + EXPECT_EQ(request.height, 720u); + EXPECT_TRUE(request.fullPlanarIsOptional); +} + +} // namespace +} // namespace lcamera_buff +} // namespace stim_buff +} // namespace smo diff --git a/stimBuffApis/lcameraBuff/tests/pixelAndColorFormatDecisions_tests.cpp b/stimBuffApis/lcameraBuff/tests/pixelAndColorFormatDecisions_tests.cpp new file mode 100644 index 0000000..f3e771d --- /dev/null +++ b/stimBuffApis/lcameraBuff/tests/pixelAndColorFormatDecisions_tests.cpp @@ -0,0 +1,82 @@ +#include +#include + +namespace smo { +namespace stim_buff { +namespace lcamera_buff { +namespace { + +lcamera_dev::LcameraDevConfiguredCameraMode makeConfiguredMode( + const char *pixelFormatName) +{ + lcamera_dev::LcameraDevConfiguredCameraMode mode; + mode.width = 640; + mode.height = 480; + mode.pixelFormatName = pixelFormatName; + return mode; +} + +TEST(PixelAndColorFormatDecisionsTest, YuyvClassifiesAsYuv422) +{ + const lcamera_dev::LcameraDevConfiguredCameraMode mode = + makeConfiguredMode("YUYV"); + + EXPECT_EQ( + classifyYuvChromaSubsampling(mode), + YuvChromaSubsampling::Yuv422); +} + +TEST(PixelAndColorFormatDecisionsTest, Yuv420ChannelSizes) +{ + EXPECT_EQ( + computeDeinterleavedChannelByteSize( + YuvChannelKind::Y, + 640, + 480, + YuvChromaSubsampling::Yuv420), + 640u * 480u); + EXPECT_EQ( + computeDeinterleavedChannelByteSize( + YuvChannelKind::U, + 640, + 480, + YuvChromaSubsampling::Yuv420), + 320u * 240u); + EXPECT_EQ( + computeDeinterleavedChannelByteSize( + YuvChannelKind::V, + 640, + 480, + YuvChromaSubsampling::Yuv420), + 320u * 240u); +} + +TEST(PixelAndColorFormatDecisionsTest, Yuv422ChannelSizesForDellLaptopYuyv) +{ + EXPECT_EQ( + computeDeinterleavedChannelByteSize( + YuvChannelKind::Y, + 640, + 480, + YuvChromaSubsampling::Yuv422), + 640u * 480u); + EXPECT_EQ( + computeDeinterleavedChannelByteSize( + YuvChannelKind::U, + 640, + 480, + YuvChromaSubsampling::Yuv422), + 320u * 480u); + EXPECT_EQ( + computeDeinterleavedChannelByteSize( + YuvChannelKind::V, + 640, + 480, + YuvChromaSubsampling::Yuv422), + 320u * 480u); +} + +} // namespace +} // namespace lcamera_buff +} // namespace stim_buff +} // namespace smo diff --git a/stimBuffApis/lcameraBuff/tests/yuvLayoutClassification_tests.cpp b/stimBuffApis/lcameraBuff/tests/yuvLayoutClassification_tests.cpp new file mode 100644 index 0000000..4900b6a --- /dev/null +++ b/stimBuffApis/lcameraBuff/tests/yuvLayoutClassification_tests.cpp @@ -0,0 +1,44 @@ +#include +#include + +namespace smo { +namespace stim_buff { +namespace lcamera_buff { +namespace { + +TEST(YuvCaptureLayoutTest, PackedYuyvUsesMmapDeinterleavedBacking) +{ + lcamera_dev::LcameraDevConfiguredCameraMode mode; + mode.pixelFormatName = "YUYV"; + mode.isFullyPlanar = false; + mode.planeCount = 1; + + EXPECT_EQ( + classifyYuvCaptureLayoutPath(mode), + YuvCaptureLayoutPath::PackedDeinterleave); + EXPECT_EQ( + getChannelBackingPlanForLayoutPath( + YuvCaptureLayoutPath::PackedDeinterleave), + YuvChannelBackingPlan::MmapDeinterleaved); +} + +TEST(YuvCaptureLayoutTest, FullPlanarUsesDirectBacking) +{ + lcamera_dev::LcameraDevConfiguredCameraMode mode; + mode.pixelFormatName = "YU12"; + mode.isFullyPlanar = true; + mode.planeCount = 3; + + EXPECT_EQ( + classifyYuvCaptureLayoutPath(mode), + YuvCaptureLayoutPath::FullPlanarDirect); + EXPECT_EQ( + getChannelBackingPlanForLayoutPath( + YuvCaptureLayoutPath::FullPlanarDirect), + YuvChannelBackingPlan::LCameraDirect); +} + +} // namespace +} // namespace lcamera_buff +} // namespace stim_buff +} // namespace smo diff --git a/stimBuffApis/lcameraBuff/yuvCaptureLayout.cpp b/stimBuffApis/lcameraBuff/yuvCaptureLayout.cpp new file mode 100644 index 0000000..e8e4914 --- /dev/null +++ b/stimBuffApis/lcameraBuff/yuvCaptureLayout.cpp @@ -0,0 +1,49 @@ +#include +#include + +namespace smo { +namespace stim_buff { +namespace lcamera_buff { + +YuvCaptureLayoutPath classifyYuvCaptureLayoutPath( + const lcamera_dev::LcameraDevConfiguredCameraMode& configuredMode) +{ + if (configuredMode.isFullyPlanar && configuredMode.planeCount == 3u) { + return YuvCaptureLayoutPath::FullPlanarDirect; + } + + if (configuredMode.planeCount == 2u) { + return YuvCaptureLayoutPath::SemiPlanarDeinterleave; + } + + if (configuredMode.planeCount == 1u) { + return YuvCaptureLayoutPath::PackedDeinterleave; + } + + throw std::runtime_error( + "lcameraBuff: unsupported configured YUV layout: pixelFormat=" + + configuredMode.pixelFormatName + + " planeCount=" + std::to_string(configuredMode.planeCount)); +} + +YuvChannelBackingPlan getChannelBackingPlanForLayoutPath( + YuvCaptureLayoutPath layoutPath) +{ + switch (layoutPath) + { + case YuvCaptureLayoutPath::FullPlanarDirect: + return YuvChannelBackingPlan::LCameraDirect; + + case YuvCaptureLayoutPath::SemiPlanarDeinterleave: + case YuvCaptureLayoutPath::PackedDeinterleave: + return YuvChannelBackingPlan::MmapDeinterleaved; + } + + throw std::logic_error( + "lcameraBuff: unhandled YuvCaptureLayoutPath in " + "getChannelBackingPlanForLayoutPath"); +} + +} // namespace lcamera_buff +} // namespace stim_buff +} // namespace smo diff --git a/stimBuffApis/lcameraBuff/yuvCaptureLayout.h b/stimBuffApis/lcameraBuff/yuvCaptureLayout.h new file mode 100644 index 0000000..aa853a6 --- /dev/null +++ b/stimBuffApis/lcameraBuff/yuvCaptureLayout.h @@ -0,0 +1,33 @@ +#ifndef LCAMERA_BUFF_YUV_CAPTURE_LAYOUT_H +#define LCAMERA_BUFF_YUV_CAPTURE_LAYOUT_H + +#include + +namespace smo { +namespace stim_buff { +namespace lcamera_buff { + +enum class YuvCaptureLayoutPath +{ + FullPlanarDirect, + SemiPlanarDeinterleave, + PackedDeinterleave, +}; + +enum class YuvChannelBackingPlan +{ + LCameraDirect, + MmapDeinterleaved, +}; + +YuvCaptureLayoutPath classifyYuvCaptureLayoutPath( + const lcamera_dev::LcameraDevConfiguredCameraMode& configuredMode); + +YuvChannelBackingPlan getChannelBackingPlanForLayoutPath( + YuvCaptureLayoutPath layoutPath); + +} // namespace lcamera_buff +} // namespace stim_buff +} // namespace smo + +#endif // LCAMERA_BUFF_YUV_CAPTURE_LAYOUT_H diff --git a/stimBuffApis/lcameraBuff/yuvChannelStimulusBuffer.cpp b/stimBuffApis/lcameraBuff/yuvChannelStimulusBuffer.cpp new file mode 100644 index 0000000..d39fc7e --- /dev/null +++ b/stimBuffApis/lcameraBuff/yuvChannelStimulusBuffer.cpp @@ -0,0 +1,70 @@ +#include +#include + +namespace smo { +namespace stim_buff { +namespace lcamera_buff { + +namespace { + +constexpr const char *qualeIfaceColourYuvY = "colour-yuv-y"; +constexpr const char *qualeIfaceColourYuvU = "colour-yuv-u"; +constexpr const char *qualeIfaceColourYuvV = "colour-yuv-v"; + +} // namespace + +YuvChannelKind YuvChannelStimulusBuffer::yuvChannelKindFromQualeIfaceApi( + const std::string& qualeIfaceApi) +{ + if (qualeIfaceApi == qualeIfaceColourYuvY) { + return YuvChannelKind::Y; + } + + if (qualeIfaceApi == qualeIfaceColourYuvU) { + return YuvChannelKind::U; + } + + if (qualeIfaceApi == qualeIfaceColourYuvV) { + return YuvChannelKind::V; + } + + throw std::runtime_error( + "lcameraBuff: unsupported qualeIfaceApi '" + qualeIfaceApi + + "'; expected colour-yuv-y, colour-yuv-u, or colour-yuv-v"); +} + +StagingBuffer::IOEngineConstraints yuvChannelStagingInputConstraints( + size_t channelByteSize) +{ + const size_t pageSize = static_cast(sysconf(_SC_PAGE_SIZE)); + + return StagingBuffer::IOEngineConstraints( + 1, + channelByteSize, + pageSize, + pageSize); +} + +YuvChannelStimulusBuffer::YuvChannelStimulusBuffer( + StimulusProducer& parent, + const std::shared_ptr& deviceAttachmentSpec, + int histbuffMs, + YuvChannelKind _channelKind, + size_t _channelByteSize, + const SmoCallbacks& callbacks, + cl_mem_flags flags) +: StimulusBuffer( + parent, + deviceAttachmentSpec, + histbuffMs, + yuvChannelStagingInputConstraints(_channelByteSize), + yuvChannelStagingInputConstraints(_channelByteSize), + callbacks, + flags), +channelKind(_channelKind), +channelByteSize(_channelByteSize) +{} + +} // namespace lcamera_buff +} // namespace stim_buff +} // namespace smo diff --git a/stimBuffApis/lcameraBuff/yuvChannelStimulusBuffer.h b/stimBuffApis/lcameraBuff/yuvChannelStimulusBuffer.h new file mode 100644 index 0000000..2897b5d --- /dev/null +++ b/stimBuffApis/lcameraBuff/yuvChannelStimulusBuffer.h @@ -0,0 +1,51 @@ +#ifndef LCAMERA_BUFF_YUV_CHANNEL_STIMULUS_BUFFER_H +#define LCAMERA_BUFF_YUV_CHANNEL_STIMULUS_BUFFER_H + +#include +#include +#include +#include +#include +#define CL_TARGET_OPENCL_VERSION 120 +#include +#include + +namespace smo { +namespace stim_buff { +namespace lcamera_buff { + +class YuvStimProducer; + +StagingBuffer::IOEngineConstraints yuvChannelStagingInputConstraints( + size_t channelByteSize); + +/** + * Channel stimbuff placeholder for Stage 2 setup-only attach. Uses mmap-backed + * StagingBuffer sized for deinterleaved Y/U/V component storage. + */ +class YuvChannelStimulusBuffer +: public StimulusBuffer +{ +public: + static YuvChannelKind yuvChannelKindFromQualeIfaceApi( + const std::string& qualeIfaceApi); + + YuvChannelStimulusBuffer( + StimulusProducer& parent, + const std::shared_ptr& deviceAttachmentSpec, + int histbuffMs, + YuvChannelKind channelKind, + size_t channelByteSize, + const SmoCallbacks& callbacks, + cl_mem_flags flags); + +public: + YuvChannelKind channelKind; + size_t channelByteSize; +}; + +} // namespace lcamera_buff +} // namespace stim_buff +} // namespace smo + +#endif // LCAMERA_BUFF_YUV_CHANNEL_STIMULUS_BUFFER_H diff --git a/stimBuffApis/lcameraBuff/yuvStimProducer.cpp b/stimBuffApis/lcameraBuff/yuvStimProducer.cpp new file mode 100644 index 0000000..674e8aa --- /dev/null +++ b/stimBuffApis/lcameraBuff/yuvStimProducer.cpp @@ -0,0 +1,154 @@ +#include +#include +#include +#include +#include +#define CL_TARGET_OPENCL_VERSION 120 +#include +#include + +namespace smo { +namespace stim_buff { +namespace lcamera_buff { + +YuvStimProducer::YuvStimProducer( + const std::shared_ptr& deviceAttachmentSpec, + boost::asio::io_context& ioContext, + const std::shared_ptr& _deviceSession, + const lcamera_dev::CameraIdentityRecord& resolvedIdentity, + const LcameraBuffParsedParams& _parsedParams, + const lcamera_dev::LcameraDevConfiguredCameraMode& _configuredMode) +: StimulusProducer(deviceAttachmentSpec, ioContext), + deviceSession(_deviceSession), + resolvedCameraId(resolvedIdentity.id), + parsedParams(_parsedParams), + configuredMode(_configuredMode), + layoutPath(classifyYuvCaptureLayoutPath(_configuredMode)), + chromaSubsampling(classifyYuvChromaSubsampling(_configuredMode)), + channelBackingPlan(getChannelBackingPlanForLayoutPath(layoutPath)) +{} + +bool YuvStimProducer::supportsQualeIfaceApi(const std::string& qualeIfaceApi) +{ + return qualeIfaceApi == "colour-yuv-y" + || qualeIfaceApi == "colour-yuv-u" + || qualeIfaceApi == "colour-yuv-v"; +} + +bool YuvStimProducer::exportsQualeIfaceApi( + const std::string& qualeIfaceApi) const +{ + return supportsQualeIfaceApi(qualeIfaceApi); +} + +std::shared_ptr YuvStimProducer::getOrCreateAttachedStimulusBuffer( + const std::shared_ptr& attachSpec) +{ + if (!lcameraBuffSmoHooksPtr) + { + throw std::runtime_error( + "lcameraBuff: SMO hooks not initialized"); + } + + if (!supportsQualeIfaceApi(attachSpec->qualeIfaceApi)) + { + throw std::runtime_error( + "lcameraBuff: unsupported qualeIfaceApi '" + + attachSpec->qualeIfaceApi + "'"); + } + + const LcameraBuffParsedParams attachParsedParams = + parseLcameraBuffParams(*attachSpec); + + if (!lcameraBuffParamsEqual(attachParsedParams, parsedParams)) + { + throw std::runtime_error( + "lcameraBuff: conflicting stimbuff-api params across Y/U/V " + "attachments for camera '" + resolvedCameraId + "'"); + } + + auto existingBuffer = getAttachedStimulusBuffer(attachSpec); + if (existingBuffer) { + return existingBuffer; + } + + /* One YuvStimProducer == one libcamera session. A second DAP line may use + * a different deviceSelector yet resolve to this same session; it must not + * attach the same qualeIface API twice. + */ + if (hasBufferWithQualeIfaceApi(attachSpec->qualeIfaceApi)) + { + throw std::runtime_error( + "lcameraBuff: qualeIface '" + attachSpec->qualeIfaceApi + + "' is already attached for camera session '" + + resolvedCameraId + "'; each producer allows one buffer per " + "qualeIface API"); + } + + const YuvChannelKind channelKind = + YuvChannelStimulusBuffer::yuvChannelKindFromQualeIfaceApi( + attachSpec->qualeIfaceApi); + + const size_t channelByteSize = computeDeinterleavedChannelByteSize( + channelKind, + configuredMode.width, + configuredMode.height, + chromaSubsampling); + + auto channelBuffer = std::make_shared( + *this, + attachSpec, + CONFIG_STIMBUFF_FRAME_PERIOD_MS, + channelKind, + channelByteSize, + *lcameraBuffSmoHooksPtr, + CL_MEM_READ_WRITE); + + if (!addAttachedStimulusBufferIfNotExists(channelBuffer)) + { + if (hasBufferWithQualeIfaceApi(attachSpec->qualeIfaceApi)) + { + throw std::runtime_error( + "lcameraBuff: qualeIface '" + + attachSpec->qualeIfaceApi + + "' is already attached for camera session '" + + resolvedCameraId + "'"); + } + + throw std::runtime_error( + "lcameraBuff: duplicate stimbuff attachment for " + + attachSpec->stringify()); + } + + return channelBuffer; +} + +void YuvStimProducer::destroyAttachedStimulusBuffer( + const std::shared_ptr& buffer) +{ + StimulusProducer::destroyAttachedStimulusBuffer(buffer); +} + +void YuvStimProducer::start() +{ + // Stage 2: setup-only attach; capture daemon deferred to Stage 8. +} + +void YuvStimProducer::stop() +{ + // Stage 2: setup-only attach; capture daemon deferred to Stage 8. +} + +sscl::co::ViralNonPostingInvoker +YuvStimProducer::stimFrameProductionTimesliceCInd( + sscl::SyncCancelerForAsyncWork& canceler) +{ + (void)canceler; + throw std::logic_error( + "lcameraBuff: capture daemon not implemented in Stage 2"); + co_return; +} + +} // namespace lcamera_buff +} // namespace stim_buff +} // namespace smo diff --git a/stimBuffApis/lcameraBuff/yuvStimProducer.h b/stimBuffApis/lcameraBuff/yuvStimProducer.h new file mode 100644 index 0000000..1e48e07 --- /dev/null +++ b/stimBuffApis/lcameraBuff/yuvStimProducer.h @@ -0,0 +1,65 @@ +#ifndef LCAMERA_BUFF_YUV_STIM_PRODUCER_H +#define LCAMERA_BUFF_YUV_STIM_PRODUCER_H + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace smo { +namespace stim_buff { +namespace lcamera_buff { + +class YuvChannelStimulusBuffer; + +class YuvStimProducer +: public StimulusProducer +{ +public: + YuvStimProducer( + const std::shared_ptr& deviceAttachmentSpec, + boost::asio::io_context& ioContext, + const std::shared_ptr& deviceSession, + const lcamera_dev::CameraIdentityRecord& resolvedIdentity, + const LcameraBuffParsedParams& parsedParams, + const lcamera_dev::LcameraDevConfiguredCameraMode& configuredMode); + + static bool supportsQualeIfaceApi(const std::string& qualeIfaceApi); + + bool exportsQualeIfaceApi( + const std::string& qualeIfaceApi) const override; + + std::shared_ptr getOrCreateAttachedStimulusBuffer( + const std::shared_ptr& + deviceAttachmentSpec) override; + + void destroyAttachedStimulusBuffer( + const std::shared_ptr& buffer) override; + + void start() override; + void stop() override; + +protected: + sscl::co::ViralNonPostingInvoker + stimFrameProductionTimesliceCInd( + sscl::SyncCancelerForAsyncWork& canceler) override; + +public: + std::shared_ptr deviceSession; + std::string resolvedCameraId; + LcameraBuffParsedParams parsedParams; + lcamera_dev::LcameraDevConfiguredCameraMode configuredMode; + YuvCaptureLayoutPath layoutPath; + YuvChromaSubsampling chromaSubsampling; + YuvChannelBackingPlan channelBackingPlan; +}; + +} // namespace lcamera_buff +} // namespace stim_buff +} // namespace smo + +#endif // LCAMERA_BUFF_YUV_STIM_PRODUCER_H