diff --git a/.github/workflows/linux-build.yml b/.github/workflows/linux-build.yml new file mode 100644 index 00000000..1fea654e --- /dev/null +++ b/.github/workflows/linux-build.yml @@ -0,0 +1,267 @@ +name: Linux Build + +on: + push: + branches: [ main, develop, support_linux ] + pull_request: + branches: [ main, develop ] + +jobs: + build-ubuntu: + name: "Build (${{ matrix.build_type }}-gcc)" + runs-on: ubuntu-latest + + strategy: + matrix: + build_type: [Debug, Release] + + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install base dependencies + run: | + sudo apt-get update + sudo apt-get install -y cmake build-essential gcc + + - name: Install GLFW (for Release builds only) + if: matrix.build_type == 'Release' + run: | + sudo apt-get install -y libglfw3-dev + echo "GLFW_INSTALLED=true" >> $GITHUB_ENV + + - name: Setup compiler + run: | + echo "CC=gcc" >> $GITHUB_ENV + echo "CXX=g++" >> $GITHUB_ENV + + - name: Configure CMake + run: | + echo "Configuring build ${{ matrix.build_type }} with gcc" + cmake -B build/${{ matrix.build_type }} \ + -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} \ + -DCCAP_BUILD_EXAMPLES=ON \ + -DCCAP_BUILD_TESTS=ON + + - name: Build + run: cmake --build build/${{ matrix.build_type }} --config ${{ matrix.build_type }} --parallel $(nproc) + + - name: Verify GLFW examples build status + working-directory: build/${{ matrix.build_type }} + run: | + echo "Checking built examples:" + ls -la | grep -E "(glfw|example)" || echo "No GLFW examples found" + + if [ "${{ matrix.build_type }}" = "Release" ]; then + echo "Release build - checking if GLFW examples were built with system GLFW:" + if [ -f "4-example_with_glfw" ]; then + echo "✓ GLFW example successfully built with system GLFW" + else + echo "✗ GLFW example not found - this might indicate a build issue" + exit 1 + fi + else + echo "Debug build - checking if bundled GLFW was used:" + if [ -f "4-example_with_glfw" ]; then + echo "✓ GLFW example successfully built with bundled GLFW" + else + echo "ℹ GLFW example not built - using bundled GLFW or GLFW disabled" + fi + fi + + - name: Run Unit Tests + if: matrix.build_type == 'Release' + run: | + cd scripts + ./run_tests.sh --functional --exit-when-failed + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: ccap-linux-gcc-${{ matrix.build_type }} + path: | + build/${{ matrix.build_type }}/libccap.a + build/${{ matrix.build_type }}/0-print_camera + build/${{ matrix.build_type }}/1-minimal_example + build/${{ matrix.build_type }}/2-capture_grab + build/${{ matrix.build_type }}/3-capture_callback + build/${{ matrix.build_type }}/4-example_with_glfw + build/${{ matrix.build_type }}/*_results.xml + if-no-files-found: warn + + build-ubuntu-clang: + name: "Build (${{ matrix.build_type }}-clang)" + runs-on: ubuntu-latest + # Only run clang builds on push to main branch + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + + strategy: + matrix: + build_type: [Debug, Release] + + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y cmake build-essential clang libglfw3-dev + + - name: Setup compiler + run: | + echo "CC=clang" >> $GITHUB_ENV + echo "CXX=clang++" >> $GITHUB_ENV + + - name: Configure CMake + run: | + echo "Configuring build ${{ matrix.build_type }} with clang (with system GLFW)" + cmake -B build/${{ matrix.build_type }} \ + -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} \ + -DCCAP_BUILD_EXAMPLES=ON \ + -DCCAP_BUILD_TESTS=ON + + - name: Build + run: cmake --build build/${{ matrix.build_type }} --config ${{ matrix.build_type }} --parallel $(nproc) + + - name: Verify build outputs + working-directory: build/${{ matrix.build_type }} + run: | + echo "Checking built examples:" + ls -la | grep -E "(glfw|example)" || echo "No examples found" + echo "✓ Build completed successfully" + + - name: Run Unit Tests + if: matrix.build_type == 'Release' + run: | + cd scripts + ./run_tests.sh --functional --exit-when-failed + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: ccap-linux-clang-${{ matrix.build_type }} + path: | + build/${{ matrix.build_type }}/libccap.a + build/${{ matrix.build_type }}/0-print_camera + build/${{ matrix.build_type }}/1-minimal_example + build/${{ matrix.build_type }}/2-capture_grab + build/${{ matrix.build_type }}/3-capture_callback + build/${{ matrix.build_type }}/4-example_with_glfw + build/${{ matrix.build_type }}/*_results.xml + if-no-files-found: warn + + build-ubuntu-arm64: + name: "Build ARM64 (${{ matrix.build_type }}-gcc)" + runs-on: ubuntu-latest + + strategy: + matrix: + build_type: [Debug, Release] + + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install cross-compilation toolchain + run: | + sudo apt-get update + sudo apt-get install -y cmake build-essential gcc-aarch64-linux-gnu g++-aarch64-linux-gnu + echo "Cross-compilation toolchain installed" + + - name: Configure CMake for ARM64 + run: | + echo "Configuring build ${{ matrix.build_type }} for ARM64 (cross-compilation)" + cmake -B build/arm64/${{ matrix.build_type }} \ + -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} \ + -DCMAKE_C_COMPILER=aarch64-linux-gnu-gcc \ + -DCMAKE_CXX_COMPILER=aarch64-linux-gnu-g++ \ + -DCMAKE_SYSTEM_NAME=Linux \ + -DCMAKE_SYSTEM_PROCESSOR=aarch64 \ + -DCMAKE_CROSSCOMPILING=ON \ + -DCCAP_BUILD_EXAMPLES=ON \ + -DCCAP_BUILD_TESTS=OFF + + - name: Build ARM64 + run: cmake --build build/arm64/${{ matrix.build_type }} --config ${{ matrix.build_type }} --parallel $(nproc) + + - name: Verify build outputs + working-directory: build/arm64/${{ matrix.build_type }} + run: | + echo "Checking built ARM64 binaries:" + ls -la | grep -E "(ccap|example)" || echo "No examples found" + echo "Verifying ARM64 architecture:" + file libccap.a | grep -q "aarch64" && echo "✓ Library is ARM64" || echo "⚠ Library architecture verification failed" + if [ -f "0-print_camera" ]; then + file 0-print_camera | grep -q "aarch64" && echo "✓ Binary is ARM64" || echo "⚠ Binary architecture verification failed" + fi + echo "✓ ARM64 cross-compilation completed successfully" + + - name: Upload ARM64 artifacts + uses: actions/upload-artifact@v4 + with: + name: ccap-linux-arm64-gcc-${{ matrix.build_type }} + path: | + build/arm64/${{ matrix.build_type }}/libccap.a + build/arm64/${{ matrix.build_type }}/0-print_camera + build/arm64/${{ matrix.build_type }}/1-minimal_example + build/arm64/${{ matrix.build_type }}/2-capture_grab + build/arm64/${{ matrix.build_type }}/3-capture_callback + build/arm64/${{ matrix.build_type }}/4-example_with_glfw + build/arm64/${{ matrix.build_type }}/*_results.xml + if-no-files-found: warn + + build-fedora: + name: "Build Fedora (${{ matrix.build_type }})" + runs-on: ubuntu-latest + container: fedora:latest + + strategy: + matrix: + build_type: [Debug, Release] + + steps: + - name: Install Git first + run: | + dnf update -y + dnf install -y git + + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install dependencies + run: | + dnf install -y cmake gcc-c++ make + + - name: Configure CMake + run: | + cmake -B build/${{ matrix.build_type }} \ + -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} \ + -DCCAP_BUILD_EXAMPLES=ON \ + -DCCAP_BUILD_TESTS=ON + + - name: Build + run: cmake --build build/${{ matrix.build_type }} --config ${{ matrix.build_type }} --parallel $(nproc) + + - name: Run Unit Tests + if: matrix.build_type == 'Release' + run: | + cd scripts + ./run_tests.sh --functional --exit-when-failed + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: ccap-fedora-${{ matrix.build_type }} + path: | + build/${{ matrix.build_type }}/libccap.a + build/${{ matrix.build_type }}/0-print_camera + build/${{ matrix.build_type }}/1-minimal_example + build/${{ matrix.build_type }}/2-capture_grab + build/${{ matrix.build_type }}/3-capture_callback + build/${{ matrix.build_type }}/*_results.xml + if-no-files-found: warn diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4635fcac..aca5b9b7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,12 +35,30 @@ jobs: name: Windows artifact_name: ccap-msvc-x86_64 + - os: ubuntu-latest + name: Linux + artifact_name: ccap-linux-x86_64 + build_type: Release + + - os: ubuntu-latest + name: Linux ARM64 + artifact_name: ccap-linux-arm64 + build_type: Release + arch: arm64 + runs-on: ${{ matrix.os }} steps: - name: Checkout code uses: actions/checkout@v4 + - name: Setup cross-compilation for ARM64 + if: matrix.arch == 'arm64' + shell: bash + run: | + sudo apt-get update + sudo apt-get install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu + - name: Setup Visual Studio (Windows) if: matrix.os == 'windows-latest' uses: microsoft/setup-msbuild@v1.1 @@ -51,8 +69,22 @@ jobs: if [ "${{ matrix.os }}" = "windows-latest" ]; then # Windows: Use multi-config generator (Visual Studio) cmake -B build -G "Visual Studio 17 2022" -A x64 -DCCAP_BUILD_EXAMPLES=ON -DCCAP_BUILD_TESTS=OFF + elif [ "${{ matrix.os }}" = "ubuntu-latest" ]; then + # Linux: Use single-config generator + if [ "${{ matrix.arch }}" = "arm64" ]; then + # ARM64 cross-compilation + cmake -B build -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} \ + -DCMAKE_C_COMPILER=aarch64-linux-gnu-gcc \ + -DCMAKE_CXX_COMPILER=aarch64-linux-gnu-g++ \ + -DCMAKE_SYSTEM_NAME=Linux \ + -DCMAKE_SYSTEM_PROCESSOR=aarch64 \ + -DCCAP_BUILD_EXAMPLES=ON -DCCAP_BUILD_TESTS=OFF + else + # Regular Linux x86_64 + cmake -B build -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -DCCAP_BUILD_EXAMPLES=ON -DCCAP_BUILD_TESTS=OFF + fi else - # Other platforms: Use single-config generator + # macOS: Use single-config generator with universal binary cmake -B build -DCMAKE_OSX_ARCHITECTURES='arm64;x86_64' -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -DCCAP_BUILD_EXAMPLES=ON -DCCAP_BUILD_TESTS=OFF fi @@ -122,6 +154,40 @@ jobs: find build -name "*-capture_callback" -exec cp {} package/examples/ \; || echo "Examples not found" find build -name "*-example_with_glfw" -exec cp {} package/examples/ \; || echo "Examples not found" + - name: Copy libraries (Linux) + if: matrix.os == 'ubuntu-latest' && matrix.arch != 'arm64' + shell: bash + run: | + # Copy static library + cp build/libccap.a package/lib/ || echo "Static library not found" + + # Copy dynamic library (if exists) + cp build/libccap.so package/lib/ || echo "Dynamic library not found" + + # Copy example programs + find build -name "0-print_camera" -exec cp {} package/examples/ \; || echo "Examples not found" + find build -name "1-minimal_example" -exec cp {} package/examples/ \; || echo "Examples not found" + find build -name "2-capture_grab" -exec cp {} package/examples/ \; || echo "Examples not found" + find build -name "3-capture_callback" -exec cp {} package/examples/ \; || echo "Examples not found" + find build -name "4-example_with_glfw" -exec cp {} package/examples/ \; || echo "Examples not found" + + - name: Copy libraries (Linux ARM64) + if: matrix.os == 'ubuntu-latest' && matrix.arch == 'arm64' + shell: bash + run: | + # Copy static library + cp build/libccap.a package/lib/ || echo "Static library not found" + + # Copy dynamic library (if exists) + cp build/libccap.so package/lib/ || echo "Dynamic library not found" + + # Copy example programs (ARM64 cross-compiled) + find build -name "0-print_camera" -exec cp {} package/examples/ \; || echo "Examples not found" + find build -name "1-minimal_example" -exec cp {} package/examples/ \; || echo "Examples not found" + find build -name "2-capture_grab" -exec cp {} package/examples/ \; || echo "Examples not found" + find build -name "3-capture_callback" -exec cp {} package/examples/ \; || echo "Examples not found" + find build -name "4-example_with_glfw" -exec cp {} package/examples/ \; || echo "Examples not found" + - name: Copy headers and other files shell: bash run: | @@ -159,7 +225,7 @@ jobs: # Windows: Create ZIP file 7z a ../${{ matrix.artifact_name }}.zip ./* else - # macOS: Create tar.gz file + # macOS and Linux: Create tar.gz file tar -czf ../${{ matrix.artifact_name }}.tar.gz . fi cd .. @@ -235,16 +301,22 @@ jobs: **Supported Platforms:** - **macOS** (Universal Binary - supports Intel & Apple Silicon): `ccap-macos-universal.tar.gz` - **Windows** (MSVC x64 - includes Debug and Release versions): `ccap-msvc-x86_64.zip` + - **Linux x86_64** (compatible with most distributions): `ccap-linux-x86_64.tar.gz` + - **Linux ARM64** (compatible with Raspberry Pi, ARM servers, and other ARM64 boards): `ccap-linux-arm64.tar.gz` ### 📁 Package Contents - **Library Files**: Static library files for linking - Windows: `lib/ccap.lib` (Release version) and `lib/ccapd.lib` (Debug version) - macOS: `lib/libccap.a` (Universal Binary) + - Linux x86_64: `lib/libccap.a` (x86_64) + - Linux ARM64: `lib/libccap.a` (ARM64) - **Header Files**: Complete C++ API header files - **Example Programs**: 5 complete usage examples - Windows: `examples/Release/` and `examples/Debug/` directories contain corresponding versions - macOS: `examples/` directory contains executable files + - Linux x86_64: `examples/` directory contains executable files + - Linux ARM64: `examples/` directory contains executable files - **Example Source Code**: Ready-to-compile example code - **Documentation**: README and build instructions - **CMake Configuration**: Easy integration with other CMake projects @@ -258,12 +330,15 @@ jobs: - **Windows Debug**: Link `lib/ccapd.lib` - **Windows Release**: Link `lib/ccap.lib` - **macOS**: Link `lib/libccap.a` + - **Linux x86_64**: Link `lib/libccap.a` + - **Linux ARM64**: Link `lib/libccap.a` 5. Refer to example code in the `examples` directory ### 📋 System Requirements - **macOS**: 10.13 or higher - **Windows**: Windows 10 or higher (requires Visual C++ Redistributable) + - **Linux**: Modern Linux distribution with kernel 2.6+ (supports V4L2) --- @@ -300,5 +375,7 @@ jobs: echo "### 📦 Included Files:" >> $GITHUB_STEP_SUMMARY echo "- ccap-macos-universal.tar.gz" >> $GITHUB_STEP_SUMMARY echo "- ccap-msvc-x86_64.zip (includes Debug and Release versions)" >> $GITHUB_STEP_SUMMARY + echo "- ccap-linux-x86_64.tar.gz" >> $GITHUB_STEP_SUMMARY + echo "- ccap-linux-arm64.tar.gz" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "Release page: ${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY diff --git a/CMakeLists.txt b/CMakeLists.txt index e6ee6528..70f1668a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,13 +17,6 @@ endif() project(ccap VERSION 1.1.0 LANGUAGES C CXX) -# Currently only Windows and macOS are supported; other platforms are not supported yet -if(NOT WIN32 AND NOT APPLE) - message(WARNING "ccap: Unsupported platform, only Windows and macOS are supported by now.") - set(CCAP_BUILD_EXAMPLES OFF CACHE BOOL "Disable examples for unsupported platforms" FORCE) - set(CCAP_BUILD_TESTS OFF CACHE BOOL "Disable tests for unsupported platforms" FORCE) -endif() - # Installation options option(CCAP_INSTALL "Generate installation target" ${CCAP_IS_ROOT_PROJECT}) @@ -117,8 +110,15 @@ if(APPLE) # Set pkg-config private libs for macOS set(PKG_CONFIG_LIBS_PRIVATE "Libs.private: -framework Foundation -framework AVFoundation -framework CoreVideo -framework CoreMedia -framework Accelerate") +elseif(UNIX AND NOT APPLE AND NOT WIN32) + # Linux – link pthread for std::thread support + find_package(Threads REQUIRED) + target_link_libraries(ccap PUBLIC Threads::Threads) + + # Propagate to pkg-config for consumers + set(PKG_CONFIG_LIBS_PRIVATE "Libs.private: -lpthread") else() - # Windows or other platforms + # Windows (including MinGW) or other platforms set(PKG_CONFIG_LIBS_PRIVATE "") endif() diff --git a/README.md b/README.md index 4710cdff..55cce356 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,10 @@ [![Windows Build](https://github.com/wysaid/CameraCapture/actions/workflows/windows-build.yml/badge.svg)](https://github.com/wysaid/CameraCapture/actions/workflows/windows-build.yml) [![macOS Build](https://github.com/wysaid/CameraCapture/actions/workflows/macos-build.yml/badge.svg)](https://github.com/wysaid/CameraCapture/actions/workflows/macos-build.yml) +[![Linux Build](https://github.com/wysaid/CameraCapture/actions/workflows/linux-build.yml/badge.svg)](https://github.com/wysaid/CameraCapture/actions/workflows/linux-build.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![C++17](https://img.shields.io/badge/C++-17-blue.svg)](https://isocpp.org/) -[![Platform](https://img.shields.io/badge/Platform-Windows%20%7C%20macOS%20%7C%20iOS-brightgreen)](https://github.com/wysaid/CameraCapture) +[![Platform](https://img.shields.io/badge/Platform-Windows%20%7C%20macOS%20%7C%20iOS%20%7C%20Linux-brightgreen)](https://github.com/wysaid/CameraCapture) [English](./README.md) | [中文](./README.zh-CN.md) @@ -25,7 +26,7 @@ A high-performance, lightweight cross-platform camera capture library with hardw - **High Performance**: Hardware-accelerated pixel format conversion with up to 10x speedup (AVX2, Apple Accelerate) - **Lightweight**: Zero external dependencies - uses only system frameworks -- **Cross Platform**: Windows (DirectShow), macOS/iOS (AVFoundation) +- **Cross Platform**: Windows (DirectShow), macOS/iOS (AVFoundation), Linux (V4L2) - **Multiple Formats**: RGB, BGR, YUV (NV12/I420) with automatic conversion - **Dual Language APIs**: ✨ **New Complete Pure C Interface** - Both modern C++ API and traditional C99 interface for various project integration and language bindings - **Production Ready**: Comprehensive test suite with 95%+ accuracy validation @@ -168,9 +169,19 @@ int main() { | **Windows** | MSVC 2019+ | DirectShow | | **macOS** | Xcode 11+ | macOS 10.13+ | | **iOS** | Xcode 11+ | iOS 13.0+ | +| **Linux** | GCC 7+ / Clang 6+ | V4L2 (Linux 2.6+) | **Build Requirements**: CMake 3.14+, C++17 +### Supported Linux Distributions + +- [x] **Ubuntu/Debian** - All versions with Linux 2.6+ kernel +- [x] **CentOS/RHEL/Fedora** - All versions with Linux 2.6+ kernel +- [x] **SUSE/openSUSE** - All versions with Linux 2.6+ kernel +- [x] **Arch Linux** - All versions +- [x] **Alpine Linux** - All versions +- [x] **Embedded Linux** - Any distribution with V4L2 support + ## Examples | Example | Description | Language | Platform | diff --git a/README.zh-CN.md b/README.zh-CN.md index e3bbc101..73d5069b 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -2,9 +2,10 @@ [![Windows Build](https://github.com/wysaid/CameraCapture/actions/workflows/windows-build.yml/badge.svg)](https://github.com/wysaid/CameraCapture/actions/workflows/windows-build.yml) [![macOS Build](https://github.com/wysaid/CameraCapture/actions/workflows/macos-build.yml/badge.svg)](https://github.com/wysaid/CameraCapture/actions/workflows/macos-build.yml) +[![Linux Build](https://github.com/wysaid/CameraCapture/actions/workflows/linux-build.yml/badge.svg)](https://github.com/wysaid/CameraCapture/actions/workflows/linux-build.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![C++17](https://img.shields.io/badge/C++-17-blue.svg)](https://isocpp.org/) -[![Platform](https://img.shields.io/badge/Platform-Windows%20%7C%20macOS%20%7C%20iOS-brightgreen)](https://github.com/wysaid/CameraCapture) +[![Platform](https://img.shields.io/badge/Platform-Windows%20%7C%20macOS%20%7C%20iOS%20%7C%20Linux-brightgreen)](https://github.com/wysaid/CameraCapture) [English](./README.md) | [中文](./README.zh-CN.md) @@ -22,7 +23,7 @@ - **高性能**:硬件加速的像素格式转换,提升高达 10 倍性能(AVX2、Apple Accelerate) - **轻量级**:零外部依赖,仅使用系统框架 -- **跨平台**:Windows(DirectShow)、macOS/iOS(AVFoundation) +- **跨平台**:Windows(DirectShow)、macOS/iOS(AVFoundation)、Linux(V4L2) - **多种格式**:RGB、BGR、YUV(NV12/I420)及自动转换 - **双语言接口**:✨ **新增完整纯 C 接口**,同时提供现代化 C++ API 和传统 C99 接口,支持各种项目集成和语言绑定 - **生产就绪**:完整测试套件,95%+ 精度验证 @@ -165,9 +166,19 @@ int main() { | **Windows** | MSVC 2019+ | DirectShow | | **macOS** | Xcode 11+ | macOS 10.13+ | | **iOS** | Xcode 11+ | iOS 13.0+ | +| **Linux** | GCC 7+ / Clang 6+ | V4L2 (Linux 2.6+) | **构建要求**:CMake 3.14+,C++17 +### 支持的 Linux 发行版 + +- [x] **Ubuntu/Debian** - 所有带有 Linux 2.6+ 内核的版本 +- [x] **CentOS/RHEL/Fedora** - 所有带有 Linux 2.6+ 内核的版本 +- [x] **SUSE/openSUSE** - 所有版本 +- [x] **Arch Linux** - 所有版本 +- [x] **Alpine Linux** - 所有版本 +- [x] **嵌入式 Linux** - 任何支持 V4L2 的发行版 + ## 示例代码 | 示例 | 描述 | 语言 | 平台 | diff --git a/examples/desktop.cmake b/examples/desktop.cmake index 4469a614..b9f98819 100644 --- a/examples/desktop.cmake +++ b/examples/desktop.cmake @@ -1,15 +1,40 @@ # examples.cmake cmake_minimum_required(VERSION 3.14) -set(GLFW_BUILD_EXAMPLES OFF CACHE BOOL "Build GLFW example programs" FORCE) -set(GLFW_BUILD_TESTS OFF CACHE BOOL "Build GLFW test programs" FORCE) -set(GLFW_BUILD_DOCS OFF CACHE BOOL "Build GLFW documentation" FORCE) -set(GLFW_INSTALL OFF CACHE BOOL "Generate installation target" FORCE) - set(CMAKE_INCLUDE_CURRENT_DIR ON) set(DESKTOP_EXAMPLES_DIR ${CMAKE_CURRENT_LIST_DIR}/desktop) -add_subdirectory(${DESKTOP_EXAMPLES_DIR}/glfw) +# GLFW detection and setup +set(GLFW_AVAILABLE OFF) + +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + if(NOT GLFW_AVAILABLE) + find_package(glfw3 QUIET) + + if(glfw3_FOUND) + set(GLFW_AVAILABLE ON) + set(GLFW_TARGET glfw) + endif() + endif() + + if(NOT GLFW_AVAILABLE) + message(STATUS "ccap: GLFW not found on system. GLFW-dependent examples will be disabled.") + message(STATUS "ccap: Install GLFW with: sudo apt-get install libglfw3-dev (Ubuntu/Debian) or equivalent") + endif() +else() + # On non-Linux platforms, use bundled GLFW + set(GLFW_BUILD_EXAMPLES OFF CACHE BOOL "Build GLFW example programs" FORCE) + set(GLFW_BUILD_TESTS OFF CACHE BOOL "Build GLFW test programs" FORCE) + set(GLFW_BUILD_DOCS OFF CACHE BOOL "Build GLFW documentation" FORCE) + set(GLFW_INSTALL OFF CACHE BOOL "Generate installation target" FORCE) + + add_subdirectory(${DESKTOP_EXAMPLES_DIR}/glfw) + set(GLFW_AVAILABLE ON) + set(GLFW_TARGET glfw) + message(STATUS "ccap: Using bundled GLFW") +endif() + +message(STATUS "ccap: GLFW available: ${GLFW_AVAILABLE}") file(GLOB EXAMPLE_SOURCE ${DESKTOP_EXAMPLES_DIR}/*.cpp ${DESKTOP_EXAMPLES_DIR}/*.c) @@ -17,6 +42,11 @@ foreach(EXAMPLE ${EXAMPLE_SOURCE}) get_filename_component(EXAMPLE_NAME ${EXAMPLE} NAME) string(REGEX REPLACE "\\.(cpp|c)$" "" EXAMPLE_NAME ${EXAMPLE_NAME}) + if(${EXAMPLE_NAME} MATCHES "glfw" AND NOT GLFW_AVAILABLE) + # Skip GLFW examples if GLFW is not available + continue() + endif() + add_executable(${EXAMPLE_NAME} ${EXAMPLE}) target_link_libraries(${EXAMPLE_NAME} PRIVATE ccap) @@ -26,24 +56,24 @@ foreach(EXAMPLE ${EXAMPLE_SOURCE}) ) endif() - # If NAME contains glfw, link glfw3 and OpenGL, etc. + # If NAME contains glfw, link glfw3 and optionally OpenGL if(${EXAMPLE_NAME} MATCHES "glfw") - target_link_libraries(${EXAMPLE_NAME} PRIVATE - glfw - ) + target_link_libraries(${EXAMPLE_NAME} PRIVATE ${GLFW_TARGET}) + + # On Linux, OpenGL functions are loaded dynamically via GLAD + # GLAD uses function pointers provided by GLFW (glfwGetProcAddress) + # No additional libraries needed for dynamic loading + if(NOT CMAKE_SYSTEM_NAME STREQUAL "Linux") + if(APPLE) + target_link_libraries(${EXAMPLE_NAME} PRIVATE "-framework OpenGL") + endif() - if(APPLE) - target_link_libraries(${EXAMPLE_NAME} PRIVATE - "-framework OpenGL" - ) + target_include_directories(${EXAMPLE_NAME} PUBLIC ${DESKTOP_EXAMPLES_DIR}/glfw/include) endif() - target_include_directories(${EXAMPLE_NAME} PUBLIC - ${DESKTOP_EXAMPLES_DIR} - ${DESKTOP_EXAMPLES_DIR}/glfw/include - ) + target_include_directories(${EXAMPLE_NAME} PUBLIC ${DESKTOP_EXAMPLES_DIR}) - message(STATUS "ccap: Add example: ${EXAMPLE_NAME} with glfw3") + message(STATUS "ccap: Add example: ${EXAMPLE_NAME} with GLFW") else() message(STATUS "ccap: Add example: ${EXAMPLE_NAME}") endif() diff --git a/examples/desktop/0-print_camera_c.c b/examples/desktop/0-print_camera_c.c index 49f8764a..33ce052a 100644 --- a/examples/desktop/0-print_camera_c.c +++ b/examples/desktop/0-print_camera_c.c @@ -6,6 +6,7 @@ */ #include "ccap_c.h" + #include #include @@ -13,14 +14,14 @@ bool frame_callback(const CcapVideoFrame* frame, void* userData) { static int frameCount = 0; frameCount++; - + CcapVideoFrameInfo frameInfo; if (ccap_video_frame_get_info(frame, &frameInfo)) { - printf("Frame %d: %dx%d, format=%d, timestamp=%llu\n", - frameCount, frameInfo.width, frameInfo.height, + printf("Frame %d: %dx%d, format=%d, timestamp=%llu\n", + frameCount, frameInfo.width, frameInfo.height, frameInfo.pixelFormat, frameInfo.timestamp); } - + // Return false to keep the frame available for grab() // Return true to consume the frame (won't be available for grab()) return false; @@ -29,14 +30,14 @@ bool frame_callback(const CcapVideoFrame* frame, void* userData) { int main() { printf("ccap C Interface Example\n"); printf("Version: %s\n\n", ccap_get_version()); - + // Create provider CcapProvider* provider = ccap_provider_create(); if (!provider) { printf("Failed to create provider\n"); return -1; } - + // Find available devices char** deviceNames; size_t deviceCount; @@ -46,55 +47,55 @@ int main() { printf(" %zu: %s\n", i, deviceNames[i]); } printf("\n"); - + // Free device names ccap_provider_free_device_names(deviceNames, deviceCount); } else { printf("Failed to enumerate devices\n"); } - + // Open default camera if (!ccap_provider_open(provider, NULL, false)) { printf("Failed to open camera\n"); ccap_provider_destroy(provider); return -1; } - + printf("Camera opened successfully\n"); - + // Get device info CcapDeviceInfo deviceInfo; if (ccap_provider_get_device_info(provider, &deviceInfo)) { printf("Device: %s\n", deviceInfo.deviceName ? deviceInfo.deviceName : "Unknown"); printf("Supported pixel formats: %zu\n", deviceInfo.pixelFormatCount); printf("Supported resolutions: %zu\n", deviceInfo.resolutionCount); - + if (deviceInfo.resolutionCount > 0) { - printf("First resolution: %dx%d\n", + printf("First resolution: %dx%d\n", deviceInfo.supportedResolutions[0].width, deviceInfo.supportedResolutions[0].height); } - + ccap_provider_free_device_info(&deviceInfo); } - + // Set camera properties ccap_provider_set_property(provider, CCAP_PROPERTY_WIDTH, 640); ccap_provider_set_property(provider, CCAP_PROPERTY_HEIGHT, 480); ccap_provider_set_property(provider, CCAP_PROPERTY_FRAME_RATE, 30.0); - + // Set frame callback ccap_provider_set_new_frame_callback(provider, frame_callback, NULL); - + // Start capturing if (!ccap_provider_start(provider)) { printf("Failed to start camera\n"); ccap_provider_destroy(provider); return -1; } - + printf("Camera started, capturing frames...\n"); - + // Capture frames for 5 seconds using both callback and grab methods for (int i = 0; i < 10; i++) { // Try to grab a frame (synchronous method) @@ -102,25 +103,25 @@ int main() { if (frame) { CcapVideoFrameInfo frameInfo; if (ccap_video_frame_get_info(frame, &frameInfo)) { - printf("Grabbed frame: %dx%d, size=%u bytes\n", + printf("Grabbed frame: %dx%d, size=%u bytes\n", frameInfo.width, frameInfo.height, frameInfo.sizeInBytes); } - + // Release the frame ccap_video_frame_release(frame); } else { printf("Failed to grab frame or timeout\n"); } } - + // Stop capturing ccap_provider_stop(provider); printf("Camera stopped\n"); - + // Close and cleanup ccap_provider_close(provider); ccap_provider_destroy(provider); - + printf("Example completed successfully\n"); return 0; } diff --git a/include/ccap_convert.h b/include/ccap_convert.h index e0eb7b4c..c3c3fc43 100644 --- a/include/ccap_convert.h +++ b/include/ccap_convert.h @@ -251,6 +251,40 @@ void i420ToRgba32(const uint8_t* srcY, int srcYStride, uint8_t* dst, int dstStride, int width, int height, ConvertFlag flag = ConvertFlag::Default); +// YUYV (YUV 4:2:2 packed) conversion functions +void yuyvToBgr24(const uint8_t* src, int srcStride, + uint8_t* dst, int dstStride, + int width, int height, ConvertFlag flag = ConvertFlag::Default); + +void yuyvToRgb24(const uint8_t* src, int srcStride, + uint8_t* dst, int dstStride, + int width, int height, ConvertFlag flag = ConvertFlag::Default); + +void yuyvToBgra32(const uint8_t* src, int srcStride, + uint8_t* dst, int dstStride, + int width, int height, ConvertFlag flag = ConvertFlag::Default); + +void yuyvToRgba32(const uint8_t* src, int srcStride, + uint8_t* dst, int dstStride, + int width, int height, ConvertFlag flag = ConvertFlag::Default); + +// UYVY (YUV 4:2:2 packed) conversion functions +void uyvyToBgr24(const uint8_t* src, int srcStride, + uint8_t* dst, int dstStride, + int width, int height, ConvertFlag flag = ConvertFlag::Default); + +void uyvyToRgb24(const uint8_t* src, int srcStride, + uint8_t* dst, int dstStride, + int width, int height, ConvertFlag flag = ConvertFlag::Default); + +void uyvyToBgra32(const uint8_t* src, int srcStride, + uint8_t* dst, int dstStride, + int width, int height, ConvertFlag flag = ConvertFlag::Default); + +void uyvyToRgba32(const uint8_t* src, int srcStride, + uint8_t* dst, int dstStride, + int width, int height, ConvertFlag flag = ConvertFlag::Default); + class Allocator; /// @brief Used to store some intermediate results, avoiding repeated memory allocation. /// If no shared memory allocator is set externally, use the default allocator. diff --git a/include/ccap_core.h b/include/ccap_core.h index d91342cc..beb9eb9e 100644 --- a/include/ccap_core.h +++ b/include/ccap_core.h @@ -20,11 +20,9 @@ #include // ccap is short for (C)amera(CAP)ture -namespace ccap -{ +namespace ccap { /// A default allocator -class DefaultAllocator : public Allocator -{ +class DefaultAllocator : public Allocator { public: ~DefaultAllocator() override; void resize(size_t size) override; @@ -36,8 +34,7 @@ class DefaultAllocator : public Allocator size_t m_size = 0; }; -enum -{ +enum { /// @brief The default maximum number of frames that can be cached. DEFAULT_MAX_CACHE_FRAME_SIZE = 15, @@ -53,8 +50,7 @@ class ProviderImp; * @note This class is not thread-safe. It is recommended to use it in a single thread. * If you need to use it in multiple threads, consider using a mutex or other synchronization methods. */ -class Provider final -{ +class Provider final { public: /// @brief Default constructor. The camera device is not opened yet. /// You can use the `open` method to open a camera device later. @@ -149,8 +145,7 @@ class Provider final bool set(PropertyName prop, double value); template - bool set(PropertyName prop, T value) - { + bool set(PropertyName prop, T value) { return set(prop, static_cast(value)); } diff --git a/include/ccap_def.h b/include/ccap_def.h index 562fc7c6..0b696faf 100644 --- a/include/ccap_def.h +++ b/include/ccap_def.h @@ -37,10 +37,8 @@ #include // ccap is short for (C)amera(CAP)ture -namespace ccap -{ -enum PixelFormatConstants : uint32_t -{ +namespace ccap { +enum PixelFormatConstants : uint32_t { /// `kPixelFormatRGBBit` indicates that the pixel format is RGB or RGBA. kPixelFormatRGBBit = 1 << 3, /// `kPixelFormatRGBBit` indicates that the pixel format is BGR or BGRA. @@ -51,7 +49,7 @@ enum PixelFormatConstants : uint32_t kPixelFormatFullRangeBit = 1 << 17, kPixelFormatYUVColorFullRangeBit = kPixelFormatFullRangeBit | kPixelFormatYUVColorBit, - /// `kPixelFormatRGBColorBit` indicates that the pixel format is RGB/RGBA/BGR/BGRA. + /// `kPixelFormatRGBColorBit` indicates that the pixel format is RGB/RGBA/BGR/BGRA. /// Which means it has RGB or RGBA color channels, and is not a YUV format. kPixelFormatRGBColorBit = 1 << 18, @@ -69,8 +67,7 @@ enum PixelFormatConstants : uint32_t * For better performance, consider using the NV12v or NV12f formats. These two formats are * often referred to as YUV formats and are supported by almost all platforms. */ -enum class PixelFormat : uint32_t -{ +enum class PixelFormat : uint32_t { Unknown = 0, /** @@ -97,6 +94,25 @@ enum class PixelFormat : uint32_t I420f = I420 | kPixelFormatYUVColorFullRangeBit, + /** + * @brief YUV 4:2:2 packed format (YUYV/YUY2). 2 bytes per pixel. + * @note Common format for many USB cameras and video capture devices. + * This is a packed format where Y, U, and V components are interleaved. + */ + YUYV = 1 << 3 | kPixelFormatYUVColorBit, + + /// @brief FullRange YUV 4:2:2 packed format (YUYV/YUY2) + YUYVf = YUYV | kPixelFormatYUVColorFullRangeBit, + + /** + * @brief YUV 4:2:2 packed format (UYVY). 2 bytes per pixel. + * @note Similar to YUYV but with different component ordering. + */ + UYVY = 1 << 4 | kPixelFormatYUVColorBit, + + /// @brief FullRange YUV 4:2:2 packed format (UYVY) + UYVYf = UYVY | kPixelFormatYUVColorFullRangeBit, + /// @brief Not commonly used, likely unsupported, may fall back to BGR24 (Windows) or BGRA32 (MacOS) RGB24 = kPixelFormatRGBBit | kPixelFormatRGBColorBit, /// 3 bytes per pixel @@ -116,8 +132,7 @@ enum class PixelFormat : uint32_t BGRA32 = BGR24 | kPixelFormatRGBAColorBit, }; -enum class FrameOrientation -{ +enum class FrameOrientation { /** * @brief The frame is laid out in a top-to-bottom format. * The first row of data corresponds to the first row of the image. @@ -140,18 +155,15 @@ enum class FrameOrientation }; /// check if the pixel format `lhs` includes all bits of the pixel format `rhs`. -inline bool pixelFormatInclude(PixelFormat lhs, PixelFormatConstants rhs) -{ +inline bool pixelFormatInclude(PixelFormat lhs, PixelFormatConstants rhs) { return (static_cast(lhs) & rhs) == rhs; } -inline bool pixelFormatInclude(PixelFormat lhs, PixelFormat rhs) -{ +inline bool pixelFormatInclude(PixelFormat lhs, PixelFormat rhs) { return (static_cast(lhs) & static_cast(rhs)) == static_cast(rhs); } -enum class PropertyName -{ +enum class PropertyName { /** * @brief The width of the frame. * @note When used to set the capture resolution, the closest available resolution will be chosen. @@ -205,8 +217,7 @@ enum class PropertyName * @brief Interface for memory allocation, primarily used to allocate the `data` field in `ccap::Frame`. * @note If you want to implement your own Allocator, you need to ensure that the allocated memory is 32-byte aligned to enable SIMD instruction set acceleration. */ -class Allocator -{ +class Allocator { public: virtual ~Allocator() = 0; @@ -222,8 +233,7 @@ class Allocator virtual size_t size() = 0; }; -struct VideoFrame -{ +struct VideoFrame { VideoFrame(); ~VideoFrame(); VideoFrame(const VideoFrame&) = delete; @@ -278,15 +288,30 @@ struct VideoFrame * @note Currently defined as follows: * - Windows: When the backend is DirectShow, the actual type of nativeHandle is `IMediaSample*` * - macOS/iOS: The actual type of nativeHandle is `CMSampleBufferRef` + * - Linux: The actual type is uint32_t, stands for `v4l2_buffer::index`. */ void* nativeHandle = nullptr; ///< Native handle for the frame, used for platform-specific operations + + /** + * @brief When (allocator == nullptr || data[0] != allocator->data()), the data is stored in a hardware buffer. + * If you hold multiple VideoFrame objects for a long time, it may prevent the camera hardware buffer from being reused, + * affecting performance or causing the camera to stop working. + * Therefore, if you need to hold a VideoFrame object for a long time, you should call the `detach()` method to release nativeHandle. + * If data[0] == allocator->data(), calling `detach()` has no extra cost. + * If data[0] != allocator->data(), calling `detach()` will copy the data into the allocator. + * After calling detach, nativeHandle will be set to nullptr, and data[0] will point to allocator->data(). + * + * @note Best practice: If you need to pass a std::shared_ptr object across threads or hold it across frames, + * you should call `detach()` immediately after obtaining the std::shared_ptr object. + * + */ + void detach(); }; /** * @brief Device information structure. This structure contains some information about the device. */ -struct DeviceInfo -{ +struct DeviceInfo { std::string deviceName; /** @@ -294,8 +319,7 @@ struct DeviceInfo */ std::vector supportedPixelFormats; - struct Resolution - { + struct Resolution { uint32_t width; uint32_t height; }; diff --git a/scripts/build_and_install.sh b/scripts/build_and_install.sh index c8d7477d..f8454017 100755 --- a/scripts/build_and_install.sh +++ b/scripts/build_and_install.sh @@ -22,19 +22,25 @@ function detectCores() { } if isWsl; then - # Switch to Git Bash when running in WSL - echo "You're using WSL, but WSL linux is not supported! Tring to run with Git Bash!" >&2 - GIT_BASH_PATH_WIN=$(/mnt/c/Windows/system32/cmd.exe /C "where bash.exe" | grep -i Git | head -n 1 | tr -d '\n\r') - GIT_BASH_PATH_WSL=$(wslpath -u "$GIT_BASH_PATH_WIN") - echo "== GIT_BASH_PATH_WIN=$GIT_BASH_PATH_WIN" - echo "== GIT_BASH_PATH_WSL=$GIT_BASH_PATH_WSL" - if [[ -f "$GIT_BASH_PATH_WSL" ]]; then - THIS_BASE_NAME=$(basename "$0") - "$GIT_BASH_PATH_WSL" "$THIS_BASE_NAME" $@ - exit $? + # Check if this is a Windows mount point or native WSL Linux + if [[ "$(pwd)" == /mnt/* ]]; then + # Windows mount point - switch to Git Bash for Windows builds + echo "You're using WSL with Windows mount point. Switching to Git Bash for Windows build!" >&2 + GIT_BASH_PATH_WIN=$(/mnt/c/Windows/system32/cmd.exe /C "where bash.exe" | grep -i Git | head -n 1 | tr -d '\n\r') + GIT_BASH_PATH_WSL=$(wslpath -u "$GIT_BASH_PATH_WIN") + echo "== GIT_BASH_PATH_WIN=$GIT_BASH_PATH_WIN" + echo "== GIT_BASH_PATH_WSL=$GIT_BASH_PATH_WSL" + if [[ -f "$GIT_BASH_PATH_WSL" ]]; then + THIS_BASE_NAME=$(basename "$0") + "$GIT_BASH_PATH_WSL" "$THIS_BASE_NAME" "$@" + exit $? + else + echo "Git Bash not found, please install Git Bash!" >&2 + exit 1 + fi else - echo "Git Bash not found, please install Git Bash!" >&2 - exit 1 + # Native WSL Linux environment - continue with Linux build + echo "Using native WSL Linux environment for Linux build" fi fi @@ -54,13 +60,13 @@ build_and_install_config() { echo "=========================================" echo "Building $config configuration..." echo "=========================================" - + # Create build directory BUILD_DIR="$PROJECT_ROOT/build/$config" mkdir -p "$BUILD_DIR" - + cd "$BUILD_DIR" - + # Configure echo "Configuring $config..." cmake ../.. \ @@ -69,15 +75,15 @@ build_and_install_config() { -DCCAP_BUILD_EXAMPLES=OFF \ -DCCAP_BUILD_TESTS=OFF \ -DCCAP_INSTALL=ON - + # Build echo "Building $config..." - cmake --build . --config "$config" --parallel $(detectCores 2>/dev/null) - + cmake --build . --config "$config" --parallel "$(detectCores 2>/dev/null)" + # Install echo "Installing $config..." cmake --install . --config "$config" - + echo "$config build and install completed!" } @@ -85,13 +91,13 @@ build_and_install_config() { if isWindows; then echo "Windows environment detected - building both Debug and Release versions" echo "Debug libraries will have 'd' suffix (e.g., ccapd.lib)" - + # Build Debug version build_and_install_config "Debug" - - # Build Release version + + # Build Release version build_and_install_config "Release" - + echo "" echo "=========================================" echo "All builds completed successfully!" @@ -103,14 +109,14 @@ if isWindows; then echo " - ccap.lib (Release)" fi if [[ -f "$INSTALL_DIR/lib/ccapd.lib" ]]; then - echo " - ccapd.lib (Debug)" + echo " - ccapd.lib (Debug)" fi - + else # Non-Windows: use the specified build type (default: Release) echo "Build Type: $BUILD_TYPE" build_and_install_config "$BUILD_TYPE" - + echo "Build and install completed successfully!" echo "Installation directory: $INSTALL_DIR" fi diff --git a/scripts/format_all.sh b/scripts/format_all.sh index 7dd1b741..6bb15e87 100755 --- a/scripts/format_all.sh +++ b/scripts/format_all.sh @@ -1,15 +1,14 @@ #!/usr/bin/env bash -# 定位到项目根目录(脚本文件的上一级目录) cd "$(dirname "$0")/.." -# 对 src、tests、examples 目录执行 clang-format -# 排除 examples/desktop/glfw 和 examples/desktop/glad 目录 -find src tests examples \ +# Run clang-format on src, tests, and examples directories +# Exclude the examples/desktop/glfw and examples/desktop/glad directories +find src examples \ -type f \ \( -name "*.c" -o -name "*.cpp" -o -name "*.cc" -o -name "*.cxx" -o -name "*.h" -o -name "*.hpp" -o -name "*.hxx" \) \ -not -path "examples/desktop/glfw/*" \ -not -path "examples/desktop/glad/*" \ -exec clang-format -i {} + -echo "代码格式化完成!" +echo "Code formatting completed!" diff --git a/scripts/run_tests.sh b/scripts/run_tests.sh index b34c9fa6..65dcf002 100755 --- a/scripts/run_tests.sh +++ b/scripts/run_tests.sh @@ -17,12 +17,23 @@ set -e # Exit on any error +cd "$(dirname "$0")/.." + function isWsl() { [[ -d "/mnt/c" ]] || command -v wslpath &>/dev/null } function isWindows() { - [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]] || isWsl || [[ -n "$WINDIR" ]] + if isWsl; then + # 当当前路径以 /mnt/ 开头时,认为是 Windows, 否则不是 + if [[ "$(pwd)" == /mnt/* ]]; then + return 0 + else + return 1 + fi + fi + + [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]] || [[ -n "$WINDIR" ]] } function detectCores() { @@ -33,9 +44,9 @@ function detectCores() { fi } -if isWsl; then - # Switch to Git Bash when running in WSL - echo "You're using WSL, but WSL linux is not supported! Tring to run with Git Bash!" >&2 +if isWindows && isWsl; then + # Check if this is a Windows mount point or native WSL Linux + echo "You're using WSL with Windows mount point. Switching to Git Bash for Windows tests!" >&2 GIT_BASH_PATH_WIN=$(/mnt/c/Windows/system32/cmd.exe /C "where bash.exe" | grep -i Git | head -n 1 | tr -d '\n\r') GIT_BASH_PATH_WSL=$(wslpath -u "$GIT_BASH_PATH_WIN") echo "== GIT_BASH_PATH_WIN=$GIT_BASH_PATH_WIN" @@ -120,11 +131,9 @@ BLUE='\033[0;34m' PURPLE='\033[0;35m' NC='\033[0m' # No Color -cd "$(dirname "$0")/.." - # Check if we're in the right directory if [ ! -f "CMakeLists.txt" ] || [ ! -d "tests" ]; then - echo -e "${RED}Error: Please run this script from the ccap root directory${NC}" + echo -e "${RED}Error: Please run this script from the ccap root directory, cwd: $(pwd)" exit 1 fi diff --git a/src/ccap_c.cpp b/src/ccap_c.cpp index 4307a7b3..3fd4d8bd 100644 --- a/src/ccap_c.cpp +++ b/src/ccap_c.cpp @@ -7,13 +7,14 @@ */ #include "ccap_c.h" + #include "ccap.h" +#include #include +#include #include #include -#include -#include extern "C" { @@ -60,8 +61,9 @@ char* allocate_c_string(const std::string& str) { struct CallbackWrapper { CcapNewFrameCallback callback; void* userData; - - CallbackWrapper(CcapNewFrameCallback cb, void* data) : callback(cb), userData(data) {} + + CallbackWrapper(CcapNewFrameCallback cb, void* data) : + callback(cb), userData(data) {} }; } // anonymous namespace @@ -105,20 +107,20 @@ void ccap_provider_destroy(CcapProvider* provider) { bool ccap_provider_find_device_names(CcapProvider* provider, char*** deviceNames, size_t* count) { if (!provider || !deviceNames || !count) return false; - + try { auto* cppProvider = reinterpret_cast(provider); auto devices = cppProvider->findDeviceNames(); - + *count = devices.size(); if (*count == 0) { *deviceNames = nullptr; return true; } - + char** names = static_cast(malloc(*count * sizeof(char*))); if (!names) return false; - + for (size_t i = 0; i < *count; ++i) { names[i] = allocate_c_string(devices[i]); if (!names[i]) { @@ -130,7 +132,7 @@ bool ccap_provider_find_device_names(CcapProvider* provider, char*** deviceNames return false; } } - + *deviceNames = names; return true; } catch (...) { @@ -151,7 +153,7 @@ void ccap_provider_free_device_names(char** deviceNames, size_t count) { bool ccap_provider_open(CcapProvider* provider, const char* deviceName, bool autoStart) { if (!provider) return false; - + try { auto* cppProvider = reinterpret_cast(provider); std::string_view deviceNameView = deviceName ? deviceName : ""; @@ -163,7 +165,7 @@ bool ccap_provider_open(CcapProvider* provider, const char* deviceName, bool aut bool ccap_provider_open_by_index(CcapProvider* provider, int deviceIndex, bool autoStart) { if (!provider) return false; - + try { auto* cppProvider = reinterpret_cast(provider); return cppProvider->open(deviceIndex, autoStart); @@ -174,7 +176,7 @@ bool ccap_provider_open_by_index(CcapProvider* provider, int deviceIndex, bool a bool ccap_provider_is_opened(const CcapProvider* provider) { if (!provider) return false; - + try { auto* cppProvider = reinterpret_cast(provider); return cppProvider->isOpened(); @@ -185,40 +187,40 @@ bool ccap_provider_is_opened(const CcapProvider* provider) { bool ccap_provider_get_device_info(const CcapProvider* provider, CcapDeviceInfo* deviceInfo) { if (!provider || !deviceInfo) return false; - + try { auto* cppProvider = reinterpret_cast(provider); auto infoOpt = cppProvider->getDeviceInfo(); - + if (!infoOpt.has_value()) return false; - + const auto& info = infoOpt.value(); - + // Initialize structure memset(deviceInfo, 0, sizeof(CcapDeviceInfo)); - + // Copy device name deviceInfo->deviceName = allocate_c_string(info.deviceName); - + // Copy supported pixel formats deviceInfo->pixelFormatCount = info.supportedPixelFormats.size(); if (deviceInfo->pixelFormatCount > 0) { deviceInfo->supportedPixelFormats = static_cast( malloc(deviceInfo->pixelFormatCount * sizeof(CcapPixelFormat))); - + if (deviceInfo->supportedPixelFormats) { for (size_t i = 0; i < deviceInfo->pixelFormatCount; ++i) { deviceInfo->supportedPixelFormats[i] = convert_pixel_format_to_c(info.supportedPixelFormats[i]); } } } - + // Copy supported resolutions deviceInfo->resolutionCount = info.supportedResolutions.size(); if (deviceInfo->resolutionCount > 0) { deviceInfo->supportedResolutions = static_cast( malloc(deviceInfo->resolutionCount * sizeof(CcapResolution))); - + if (deviceInfo->supportedResolutions) { for (size_t i = 0; i < deviceInfo->resolutionCount; ++i) { deviceInfo->supportedResolutions[i].width = info.supportedResolutions[i].width; @@ -226,7 +228,7 @@ bool ccap_provider_get_device_info(const CcapProvider* provider, CcapDeviceInfo* } } } - + return true; } catch (...) { return false; @@ -257,7 +259,7 @@ void ccap_provider_close(CcapProvider* provider) { bool ccap_provider_start(CcapProvider* provider) { if (!provider) return false; - + try { auto* cppProvider = reinterpret_cast(provider); return cppProvider->start(); @@ -279,7 +281,7 @@ void ccap_provider_stop(CcapProvider* provider) { bool ccap_provider_is_started(const CcapProvider* provider) { if (!provider) return false; - + try { auto* cppProvider = reinterpret_cast(provider); return cppProvider->isStarted(); @@ -292,7 +294,7 @@ bool ccap_provider_is_started(const CcapProvider* provider) { bool ccap_provider_set_property(CcapProvider* provider, CcapPropertyName prop, double value) { if (!provider) return false; - + try { auto* cppProvider = reinterpret_cast(provider); return cppProvider->set(convert_property_name_from_c(prop), value); @@ -303,7 +305,7 @@ bool ccap_provider_set_property(CcapProvider* provider, CcapPropertyName prop, d double ccap_provider_get_property(CcapProvider* provider, CcapPropertyName prop) { if (!provider) return NAN; - + try { auto* cppProvider = reinterpret_cast(provider); return cppProvider->get(convert_property_name_from_c(prop)); @@ -316,13 +318,13 @@ double ccap_provider_get_property(CcapProvider* provider, CcapPropertyName prop) CcapVideoFrame* ccap_provider_grab(CcapProvider* provider, uint32_t timeoutMs) { if (!provider) return nullptr; - + try { auto* cppProvider = reinterpret_cast(provider); auto frame = cppProvider->grab(timeoutMs); - + if (!frame) return nullptr; - + // Transfer ownership to a heap-allocated shared_ptr auto* framePtr = new std::shared_ptr(std::move(frame)); return reinterpret_cast(framePtr); @@ -333,25 +335,25 @@ CcapVideoFrame* ccap_provider_grab(CcapProvider* provider, uint32_t timeoutMs) { bool ccap_provider_set_new_frame_callback(CcapProvider* provider, CcapNewFrameCallback callback, void* userData) { if (!provider) return false; - + try { auto* cppProvider = reinterpret_cast(provider); - + if (callback) { // Create wrapper for the C callback auto wrapper = std::make_shared(callback, userData); - + cppProvider->setNewFrameCallback([wrapper](const std::shared_ptr& frame) -> bool { if (wrapper->callback) { // Transfer ownership to a heap-allocated shared_ptr for the callback auto* framePtr = new std::shared_ptr(frame); bool result = wrapper->callback(reinterpret_cast(framePtr), wrapper->userData); - + // Clean up the frame if the callback returned true (indicating it consumed the frame) if (result) { delete framePtr; } - + return result; } return false; @@ -360,7 +362,7 @@ bool ccap_provider_set_new_frame_callback(CcapProvider* provider, CcapNewFrameCa // Remove callback cppProvider->setNewFrameCallback(nullptr); } - + return true; } catch (...) { return false; @@ -371,17 +373,17 @@ bool ccap_provider_set_new_frame_callback(CcapProvider* provider, CcapNewFrameCa bool ccap_video_frame_get_info(const CcapVideoFrame* frame, CcapVideoFrameInfo* frameInfo) { if (!frame || !frameInfo) return false; - + try { auto* framePtr = reinterpret_cast*>(frame); const auto& cppFrame = **framePtr; - + // Copy frame information for (int i = 0; i < 3; ++i) { frameInfo->data[i] = cppFrame.data[i]; frameInfo->stride[i] = cppFrame.stride[i]; } - + frameInfo->pixelFormat = convert_pixel_format_to_c(cppFrame.pixelFormat); frameInfo->width = cppFrame.width; frameInfo->height = cppFrame.height; @@ -390,7 +392,7 @@ bool ccap_video_frame_get_info(const CcapVideoFrame* frame, CcapVideoFrameInfo* frameInfo->frameIndex = cppFrame.frameIndex; frameInfo->orientation = convert_frame_orientation_to_c(cppFrame.orientation); frameInfo->nativeHandle = cppFrame.nativeHandle; - + return true; } catch (...) { return false; diff --git a/src/ccap_convert.cpp b/src/ccap_convert.cpp index 750084c3..b119fcc1 100644 --- a/src/ccap_convert.cpp +++ b/src/ccap_convert.cpp @@ -390,6 +390,202 @@ void i420ToRgba32(const uint8_t* srcY, int srcYStride, const uint8_t* srcU, int i420ToRgb_common(srcY, srcYStride, srcU, srcUStride, srcV, srcVStride, dst, dstStride, width, height, flag); } +///////////// YUYV/UYVY to RGB functions ///////////// + +template +void yuyvToRgb_common(const uint8_t* src, int srcStride, uint8_t* dst, int dstStride, int width, int height, ConvertFlag flag) { + // 如果 height < 0,则反向写入 dst,src 顺序读取 + if (height < 0) { + height = -height; + dst = dst + (height - 1) * dstStride; + dstStride = -dstStride; + } + + const bool is601 = (flag & ConvertFlag::BT601) != 0; + const bool isFullRange = (flag & ConvertFlag::FullRange) != 0; + const auto convertFunc = getYuvToRgbFunc(is601, isFullRange); + constexpr int channels = hasAlpha ? 4 : 3; + + for (int y = 0; y < height; ++y) { + const uint8_t* srcRow = src + y * srcStride; + uint8_t* dstRow = dst + y * dstStride; + + for (int x = 0; x < width; x += 2) { + // YUYV format: Y0 U0 Y1 V0 (4 bytes for 2 pixels) + int baseIdx = (x / 2) * 4; + int y0 = srcRow[baseIdx + 0]; // Y0 + int u = srcRow[baseIdx + 1]; // U0 + int y1 = srcRow[baseIdx + 2]; // Y1 + int v = srcRow[baseIdx + 3]; // V0 + + int r0, g0, b0, r1, g1, b1; + convertFunc(y0, u, v, r0, g0, b0); + convertFunc(y1, u, v, r1, g1, b1); + + if constexpr (isBgrColor) { + dstRow[x * channels + 0] = b0; + dstRow[x * channels + 1] = g0; + dstRow[x * channels + 2] = r0; + + dstRow[(x + 1) * channels + 0] = b1; + dstRow[(x + 1) * channels + 1] = g1; + dstRow[(x + 1) * channels + 2] = r1; + } else { + dstRow[x * channels + 0] = r0; + dstRow[x * channels + 1] = g0; + dstRow[x * channels + 2] = b0; + + dstRow[(x + 1) * channels + 0] = r1; + dstRow[(x + 1) * channels + 1] = g1; + dstRow[(x + 1) * channels + 2] = b1; + } + + if constexpr (hasAlpha) { + dstRow[x * channels + 3] = 255; + dstRow[(x + 1) * channels + 3] = 255; + } + } + } +} + +template +void uyvyToRgb_common(const uint8_t* src, int srcStride, uint8_t* dst, int dstStride, int width, int height, ConvertFlag flag) { + // 如果 height < 0,则反向写入 dst,src 顺序读取 + if (height < 0) { + height = -height; + dst = dst + (height - 1) * dstStride; + dstStride = -dstStride; + } + + const bool is601 = (flag & ConvertFlag::BT601) != 0; + const bool isFullRange = (flag & ConvertFlag::FullRange) != 0; + const auto convertFunc = getYuvToRgbFunc(is601, isFullRange); + constexpr int channels = hasAlpha ? 4 : 3; + + for (int y = 0; y < height; ++y) { + const uint8_t* srcRow = src + y * srcStride; + uint8_t* dstRow = dst + y * dstStride; + + for (int x = 0; x < width; x += 2) { + // UYVY format: U0 Y0 V0 Y1 (4 bytes for 2 pixels) + int baseIdx = (x / 2) * 4; + int u = srcRow[baseIdx + 0]; // U0 + int y0 = srcRow[baseIdx + 1]; // Y0 + int v = srcRow[baseIdx + 2]; // V0 + int y1 = srcRow[baseIdx + 3]; // Y1 + + int r0, g0, b0, r1, g1, b1; + convertFunc(y0, u, v, r0, g0, b0); + convertFunc(y1, u, v, r1, g1, b1); + + if constexpr (isBgrColor) { + dstRow[x * channels + 0] = b0; + dstRow[x * channels + 1] = g0; + dstRow[x * channels + 2] = r0; + + dstRow[(x + 1) * channels + 0] = b1; + dstRow[(x + 1) * channels + 1] = g1; + dstRow[(x + 1) * channels + 2] = r1; + } else { + dstRow[x * channels + 0] = r0; + dstRow[x * channels + 1] = g0; + dstRow[x * channels + 2] = b0; + + dstRow[(x + 1) * channels + 0] = r1; + dstRow[(x + 1) * channels + 1] = g1; + dstRow[(x + 1) * channels + 2] = b1; + } + + if constexpr (hasAlpha) { + dstRow[x * channels + 3] = 255; + dstRow[(x + 1) * channels + 3] = 255; + } + } + } +} + +// YUYV conversion functions +void yuyvToBgr24(const uint8_t* src, int srcStride, uint8_t* dst, int dstStride, int width, int height, ConvertFlag flag) { +#if ENABLE_AVX2_IMP + if (canUseAVX2()) { + yuyvToBgr24_avx2(src, srcStride, dst, dstStride, width, height, flag); + return; + } +#endif + yuyvToRgb_common(src, srcStride, dst, dstStride, width, height, flag); +} + +void yuyvToRgb24(const uint8_t* src, int srcStride, uint8_t* dst, int dstStride, int width, int height, ConvertFlag flag) { +#if ENABLE_AVX2_IMP + if (canUseAVX2()) { + yuyvToRgb24_avx2(src, srcStride, dst, dstStride, width, height, flag); + return; + } +#endif + yuyvToRgb_common(src, srcStride, dst, dstStride, width, height, flag); +} + +void yuyvToBgra32(const uint8_t* src, int srcStride, uint8_t* dst, int dstStride, int width, int height, ConvertFlag flag) { +#if ENABLE_AVX2_IMP + if (canUseAVX2()) { + yuyvToBgra32_avx2(src, srcStride, dst, dstStride, width, height, flag); + return; + } +#endif + yuyvToRgb_common(src, srcStride, dst, dstStride, width, height, flag); +} + +void yuyvToRgba32(const uint8_t* src, int srcStride, uint8_t* dst, int dstStride, int width, int height, ConvertFlag flag) { +#if ENABLE_AVX2_IMP + if (canUseAVX2()) { + yuyvToRgba32_avx2(src, srcStride, dst, dstStride, width, height, flag); + return; + } +#endif + yuyvToRgb_common(src, srcStride, dst, dstStride, width, height, flag); +} + +// UYVY conversion functions +void uyvyToBgr24(const uint8_t* src, int srcStride, uint8_t* dst, int dstStride, int width, int height, ConvertFlag flag) { +#if ENABLE_AVX2_IMP + if (canUseAVX2()) { + uyvyToBgr24_avx2(src, srcStride, dst, dstStride, width, height, flag); + return; + } +#endif + uyvyToRgb_common(src, srcStride, dst, dstStride, width, height, flag); +} + +void uyvyToRgb24(const uint8_t* src, int srcStride, uint8_t* dst, int dstStride, int width, int height, ConvertFlag flag) { +#if ENABLE_AVX2_IMP + if (canUseAVX2()) { + uyvyToRgb24_avx2(src, srcStride, dst, dstStride, width, height, flag); + return; + } +#endif + uyvyToRgb_common(src, srcStride, dst, dstStride, width, height, flag); +} + +void uyvyToBgra32(const uint8_t* src, int srcStride, uint8_t* dst, int dstStride, int width, int height, ConvertFlag flag) { +#if ENABLE_AVX2_IMP + if (canUseAVX2()) { + uyvyToBgra32_avx2(src, srcStride, dst, dstStride, width, height, flag); + return; + } +#endif + uyvyToRgb_common(src, srcStride, dst, dstStride, width, height, flag); +} + +void uyvyToRgba32(const uint8_t* src, int srcStride, uint8_t* dst, int dstStride, int width, int height, ConvertFlag flag) { +#if ENABLE_AVX2_IMP + if (canUseAVX2()) { + uyvyToRgba32_avx2(src, srcStride, dst, dstStride, width, height, flag); + return; + } +#endif + uyvyToRgb_common(src, srcStride, dst, dstStride, width, height, flag); +} + static thread_local std::shared_ptr sSharedAllocator, sSharedAllocator2; static std::mutex sAllocatorMutex; static std::vector, std::shared_ptr*>> sAllAllocators; diff --git a/src/ccap_convert_avx2.cpp b/src/ccap_convert_avx2.cpp index ce0a9248..de04f383 100644 --- a/src/ccap_convert_avx2.cpp +++ b/src/ccap_convert_avx2.cpp @@ -8,6 +8,7 @@ #include "ccap_convert_avx2.h" #include +#include #if ENABLE_AVX2_IMP /// macOS 上直接使用 Accelerate.framework, 暂时不需要单独实现 @@ -36,26 +37,36 @@ inline bool hasAVX2_() { __cpuid(cpuInfo, 7); return (cpuInfo[1] & (1 << 5)) != 0; } -#elif defined(__GNUC__) && (defined(_WIN32) || defined(__APPLE__)) +#elif defined(__GNUC__) || defined(__clang__) #include inline bool hasAVX2_() { unsigned int eax, ebx, ecx, edx; - // 1. 检查 AVX 和 OSXSAVE + + // 1. 检查基本 CPUID 支持 + if (!__get_cpuid(0, &eax, &ebx, &ecx, &edx)) return false; + if (eax < 1) return false; // 需要支持 CPUID function 1 + + // 2. 检查 AVX 和 OSXSAVE if (!__get_cpuid(1, &eax, &ebx, &ecx, &edx)) return false; bool osxsave = (ecx & (1 << 27)) != 0; bool avx = (ecx & (1 << 28)) != 0; if (!(osxsave && avx)) return false; - // 2. 检查 XGETBV + + // 3. 检查 XGETBV,确认 OS 支持 YMM + // 只有在 OSXSAVE 为 true 时才能安全调用 XGETBV unsigned int xcr0_lo = 0, xcr0_hi = 0; -#if defined(_XCR_XFEATURE_ENABLED_MASK) - asm volatile("xgetbv" : "=a"(xcr0_lo), "=d"(xcr0_hi) : "c"(0)); -#else - asm volatile("xgetbv" : "=a"(xcr0_lo), "=d"(xcr0_hi) : "c"(0)); -#endif - if ((xcr0_lo & 0x6) != 0x6) return false; - // 3. 检查 AVX2 + asm volatile("xgetbv" + : "=a"(xcr0_lo), "=d"(xcr0_hi) + : "c"(0)); + if ((xcr0_lo & 0x6) != 0x6) return false; // XMM 和 YMM 状态必须都被保存 + + // 4. 检查扩展功能支持 + if (!__get_cpuid(0, &eax, &ebx, &ecx, &edx)) return false; + if (eax < 7) return false; // 需要支持 CPUID function 7 + + // 5. 检查 AVX2 if (!__get_cpuid_count(7, 0, &eax, &ebx, &ecx, &edx)) return false; - return (ebx & (1 << 5)) != 0; + return (ebx & (1 << 5)) != 0; // AVX2 位 } #else inline bool hasAVX2_() { return false; } @@ -84,12 +95,31 @@ bool canUseAVX2() { return hasAVX2() && sEnableAVX2; } +const char* getAVX2SupportInfo() { +#if ENABLE_AVX2_IMP + static const char* info = nullptr; + if (info == nullptr) { + if (hasAVX2()) { + if (sEnableAVX2) { + info = "AVX2: Hardware supported and enabled"; + } else { + info = "AVX2: Hardware supported but disabled by software"; + } + } else { + info = "AVX2: Not supported by hardware or OS"; + } + } + return info; +#else + return "AVX2: Disabled at compile time"; +#endif +} + #if ENABLE_AVX2_IMP template -AVX2_TARGET -void colorShuffle_avx2(const uint8_t* src, int srcStride, uint8_t* dst, int dstStride, int width, - int height) { // Implement a general colorShuffle, accelerated by AVX2 +AVX2_TARGET void colorShuffle_avx2(const uint8_t* src, int srcStride, uint8_t* dst, int dstStride, int width, + int height) { // Implement a general colorShuffle, accelerated by AVX2 static_assert((inputChannels == 3 || inputChannels == 4) && (outputChannels == 3 || outputChannels == 4), "inputChannels and outputChannels must be 3 or 4"); @@ -266,9 +296,8 @@ inline void getYuvToRgbCoefficients(bool isBT601, bool isFullRange, int& cy, int } template -AVX2_TARGET -void nv12ToRgbaColor_avx2_imp(const uint8_t* srcY, int srcYStride, const uint8_t* srcUV, int srcUVStride, uint8_t* dst, int dstStride, - int width, int height, bool is601) { +AVX2_TARGET void nv12ToRgbaColor_avx2_imp(const uint8_t* srcY, int srcYStride, const uint8_t* srcUV, int srcUVStride, uint8_t* dst, int dstStride, + int width, int height, bool is601) { if (height < 0) { height = -height; dst = dst + (height - 1) * dstStride; @@ -432,9 +461,8 @@ void nv12ToRgbaColor_avx2_imp(const uint8_t* srcY, int srcYStride, const uint8_t } template -AVX2_TARGET -void _nv12ToRgbColor_avx2_imp(const uint8_t* srcY, int srcYStride, const uint8_t* srcUV, int srcUVStride, uint8_t* dst, int dstStride, - int width, int height, bool is601) { +AVX2_TARGET void _nv12ToRgbColor_avx2_imp(const uint8_t* srcY, int srcYStride, const uint8_t* srcUV, int srcUVStride, uint8_t* dst, int dstStride, + int width, int height, bool is601) { if (height < 0) { height = -height; dst = dst + (height - 1) * dstStride; @@ -551,19 +579,19 @@ void _nv12ToRgbColor_avx2_imp(const uint8_t* srcY, int srcYStride, const uint8_t convertFunc(y1, u, v, r1, g1, b1); if constexpr (isBGR) { - dstRow[x * 3] = b0; + dstRow[x * 3 + 0] = b0; dstRow[x * 3 + 1] = g0; dstRow[x * 3 + 2] = r0; - dstRow[(x + 1) * 3] = b1; + dstRow[(x + 1) * 3 + 0] = b1; dstRow[(x + 1) * 3 + 1] = g1; dstRow[(x + 1) * 3 + 2] = r1; } else { - dstRow[x * 3] = r0; + dstRow[x * 3 + 0] = r0; dstRow[x * 3 + 1] = g0; dstRow[x * 3 + 2] = b0; - dstRow[(x + 1) * 3] = r1; + dstRow[(x + 1) * 3 + 0] = r1; dstRow[(x + 1) * 3 + 1] = g1; dstRow[(x + 1) * 3 + 2] = b1; } @@ -572,9 +600,8 @@ void _nv12ToRgbColor_avx2_imp(const uint8_t* srcY, int srcYStride, const uint8_t } template -AVX2_TARGET -void _i420ToRgba_avx2_imp(const uint8_t* srcY, int srcYStride, const uint8_t* srcU, int srcUStride, const uint8_t* srcV, int srcVStride, - uint8_t* dst, int dstStride, int width, int height, bool is601) { +AVX2_TARGET void _i420ToRgba_avx2_imp(const uint8_t* srcY, int srcYStride, const uint8_t* srcU, int srcUStride, const uint8_t* srcV, int srcVStride, + uint8_t* dst, int dstStride, int width, int height, bool is601) { // 如果 height < 0,则反向写入 dst,src 顺序读取 if (height < 0) { height = -height; @@ -730,9 +757,8 @@ void _i420ToRgba_avx2_imp(const uint8_t* srcY, int srcYStride, const uint8_t* sr } template -AVX2_TARGET -void _i420ToRgb_avx2_imp(const uint8_t* srcY, int srcYStride, const uint8_t* srcU, int srcUStride, const uint8_t* srcV, int srcVStride, - uint8_t* dst, int dstStride, int width, int height, bool is601) { +AVX2_TARGET void _i420ToRgb_avx2_imp(const uint8_t* srcY, int srcYStride, const uint8_t* srcU, int srcUStride, const uint8_t* srcV, int srcVStride, + uint8_t* dst, int dstStride, int width, int height, bool is601) { // 如果 height < 0,则反向写入 dst,src 顺序读取 if (height < 0) { height = -height; @@ -970,5 +996,558 @@ void i420ToRgb24_avx2(const uint8_t* srcY, int srcYStride, const uint8_t* srcU, } } +///////////// YUYV/UYVY to RGB functions ///////////// + +template +AVX2_TARGET void yuyvToRgb_avx2_imp(const uint8_t* src, int srcStride, uint8_t* dst, int dstStride, int width, int height, bool is601) { + // 如果 height < 0,则反向写入 dst,src 顺序读取 + if (height < 0) { + height = -height; + dst = dst + (height - 1) * dstStride; + dstStride = -dstStride; + } + + // 根据标志选择系数 + int cy, cr, cgu, cgv, cb; + getYuvToRgbCoefficients(is601, isFullRange, cy, cr, cgu, cgv, cb); + + __m256i c_y = _mm256_set1_epi16(cy); + __m256i c_r = _mm256_set1_epi16(cr); + __m256i c_gu = _mm256_set1_epi16(cgu); + __m256i c_gv = _mm256_set1_epi16(cgv); + __m256i c_b = _mm256_set1_epi16(cb); + + __m256i c128 = _mm256_set1_epi16(128); + __m128i a8 = _mm_set1_epi8((char)255); + + constexpr int channels = hasAlpha ? 4 : 3; + const int vectorWidth = 16; // 处理16个像素(32字节YUYV数据) + YuvToRgbFunc convertFunc = getYuvToRgbFunc(is601, isFullRange); + + for (int y = 0; y < height; ++y) { + const uint8_t* srcRow = src + y * srcStride; + uint8_t* dstRow = dst + y * dstStride; + int x = 0; + + // AVX2 优化处理,每次处理16个像素(32字节YUYV数据) + for (; x + vectorWidth <= width; x += vectorWidth) { + // 1. 加载32字节YUYV数据 (16个像素 = 32字节) + __m256i yuyv_data = _mm256_loadu_si256((const __m256i*)(srcRow + x * 2)); + + // 2. 直接使用shuffle分离YUYV分量 + // YUYV格式: Y0 U0 Y1 V0 Y2 U1 Y3 V1 ... + // 使用正确的shuffle掩码,考虑AVX2的lane限制 + + // 创建正确的shuffle掩码(每个lane独立工作,索引范围0-15) + __m256i shuffle_y = _mm256_setr_epi8( + 0, 2, 4, 6, 8, 10, 12, 14, // Lane 0: 提取Y0,Y1,Y2,Y3,Y4,Y5,Y6,Y7 + -1, -1, -1, -1, -1, -1, -1, -1, // Lane 0: 填充区域 + 0, 2, 4, 6, 8, 10, 12, 14, // Lane 1: 提取Y8,Y9,Y10,Y11,Y12,Y13,Y14,Y15 + -1, -1, -1, -1, -1, -1, -1, -1 // Lane 1: 填充区域 + ); + + // U分量:在位置1,5,9,13...,每个U对应两个Y(4:2:2子采样) + __m256i shuffle_u = _mm256_setr_epi8( + 1, 1, 5, 5, 9, 9, 13, 13, // Lane 0: U0,U0,U1,U1,U2,U2,U3,U3 + -1, -1, -1, -1, -1, -1, -1, -1, // Lane 0: 填充区域 + 1, 1, 5, 5, 9, 9, 13, 13, // Lane 1: U4,U4,U5,U5,U6,U6,U7,U7 + -1, -1, -1, -1, -1, -1, -1, -1 // Lane 1: 填充区域 + ); + + // V分量:在位置3,7,11,15...,每个V对应两个Y(4:2:2子采样) + __m256i shuffle_v = _mm256_setr_epi8( + 3, 3, 7, 7, 11, 11, 15, 15, // Lane 0: V0,V0,V1,V1,V2,V2,V3,V3 + -1, -1, -1, -1, -1, -1, -1, -1, // Lane 0: 填充区域 + 3, 3, 7, 7, 11, 11, 15, 15, // Lane 1: V4,V4,V5,V5,V6,V6,V7,V7 + -1, -1, -1, -1, -1, -1, -1, -1 // Lane 1: 填充区域 + ); + + // 执行shuffle分离 + __m256i y_shuffled = _mm256_shuffle_epi8(yuyv_data, shuffle_y); + __m256i u_shuffled = _mm256_shuffle_epi8(yuyv_data, shuffle_u); + __m256i v_shuffled = _mm256_shuffle_epi8(yuyv_data, shuffle_v); + + // 提取有效数据(低64位包含真实数据,高64位是填充的-1) + __m128i y_lo = _mm256_castsi256_si128(y_shuffled); // Lane 0的Y值 + __m128i y_hi = _mm256_extracti128_si256(y_shuffled, 1); // Lane 1的Y值 + __m128i u_lo = _mm256_castsi256_si128(u_shuffled); // Lane 0的U值 + __m128i u_hi = _mm256_extracti128_si256(u_shuffled, 1); // Lane 1的U值 + __m128i v_lo = _mm256_castsi256_si128(v_shuffled); // Lane 0的V值 + __m128i v_hi = _mm256_extracti128_si256(v_shuffled, 1); // Lane 1的V值 + + // 合并两个lane的数据,只取有效的前8字节 + __m128i y_final = _mm_unpacklo_epi64(y_lo, y_hi); // Y0-Y7 + Y8-Y15 + __m128i u_final = _mm_unpacklo_epi64(u_lo, u_hi); // U0,U0,U1,U1...U3,U3 + U4,U4,U5,U5...U7,U7 + __m128i v_final = _mm_unpacklo_epi64(v_lo, v_hi); // V0,V0,V1,V1...V3,V3 + V4,V4,V5,V5...V7,V7 + + // 转换为16位整数 + __m256i y_16 = _mm256_cvtepu8_epi16(y_final); + __m256i u_16 = _mm256_cvtepu8_epi16(u_final); + __m256i v_16 = _mm256_cvtepu8_epi16(v_final); + + // 3. YUV偏移处理 + u_16 = _mm256_sub_epi16(u_16, c128); + v_16 = _mm256_sub_epi16(v_16, c128); + + if constexpr (!isFullRange) { + y_16 = _mm256_sub_epi16(y_16, _mm256_set1_epi16(16)); + } + + // 4. YUV到RGB转换 + __m256i y_scaled = _mm256_mullo_epi16(y_16, c_y); + + __m256i r = _mm256_add_epi16(y_scaled, _mm256_mullo_epi16(v_16, c_r)); + r = _mm256_add_epi16(r, _mm256_set1_epi16(32)); + r = _mm256_srai_epi16(r, 6); + + __m256i g = _mm256_sub_epi16(y_scaled, _mm256_mullo_epi16(u_16, c_gu)); + g = _mm256_sub_epi16(g, _mm256_mullo_epi16(v_16, c_gv)); + g = _mm256_add_epi16(g, _mm256_set1_epi16(32)); + g = _mm256_srai_epi16(g, 6); + + __m256i b = _mm256_add_epi16(y_scaled, _mm256_mullo_epi16(u_16, c_b)); + b = _mm256_add_epi16(b, _mm256_set1_epi16(32)); + b = _mm256_srai_epi16(b, 6); + + // 5. 钳制到0-255范围 + __m256i zero = _mm256_setzero_si256(); + __m256i maxv = _mm256_set1_epi16(255); + r = _mm256_max_epi16(zero, _mm256_min_epi16(r, maxv)); + g = _mm256_max_epi16(zero, _mm256_min_epi16(g, maxv)); + b = _mm256_max_epi16(zero, _mm256_min_epi16(b, maxv)); + + // 6. 转换为8位并打包输出 + __m128i r8 = _mm_packus_epi16(_mm256_castsi256_si128(r), _mm256_extracti128_si256(r, 1)); + __m128i g8 = _mm_packus_epi16(_mm256_castsi256_si128(g), _mm256_extracti128_si256(g, 1)); + __m128i b8 = _mm_packus_epi16(_mm256_castsi256_si128(b), _mm256_extracti128_si256(b, 1)); + + // 7. 根据输出格式存储 + if constexpr (hasAlpha) { + if constexpr (isBgrColor) { + // BGRA格式 + __m128i bg0 = _mm_unpacklo_epi8(b8, g8); + __m128i ra0 = _mm_unpacklo_epi8(r8, a8); + __m128i bgra0 = _mm_unpacklo_epi16(bg0, ra0); + __m128i bgra1 = _mm_unpackhi_epi16(bg0, ra0); + + __m128i bg1 = _mm_unpackhi_epi8(b8, g8); + __m128i ra1 = _mm_unpackhi_epi8(r8, a8); + __m128i bgra2 = _mm_unpacklo_epi16(bg1, ra1); + __m128i bgra3 = _mm_unpackhi_epi16(bg1, ra1); + + _mm_storeu_si128((__m128i*)(dstRow + x * 4), bgra0); + _mm_storeu_si128((__m128i*)(dstRow + x * 4 + 16), bgra1); + _mm_storeu_si128((__m128i*)(dstRow + x * 4 + 32), bgra2); + _mm_storeu_si128((__m128i*)(dstRow + x * 4 + 48), bgra3); + } else { + // RGBA格式 + __m128i rg0 = _mm_unpacklo_epi8(r8, g8); + __m128i ba0 = _mm_unpacklo_epi8(b8, a8); + __m128i rgba0 = _mm_unpacklo_epi16(rg0, ba0); + __m128i rgba1 = _mm_unpackhi_epi16(rg0, ba0); + + __m128i rg1 = _mm_unpackhi_epi8(r8, g8); + __m128i ba1 = _mm_unpackhi_epi8(b8, a8); + __m128i rgba2 = _mm_unpacklo_epi16(rg1, ba1); + __m128i rgba3 = _mm_unpackhi_epi16(rg1, ba1); + + _mm_storeu_si128((__m128i*)(dstRow + x * 4), rgba0); + _mm_storeu_si128((__m128i*)(dstRow + x * 4 + 16), rgba1); + _mm_storeu_si128((__m128i*)(dstRow + x * 4 + 32), rgba2); + _mm_storeu_si128((__m128i*)(dstRow + x * 4 + 48), rgba3); + } + } else { + // RGB24或BGR24格式 - 使用标量存储避免复杂的3字节打包 + uint8_t r_vals[16], g_vals[16], b_vals[16]; + _mm_storeu_si128((__m128i*)r_vals, r8); + _mm_storeu_si128((__m128i*)g_vals, g8); + _mm_storeu_si128((__m128i*)b_vals, b8); + + for (int i = 0; i < 16 && (x + i) < width; ++i) { + if constexpr (isBgrColor) { + dstRow[(x + i) * 3 + 0] = b_vals[i]; + dstRow[(x + i) * 3 + 1] = g_vals[i]; + dstRow[(x + i) * 3 + 2] = r_vals[i]; + } else { + dstRow[(x + i) * 3 + 0] = r_vals[i]; + dstRow[(x + i) * 3 + 1] = g_vals[i]; + dstRow[(x + i) * 3 + 2] = b_vals[i]; + } + } + } + } + + // 处理剩余像素(标量实现) + for (; x < width; x += 2) { + if (x + 1 >= width) break; // YUYV需要成对处理 + + // YUYV format: Y0 U0 Y1 V0 (4 bytes for 2 pixels) + int baseIdx = x * 2; + int y0 = srcRow[baseIdx + 0]; // Y0 + int u = srcRow[baseIdx + 1]; // U0 + int y1 = srcRow[baseIdx + 2]; // Y1 + int v = srcRow[baseIdx + 3]; // V0 + + int r0, g0, b0, r1, g1, b1; + convertFunc(y0, u, v, r0, g0, b0); + convertFunc(y1, u, v, r1, g1, b1); + + if constexpr (isBgrColor) { + dstRow[x * channels + 0] = b0; + dstRow[x * channels + 1] = g0; + dstRow[x * channels + 2] = r0; + + if (x + 1 < width) { + dstRow[(x + 1) * channels + 0] = b1; + dstRow[(x + 1) * channels + 1] = g1; + dstRow[(x + 1) * channels + 2] = r1; + } + } else { + dstRow[x * channels + 0] = r0; + dstRow[x * channels + 1] = g0; + dstRow[x * channels + 2] = b0; + + if (x + 1 < width) { + dstRow[(x + 1) * channels + 0] = r1; + dstRow[(x + 1) * channels + 1] = g1; + dstRow[(x + 1) * channels + 2] = b1; + } + } + + if constexpr (hasAlpha) { + dstRow[x * channels + 3] = 255; + if (x + 1 < width) { + dstRow[(x + 1) * channels + 3] = 255; + } + } + } + } +} + +template +AVX2_TARGET void uyvyToRgb_avx2_imp(const uint8_t* src, int srcStride, uint8_t* dst, int dstStride, int width, int height, bool is601) { + // 如果 height < 0,则反向写入 dst,src 顺序读取 + if (height < 0) { + height = -height; + dst = dst + (height - 1) * dstStride; + dstStride = -dstStride; + } + + // 根据标志选择系数 + int cy, cr, cgu, cgv, cb; + getYuvToRgbCoefficients(is601, isFullRange, cy, cr, cgu, cgv, cb); + + __m256i c_y = _mm256_set1_epi16(cy); + __m256i c_r = _mm256_set1_epi16(cr); + __m256i c_gu = _mm256_set1_epi16(cgu); + __m256i c_gv = _mm256_set1_epi16(cgv); + __m256i c_b = _mm256_set1_epi16(cb); + + __m256i c128 = _mm256_set1_epi16(128); + __m128i a8 = _mm_set1_epi8((char)255); + + constexpr int channels = hasAlpha ? 4 : 3; + const int vectorWidth = 16; // 处理16个像素(32字节UYVY数据) + + for (int y = 0; y < height; ++y) { + const uint8_t* srcRow = src + y * srcStride; + uint8_t* dstRow = dst + y * dstStride; + int x = 0; + + // AVX2 优化处理,每次处理16个像素(32字节UYVY数据) + for (; x + vectorWidth <= width; x += vectorWidth) { + // 1. 加载32字节UYVY数据 (16个像素 = 32字节) + __m256i uyvy_data = _mm256_loadu_si256((const __m256i*)(srcRow + x * 2)); + + // 2. 直接使用shuffle分离UYVY分量 + // UYVY格式: U0 Y0 V0 Y1 U1 Y2 V1 Y3 ... + // 使用正确的shuffle掩码,考虑AVX2的lane限制 + + // 创建正确的shuffle掩码(每个lane独立工作,索引范围0-15) + // 对于UYVY,Y在位置1,3,5,7... + __m256i shuffle_y = _mm256_setr_epi8( + 1, 3, 5, 7, 9, 11, 13, 15, // Lane 0: 提取Y0,Y1,Y2,Y3,Y4,Y5,Y6,Y7 + -1, -1, -1, -1, -1, -1, -1, -1, // Lane 0: 填充区域 + 1, 3, 5, 7, 9, 11, 13, 15, // Lane 1: 提取Y8,Y9,Y10,Y11,Y12,Y13,Y14,Y15 + -1, -1, -1, -1, -1, -1, -1, -1 // Lane 1: 填充区域 + ); + + // U分量:在位置0,4,8,12...,每个U对应两个Y(4:2:2子采样) + __m256i shuffle_u = _mm256_setr_epi8( + 0, 0, 4, 4, 8, 8, 12, 12, // Lane 0: U0,U0,U1,U1,U2,U2,U3,U3 + -1, -1, -1, -1, -1, -1, -1, -1, // Lane 0: 填充区域 + 0, 0, 4, 4, 8, 8, 12, 12, // Lane 1: U4,U4,U5,U5,U6,U6,U7,U7 + -1, -1, -1, -1, -1, -1, -1, -1 // Lane 1: 填充区域 + ); + + // V分量:在位置2,6,10,14...,每个V对应两个Y(4:2:2子采样) + __m256i shuffle_v = _mm256_setr_epi8( + 2, 2, 6, 6, 10, 10, 14, 14, // Lane 0: V0,V0,V1,V1,V2,V2,V3,V3 + -1, -1, -1, -1, -1, -1, -1, -1, // Lane 0: 填充区域 + 2, 2, 6, 6, 10, 10, 14, 14, // Lane 1: V4,V4,V5,V5,V6,V6,V7,V7 + -1, -1, -1, -1, -1, -1, -1, -1 // Lane 1: 填充区域 + ); + + // 执行shuffle分离 + __m256i y_shuffled = _mm256_shuffle_epi8(uyvy_data, shuffle_y); + __m256i u_shuffled = _mm256_shuffle_epi8(uyvy_data, shuffle_u); + __m256i v_shuffled = _mm256_shuffle_epi8(uyvy_data, shuffle_v); + + // 提取有效数据(低64位包含真实数据,高64位是填充的-1) + __m128i y_lo = _mm256_castsi256_si128(y_shuffled); // Lane 0的Y值 + __m128i y_hi = _mm256_extracti128_si256(y_shuffled, 1); // Lane 1的Y值 + __m128i u_lo = _mm256_castsi256_si128(u_shuffled); // Lane 0的U值 + __m128i u_hi = _mm256_extracti128_si256(u_shuffled, 1); // Lane 1的U值 + __m128i v_lo = _mm256_castsi256_si128(v_shuffled); // Lane 0的V值 + __m128i v_hi = _mm256_extracti128_si256(v_shuffled, 1); // Lane 1的V值 + + // 合并两个lane的数据,只取有效的前8字节 + __m128i y_final = _mm_unpacklo_epi64(y_lo, y_hi); // Y0-Y7 + Y8-Y15 + __m128i u_final = _mm_unpacklo_epi64(u_lo, u_hi); // U0,U0,U1,U1...U3,U3 + U4,U4,U5,U5...U7,U7 + __m128i v_final = _mm_unpacklo_epi64(v_lo, v_hi); // V0,V0,V1,V1...V3,V3 + V4,V4,V5,V5...V7,V7 + + // 转换为16位整数 + __m256i y_16 = _mm256_cvtepu8_epi16(y_final); + __m256i u_16 = _mm256_cvtepu8_epi16(u_final); + __m256i v_16 = _mm256_cvtepu8_epi16(v_final); + + // 3. YUV偏移处理 + u_16 = _mm256_sub_epi16(u_16, c128); + v_16 = _mm256_sub_epi16(v_16, c128); + + if constexpr (!isFullRange) { + y_16 = _mm256_sub_epi16(y_16, _mm256_set1_epi16(16)); + } + + // 4. YUV到RGB转换 + __m256i y_scaled = _mm256_mullo_epi16(y_16, c_y); + + __m256i r = _mm256_add_epi16(y_scaled, _mm256_mullo_epi16(v_16, c_r)); + r = _mm256_add_epi16(r, _mm256_set1_epi16(32)); + r = _mm256_srai_epi16(r, 6); + + __m256i g = _mm256_sub_epi16(y_scaled, _mm256_mullo_epi16(u_16, c_gu)); + g = _mm256_sub_epi16(g, _mm256_mullo_epi16(v_16, c_gv)); + g = _mm256_add_epi16(g, _mm256_set1_epi16(32)); + g = _mm256_srai_epi16(g, 6); + + __m256i b = _mm256_add_epi16(y_scaled, _mm256_mullo_epi16(u_16, c_b)); + b = _mm256_add_epi16(b, _mm256_set1_epi16(32)); + b = _mm256_srai_epi16(b, 6); + + // 5. 钳制到0-255范围 + __m256i zero = _mm256_setzero_si256(); + __m256i maxv = _mm256_set1_epi16(255); + r = _mm256_max_epi16(zero, _mm256_min_epi16(r, maxv)); + g = _mm256_max_epi16(zero, _mm256_min_epi16(g, maxv)); + b = _mm256_max_epi16(zero, _mm256_min_epi16(b, maxv)); + + // 6. 转换为8位并打包输出 + __m128i r8 = _mm_packus_epi16(_mm256_castsi256_si128(r), _mm256_extracti128_si256(r, 1)); + __m128i g8 = _mm_packus_epi16(_mm256_castsi256_si128(g), _mm256_extracti128_si256(g, 1)); + __m128i b8 = _mm_packus_epi16(_mm256_castsi256_si128(b), _mm256_extracti128_si256(b, 1)); + + // 7. 根据输出格式存储 + if constexpr (hasAlpha) { + if constexpr (isBgrColor) { + // BGRA格式 + __m128i bg0 = _mm_unpacklo_epi8(b8, g8); + __m128i ra0 = _mm_unpacklo_epi8(r8, a8); + __m128i bgra0 = _mm_unpacklo_epi16(bg0, ra0); + __m128i bgra1 = _mm_unpackhi_epi16(bg0, ra0); + + __m128i bg1 = _mm_unpackhi_epi8(b8, g8); + __m128i ra1 = _mm_unpackhi_epi8(r8, a8); + __m128i bgra2 = _mm_unpacklo_epi16(bg1, ra1); + __m128i bgra3 = _mm_unpackhi_epi16(bg1, ra1); + + _mm_storeu_si128((__m128i*)(dstRow + x * 4), bgra0); + _mm_storeu_si128((__m128i*)(dstRow + x * 4 + 16), bgra1); + _mm_storeu_si128((__m128i*)(dstRow + x * 4 + 32), bgra2); + _mm_storeu_si128((__m128i*)(dstRow + x * 4 + 48), bgra3); + } else { + // RGBA格式 + __m128i rg0 = _mm_unpacklo_epi8(r8, g8); + __m128i ba0 = _mm_unpacklo_epi8(b8, a8); + __m128i rgba0 = _mm_unpacklo_epi16(rg0, ba0); + __m128i rgba1 = _mm_unpackhi_epi16(rg0, ba0); + + __m128i rg1 = _mm_unpackhi_epi8(r8, g8); + __m128i ba1 = _mm_unpackhi_epi8(b8, a8); + __m128i rgba2 = _mm_unpacklo_epi16(rg1, ba1); + __m128i rgba3 = _mm_unpackhi_epi16(rg1, ba1); + + _mm_storeu_si128((__m128i*)(dstRow + x * 4), rgba0); + _mm_storeu_si128((__m128i*)(dstRow + x * 4 + 16), rgba1); + _mm_storeu_si128((__m128i*)(dstRow + x * 4 + 32), rgba2); + _mm_storeu_si128((__m128i*)(dstRow + x * 4 + 48), rgba3); + } + } else { + // RGB24或BGR24格式 - 使用标量存储避免复杂的3字节打包 + uint8_t r_vals[16], g_vals[16], b_vals[16]; + _mm_storeu_si128((__m128i*)r_vals, r8); + _mm_storeu_si128((__m128i*)g_vals, g8); + _mm_storeu_si128((__m128i*)b_vals, b8); + + for (int i = 0; i < 16 && (x + i) < width; ++i) { + if constexpr (isBgrColor) { + dstRow[(x + i) * 3 + 0] = b_vals[i]; + dstRow[(x + i) * 3 + 1] = g_vals[i]; + dstRow[(x + i) * 3 + 2] = r_vals[i]; + } else { + dstRow[(x + i) * 3 + 0] = r_vals[i]; + dstRow[(x + i) * 3 + 1] = g_vals[i]; + dstRow[(x + i) * 3 + 2] = b_vals[i]; + } + } + } + } + + // 处理剩余像素(标量实现) + YuvToRgbFunc convertFunc = getYuvToRgbFunc(is601, isFullRange); + for (; x < width; x += 2) { + if (x + 1 >= width) break; // UYVY需要成对处理 + + // UYVY format: U0 Y0 V0 Y1 (4 bytes for 2 pixels) + int baseIdx = x * 2; + int u = srcRow[baseIdx + 0]; // U0 + int y0 = srcRow[baseIdx + 1]; // Y0 + int v = srcRow[baseIdx + 2]; // V0 + int y1 = srcRow[baseIdx + 3]; // Y1 + + int r0, g0, b0, r1, g1, b1; + convertFunc(y0, u, v, r0, g0, b0); + convertFunc(y1, u, v, r1, g1, b1); + + if constexpr (isBgrColor) { + dstRow[x * channels + 0] = b0; + dstRow[x * channels + 1] = g0; + dstRow[x * channels + 2] = r0; + + if (x + 1 < width) { + dstRow[(x + 1) * channels + 0] = b1; + dstRow[(x + 1) * channels + 1] = g1; + dstRow[(x + 1) * channels + 2] = r1; + } + } else { + dstRow[x * channels + 0] = r0; + dstRow[x * channels + 1] = g0; + dstRow[x * channels + 2] = b0; + + if (x + 1 < width) { + dstRow[(x + 1) * channels + 0] = r1; + dstRow[(x + 1) * channels + 1] = g1; + dstRow[(x + 1) * channels + 2] = b1; + } + } + + if constexpr (hasAlpha) { + dstRow[x * channels + 3] = 255; + if (x + 1 < width) { + dstRow[(x + 1) * channels + 3] = 255; + } + } + } + } +} + +// YUYV conversion functions +AVX2_TARGET +void yuyvToBgr24_avx2(const uint8_t* src, int srcStride, uint8_t* dst, int dstStride, int width, int height, ConvertFlag flag) { + const bool is601 = (flag & ConvertFlag::BT601) != 0; + const bool isFullRange = (flag & ConvertFlag::FullRange) != 0; + + if (isFullRange) { + yuyvToRgb_avx2_imp(src, srcStride, dst, dstStride, width, height, is601); + } else { + yuyvToRgb_avx2_imp(src, srcStride, dst, dstStride, width, height, is601); + } +} + +AVX2_TARGET +void yuyvToRgb24_avx2(const uint8_t* src, int srcStride, uint8_t* dst, int dstStride, int width, int height, ConvertFlag flag) { + const bool is601 = (flag & ConvertFlag::BT601) != 0; + const bool isFullRange = (flag & ConvertFlag::FullRange) != 0; + + if (isFullRange) { + yuyvToRgb_avx2_imp(src, srcStride, dst, dstStride, width, height, is601); + } else { + yuyvToRgb_avx2_imp(src, srcStride, dst, dstStride, width, height, is601); + } +} + +AVX2_TARGET +void yuyvToBgra32_avx2(const uint8_t* src, int srcStride, uint8_t* dst, int dstStride, int width, int height, ConvertFlag flag) { + const bool is601 = (flag & ConvertFlag::BT601) != 0; + const bool isFullRange = (flag & ConvertFlag::FullRange) != 0; + + if (isFullRange) { + yuyvToRgb_avx2_imp(src, srcStride, dst, dstStride, width, height, is601); + } else { + yuyvToRgb_avx2_imp(src, srcStride, dst, dstStride, width, height, is601); + } +} + +AVX2_TARGET +void yuyvToRgba32_avx2(const uint8_t* src, int srcStride, uint8_t* dst, int dstStride, int width, int height, ConvertFlag flag) { + const bool is601 = (flag & ConvertFlag::BT601) != 0; + const bool isFullRange = (flag & ConvertFlag::FullRange) != 0; + + if (isFullRange) { + yuyvToRgb_avx2_imp(src, srcStride, dst, dstStride, width, height, is601); + } else { + yuyvToRgb_avx2_imp(src, srcStride, dst, dstStride, width, height, is601); + } +} + +// UYVY conversion functions +AVX2_TARGET +void uyvyToBgr24_avx2(const uint8_t* src, int srcStride, uint8_t* dst, int dstStride, int width, int height, ConvertFlag flag) { + const bool is601 = (flag & ConvertFlag::BT601) != 0; + const bool isFullRange = (flag & ConvertFlag::FullRange) != 0; + + if (isFullRange) { + uyvyToRgb_avx2_imp(src, srcStride, dst, dstStride, width, height, is601); + } else { + uyvyToRgb_avx2_imp(src, srcStride, dst, dstStride, width, height, is601); + } +} + +AVX2_TARGET +void uyvyToRgb24_avx2(const uint8_t* src, int srcStride, uint8_t* dst, int dstStride, int width, int height, ConvertFlag flag) { + const bool is601 = (flag & ConvertFlag::BT601) != 0; + const bool isFullRange = (flag & ConvertFlag::FullRange) != 0; + + if (isFullRange) { + uyvyToRgb_avx2_imp(src, srcStride, dst, dstStride, width, height, is601); + } else { + uyvyToRgb_avx2_imp(src, srcStride, dst, dstStride, width, height, is601); + } +} + +AVX2_TARGET +void uyvyToBgra32_avx2(const uint8_t* src, int srcStride, uint8_t* dst, int dstStride, int width, int height, ConvertFlag flag) { + const bool is601 = (flag & ConvertFlag::BT601) != 0; + const bool isFullRange = (flag & ConvertFlag::FullRange) != 0; + + if (isFullRange) { + uyvyToRgb_avx2_imp(src, srcStride, dst, dstStride, width, height, is601); + } else { + uyvyToRgb_avx2_imp(src, srcStride, dst, dstStride, width, height, is601); + } +} + +AVX2_TARGET +void uyvyToRgba32_avx2(const uint8_t* src, int srcStride, uint8_t* dst, int dstStride, int width, int height, ConvertFlag flag) { + const bool is601 = (flag & ConvertFlag::BT601) != 0; + const bool isFullRange = (flag & ConvertFlag::FullRange) != 0; + + if (isFullRange) { + uyvyToRgb_avx2_imp(src, srcStride, dst, dstStride, width, height, is601); + } else { + uyvyToRgb_avx2_imp(src, srcStride, dst, dstStride, width, height, is601); + } +} + #endif // ENABLE_AVX2_IMP } // namespace ccap diff --git a/src/ccap_convert_avx2.h b/src/ccap_convert_avx2.h index 651b8323..2de73e5d 100644 --- a/src/ccap_convert_avx2.h +++ b/src/ccap_convert_avx2.h @@ -20,7 +20,8 @@ #ifndef ENABLE_AVX2_IMP #if ((defined(_MSC_VER) || defined(_WIN32)) && !defined(__arm__) && !defined(__aarch64__) && !defined(_M_ARM) && !defined(_M_ARM64)) || \ (defined(__APPLE__) && defined(__x86_64__) && \ - !((defined(TARGET_OS_IOS) && TARGET_OS_IOS) || (defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE))) + !((defined(TARGET_OS_IOS) && TARGET_OS_IOS) || (defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE))) || \ + (defined(__linux__) && defined(__x86_64__)) #define ENABLE_AVX2_IMP 1 #else #define ENABLE_AVX2_IMP 0 @@ -30,8 +31,13 @@ namespace ccap { bool hasAVX2(); +bool canUseAVX2(); // 检查 AVX2 是否可用(硬件支持且未被禁用) + bool enableAVX2(bool enable); // Disable AVX2 implementation, useful for testing +// 获取详细的 AVX2 支持信息(用于调试) +const char* getAVX2SupportInfo(); + #if ENABLE_AVX2_IMP template @@ -90,6 +96,46 @@ void i420ToRgb24_avx2(const uint8_t* srcY, int srcYStride, const uint8_t* srcV, int srcVStride, uint8_t* dst, int dstStride, int width, int height, ConvertFlag flag); + +// YUYV to BGR24, AVX2 accelerated +void yuyvToBgr24_avx2(const uint8_t* src, int srcStride, + uint8_t* dst, int dstStride, + int width, int height, ConvertFlag flag); + +// YUYV to RGB24, AVX2 accelerated +void yuyvToRgb24_avx2(const uint8_t* src, int srcStride, + uint8_t* dst, int dstStride, + int width, int height, ConvertFlag flag); + +// YUYV to BGRA32, AVX2 accelerated +void yuyvToBgra32_avx2(const uint8_t* src, int srcStride, + uint8_t* dst, int dstStride, + int width, int height, ConvertFlag flag); + +// YUYV to RGBA32, AVX2 accelerated +void yuyvToRgba32_avx2(const uint8_t* src, int srcStride, + uint8_t* dst, int dstStride, + int width, int height, ConvertFlag flag); + +// UYVY to BGR24, AVX2 accelerated +void uyvyToBgr24_avx2(const uint8_t* src, int srcStride, + uint8_t* dst, int dstStride, + int width, int height, ConvertFlag flag); + +// UYVY to RGB24, AVX2 accelerated +void uyvyToRgb24_avx2(const uint8_t* src, int srcStride, + uint8_t* dst, int dstStride, + int width, int height, ConvertFlag flag); + +// UYVY to BGRA32, AVX2 accelerated +void uyvyToBgra32_avx2(const uint8_t* src, int srcStride, + uint8_t* dst, int dstStride, + int width, int height, ConvertFlag flag); + +// UYVY to RGBA32, AVX2 accelerated +void uyvyToRgba32_avx2(const uint8_t* src, int srcStride, + uint8_t* dst, int dstStride, + int width, int height, ConvertFlag flag); #else #define nv12ToBgr24_avx2(...) assert(0 && "AVX2 not supported") @@ -100,6 +146,14 @@ void i420ToRgb24_avx2(const uint8_t* srcY, int srcYStride, #define i420ToRgba32_avx2(...) assert(0 && "AVX2 not supported") #define i420ToBgr24_avx2(...) assert(0 && "AVX2 not supported") #define i420ToRgb24_avx2(...) assert(0 && "AVX2 not supported") +#define yuyvToBgr24_avx2(...) assert(0 && "AVX2 not supported") +#define yuyvToRgb24_avx2(...) assert(0 && "AVX2 not supported") +#define yuyvToBgra32_avx2(...) assert(0 && "AVX2 not supported") +#define yuyvToRgba32_avx2(...) assert(0 && "AVX2 not supported") +#define uyvyToBgr24_avx2(...) assert(0 && "AVX2 not supported") +#define uyvyToRgb24_avx2(...) assert(0 && "AVX2 not supported") +#define uyvyToBgra32_avx2(...) assert(0 && "AVX2 not supported") +#define uyvyToRgba32_avx2(...) assert(0 && "AVX2 not supported") #endif diff --git a/src/ccap_convert_frame.cpp b/src/ccap_convert_frame.cpp index 1df3a155..76d99360 100644 --- a/src/ccap_convert_frame.cpp +++ b/src/ccap_convert_frame.cpp @@ -15,13 +15,15 @@ #include namespace ccap { -bool inplaceConvertFrameYUV2RGBColor(VideoFrame* frame, PixelFormat toFormat, bool verticalFlip) { /// (NV12/I420) -> (BGR24/BGRA32) +bool inplaceConvertFrameYUV2RGBColor(VideoFrame* frame, PixelFormat toFormat, bool verticalFlip) { /// (NV12/I420/YUYV/UYVY) -> (BGR24/BGRA32) /// TODO: 这里修正一下 toFormat, 只支持 YUV -> (BGR24/BGRA24). 简化一下 SDK 的设计. 后续再完善. auto inputFormat = frame->pixelFormat; assert((inputFormat & kPixelFormatYUVColorBit) != 0 && (toFormat & kPixelFormatYUVColorBit) == 0); bool isInputNV12 = pixelFormatInclude(inputFormat, PixelFormat::NV12); + bool isInputYUYV = pixelFormatInclude(inputFormat, PixelFormat::YUYV); + bool isInputUYVY = pixelFormatInclude(inputFormat, PixelFormat::UYVY); bool outputHasAlpha = toFormat & kPixelFormatAlphaColorBit; bool isOutputBGR = toFormat & kPixelFormatBGRBit; // 不是 BGR 就是 RGB @@ -48,54 +50,70 @@ bool inplaceConvertFrameYUV2RGBColor(VideoFrame* frame, PixelFormat toFormat, bo if (isInputNV12) { // NV12 -> BGR24, libyuv 里面的 RGB24 实际上是 BGR24 if (outputHasAlpha) { -#if ENABLE_LIBYUV - return libyuv::NV12ToARGB(inputData0, stride0, inputData1, stride1, frame->data[0], newLineSize, width, height) == 0; -#else if (isOutputBGR) { nv12ToBgra32(inputData0, stride0, inputData1, stride1, frame->data[0], newLineSize, width, height); } else { nv12ToRgba32(inputData0, stride0, inputData1, stride1, frame->data[0], newLineSize, width, height); } return true; -#endif } else { -#if ENABLE_LIBYUV - return libyuv::NV12ToRGB24(inputData0, stride0, inputData1, stride1, frame->data[0], newLineSize, width, height) == 0; -#else if (isOutputBGR) { nv12ToBgr24(inputData0, stride0, inputData1, stride1, frame->data[0], newLineSize, width, height); } else { nv12ToRgb24(inputData0, stride0, inputData1, stride1, frame->data[0], newLineSize, width, height); } return true; -#endif + } + } else if (isInputYUYV) { // YUYV -> BGR24/BGRA32 + + if (outputHasAlpha) { + if (isOutputBGR) { + yuyvToBgra32(inputData0, stride0, frame->data[0], newLineSize, width, height); + } else { + yuyvToRgba32(inputData0, stride0, frame->data[0], newLineSize, width, height); + } + return true; + } else { + if (isOutputBGR) { + yuyvToBgr24(inputData0, stride0, frame->data[0], newLineSize, width, height); + } else { + yuyvToRgb24(inputData0, stride0, frame->data[0], newLineSize, width, height); + } + return true; + } + } else if (isInputUYVY) { // UYVY -> BGR24/BGRA32 + + if (outputHasAlpha) { + if (isOutputBGR) { + uyvyToBgra32(inputData0, stride0, frame->data[0], newLineSize, width, height); + } else { + uyvyToRgba32(inputData0, stride0, frame->data[0], newLineSize, width, height); + } + return true; + } else { + if (isOutputBGR) { + uyvyToBgr24(inputData0, stride0, frame->data[0], newLineSize, width, height); + } else { + uyvyToRgb24(inputData0, stride0, frame->data[0], newLineSize, width, height); + } + return true; } } else { // I420 -> BGR24 if (outputHasAlpha) { -#if ENABLE_LIBYUV - return libyuv::I420ToARGB(inputData0, stride0, inputData1, stride1, inputData2, stride2, frame->data[0], newLineSize, width, - height) == 0; -#else if (isOutputBGR) { i420ToBgra32(inputData0, stride0, inputData1, stride1, inputData2, stride2, frame->data[0], newLineSize, width, height); } else { i420ToRgba32(inputData0, stride0, inputData1, stride1, inputData2, stride2, frame->data[0], newLineSize, width, height); } return true; -#endif } else { -#if ENABLE_LIBYUV - return libyuv::I420ToRGB24(inputData0, stride0, inputData1, stride1, inputData2, stride2, frame->data[0], newLineSize, width, - height) == 0; -#else if (isOutputBGR) { i420ToBgr24(inputData0, stride0, inputData1, stride1, inputData2, stride2, frame->data[0], newLineSize, width, height); } else { i420ToRgb24(inputData0, stride0, inputData1, stride1, inputData2, stride2, frame->data[0], newLineSize, width, height); } return true; -#endif } } @@ -162,7 +180,7 @@ bool inplaceConvertFrameRGB(VideoFrame* frame, PixelFormat toFormat, bool vertic return true; } -bool inplaceConvertFrame(VideoFrame* frame, PixelFormat toFormat, bool verticalFlip) { +inline bool inplaceConvertFrameImp(VideoFrame* frame, PixelFormat toFormat, bool verticalFlip) { if (frame->pixelFormat == toFormat) { if (verticalFlip && (toFormat & kPixelFormatRGBColorBit)) { // flip upside down int srcStride = (int)frame->stride[0]; @@ -204,4 +222,14 @@ bool inplaceConvertFrame(VideoFrame* frame, PixelFormat toFormat, bool verticalF return inplaceConvertFrameRGB(frame, toFormat, verticalFlip); } +bool inplaceConvertFrame(VideoFrame* frame, PixelFormat toFormat, bool verticalFlip) { + auto ret = inplaceConvertFrameImp(frame, toFormat, verticalFlip); + if (ret) { + assert(frame->pixelFormat == toFormat); + assert(frame->allocator != nullptr && frame->data[0] == frame->allocator->data()); + frame->sizeInBytes = frame->allocator->size(); + } + return ret; +} + } // namespace ccap diff --git a/src/ccap_core.cpp b/src/ccap_core.cpp index fd21ce6c..a6fb4b8f 100644 --- a/src/ccap_core.cpp +++ b/src/ccap_core.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #ifdef _MSC_VER #include @@ -31,6 +32,7 @@ namespace ccap { ProviderImp* createProviderApple(); ProviderImp* createProviderDirectShow(); +ProviderImp* createProviderV4L2(); Allocator::~Allocator() { CCAP_LOG_V("ccap: Allocator::~Allocator() called, this=%p\n", this); } @@ -55,11 +57,42 @@ void DefaultAllocator::resize(size_t size) { VideoFrame::VideoFrame() = default; VideoFrame::~VideoFrame() { CCAP_LOG_V("ccap: VideoFrame::VideoFrameFrame() called, this=%p\n", this); } +void VideoFrame::detach() { + if (!allocator || data[0] != allocator->data()) { + if (!allocator) { + allocator = std::make_shared(); + } + + allocator->resize(sizeInBytes); + // Copy data to allocator + std::memcpy(allocator->data(), data[0], sizeInBytes); + + // Update data pointers + data[0] = allocator->data(); + if (stride[1] > 0) { + data[1] = data[0] + stride[0] * height; + if (stride[2] > 0) { + // Currently, only I420 needs to use data[2] + data[2] = data[1] + stride[1] * height / 2; + } else { + data[2] = nullptr; + } + } else { + data[1] = nullptr; + data[2] = nullptr; + } + + nativeHandle = nullptr; // Detach native handle + } +} + ProviderImp* createProvider(std::string_view extraInfo) { #if __APPLE__ return createProviderApple(); #elif defined(_MSC_VER) || defined(_WIN32) return createProviderDirectShow(); +#elif defined(__linux__) || defined(__linux) || defined(linux) || defined(__gnu_linux__) + return createProviderV4L2(); #else if (warningLogEnabled()) { CCAP_LOG_W("ccap: Unsupported platform!\n"); diff --git a/src/ccap_imp.cpp b/src/ccap_imp.cpp index a032dd4b..3b2d3241 100644 --- a/src/ccap_imp.cpp +++ b/src/ccap_imp.cpp @@ -8,9 +8,9 @@ #include "ccap_imp.h" +#include #include #include -#include namespace ccap { void resetSharedAllocator(); diff --git a/src/ccap_imp_apple.mm b/src/ccap_imp_apple.mm index e729e443..addd5dd0 100644 --- a/src/ccap_imp_apple.mm +++ b/src/ccap_imp_apple.mm @@ -753,22 +753,7 @@ - (void)captureOutput:(AVCaptureOutput*)output bool zeroCopy = ((internalFormat & kPixelFormatYUVColorBit) && (outputFormat & kPixelFormatYUVColorBit)) || (internalFormat == outputFormat && _provider->frameOrientation() == kDefaultFrameOrientation); - if (zeroCopy) { - newFrame->orientation = kDefaultFrameOrientation; - CFRetain(imageBuffer); - auto manager = std::make_shared([imageBuffer, newFrame]() mutable { - CVPixelBufferUnlockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly); - CFRelease(imageBuffer); - CCAP_NSLOG_V(@"ccap: recycled frame, width: %d, height: %d", (int)newFrame->width, (int)newFrame->height); - - newFrame->nativeHandle = nullptr; - newFrame = nullptr; - }); - - auto fakeFrame = std::shared_ptr(manager, newFrame.get()); - newFrame = fakeFrame; - } else /// yuv/rgb color -> rgb color - { + if (!zeroCopy) { newFrame->orientation = _provider->frameOrientation(); if (!newFrame->allocator) { @@ -782,7 +767,7 @@ - (void)captureOutput:(AVCaptureOutput*)output startConvertTime = std::chrono::steady_clock::now(); } - inplaceConvertFrame(newFrame.get(), outputFormat, (int)(newFrame->orientation != kDefaultFrameOrientation)); + zeroCopy = !inplaceConvertFrame(newFrame.get(), outputFormat, (int)(newFrame->orientation != kDefaultFrameOrientation)); CVPixelBufferUnlockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly); @@ -813,6 +798,22 @@ - (void)captureOutput:(AVCaptureOutput*)output } } + if (zeroCopy) { + newFrame->orientation = kDefaultFrameOrientation; + CFRetain(imageBuffer); + auto manager = std::make_shared([imageBuffer, newFrame]() mutable { + CVPixelBufferUnlockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly); + CFRelease(imageBuffer); + CCAP_NSLOG_V(@"ccap: recycled frame, width: %d, height: %d", (int)newFrame->width, (int)newFrame->height); + + newFrame->nativeHandle = nullptr; + newFrame = nullptr; + }); + + auto fakeFrame = std::shared_ptr(manager, newFrame.get()); + newFrame = fakeFrame; + } + newFrame->frameIndex = _provider->frameIndex()++; if (verboseLogEnabled()) { /// Generally, camera interfaces are not called in multiple threads, and verbose logs are only for debugging, diff --git a/src/ccap_imp_linux.cpp b/src/ccap_imp_linux.cpp new file mode 100644 index 00000000..300a6a2a --- /dev/null +++ b/src/ccap_imp_linux.cpp @@ -0,0 +1,759 @@ +/** + * @file ccap_imp_linux.cpp + * @author wysaid (this@wysaid.org) + * @brief Linux implementation of ccap::Provider class using V4L2. + * @date 2025-04 + * + */ + +#if defined(__linux__) || defined(__linux) || defined(linux) || defined(__gnu_linux__) + +#include "ccap_imp_linux.h" + +#include "ccap_convert_frame.h" +#include "ccap_utils.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ccap { + +// Supported V4L2 pixel formats mapping +const std::vector ProviderV4L2::s_supportedV4L2Formats = { + { V4L2_PIX_FMT_YUYV, PixelFormat::YUYV, "YUYV" }, + { V4L2_PIX_FMT_UYVY, PixelFormat::UYVY, "UYVY" }, + { V4L2_PIX_FMT_NV12, PixelFormat::NV12, "NV12" }, + { V4L2_PIX_FMT_YUV420, PixelFormat::I420, "YUV420" }, + { V4L2_PIX_FMT_RGB24, PixelFormat::RGB24, "RGB24" }, + { V4L2_PIX_FMT_BGR24, PixelFormat::BGR24, "BGR24" }, + { V4L2_PIX_FMT_RGB32, PixelFormat::RGBA32, "RGB32" }, + { V4L2_PIX_FMT_BGR32, PixelFormat::BGRA32, "BGR32" }, + { V4L2_PIX_FMT_MJPEG, PixelFormat::Unknown, "MJPEG" }, +}; + +ProviderV4L2::ProviderV4L2() { + CCAP_LOG_V("ccap: ProviderV4L2 created\n"); + m_lifeHolder = std::make_shared(1); // Keep the provider alive while frames are being processed +} + +ProviderV4L2::~ProviderV4L2() { + std::weak_ptr holder = m_lifeHolder; + m_lifeHolder.reset(); // Release the life holder to allow cleanup + while (!holder.expired()) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); // Wait for cleanup + CCAP_LOG_W("ccap: life holder is in use, waiting for cleanup...\n"); + } + + close(); + CCAP_LOG_V("ccap: ProviderV4L2 destroyed\n"); +} + +std::vector ProviderV4L2::findDeviceNames() { + std::vector deviceNames; + + // Scan /dev/video* devices + for (const auto& entry : std::filesystem::directory_iterator("/dev")) { + const std::string filename = entry.path().filename().string(); + if (filename.find("video") == 0) { + std::string devicePath = entry.path().string(); + if (isVideoDevice(devicePath)) { + std::string description = getDeviceDescription(devicePath); + if (!description.empty()) { + deviceNames.push_back(std::move(description)); + } else { + deviceNames.push_back(devicePath); + } + CCAP_LOG_I("ccap: Found video device: %s -> %s\n", devicePath.c_str(), deviceNames.back().c_str()); + } + } + } + + return deviceNames; +} + +bool ProviderV4L2::open(std::string_view deviceName) { + if (m_isOpened) { + CCAP_LOG_E("ccap: Device already opened\n"); + return false; + } + + // Find device path + if (deviceName.empty()) { + // Use first available device + auto devices = findDeviceNames(); + if (devices.empty()) { + CCAP_LOG_E("ccap: No video devices found\n"); + return false; + } + m_deviceName = devices[0]; + m_devicePath = "/dev/video0"; // Default to first device + } else { + m_deviceName = deviceName; + // Try to find device path by name + bool found = false; + for (const auto& entry : std::filesystem::directory_iterator("/dev")) { + const std::string filename = entry.path().filename().string(); + if (filename.find("video") == 0) { + std::string devicePath = entry.path().string(); + std::string description = getDeviceDescription(devicePath); + if (description == deviceName || devicePath == deviceName) { + m_devicePath = devicePath; + found = true; + break; + } + } + } + if (!found) { + CCAP_LOG_E("ccap: Device not found: %s\n", deviceName.data()); + return false; + } + } + + // Open device + m_fd = ::open(m_devicePath.c_str(), O_RDWR | O_NONBLOCK); + if (m_fd < 0) { + CCAP_LOG_E("ccap: Failed to open device %s: %s\n", m_devicePath.c_str(), strerror(errno)); + return false; + } + + if (!setupDevice()) { + ::close(m_fd); + m_fd = -1; + return false; + } + + m_isOpened = true; + CCAP_LOG_I("ccap: Successfully opened device: %s\n", m_deviceName.c_str()); + return true; +} + +bool ProviderV4L2::isOpened() const { + return m_isOpened && m_fd >= 0; +} + +std::optional ProviderV4L2::getDeviceInfo() const { + if (!isOpened()) { + return std::nullopt; + } + + DeviceInfo info; + info.deviceName = m_deviceName; + + // Get supported pixel formats + for (const auto& format : m_supportedFormats) { + if (format.ccapFormat != PixelFormat::Unknown) { + info.supportedPixelFormats.push_back(format.ccapFormat); + } + } + + info.supportedResolutions = m_supportedResolutions; + + return info; +} + +void ProviderV4L2::close() { + if (isStarted()) { + stop(); + } + + if (m_fd >= 0) { + ::close(m_fd); + m_fd = -1; + } + + m_isOpened = false; + m_isStreaming = false; + + CCAP_LOG_V("ccap: Device closed\n"); +} + +bool ProviderV4L2::start() { + if (!isOpened()) { + CCAP_LOG_E("ccap: Device not opened\n"); + return false; + } + + if (m_isStreaming) { + CCAP_LOG_W("ccap: Already streaming\n"); + return true; + } + + if (!negotiateFormat() || !allocateBuffers() || !startStreaming()) { + return false; + } + + m_shouldStop = false; + m_startTime = std::chrono::steady_clock::now(); + m_frameIndex = 0; + + // Start capture thread + m_captureThread = std::make_unique(&ProviderV4L2::captureThread, this); + + m_isStreaming = true; + CCAP_LOG_I("ccap: Streaming started\n"); + return true; +} + +void ProviderV4L2::stop() { + if (!m_isStreaming) { + return; + } + + m_shouldStop = true; + + // Wait for capture thread to finish + if (m_captureThread && m_captureThread->joinable()) { + m_captureThread->join(); + m_captureThread.reset(); + } + + stopStreaming(); + releaseBuffers(); + + m_isStreaming = false; + CCAP_LOG_I("ccap: Streaming stopped\n"); +} + +bool ProviderV4L2::isStarted() const { + return m_isStreaming && !m_shouldStop; +} + +// Private implementation methods + +bool ProviderV4L2::setupDevice() { + if (!queryCapabilities()) { + return false; + } + + if (!enumerateFormats()) { + return false; + } + + return true; +} + +bool ProviderV4L2::queryCapabilities() { + if (ioctl(m_fd, VIDIOC_QUERYCAP, &m_caps) < 0) { + CCAP_LOG_E("ccap: VIDIOC_QUERYCAP failed: %s\n", strerror(errno)); + return false; + } + + if (!(m_caps.capabilities & V4L2_CAP_VIDEO_CAPTURE)) { + CCAP_LOG_E("ccap: Device does not support video capture\n"); + return false; + } + + if (!(m_caps.capabilities & V4L2_CAP_STREAMING)) { + CCAP_LOG_E("ccap: Device does not support streaming\n"); + return false; + } + + CCAP_LOG_V("ccap: Device capabilities: %s\n", m_caps.card); + return true; +} + +bool ProviderV4L2::enumerateFormats() { + m_supportedFormats.clear(); + + struct v4l2_fmtdesc fmt = {}; + fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + + for (fmt.index = 0; ioctl(m_fd, VIDIOC_ENUM_FMT, &fmt) == 0; fmt.index++) { + // Find matching format + for (const auto& supportedFormat : s_supportedV4L2Formats) { + if (supportedFormat.pixelformat == fmt.pixelformat) { + m_supportedFormats.push_back(supportedFormat); + CCAP_LOG_V("ccap: Supported format: %s (%c%c%c%c)\n", + supportedFormat.name, + fmt.pixelformat & 0xFF, + (fmt.pixelformat >> 8) & 0xFF, + (fmt.pixelformat >> 16) & 0xFF, + (fmt.pixelformat >> 24) & 0xFF); + + // Get supported resolutions for this format + auto resolutions = getSupportedResolutions(fmt.pixelformat); + m_supportedResolutions.insert(m_supportedResolutions.end(), + resolutions.begin(), resolutions.end()); + break; + } + } + } + + return !m_supportedFormats.empty(); +} + +std::vector ProviderV4L2::getSupportedResolutions(uint32_t pixelformat) { + std::vector resolutions; + + struct v4l2_frmsizeenum framesize = {}; + framesize.pixel_format = pixelformat; + + for (framesize.index = 0; ioctl(m_fd, VIDIOC_ENUM_FRAMESIZES, &framesize) == 0; framesize.index++) { + if (framesize.type == V4L2_FRMSIZE_TYPE_DISCRETE) { + resolutions.push_back({ framesize.discrete.width, framesize.discrete.height }); + } else if (framesize.type == V4L2_FRMSIZE_TYPE_STEPWISE) { + // Add some common resolutions within the range + uint32_t commonWidths[] = { 320, 640, 800, 1024, 1280, 1920, 2560, 3840 }; + uint32_t commonHeights[] = { 240, 480, 600, 768, 720, 1080, 1440, 2160 }; + + for (size_t i = 0; i < sizeof(commonWidths) / sizeof(commonWidths[0]); i++) { + if (commonWidths[i] >= framesize.stepwise.min_width && + commonWidths[i] <= framesize.stepwise.max_width && + commonHeights[i] >= framesize.stepwise.min_height && + commonHeights[i] <= framesize.stepwise.max_height) { + resolutions.push_back({ commonWidths[i], commonHeights[i] }); + } + } + } + } + + return resolutions; +} + +bool ProviderV4L2::negotiateFormat() { + // Get current format + m_currentFormat.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + if (ioctl(m_fd, VIDIOC_G_FMT, &m_currentFormat) < 0) { + CCAP_LOG_E("ccap: VIDIOC_G_FMT failed: %s\n", strerror(errno)); + return false; + } + + // Set desired format if specified + bool formatChanged = false; + auto& pix = m_currentFormat.fmt.pix; + + if (m_frameProp.width > 0 && m_frameProp.height > 0) { + if (pix.width != m_frameProp.width || pix.height != m_frameProp.height) { + pix.width = m_frameProp.width; + pix.height = m_frameProp.height; + formatChanged = true; + } + } + + // Try to set a supported format + if (m_frameProp.cameraPixelFormat != PixelFormat::Unknown) { + uint32_t v4l2Format = ccapFormatToV4l2Format(m_frameProp.cameraPixelFormat); + if (v4l2Format != 0 && pix.pixelformat != v4l2Format) { + pix.pixelformat = v4l2Format; + formatChanged = true; + } + } + + // Apply format if changed + if (formatChanged) { + if (ioctl(m_fd, VIDIOC_S_FMT, &m_currentFormat) < 0) { + CCAP_LOG_W("ccap: VIDIOC_S_FMT failed, using current format: %s\n", strerror(errno)); + } + + // Get the actual format set by the driver + if (ioctl(m_fd, VIDIOC_G_FMT, &m_currentFormat) < 0) { + CCAP_LOG_E("ccap: VIDIOC_G_FMT failed after set: %s\n", strerror(errno)); + return false; + } + } + + // Update frame properties + m_frameProp.width = pix.width; + m_frameProp.height = pix.height; + m_frameProp.cameraPixelFormat = v4l2FormatToCcapFormat(pix.pixelformat); + + CCAP_LOG_I("ccap: Format negotiated: %dx%d, format=%s\n", + pix.width, pix.height, getFormatName(pix.pixelformat)); + + return true; +} + +bool ProviderV4L2::allocateBuffers() { + struct v4l2_requestbuffers req = {}; + req.count = kBufferCount; + req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + req.memory = V4L2_MEMORY_MMAP; + + if (ioctl(m_fd, VIDIOC_REQBUFS, &req) < 0) { + CCAP_LOG_E("ccap: VIDIOC_REQBUFS failed: %s\n", strerror(errno)); + return false; + } + + if (req.count < 2) { + CCAP_LOG_E("ccap: Insufficient buffer memory\n"); + return false; + } + + m_buffers.resize(req.count); + + for (size_t i = 0; i < req.count; i++) { + struct v4l2_buffer buf = {}; + buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + buf.memory = V4L2_MEMORY_MMAP; + buf.index = i; + + if (ioctl(m_fd, VIDIOC_QUERYBUF, &buf) < 0) { + CCAP_LOG_E("ccap: VIDIOC_QUERYBUF failed: %s\n", strerror(errno)); + return false; + } + + m_buffers[i].length = buf.length; + m_buffers[i].start = mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, buf.m.offset); + m_buffers[i].index = i; + + if (m_buffers[i].start == MAP_FAILED) { + CCAP_LOG_E("ccap: mmap failed: %s\n", strerror(errno)); + return false; + } + } + + CCAP_LOG_V("ccap: Allocated %zu buffers\n", m_buffers.size()); + return true; +} + +void ProviderV4L2::releaseBuffers() { + for (auto& buffer : m_buffers) { + if (buffer.start != nullptr && buffer.start != MAP_FAILED) { + munmap(buffer.start, buffer.length); + } + } + m_buffers.clear(); +} + +bool ProviderV4L2::startStreaming() { + // Queue all buffers + for (size_t i = 0; i < m_buffers.size(); i++) { + struct v4l2_buffer buf = {}; + buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + buf.memory = V4L2_MEMORY_MMAP; + buf.index = i; + + if (ioctl(m_fd, VIDIOC_QBUF, &buf) < 0) { + CCAP_LOG_E("ccap: VIDIOC_QBUF failed: %s\n", strerror(errno)); + return false; + } + } + + enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + if (ioctl(m_fd, VIDIOC_STREAMON, &type) < 0) { + CCAP_LOG_E("ccap: VIDIOC_STREAMON failed: %s\n", strerror(errno)); + return false; + } + + return true; +} + +void ProviderV4L2::stopStreaming() { + enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + if (ioctl(m_fd, VIDIOC_STREAMOFF, &type) < 0) { + CCAP_LOG_E("ccap: VIDIOC_STREAMOFF failed: %s\n", strerror(errno)); + } +} + +void ProviderV4L2::captureThread() { + CCAP_LOG_V("ccap: Capture thread started\n"); + + while (!m_shouldStop) { + if (!readFrame()) { + // Error or timeout, continue + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + } + + CCAP_LOG_V("ccap: Capture thread finished\n"); +} + +bool ProviderV4L2::readFrame() { + // Use poll to wait for data + struct pollfd fds[1]; + fds[0].fd = m_fd; + fds[0].events = POLLIN; + + int ret = poll(fds, 1, 100); // 100ms timeout + if (ret < 0) { + if (errno != EINTR) { + CCAP_LOG_E("ccap: poll failed: %s\n", strerror(errno)); + } + return false; + } else if (ret == 0) { + // Timeout + return false; + } + + // Check frame availability before dequeuing buffer + if (tooManyNewFrames()) { + if (m_callback && *m_callback) { + CCAP_LOG_I("ccap: new frame callback returned false, but grab() was not called or is called less frequently than the camera frame rate.\n"); + } else { + CCAP_LOG_I("ccap: VideoFrame dropped to avoid memory leak: grab() called less frequently than camera frame rate.\n"); + } + return false; // Don't dequeue if we're going to drop the frame anyway + } + + auto frame = getFreeFrame(); + if (!frame) { + CCAP_LOG_W("ccap: VideoFrame pool is full, a new frame skipped...\n"); + return false; + } + + struct v4l2_buffer buf = {}; + buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + buf.memory = V4L2_MEMORY_MMAP; + + if (ioctl(m_fd, VIDIOC_DQBUF, &buf) < 0) { + if (errno != EAGAIN) { + CCAP_LOG_E("ccap: VIDIOC_DQBUF failed: %s\n", strerror(errno)); + } + return false; + } + + // Fill frame metadata + frame->width = m_frameProp.width; + frame->height = m_frameProp.height; + frame->pixelFormat = m_frameProp.cameraPixelFormat; + frame->timestamp = (std::chrono::steady_clock::now() - m_startTime).count(); + frame->frameIndex = m_frameIndex++; + frame->sizeInBytes = buf.bytesused; + + assert(frame->pixelFormat != PixelFormat::Unknown); + + // Check input/output format types and orientations + bool isInputYUV = (frame->pixelFormat & kPixelFormatYUVColorBit) != 0; + bool isOutputYUV = (m_frameProp.outputPixelFormat & kPixelFormatYUVColorBit) != 0; + auto inputOrientation = FrameOrientation::TopToBottom; // V4L2 always provides TopToBottom + + // Set output orientation based on format type + frame->orientation = isOutputYUV ? FrameOrientation::TopToBottom : m_frameOrientation; + + // Check if we need conversion or flipping + bool shouldFlip = frame->orientation != inputOrientation && !isOutputYUV; + bool shouldConvert = (m_frameProp.outputPixelFormat != PixelFormat::Unknown && + m_frameProp.outputPixelFormat != frame->pixelFormat); + bool zeroCopy = !shouldConvert && !shouldFlip; + + uint8_t* bufferData = static_cast(m_buffers[buf.index].start); + + if (isInputYUV) { + // Setup YUV planes for zero-copy + frame->data[0] = bufferData; + frame->stride[0] = m_frameProp.width; + + if (pixelFormatInclude(frame->pixelFormat, PixelFormat::NV12)) { + // NV12: Y plane + interleaved UV plane + frame->data[1] = bufferData + m_frameProp.width * m_frameProp.height; + frame->data[2] = nullptr; + frame->stride[1] = m_frameProp.width; + frame->stride[2] = 0; + } else if (pixelFormatInclude(frame->pixelFormat, PixelFormat::I420)) { + // I420: Y + U + V planes + frame->data[1] = bufferData + m_frameProp.width * m_frameProp.height; + frame->data[2] = bufferData + m_frameProp.width * m_frameProp.height * 5 / 4; + frame->stride[1] = m_frameProp.width / 2; + frame->stride[2] = m_frameProp.width / 2; + } else { + // YUYV/UYVY: packed format + frame->data[1] = nullptr; + frame->data[2] = nullptr; + frame->stride[0] = m_currentFormat.fmt.pix.bytesperline; + frame->stride[1] = 0; + frame->stride[2] = 0; + } + } else { + // RGB formats: single plane + frame->data[0] = bufferData; + frame->data[1] = nullptr; + frame->data[2] = nullptr; + frame->stride[0] = m_currentFormat.fmt.pix.bytesperline; + frame->stride[1] = 0; + frame->stride[2] = 0; + } + + if (!zeroCopy) { + // Need conversion: copy data and requeue buffer immediately + if (!frame->allocator) { + frame->allocator = m_allocatorFactory ? m_allocatorFactory() : std::make_shared(); + } + + // Perform pixel format conversion + if (verboseLogEnabled()) { +#ifdef DEBUG + constexpr const char* mode = "(Debug)"; +#else + constexpr const char* mode = "(Release)"; +#endif + + std::chrono::steady_clock::time_point startTime = std::chrono::steady_clock::now(); + + zeroCopy = !inplaceConvertFrame(frame.get(), m_frameProp.outputPixelFormat, shouldFlip); + + double durInMs = (std::chrono::steady_clock::now() - startTime).count() / 1.e6; + static double s_allCostTime = 0; + static double s_frames = 0; + + if (s_frames > 60) { + s_allCostTime = 0; + s_frames = 0; + } + + s_allCostTime += durInMs; + ++s_frames; + + CCAP_LOG_V( + "ccap: inplaceConvertFrame requested pixel format: %s, actual pixel format: %s, flip: %s, cost time %s: (cur %g ms, avg %g ms)\n", + pixelFormatToString(m_frameProp.outputPixelFormat).data(), pixelFormatToString(m_frameProp.cameraPixelFormat).data(), + shouldFlip ? "YES" : "NO", mode, durInMs, s_allCostTime / s_frames); + } else { + zeroCopy = !inplaceConvertFrame(frame.get(), m_frameProp.outputPixelFormat, shouldFlip); + } + } + + if (zeroCopy) { + // Conversion may fail. If conversion fails, fall back to zero-copy mode. + // In this case, the returned format is the original camera input format. + frame->orientation = inputOrientation; + + // Create shared buffer manager to handle V4L2 buffer lifecycle + auto bufferIndex = buf.index; + frame->nativeHandle = (void*)(uintptr_t)bufferIndex; + std::weak_ptr lifeHolder = m_lifeHolder; + auto bufferManager = std::make_shared([lifeHolder, this, bufferIndex, frame]() mutable { + // Requeue the V4L2 buffer when frame is destroyed + auto holder = lifeHolder.lock(); + if (!holder) { + CCAP_LOG_W("ccap: Frame life holder expired, not requeuing buffer\n"); + return; + } + + if (m_fd >= 0 && m_isStreaming) { + struct v4l2_buffer requeueBuf = {}; + requeueBuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + requeueBuf.memory = V4L2_MEMORY_MMAP; + requeueBuf.index = bufferIndex; + + if (ioctl(m_fd, VIDIOC_QBUF, &requeueBuf) < 0) { + CCAP_LOG_E("ccap: VIDIOC_QBUF failed in destructor: %s\n", strerror(errno)); + } + } + frame = nullptr; + }); + + // Replace frame with shared_ptr that manages V4L2 buffer lifecycle + auto sharedFrame = std::shared_ptr(bufferManager, frame.get()); + frame = sharedFrame; + } else { + // Update sizeInBytes after conversion + frame->sizeInBytes = frame->stride[0] * frame->height + (frame->stride[1] + frame->stride[2]) * frame->height / 2; + + // Requeue buffer immediately after copying data + if (ioctl(m_fd, VIDIOC_QBUF, &buf) < 0) { + CCAP_LOG_E("ccap: VIDIOC_QBUF failed: %s\n", strerror(errno)); + return false; + } + } + + frame->frameIndex = m_frameIndex++; + + if (verboseLogEnabled()) { // Usually camera interfaces are not called from multiple threads, and verbose log is for debugging, so + // no lock here. + static uint64_t s_lastFrameTime; + static std::deque s_durations; + + if (s_lastFrameTime != 0) { + auto dur = frame->timestamp - s_lastFrameTime; + s_durations.emplace_back(dur); + } + + s_lastFrameTime = frame->timestamp; + + // use a window of 30 frames to calculate the fps + if (s_durations.size() > 30) { + s_durations.pop_front(); + } + + double fps = 0.0; + + if (!s_durations.empty()) { + double sum = 0.0; + for (auto& d : s_durations) { + sum += d / 1e9f; + } + fps = std::round(s_durations.size() / sum * 10) / 10.0; + } + + CCAP_LOG_V("ccap: New frame available: %ux%u, bytes %u, Data address: %p, fps: %g\n", frame->width, frame->height, + frame->sizeInBytes, frame->data[0], fps); + } + + newFrameAvailable(std::move(frame)); + return true; +} + +// Utility methods + +PixelFormat ProviderV4L2::v4l2FormatToCcapFormat(uint32_t v4l2Format) { + for (const auto& format : s_supportedV4L2Formats) { + if (format.pixelformat == v4l2Format) { + return format.ccapFormat; + } + } + return PixelFormat::Unknown; +} + +uint32_t ProviderV4L2::ccapFormatToV4l2Format(PixelFormat ccapFormat) { + for (const auto& format : s_supportedV4L2Formats) { + if (format.ccapFormat == ccapFormat) { + return format.pixelformat; + } + } + return 0; +} + +const char* ProviderV4L2::getFormatName(uint32_t pixelformat) { + for (const auto& format : s_supportedV4L2Formats) { + if (format.pixelformat == pixelformat) { + return format.name; + } + } + return "Unknown"; +} + +bool ProviderV4L2::isVideoDevice(const std::string& devicePath) { + int fd = ::open(devicePath.c_str(), O_RDWR | O_NONBLOCK); + if (fd < 0) { + return false; + } + + struct v4l2_capability cap; + bool isVideo = (ioctl(fd, VIDIOC_QUERYCAP, &cap) == 0) && + (cap.capabilities & V4L2_CAP_VIDEO_CAPTURE); + + ::close(fd); + return isVideo; +} + +std::string ProviderV4L2::getDeviceDescription(const std::string& devicePath) { + int fd = ::open(devicePath.c_str(), O_RDWR | O_NONBLOCK); + if (fd < 0) { + return ""; + } + + struct v4l2_capability cap; + std::string description; + if (ioctl(fd, VIDIOC_QUERYCAP, &cap) == 0) { + description = reinterpret_cast(cap.card); + } + + ::close(fd); + return description; +} + +// Factory function +ProviderImp* createProviderV4L2() { + return new ProviderV4L2(); +} + +} // namespace ccap + +#endif // Linux check \ No newline at end of file diff --git a/src/ccap_imp_linux.h b/src/ccap_imp_linux.h new file mode 100644 index 00000000..b96243f2 --- /dev/null +++ b/src/ccap_imp_linux.h @@ -0,0 +1,137 @@ +/** + * @file ccap_imp_linux.h + * @author wysaid (this@wysaid.org) + * @brief Header file for Linux implementation of ccap::Provider class using V4L2. + * @date 2025-04 + * + */ + +#pragma once +#ifndef CAMERA_CAPTURE_LINUX_H +#define CAMERA_CAPTURE_LINUX_H + +#if defined(__linux__) || defined(__linux) || defined(linux) || defined(__gnu_linux__) + +#include "ccap_imp.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +#include +#include +#include +#include +#include +#include +#include +} + +namespace ccap { + +/** + * @brief V4L2-based camera provider implementation for Linux + */ +class ProviderV4L2 : public ProviderImp { +public: + ProviderV4L2(); + ~ProviderV4L2() override; + + // ProviderImp interface implementation + std::vector findDeviceNames() override; + bool open(std::string_view deviceName) override; + bool isOpened() const override; + std::optional getDeviceInfo() const override; + void close() override; + bool start() override; + void stop() override; + bool isStarted() const override; + +private: + struct V4L2Buffer { + void* start = nullptr; + size_t length = 0; + uint32_t index = 0; + }; + + struct V4L2Format { + uint32_t pixelformat; // V4L2 fourcc + PixelFormat ccapFormat; // ccap pixel format + const char* name; + }; + + // Internal helper methods + bool setupDevice(); + bool negotiateFormat(); + bool allocateBuffers(); + void releaseBuffers(); + bool startStreaming(); + void stopStreaming(); + void captureThread(); + bool readFrame(); + + // V4L2 utility methods + bool queryCapabilities(); + bool enumerateFormats(); + bool enumerateFrameSizes(); + std::vector getSupportedResolutions(uint32_t pixelformat); + PixelFormat v4l2FormatToCcapFormat(uint32_t v4l2Format); + uint32_t ccapFormatToV4l2Format(PixelFormat ccapFormat); + const char* getFormatName(uint32_t pixelformat); + + // Device discovery + bool isVideoDevice(const std::string& devicePath); + std::string getDeviceDescription(const std::string& devicePath); + +private: + // Device state + int m_fd = -1; + std::string m_devicePath; + std::string m_deviceName; + bool m_isOpened = false; + bool m_isStreaming = false; + + // V4L2 device capabilities + struct v4l2_capability m_caps{}; + std::vector m_supportedFormats; + std::vector m_supportedResolutions; + + // Current format + struct v4l2_format m_currentFormat{}; + + // Buffer management + std::vector m_buffers; + static constexpr size_t kBufferCount = 4; + + // Capture thread + std::unique_ptr m_captureThread; + std::atomic m_shouldStop{ false }; + std::mutex m_captureMutex; + std::condition_variable m_captureCondition; + + // Frame management + std::chrono::steady_clock::time_point m_startTime{}; + uint64_t m_frameIndex{ 0 }; + + std::shared_ptr m_lifeHolder; // To keep the provider alive while frames are being processed + + // Supported V4L2 formats mapping + static const std::vector s_supportedV4L2Formats; +}; + +/** + * @brief Create a V4L2 provider instance + */ +ProviderImp* createProviderV4L2(); + +} // namespace ccap + +#endif // Linux check +#endif // CAMERA_CAPTURE_LINUX_H \ No newline at end of file diff --git a/src/ccap_imp_windows.cpp b/src/ccap_imp_windows.cpp index 2ebdf012..e4a60a0c 100644 --- a/src/ccap_imp_windows.cpp +++ b/src/ccap_imp_windows.cpp @@ -723,9 +723,7 @@ HRESULT STDMETHODCALLTYPE ProviderDirectShow::SampleCB(double sampleTime, IMedia auto newFrame = getFreeFrame(); if (!newFrame) { - if (!newFrame) { - CCAP_LOG_W("ccap: VideoFrame pool is full, a new frame skipped...\n"); - } + CCAP_LOG_W("ccap: VideoFrame pool is full, a new frame skipped...\n"); return S_OK; } @@ -855,7 +853,11 @@ HRESULT STDMETHODCALLTYPE ProviderDirectShow::SampleCB(double sampleTime, IMedia } newFrame->sizeInBytes = newFrame->stride[0] * newFrame->height + (newFrame->stride[1] + newFrame->stride[2]) * newFrame->height / 2; - } else { + } + + if (zeroCopy) { + // Conversion may fail. If conversion fails, fall back to zero-copy mode. + // In this case, the returned format is the original camera input format. newFrame->sizeInBytes = bufferLen; mediaSample->AddRef(); // Ensure data lifecycle diff --git a/src/ccap_utils.cpp b/src/ccap_utils.cpp index fbb297c2..7cd3c842 100644 --- a/src/ccap_utils.cpp +++ b/src/ccap_utils.cpp @@ -201,6 +201,16 @@ std::string_view pixelFormatToString(PixelFormat format) { case PixelFormat::I420f: return "I420f"; + case PixelFormat::YUYV: + return "YUYV"; + case PixelFormat::YUYVf: + return "YUYVf"; + + case PixelFormat::UYVY: + return "UYVY"; + case PixelFormat::UYVYf: + return "UYVYf"; + case PixelFormat::RGB24: return "RGB24"; case PixelFormat::RGBA32: diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f45ad155..3415ab75 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -4,10 +4,12 @@ cmake_minimum_required(VERSION 3.14) include(FetchContent) FetchContent_Declare( googletest - URL https://github.com/google/googletest/archive/refs/tags/release-1.11.0.zip - DOWNLOAD_EXTRACT_TIMESTAMP ON + GIT_REPOSITORY https://github.com/google/googletest.git + GIT_TAG release-1.11.0 ) +message(STATUS "Fetching Google Test archive...") + # For Windows: Prevent overriding the parent project's compiler/linker settings set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) FetchContent_MakeAvailable(googletest) @@ -20,6 +22,8 @@ if(NOT DEFINED LIBYUV_REPO_URL) endif() endif() +message(STATUS "Fetching libYUV from repository: ${LIBYUV_REPO_URL}") + # LibYUV for performance comparison FetchContent_Declare( libyuv diff --git a/tests/test_performance.cpp b/tests/test_performance.cpp index 5d07817c..05b9d6df 100644 --- a/tests/test_performance.cpp +++ b/tests/test_performance.cpp @@ -74,6 +74,24 @@ struct YUVConversion { : name(n), is_nv12(nv12), dst_channels(dst_ch), ccap_func(ccap_f), libyuv_func(libyuv_f) {} }; +/** + * @brief Packed YUV to RGB conversion configuration for YUYV/UYVY formats + */ +struct PackedYUVConversion { + std::string name; + int dst_channels; + bool libyuv_available; + std::function ccap_func; + std::function libyuv_func; + + PackedYUVConversion(const std::string& n, int dst_ch, + std::function ccap_f, + std::function libyuv_f, + bool libyuv_avail = true) + : name(n), dst_channels(dst_ch), libyuv_available(libyuv_avail), + ccap_func(ccap_f), libyuv_func(libyuv_f) {} +}; + /** * @brief Performance measurement result */ @@ -375,6 +393,91 @@ class CCAPvsLibYUVComparisonTest : public ::testing::Test { ) }; } + + /** + * @brief Packed YUV (YUYV/UYVY) to RGB conversions + */ + static const std::vector getPackedYUVConversions() { + return { + // YUYV conversions + PackedYUVConversion("YUYV to RGB", 3, + [](const uint8_t* src, int src_stride, uint8_t* dst, int dst_stride, int w, int h) { + ccap::yuyvToRgb24(src, src_stride, dst, dst_stride, w, h, ccap::ConvertFlag::Default); + }, + [](const uint8_t* src, int src_stride, uint8_t* dst, int dst_stride, int w, int h) -> int { + // LibYUV doesn't have YUY2ToRGB24, use YUY2ToARGB then convert + return -1; // Not directly supported + }, + false // LibYUV RGB24 not available + ), + PackedYUVConversion("YUYV to BGR", 3, + [](const uint8_t* src, int src_stride, uint8_t* dst, int dst_stride, int w, int h) { + ccap::yuyvToBgr24(src, src_stride, dst, dst_stride, w, h, ccap::ConvertFlag::Default); + }, + [](const uint8_t* src, int src_stride, uint8_t* dst, int dst_stride, int w, int h) -> int { + // LibYUV doesn't have YUY2ToBGR24 + return -1; // Not directly supported + }, + false // LibYUV BGR24 not available + ), + PackedYUVConversion("YUYV to RGBA", 4, + [](const uint8_t* src, int src_stride, uint8_t* dst, int dst_stride, int w, int h) { + ccap::yuyvToRgba32(src, src_stride, dst, dst_stride, w, h, ccap::ConvertFlag::Default); + }, + [](const uint8_t* src, int src_stride, uint8_t* dst, int dst_stride, int w, int h) -> int { + return libyuv::YUY2ToARGB(src, src_stride, dst, dst_stride, w, h); + } + ), + PackedYUVConversion("YUYV to BGRA", 4, + [](const uint8_t* src, int src_stride, uint8_t* dst, int dst_stride, int w, int h) { + ccap::yuyvToBgra32(src, src_stride, dst, dst_stride, w, h, ccap::ConvertFlag::Default); + }, + [](const uint8_t* src, int src_stride, uint8_t* dst, int dst_stride, int w, int h) -> int { + // LibYUV doesn't have YUY2ToBGRA, use YUY2ToARGB as approximation + return libyuv::YUY2ToARGB(src, src_stride, dst, dst_stride, w, h); + } + ), + + // UYVY conversions + PackedYUVConversion("UYVY to RGB", 3, + [](const uint8_t* src, int src_stride, uint8_t* dst, int dst_stride, int w, int h) { + ccap::uyvyToRgb24(src, src_stride, dst, dst_stride, w, h, ccap::ConvertFlag::Default); + }, + [](const uint8_t* src, int src_stride, uint8_t* dst, int dst_stride, int w, int h) -> int { + // LibYUV doesn't have UYVYToRGB24 + return -1; // Not directly supported + }, + false // LibYUV RGB24 not available + ), + PackedYUVConversion("UYVY to BGR", 3, + [](const uint8_t* src, int src_stride, uint8_t* dst, int dst_stride, int w, int h) { + ccap::uyvyToBgr24(src, src_stride, dst, dst_stride, w, h, ccap::ConvertFlag::Default); + }, + [](const uint8_t* src, int src_stride, uint8_t* dst, int dst_stride, int w, int h) -> int { + // LibYUV doesn't have UYVYToBGR24 + return -1; // Not directly supported + }, + false // LibYUV BGR24 not available + ), + PackedYUVConversion("UYVY to RGBA", 4, + [](const uint8_t* src, int src_stride, uint8_t* dst, int dst_stride, int w, int h) { + ccap::uyvyToRgba32(src, src_stride, dst, dst_stride, w, h, ccap::ConvertFlag::Default); + }, + [](const uint8_t* src, int src_stride, uint8_t* dst, int dst_stride, int w, int h) -> int { + return libyuv::UYVYToARGB(src, src_stride, dst, dst_stride, w, h); + } + ), + PackedYUVConversion("UYVY to BGRA", 4, + [](const uint8_t* src, int src_stride, uint8_t* dst, int dst_stride, int w, int h) { + ccap::uyvyToBgra32(src, src_stride, dst, dst_stride, w, h, ccap::ConvertFlag::Default); + }, + [](const uint8_t* src, int src_stride, uint8_t* dst, int dst_stride, int w, int h) -> int { + // LibYUV doesn't have UYVYToBGRA, use UYVYToARGB as approximation + return libyuv::UYVYToARGB(src, src_stride, dst, dst_stride, w, h); + } + ) + }; + } protected: void SetUp() override { #ifdef NDEBUG @@ -626,6 +729,133 @@ class CCAPvsLibYUVComparisonTest : public ::testing::Test { conversion.dst_channels, ccap_func, libyuv_func); } + /** + * @brief Parameterized Packed YUV (YUYV/UYVY) conversion benchmark + */ + void benchmarkPackedYUVConversion(const Resolution& resolution, const PackedYUVConversion& conversion) { + if (!conversion.libyuv_available) { + // LibYUV not available for this conversion, only test CCAP backends + benchmarkPackedYUVCCAPOnly(resolution, conversion); + return; + } + + // For packed formats like YUYV/UYVY, each pixel pair uses 4 bytes (2 pixels = 4 bytes) + // YUYV: Y0 U Y1 V (4 bytes for 2 pixels) + // UYVY: U Y0 V Y1 (4 bytes for 2 pixels) + int packed_stride = ((resolution.width + 1) / 2) * 4; // 2 bytes per pixel, rounded up + TestImage packed_src_img(packed_stride / 2, resolution.height, 2); // Treating as 2-channel image + TestImage ccap_dst_img(resolution.width, resolution.height, conversion.dst_channels); + TestImage libyuv_dst_img(resolution.width, resolution.height, conversion.dst_channels); + + // Generate packed YUV test data + generatePackedYUVData(packed_src_img, resolution.width, resolution.height, conversion.name.find("YUYV") == 0); + + auto ccap_func = [&]() { + conversion.ccap_func(packed_src_img.data(), packed_src_img.stride(), + ccap_dst_img.data(), ccap_dst_img.stride(), + resolution.width, resolution.height); + }; + + auto libyuv_func = [&]() -> int { + return conversion.libyuv_func(packed_src_img.data(), packed_src_img.stride(), + libyuv_dst_img.data(), libyuv_dst_img.stride(), + resolution.width, resolution.height); + }; + + std::string test_name = conversion.name + " " + resolution.name; + benchmarkComparison(test_name, resolution.width, resolution.height, + conversion.dst_channels, ccap_func, libyuv_func); + } + + /** + * @brief CCAP-only benchmark for packed YUV conversions not supported by LibYUV + */ + void benchmarkPackedYUVCCAPOnly(const Resolution& resolution, const PackedYUVConversion& conversion) { + std::vector results; + auto supported_backends = BackendTestManager::getSupportedBackends(); + + // For packed formats like YUYV/UYVY, each pixel pair uses 4 bytes + int packed_stride = ((resolution.width + 1) / 2) * 4; + TestImage packed_src_img(packed_stride / 2, resolution.height, 2); + TestImage ccap_dst_img(resolution.width, resolution.height, conversion.dst_channels); + + // Generate packed YUV test data + generatePackedYUVData(packed_src_img, resolution.width, resolution.height, conversion.name.find("YUYV") == 0); + + // Test all CCAP backends + for (auto backend : supported_backends) { + ccap::setConvertBackend(backend); + std::string backend_name = "CCAP-" + BackendTestManager::getBackendName(backend); + + auto ccap_func = [&]() { + conversion.ccap_func(packed_src_img.data(), packed_src_img.stride(), + ccap_dst_img.data(), ccap_dst_img.stride(), + resolution.width, resolution.height); + }; + + // Warm up + for (int i = 0; i < 3; ++i) { + try { + ccap_func(); + } catch (...) { + break; + } + } + + // Measure CCAP performance + auto start_time = std::chrono::high_resolution_clock::now(); + bool success = true; + + try { + ccap_func(); + } catch (...) { + success = false; + } + + auto end_time = std::chrono::high_resolution_clock::now(); + auto duration = std::chrono::duration(end_time - start_time); + + results.emplace_back(backend_name, duration.count(), resolution.width, resolution.height, conversion.dst_channels, success); + } + + // Print CCAP-only results + std::string test_name = conversion.name + " " + resolution.name; + printCCAPOnlyResults(results, test_name, resolution.width, resolution.height); + } + + /** + * @brief Generate packed YUV test data (YUYV/UYVY) + */ + void generatePackedYUVData(TestImage& packed_img, int width, int height, bool is_yuyv) { + uint8_t* data = packed_img.data(); + int stride = packed_img.stride(); + + for (int y = 0; y < height; ++y) { + uint8_t* row = data + y * stride; + for (int x = 0; x < width; x += 2) { + // Generate test pattern - gradient effect + uint8_t y0 = (uint8_t)(128 + (x * 127) / width); + uint8_t y1 = (uint8_t)(128 + ((x + 1) * 127) / width); + uint8_t u = (uint8_t)(64 + (y * 127) / height); + uint8_t v = (uint8_t)(192 - (y * 127) / height); + + if (is_yuyv) { + // YUYV format: Y0 U Y1 V + row[(x / 2) * 4 + 0] = y0; + row[(x / 2) * 4 + 1] = u; + row[(x / 2) * 4 + 2] = y1; + row[(x / 2) * 4 + 3] = v; + } else { + // UYVY format: U Y0 V Y1 + row[(x / 2) * 4 + 0] = u; + row[(x / 2) * 4 + 1] = y0; + row[(x / 2) * 4 + 2] = v; + row[(x / 2) * 4 + 3] = y1; + } + } + } + } + // ============ Legacy Benchmark Methods (kept for specialized tests) ============ /** @@ -737,6 +967,58 @@ TEST_F(CCAPvsLibYUVComparisonTest, YUV_Conversions_Comprehensive) { } } +// Parameterized Packed YUV Conversion Tests (YUYV/UYVY) +TEST_F(CCAPvsLibYUVComparisonTest, PackedYUV_Conversions_Comprehensive) { + auto resolutions = getStandardResolutions(); + auto packed_yuv_conversions = getPackedYUVConversions(); + + // Test key resolution + conversion combinations for Packed YUV conversions + std::vector> key_resolution_indices = { + {1, 0}, {1, 1}, {1, 2}, {1, 3}, // 1080p with YUYV conversions + {2, 4}, {2, 5}, {2, 6}, {2, 7}, // 4K with UYVY conversions + {0, 0}, {0, 4} // VGA with YUYV->RGB and UYVY->RGB + }; + + for (const auto& [res_idx, conv_idx] : key_resolution_indices) { + if (res_idx < resolutions.size() && conv_idx < packed_yuv_conversions.size()) { + const auto& resolution = resolutions[res_idx]; + const auto& conversion = packed_yuv_conversions[conv_idx]; + benchmarkPackedYUVConversion(resolution, conversion); + } + } +} + +// Specialized Packed YUV Performance Tests +TEST_F(CCAPvsLibYUVComparisonTest, Large_8K_YUYV_To_RGBA_Performance) { + Resolution large_resolution(7680, 4320, "8K"); + auto packed_yuv_conversions = getPackedYUVConversions(); + + // Find YUYV to RGBA conversion + for (const auto& conversion : packed_yuv_conversions) { + if (conversion.name == "YUYV to RGBA") { + benchmarkPackedYUVConversion(large_resolution, conversion); + break; + } + } +} + +TEST_F(CCAPvsLibYUVComparisonTest, High_Resolution_UYVY_Performance) { + std::vector test_resolutions = { + Resolution(1920, 1080, "1080p"), + Resolution(3840, 2160, "4K") + }; + auto packed_yuv_conversions = getPackedYUVConversions(); + + // Test UYVY to RGBA conversion at different resolutions + for (const auto& resolution : test_resolutions) { + for (const auto& conversion : packed_yuv_conversions) { + if (conversion.name == "UYVY to RGBA" || conversion.name == "UYVY to BGRA") { + benchmarkPackedYUVConversion(resolution, conversion); + } + } + } +} + // Specialized Performance Tests TEST_F(CCAPvsLibYUVComparisonTest, Large_8K_RGBA_To_BGR_Performance) { Resolution large_resolution(7680, 4320, "8K"); diff --git a/tests/test_yuv_conversions.cpp b/tests/test_yuv_conversions.cpp index db95271a..ab9b900e 100644 --- a/tests/test_yuv_conversions.cpp +++ b/tests/test_yuv_conversions.cpp @@ -55,7 +55,9 @@ struct YUVConversionTestParams { */ enum class YUVFormat { NV12, - I420 + I420, + YUYV, + UYVY }; /** @@ -91,23 +93,41 @@ class YUVConversionTestHelper { int height = params.resolution.second; int channels = (rgb_format == RGBFormat::RGB24 || rgb_format == RGBFormat::BGR24) ? 3 : 4; - // Create test images - bool is_nv12 = (yuv_format == YUVFormat::NV12); - TestYUVImage yuv_img(width, height, is_nv12); + // Create test images based on format + std::unique_ptr yuv_img; + std::unique_ptr packed_img; // For YUYV/UYVY packed formats + + if (yuv_format == YUVFormat::YUYV || yuv_format == YUVFormat::UYVY) { + // YUYV/UYVY are packed formats: each pixel pair uses 4 bytes (Y0 U Y1 V or U Y0 V Y1) + // Create TestImage with width*2 to accommodate packed stride (2 bytes per pixel) + packed_img = std::make_unique(width * 2, height, 1); // stride=width*2, single channel + generatePackedYUVPattern(*packed_img, yuv_format, width, height); + } else { + // NV12/I420 use planar formats + bool is_nv12 = (yuv_format == YUVFormat::NV12); + yuv_img = std::make_unique(width, height, is_nv12); + yuv_img->generateKnownPattern(); + } + TestImage cpu_result(width, height, channels); TestImage backend_result(width, height, channels); - // Generate test pattern - yuv_img.generateKnownPattern(); - // Get CPU result (baseline) auto original_backend = ccap::getConvertBackend(); ccap::setConvertBackend(ccap::ConvertBackend::CPU); - performConversion(yuv_img, cpu_result, yuv_format, rgb_format, params.conversion_flags, width, height); + if (packed_img) { + performPackedConversion(*packed_img, cpu_result, yuv_format, rgb_format, params.conversion_flags, width, height); + } else { + performConversion(*yuv_img, cpu_result, yuv_format, rgb_format, params.conversion_flags, width, height); + } // Get backend result ccap::setConvertBackend(params.backend); - performConversion(yuv_img, backend_result, yuv_format, rgb_format, params.conversion_flags, width, height); + if (packed_img) { + performPackedConversion(*packed_img, backend_result, yuv_format, rgb_format, params.conversion_flags, width, height); + } else { + performConversion(*yuv_img, backend_result, yuv_format, rgb_format, params.conversion_flags, width, height); + } // Compare results bool images_match = PixelTestUtils::compareImages( @@ -224,6 +244,127 @@ class YUVConversionTestHelper { } private: + /** + * @brief Generate packed YUV test pattern for YUYV/UYVY formats + */ + static void generatePackedYUVPattern(TestImage& packed_img, YUVFormat format, int width, int height) { + uint8_t* data = packed_img.data(); + int stride = packed_img.stride(); + + for (int y = 0; y < height; ++y) { + uint8_t* row = data + y * stride; + for (int x = 0; x < width; x += 2) { + // Generate test pattern values + uint8_t y0 = (uint8_t)(((x + y * width) * 219 / (width * height)) + 16); // Video range Y + uint8_t y1 = (uint8_t)(((x + 1 + y * width) * 219 / (width * height)) + 16); // Video range Y + uint8_t u = (uint8_t)(((x / 2 + y) * 224 / (width / 2 + height)) + 16); // Video range U/V + uint8_t v = (uint8_t)(((x / 2 + y + 100) * 224 / (width / 2 + height)) + 16); // Video range U/V + + int base_idx = x * 2; // Each pixel pair uses 4 bytes + + if (format == YUVFormat::YUYV) { + // YUYV format: Y0 U Y1 V + row[base_idx + 0] = y0; + row[base_idx + 1] = u; + row[base_idx + 2] = y1; + row[base_idx + 3] = v; + } else { // UYVY + // UYVY format: U Y0 V Y1 + row[base_idx + 0] = u; + row[base_idx + 1] = y0; + row[base_idx + 2] = v; + row[base_idx + 3] = y1; + } + } + } + } + + /** + * @brief Shared helper for packed YUV format (YUYV/UYVY) conversions + * @param yuv_data Pointer to the packed YUV data + * @param yuv_stride Stride of the packed YUV data + * @param result Output RGB/BGR image + * @param yuv_format YUV format (should be YUYV or UYVY) + * @param rgb_format RGB output format + * @param flags Conversion flags + * @param width Image width + * @param height Image height + */ + static void performPackedYUVConversion( + const uint8_t* yuv_data, + int yuv_stride, + TestImage& result, + YUVFormat yuv_format, + RGBFormat rgb_format, + ccap::ConvertFlag flags, + int width, + int height) { + + if (yuv_format == YUVFormat::YUYV) { + switch (rgb_format) { + case RGBFormat::RGB24: + ccap::yuyvToRgb24(yuv_data, yuv_stride, + result.data(), result.stride(), + width, height, flags); + break; + case RGBFormat::RGBA32: + ccap::yuyvToRgba32(yuv_data, yuv_stride, + result.data(), result.stride(), + width, height, flags); + break; + case RGBFormat::BGR24: + ccap::yuyvToBgr24(yuv_data, yuv_stride, + result.data(), result.stride(), + width, height, flags); + break; + case RGBFormat::BGRA32: + ccap::yuyvToBgra32(yuv_data, yuv_stride, + result.data(), result.stride(), + width, height, flags); + break; + } + } else if (yuv_format == YUVFormat::UYVY) { + switch (rgb_format) { + case RGBFormat::RGB24: + ccap::uyvyToRgb24(yuv_data, yuv_stride, + result.data(), result.stride(), + width, height, flags); + break; + case RGBFormat::RGBA32: + ccap::uyvyToRgba32(yuv_data, yuv_stride, + result.data(), result.stride(), + width, height, flags); + break; + case RGBFormat::BGR24: + ccap::uyvyToBgr24(yuv_data, yuv_stride, + result.data(), result.stride(), + width, height, flags); + break; + case RGBFormat::BGRA32: + ccap::uyvyToBgra32(yuv_data, yuv_stride, + result.data(), result.stride(), + width, height, flags); + break; + } + } + } + + /** + * @brief Perform conversion for packed YUV formats (YUYV/UYVY) + */ + static void performPackedConversion( + const TestImage& packed_img, + TestImage& result, + YUVFormat yuv_format, + RGBFormat rgb_format, + ccap::ConvertFlag flags, + int width, + int height) { + + performPackedYUVConversion(packed_img.data(), packed_img.stride(), + result, yuv_format, rgb_format, flags, width, height); + } + static void performConversion( const TestYUVImage& yuv_img, TestImage& result, @@ -259,7 +400,7 @@ class YUVConversionTestHelper { width, height, flags); break; } - } else { // I420 + } else if (yuv_format == YUVFormat::I420) { switch (rgb_format) { case RGBFormat::RGB24: ccap::i420ToRgb24(yuv_img.y_data(), yuv_img.y_stride(), @@ -290,11 +431,30 @@ class YUVConversionTestHelper { width, height, flags); break; } + } else if (yuv_format == YUVFormat::YUYV || yuv_format == YUVFormat::UYVY) { + // YUYV/UYVY use packed format, stored in y_data() + performPackedYUVConversion(yuv_img.y_data(), yuv_img.y_stride(), + result, yuv_format, rgb_format, flags, width, height); } } static std::string getFormatString(YUVFormat yuv_format, RGBFormat rgb_format) { - std::string yuv_str = (yuv_format == YUVFormat::NV12) ? "NV12" : "I420"; + std::string yuv_str; + switch (yuv_format) { + case YUVFormat::NV12: + yuv_str = "NV12"; + break; + case YUVFormat::I420: + yuv_str = "I420"; + break; + case YUVFormat::YUYV: + yuv_str = "YUYV"; + break; + case YUVFormat::UYVY: + yuv_str = "UYVY"; + break; + } + std::string rgb_str; switch (rgb_format) { case RGBFormat::RGB24: @@ -378,6 +538,42 @@ TEST_P(YUVConversionMultiDimensionalTest, I420_To_BGRA32) { YUVConversionTestHelper::testYUVConversion(GetParam(), YUVFormat::I420, RGBFormat::BGRA32); } +// ============ YUYV Conversion Tests ============ + +TEST_P(YUVConversionMultiDimensionalTest, YUYV_To_RGB24) { + YUVConversionTestHelper::testYUVConversion(GetParam(), YUVFormat::YUYV, RGBFormat::RGB24); +} + +TEST_P(YUVConversionMultiDimensionalTest, YUYV_To_RGBA32) { + YUVConversionTestHelper::testYUVConversion(GetParam(), YUVFormat::YUYV, RGBFormat::RGBA32); +} + +TEST_P(YUVConversionMultiDimensionalTest, YUYV_To_BGR24) { + YUVConversionTestHelper::testYUVConversion(GetParam(), YUVFormat::YUYV, RGBFormat::BGR24); +} + +TEST_P(YUVConversionMultiDimensionalTest, YUYV_To_BGRA32) { + YUVConversionTestHelper::testYUVConversion(GetParam(), YUVFormat::YUYV, RGBFormat::BGRA32); +} + +// ============ UYVY Conversion Tests ============ + +TEST_P(YUVConversionMultiDimensionalTest, UYVY_To_RGB24) { + YUVConversionTestHelper::testYUVConversion(GetParam(), YUVFormat::UYVY, RGBFormat::RGB24); +} + +TEST_P(YUVConversionMultiDimensionalTest, UYVY_To_RGBA32) { + YUVConversionTestHelper::testYUVConversion(GetParam(), YUVFormat::UYVY, RGBFormat::RGBA32); +} + +TEST_P(YUVConversionMultiDimensionalTest, UYVY_To_BGR24) { + YUVConversionTestHelper::testYUVConversion(GetParam(), YUVFormat::UYVY, RGBFormat::BGR24); +} + +TEST_P(YUVConversionMultiDimensionalTest, UYVY_To_BGRA32) { + YUVConversionTestHelper::testYUVConversion(GetParam(), YUVFormat::UYVY, RGBFormat::BGRA32); +} + // Instantiate the multi-dimensional parameterized tests INSTANTIATE_TEST_SUITE_P( AllCombinations, @@ -387,6 +583,9 @@ INSTANTIATE_TEST_SUITE_P( return info.param.toString(); }); +// Allow uninstantiated parameterized test when only CPU backend is available +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(YUVConversionMultiDimensionalTest); + // ============ Single Pixel YUV to RGB Tests ============ class YUVPixelConversionTest : public BackendTestManager::BackendTestFixture { @@ -465,6 +664,281 @@ TEST_F(YUVPixelConversionTest, GetYuvToRgbFunc_ReturnsCorrectFunctions) { EXPECT_EQ(b1, b2) << "Function pointer should produce same result as direct call"; } +// ============ YUYV/UYVY Packed Format Tests ============ + +/** + * @brief Test class for YUYV/UYVY packed format conversions + */ +class PackedYUVConversionTest : public BackendTestManager::BackendTestFixture { +protected: + void SetUp() override { + BackendTestFixture::SetUp(); + setBackend(ccap::ConvertBackend::CPU); // Start with CPU for baseline tests + } + + /** + * @brief Create a simple test pattern for YUYV/UYVY formats + */ + void createTestPattern(TestImage& packed_img, YUVFormat format, int width, int height) { + uint8_t* data = packed_img.data(); + int stride = packed_img.stride(); + + // Create a simple gradient pattern + for (int y = 0; y < height; ++y) { + uint8_t* row = data + y * stride; + for (int x = 0; x < width; x += 2) { + // Simple test values + uint8_t y0 = 80 + (x * 100 / width); // Y values 80-180 + uint8_t y1 = 80 + ((x+1) * 100 / width); // Y values 80-180 + uint8_t u = 100 + (y * 50 / height); // U values 100-150 + uint8_t v = 140 - (y * 50 / height); // V values 140-90 + + int base_idx = x * 2; + + if (format == YUVFormat::YUYV) { + // YUYV: Y0 U Y1 V + row[base_idx + 0] = y0; + row[base_idx + 1] = u; + row[base_idx + 2] = y1; + row[base_idx + 3] = v; + } else { // UYVY + // UYVY: U Y0 V Y1 + row[base_idx + 0] = u; + row[base_idx + 1] = y0; + row[base_idx + 2] = v; + row[base_idx + 3] = y1; + } + } + } + } +}; + +TEST_F(PackedYUVConversionTest, YUYV_BasicConversion_SmallImage) { + const int width = 8, height = 4; + TestImage yuyv_img(width * 2, height, 1); // YUYV needs width*2 bytes per row + TestImage result_rgb(width, height, 3); + TestImage result_rgba(width, height, 4); + + createTestPattern(yuyv_img, YUVFormat::YUYV, width, height); + + // Test RGB24 conversion + ccap::yuyvToRgb24(yuyv_img.data(), yuyv_img.stride(), + result_rgb.data(), result_rgb.stride(), + width, height, ccap::ConvertFlag::Default); + + // Test RGBA32 conversion + ccap::yuyvToRgba32(yuyv_img.data(), yuyv_img.stride(), + result_rgba.data(), result_rgba.stride(), + width, height, ccap::ConvertFlag::Default); + + // Basic sanity checks - RGB values should be in valid range + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width; ++x) { + uint8_t r = result_rgb.data()[y * result_rgb.stride() + x * 3 + 0]; + uint8_t g = result_rgb.data()[y * result_rgb.stride() + x * 3 + 1]; + uint8_t b = result_rgb.data()[y * result_rgb.stride() + x * 3 + 2]; + + // RGB values should be in valid range + EXPECT_LE(r, 255) << "Red value out of range at (" << x << "," << y << ")"; + EXPECT_LE(g, 255) << "Green value out of range at (" << x << "," << y << ")"; + EXPECT_LE(b, 255) << "Blue value out of range at (" << x << "," << y << ")"; + + // RGBA should have alpha = 255 + uint8_t a = result_rgba.data()[y * result_rgba.stride() + x * 4 + 3]; + EXPECT_EQ(a, 255) << "Alpha should be 255 at (" << x << "," << y << ")"; + } + } +} + +TEST_F(PackedYUVConversionTest, UYVY_BasicConversion_SmallImage) { + const int width = 8, height = 4; + TestImage uyvy_img(width * 2, height, 1); // UYVY needs width*2 bytes per row + TestImage result_bgr(width, height, 3); + TestImage result_bgra(width, height, 4); + + createTestPattern(uyvy_img, YUVFormat::UYVY, width, height); + + // Test BGR24 conversion + ccap::uyvyToBgr24(uyvy_img.data(), uyvy_img.stride(), + result_bgr.data(), result_bgr.stride(), + width, height, ccap::ConvertFlag::Default); + + // Test BGRA32 conversion + ccap::uyvyToBgra32(uyvy_img.data(), uyvy_img.stride(), + result_bgra.data(), result_bgra.stride(), + width, height, ccap::ConvertFlag::Default); + + // Basic sanity checks - RGB values should be in valid range + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width; ++x) { + uint8_t b = result_bgr.data()[y * result_bgr.stride() + x * 3 + 0]; + uint8_t g = result_bgr.data()[y * result_bgr.stride() + x * 3 + 1]; + uint8_t r = result_bgr.data()[y * result_bgr.stride() + x * 3 + 2]; + + // RGB values should be in valid range + EXPECT_LE(r, 255) << "Red value out of range at (" << x << "," << y << ")"; + EXPECT_LE(g, 255) << "Green value out of range at (" << x << "," << y << ")"; + EXPECT_LE(b, 255) << "Blue value out of range at (" << x << "," << y << ")"; + + // BGRA should have alpha = 255 + uint8_t a = result_bgra.data()[y * result_bgra.stride() + x * 4 + 3]; + EXPECT_EQ(a, 255) << "Alpha should be 255 at (" << x << "," << y << ")"; + } + } +} + +/** + * @brief Test CPU vs AVX2 backend consistency for YUYV/UYVY conversions + */ +TEST_F(PackedYUVConversionTest, YUYV_CPU_vs_AVX2_Consistency) { + // Skip if AVX2 is not supported + if (!ccap::hasAVX2()) { + GTEST_SKIP() << "AVX2 not supported on this platform"; + } + + const int width = 64, height = 32; // Size suitable for AVX2 optimization + TestImage yuyv_img(width * 2, height, 1); + TestImage cpu_result(width, height, 4); + TestImage avx2_result(width, height, 4); + + createTestPattern(yuyv_img, YUVFormat::YUYV, width, height); + + // Get CPU result + ccap::setConvertBackend(ccap::ConvertBackend::CPU); + ccap::yuyvToRgba32(yuyv_img.data(), yuyv_img.stride(), + cpu_result.data(), cpu_result.stride(), + width, height, ccap::ConvertFlag::Default); + + // Get AVX2 result + ccap::setConvertBackend(ccap::ConvertBackend::AVX2); + ccap::yuyvToRgba32(yuyv_img.data(), yuyv_img.stride(), + avx2_result.data(), avx2_result.stride(), + width, height, ccap::ConvertFlag::Default); + + // Compare results + bool images_match = PixelTestUtils::compareImages( + cpu_result.data(), avx2_result.data(), + width, height, 4, + cpu_result.stride(), avx2_result.stride(), 3); + + if (!images_match) { + // Calculate MSE and PSNR for debugging + double mse = PixelTestUtils::calculateMSE( + cpu_result.data(), avx2_result.data(), + width, height, 4, + cpu_result.stride(), avx2_result.stride()); + double psnr = PixelTestUtils::calculatePSNR( + cpu_result.data(), avx2_result.data(), + width, height, 4, + cpu_result.stride(), avx2_result.stride()); + + std::cout << "[DEBUG] YUYV conversion differences - MSE: " << mse + << ", PSNR: " << psnr << " dB" << std::endl; + + // Save debug images for analysis + std::string test_name = "YUYV_To_RGBA32_CPU_vs_AVX2_" + std::to_string(width) + "x" + std::to_string(height); + PixelTestUtils::saveDebugImagesOnFailure(cpu_result, avx2_result, test_name, 3); + } + + EXPECT_TRUE(images_match) << "YUYV conversion results differ between CPU and AVX2 backends"; +} + +TEST_F(PackedYUVConversionTest, UYVY_CPU_vs_AVX2_Consistency) { + // Skip if AVX2 is not supported + if (!ccap::hasAVX2()) { + GTEST_SKIP() << "AVX2 not supported on this platform"; + } + + const int width = 64, height = 32; // Size suitable for AVX2 optimization + TestImage uyvy_img(width * 2, height, 1); + TestImage cpu_result(width, height, 4); + TestImage avx2_result(width, height, 4); + + createTestPattern(uyvy_img, YUVFormat::UYVY, width, height); + + // Get CPU result + ccap::setConvertBackend(ccap::ConvertBackend::CPU); + ccap::uyvyToBgra32(uyvy_img.data(), uyvy_img.stride(), + cpu_result.data(), cpu_result.stride(), + width, height, ccap::ConvertFlag::Default); + + // Get AVX2 result + ccap::setConvertBackend(ccap::ConvertBackend::AVX2); + ccap::uyvyToBgra32(uyvy_img.data(), uyvy_img.stride(), + avx2_result.data(), avx2_result.stride(), + width, height, ccap::ConvertFlag::Default); + + // Compare results + bool images_match = PixelTestUtils::compareImages( + cpu_result.data(), avx2_result.data(), + width, height, 4, + cpu_result.stride(), avx2_result.stride(), 3); + + if (!images_match) { + // Calculate MSE and PSNR for debugging + double mse = PixelTestUtils::calculateMSE( + cpu_result.data(), avx2_result.data(), + width, height, 4, + cpu_result.stride(), avx2_result.stride()); + double psnr = PixelTestUtils::calculatePSNR( + cpu_result.data(), avx2_result.data(), + width, height, 4, + cpu_result.stride(), avx2_result.stride()); + + std::cout << "[DEBUG] UYVY conversion differences - MSE: " << mse + << ", PSNR: " << psnr << " dB" << std::endl; + + // Save debug images for analysis + std::string test_name = "UYVY_To_BGRA32_CPU_vs_AVX2_" + std::to_string(width) + "x" + std::to_string(height); + PixelTestUtils::saveDebugImagesOnFailure(cpu_result, avx2_result, test_name, 3); + } + + EXPECT_TRUE(images_match) << "UYVY conversion results differ between CPU and AVX2 backends"; +} + +TEST_F(PackedYUVConversionTest, PackedYUV_ColorSpaceFlags_Consistency) { + const int width = 32, height = 16; + TestImage yuyv_img(width * 2, height, 1); + TestImage result_601v(width, height, 3); + TestImage result_601f(width, height, 3); + TestImage result_709v(width, height, 3); + TestImage result_709f(width, height, 3); + + createTestPattern(yuyv_img, YUVFormat::YUYV, width, height); + + // Test different color space conversions + ccap::yuyvToRgb24(yuyv_img.data(), yuyv_img.stride(), + result_601v.data(), result_601v.stride(), + width, height, ccap::ConvertFlag::BT601 | ccap::ConvertFlag::VideoRange); + + ccap::yuyvToRgb24(yuyv_img.data(), yuyv_img.stride(), + result_601f.data(), result_601f.stride(), + width, height, ccap::ConvertFlag::BT601 | ccap::ConvertFlag::FullRange); + + ccap::yuyvToRgb24(yuyv_img.data(), yuyv_img.stride(), + result_709v.data(), result_709v.stride(), + width, height, ccap::ConvertFlag::BT709 | ccap::ConvertFlag::VideoRange); + + ccap::yuyvToRgb24(yuyv_img.data(), yuyv_img.stride(), + result_709f.data(), result_709f.stride(), + width, height, ccap::ConvertFlag::BT709 | ccap::ConvertFlag::FullRange); + + // Results should be different between different color space conversions + bool same_601v_601f = PixelTestUtils::compareImages( + result_601v.data(), result_601f.data(), + width, height, 3, + result_601v.stride(), result_601f.stride(), 0); + + bool same_601v_709v = PixelTestUtils::compareImages( + result_601v.data(), result_709v.data(), + width, height, 3, + result_601v.stride(), result_709v.stride(), 0); + + // Different color space settings should produce different results + EXPECT_FALSE(same_601v_601f) << "BT601 VideoRange and FullRange should produce different results"; + EXPECT_FALSE(same_601v_709v) << "BT601 and BT709 should produce different results"; +} + // ============ Additional Edge Case Tests (Special scenarios not covered by multi-dimensional tests) ============ /**