diff --git a/.editorconfig b/.editorconfig index 66a702e..1bef2c9 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,5 +1,5 @@ # AUTOGENERATED COPYRIGHT HEADER START -# Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks +# Copyright (C) 2017-2024 Michael Fabian 'Xaymar' Dirks # AUTOGENERATED COPYRIGHT HEADER END # top-most EditorConfig file @@ -16,3 +16,6 @@ indent_size = 4 [*.yml] indent_style = space indent_size = 2 + +[*.md] +trim_trailing_whitespace = false diff --git a/.github/ISSUE_TEMPLATE/issue.yml b/.github/ISSUE_TEMPLATE/issue.yml index 066f446..58de8b0 100644 --- a/.github/ISSUE_TEMPLATE/issue.yml +++ b/.github/ISSUE_TEMPLATE/issue.yml @@ -7,12 +7,6 @@ title: "REPLACE ME" description: "This form is for bug and crash reports only, primarily used by developers. Abuse of this form will lead to a permanent interaction ban." labels: ["bug"] body: -- type: textarea - attributes: - label: "OBS Studio Logs" - description: "Paste the content or attach the log files from OBS Studio here. In the event of a crash, paste or attach both the crash log and the normal log file." - validations: - required: true - type: textarea attributes: label: "Current and Expected Behavior" @@ -25,6 +19,12 @@ body: description: "What steps are required to consistently reproduce the bug/crash/freeze?" validations: required: true +- type: textarea + attributes: + label: "Log files & Crash Dumps" + description: "Paste the content or attach the log files from OBS Studio here. In the event of a crash, paste or attach both the crash log and the normal log file." + validations: + required: false - type: textarea attributes: label: "Any additional Information we need to know?" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 62642b5..e51e252 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,5 +1,5 @@ # AUTOGENERATED COPYRIGHT HEADER START -# Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks +# Copyright (C) 2019-2024 Michael Fabian 'Xaymar' Dirks # AUTOGENERATED COPYRIGHT HEADER END name: Build @@ -19,337 +19,71 @@ concurrency: cancel-in-progress: true env: - CACHE_VERSION: "2022-12-02" + CACHE_VERSION: "2024-01-25" jobs: - windows: + build: strategy: fail-fast: false matrix: - runner: [ "windows-2022" ] - name: [ "Windows" ] - compiler: [ "MSVC", "Clang" ] - qt: [ 6 ] + runner: [ "windows-2022", "macos-12", "ubuntu-22.04" ] + compiler: [ "MSVC", "GCC-12", "Clang-17", "AppleClang" ] include: - - compiler: "MSVC" + - runner: "windows-2022" + compiler: "MSVC" + platform: "windows" + name: "Windows" CMAKE_SYSTEM_VERSION: "10.0.20348.0" CMAKE_GENERATOR: "Visual Studio 17 2022" CMAKE_GENERATOR_PLATFORM: "x64" - - compiler: "Clang" - CMAKE_SYSTEM_VERSION: "10.0.20348.0" - CMAKE_GENERATOR: "Visual Studio 17 2022" - CMAKE_GENERATOR_TOOLSET: "ClangCL" - CMAKE_GENERATOR_PLATFORM: "x64" - runs-on: "${{ matrix.runner }}" - name: "${{ matrix.name }} (${{ matrix.compiler }})" - env: - CMAKE_GENERATOR: "${{ matrix.CMAKE_GENERATOR }}" - CMAKE_GENERATOR_PLATFORM: "${{ matrix.CMAKE_GENERATOR_PLATFORM }}" - CMAKE_GENERATOR_TOOLSET: "${{ matrix.CMAKE_GENERATOR_TOOLSET }}" - CMAKE_SYSTEM_VERSION: "${{ matrix.CMAKE_SYSTEM_VERSION }}" - steps: - - name: "Clone" - uses: actions/checkout@v3 - with: - submodules: recursive - fetch-depth: 0 - - name: "Gather Information" - id: info - shell: bash - run: | - # Define buildspec file - buildspec="${{ github.workspace }}/third-party/obs-studio/buildspec.json" - - # Prebuilt Dependencies Version - IFS=$'\n' buildspecdata=($(node tools/buildspec.js "${buildspec}" "prebuilt" "windows-x64")) - echo "obs_deps_version=${buildspecdata[0]}" >> "$GITHUB_ENV" - echo "obs_deps_hash=${buildspecdata[1]}" >> "$GITHUB_ENV" - echo "obs_deps_url=${buildspecdata[2]}" >> "$GITHUB_ENV" - - # Qt Version - IFS=$'\n' buildspecdata=($(node tools/buildspec.js "${buildspec}" "qt${{ matrix.qt }}" "windows-x64")) - echo "qt_version=${buildspecdata[0]}" >> "$GITHUB_ENV" - echo "qt_hash=${buildspecdata[1]}" >> "$GITHUB_ENV" - echo "qt_url=${buildspecdata[2]}" >> "$GITHUB_ENV" - - # libOBS Version - echo "obs_version=$(cd "${{ github.workspace }}/third-party/obs-studio" && git describe --tags --long)" >> "$GITHUB_ENV" - - name: "Dependency: Qt (Cache)" - id: qt-cache - uses: actions/cache@v3 - with: - path: "${{ github.workspace }}/build/qt" - key: "qt${{ env.qt_hash }}-${{ env.CACHE_VERSION }}" - - name: "Dependency: Qt" - id: qt - if: ${{ steps.qt-cache.outputs.cache-hit != 'true' }} - shell: bash - run: | - curl --retry 5 --retry-delay 30 -jLo /tmp/qt.zip "${{ env.qt_url }}" - if [[ ! -f "${{ github.workspace }}/build/qt" ]]; then mkdir -p "${{ github.workspace }}/build/qt"; fi - 7z x -y -o"${{ github.workspace }}/build/qt" -- "/tmp/qt.zip" - - name: "Dependency: Prebuilt OBS Studio Dependencies (Cache)" - id: obsdeps-cache - uses: actions/cache@v3 - with: - path: "${{ github.workspace }}/build/obsdeps" - key: "obsdeps${{ env.obs_deps_hash }}-${{ env.CACHE_VERSION }}" - - name: "Dependency: Prebuilt OBS Studio Dependencies" - id: obsdeps - if: ${{ steps.obsdeps-cache.outputs.cache-hit != 'true' }} - shell: bash - run: | - curl --retry 5 --retry-delay 30 -jLo /tmp/obsdeps.zip "${{ env.obs_deps_url }}" - if [[ ! -f "${{ github.workspace }}/build/obsdeps" ]]; then mkdir -p "${{ github.workspace }}/build/obsdeps"; fi - 7z x -y -o"${{ github.workspace }}/build/obsdeps" -- "/tmp/obsdeps.zip" - - name: "Dependency: OBS Libraries (Cache)" - id: obs-cache - uses: actions/cache@v3 - with: - path: "${{ github.workspace }}/build/obs" - key: "obs${{ env.obs_version }}-${{ matrix.runner }}_${{ matrix.compiler }}-obsdeps${{ env.obs_deps_hash }}-qt${{ env.qt_hash }}-${{ env.CACHE_VERSION }}" - - name: "Dependency: OBS Libraries" - id: obs - if: ${{ steps.obs-cache.outputs.cache-hit != 'true' }} - env: - # obs-studio does not support ClangCL - CMAKE_GENERATOR_TOOLSET: "" - shell: bash - run: | - # Apply patches to obs-studio - pushd "${{ github.workspace }}/third-party/obs-studio" > /dev/null - for f in ../../patches/obs-studio/*.patch; do - echo "Applying patch '${f}''..." - [ -e "$f" ] || continue - git apply "$f" - done - popd > /dev/null - - # Build obs-studio - cmake \ - -S "${{ github.workspace }}/third-party/obs-studio" \ - -B "${{ github.workspace }}/build/obs" \ - -DCMAKE_SYSTEM_VERSION="${{ env.CMAKE_SYSTEM_VERSION }}" \ - -DCMAKE_INSTALL_PREFIX="${{ github.workspace }}/build/obs/install" \ - -DCMAKE_PREFIX_PATH="${{ github.workspace }}/build/obsdeps;${{ github.workspace }}/build/qt" \ - -DENABLE_PLUGINS=OFF \ - -DENABLE_UI=OFF \ - -DENABLE_SCRIPTING=OFF - cmake \ - --build "${{ github.workspace }}/build/obs" \ - --config RelWithDebInfo \ - --target obs-frontend-api - cmake \ - --install "${{ github.workspace }}/build/obs" \ - --config RelWithDebInfo \ - --component obs_libraries - - name: "Configure" - continue-on-error: true - shell: bash - run: | - cmake \ - -S "${{ github.workspace }}" \ - -B "${{ github.workspace }}/build/ci" \ - -DCMAKE_SYSTEM_VERSION="${{ env.CMAKE_SYSTEM_VERSION }}" \ - -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON \ - -Dlibobs_DIR="${{ github.workspace }}/build/obs/install" \ - -DQt${{ matrix.qt }}_DIR="${{ github.workspace }}/build/qt" \ - -DFFmpeg_DIR="${{ github.workspace }}/build/obsdeps" \ - -DCURL_DIR="${{ github.workspace }}/build/obsdeps" - - name: "Build: Debug" - continue-on-error: true - shell: bash - env: - CMAKE_BUILD_TYPE: "Debug" - run: | - cmake --build "build/ci" --config ${{ env.CMAKE_BUILD_TYPE }} --target StreamFX - - name: "Build: Release" - shell: bash - env: - CMAKE_BUILD_TYPE: "RelWithDebInfo" - run: | - cmake --build "build/ci" --config ${{ env.CMAKE_BUILD_TYPE }} --target StreamFX - - macos: - strategy: - fail-fast: false - matrix: - runner: [ "macos-12" ] - compiler: [ "Clang" ] - qt: [ 6 ] - include: - - compiler: "Clang" + - runner: "macos-12" + compiler: "AppleClang" + platform: "macos" name: "MacOS" CMAKE_GENERATOR: "Xcode" CMAKE_OSX_DEPLOYMENT_TARGET: "10.15" CMAKE_OSX_ARCHITECTURES: "x86_64;arm64" + - runner: "ubuntu-22.04" + compiler: "GCC-12" + platform: "ubuntu" + name: "Ubuntu 22.04" + CMAKE_GENERATOR: "Ninja Multi-Config" + - runner: "ubuntu-22.04" + compiler: "Clang-17" + platform: "ubuntu" + name: "Ubuntu 22.04 (Clang)" + CMAKE_GENERATOR: "Ninja Multi-Config" + exclude: + - runner: "windows-2022" + compiler: "GCC-12" + - runner: "windows-2022" + compiler: "Clang-17" + - runner: "windows-2022" + compiler: "AppleClang" + - runner: "macos-12" + compiler: "MSVC" + - runner: "macos-12" + compiler: "GCC-12" + - runner: "macos-12" + compiler: "Clang-17" + - runner: "ubuntu-22.04" + compiler: "MSVC" + - runner: "ubuntu-22.04" + compiler: "AppleClang" + name: "${{ matrix.name}} (${{ matrix.compiler}})" runs-on: "${{ matrix.runner }}" - name: "${{ matrix.name }} (${{ matrix.compiler }})" env: + CMAKE_BUILD_TYPE: "RelWithDebInfo" CMAKE_GENERATOR: "${{ matrix.CMAKE_GENERATOR }}" CMAKE_GENERATOR_PLATFORM: "${{ matrix.CMAKE_GENERATOR_PLATFORM }}" - CMAKE_GENERATOR_TOOLSET: "${{ matrix.CMAKE_GENERATOR_TOOLSET }}" CMAKE_SYSTEM_VERSION: "${{ matrix.CMAKE_SYSTEM_VERSION }}" CMAKE_OSX_DEPLOYMENT_TARGET: "${{ matrix.CMAKE_OSX_DEPLOYMENT_TARGET }}" CMAKE_OSX_ARCHITECTURES: "${{ matrix.CMAKE_OSX_ARCHITECTURES }}" steps: - - name: "Clone" - uses: actions/checkout@v3 - with: - submodules: recursive - fetch-depth: 0 - - name: "Gather Information" - id: info + - name: "Install Compiler" + if: ${{ matrix.platform == 'linux' }} shell: bash run: | - # Define buildspec file - buildspec="${{ github.workspace }}/third-party/obs-studio/buildspec.json" - - # Prebuilt Dependencies Version - IFS=$'\n' buildspecdata=($(node tools/buildspec.js "${buildspec}" "prebuilt" "macos-universal")) - echo "obs_deps_version=${buildspecdata[0]}" >> "$GITHUB_ENV" - echo "obs_deps_hash=${buildspecdata[1]}" >> "$GITHUB_ENV" - echo "obs_deps_url=${buildspecdata[2]}" >> "$GITHUB_ENV" - - # Qt Version - IFS=$'\n' buildspecdata=($(node tools/buildspec.js "${buildspec}" "qt${{ matrix.qt }}" "macos-universal")) - echo "qt_version=${buildspecdata[0]}" >> "$GITHUB_ENV" - echo "qt_hash=${buildspecdata[1]}" >> "$GITHUB_ENV" - echo "qt_url=${buildspecdata[2]}" >> "$GITHUB_ENV" - - # libOBS Version - echo "obs_version=$(cd "${{ github.workspace }}/third-party/obs-studio" && git describe --tags --long)" >> "$GITHUB_ENV" - - name: "Dependency: Qt (Cache)" - id: qt-cache - uses: actions/cache@v3 - with: - path: "${{ github.workspace }}/build/qt" - key: "qt${{ env.qt_hash }}-${{ env.CACHE_VERSION }}" - - name: "Dependency: Qt" - id: qt - if: ${{ steps.qt-cache.outputs.cache-hit != 'true' }} - shell: bash - run: | - curl --retry 5 --retry-delay 30 -jLo /tmp/qt.tar.xz "${{ env.qt_url }}" - if [[ ! -f "${{ github.workspace }}/build/qt" ]]; then mkdir -p "${{ github.workspace }}/build/qt"; fi - tar -xvf "/tmp/qt.tar.xz" -C "${{ github.workspace }}/build/qt" - - name: "Dependency: Prebuilt OBS Studio Dependencies (Cache)" - id: obsdeps-cache - uses: actions/cache@v3 - with: - path: "${{ github.workspace }}/build/obsdeps" - key: "obsdeps${{ env.obs_deps_hash }}-${{ env.CACHE_VERSION }}" - - name: "Dependency: Prebuilt OBS Studio Dependencies" - id: obsdeps - if: ${{ steps.obsdeps-cache.outputs.cache-hit != 'true' }} - shell: bash - run: | - curl --retry 5 --retry-delay 30 -jLo /tmp/obsdeps.tar.xz "${{ env.obs_deps_url }}" - if [[ ! -f "${{ github.workspace }}/build/obsdeps" ]]; then mkdir -p "${{ github.workspace }}/build/obsdeps"; fi - tar -xvf "/tmp/obsdeps.tar.xz" -C "${{ github.workspace }}/build/obsdeps" - - name: "Dependency: OBS Libraries (Cache)" - id: obs-cache - uses: actions/cache@v3 - with: - path: "${{ github.workspace }}/build/obs" - key: "obs${{ env.obs_version }}-${{ matrix.runner }}_${{ matrix.compiler }}--obsdeps${{ env.obs_deps_hash }}-qt${{ env.qt_hash }}-${{ env.CACHE_VERSION }}" - - name: "Dependency: OBS Libraries" - id: obs - if: ${{ steps.obs-cache.outputs.cache-hit != 'true' }} - shell: bash - run: | - # Apply patches to obs-studio - pushd "${{ github.workspace }}/third-party/obs-studio" > /dev/null - for f in ../../patches/obs-studio/*.patch; do - echo "Applying patch '${f}''..." - [ -e "$f" ] || continue - git apply "$f" - done - popd > /dev/null - - # Build obs-studio - cmake \ - -S "${{ github.workspace }}/third-party/obs-studio" \ - -B "${{ github.workspace }}/build/obs" \ - -DCMAKE_INSTALL_PREFIX="${{ github.workspace }}/build/obs/install" \ - -DCMAKE_PREFIX_PATH="${{ github.workspace }}/build/obsdeps;${{ github.workspace }}/build/qt" \ - -DENABLE_PLUGINS=OFF \ - -DENABLE_UI=OFF \ - -DENABLE_SCRIPTING=OFF - cmake \ - --build "${{ github.workspace }}/build/obs" \ - --config RelWithDebInfo \ - --target obs-frontend-api - cmake \ - --install "${{ github.workspace }}/build/obs" \ - --config RelWithDebInfo \ - --component obs_libraries - - name: "Configure" - continue-on-error: true - shell: bash - run: | - cmake \ - -S "${{ github.workspace }}" \ - -B "${{ github.workspace }}/build/ci" \ - -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON \ - -Dlibobs_DIR="${{ github.workspace }}/build/obs/install" \ - -DQt${{ matrix.qt }}_DIR="${{ github.workspace }}/build/qt" \ - -DFFmpeg_DIR="${{ github.workspace }}/build/obsdeps" \ - -DCURL_DIR="${{ github.workspace }}/build/obsdeps" - - name: "Build: Debug" - continue-on-error: true - shell: bash - run: | - cmake --build "build/ci" --config Debug --target StreamFX - - name: "Build: Release" - shell: bash - env: - CMAKE_BUILD_TYPE: "RelWithDebInfo" - run: | - cmake --build "build/ci" --config RelWithDebInfo --target StreamFX - - ubuntu: - strategy: - fail-fast: false - matrix: - runner: [ "ubuntu-22.04", "ubuntu-20.04" ] - compiler: [ "GCC-12", "GCC-11", "Clang-16" ] - qt: [ 5, 6 ] - CMAKE_GENERATOR: [ "Ninja Multi-Config" ] - exclude: - - runner: "ubuntu-22.04" - qt: 5 - - runner: "ubuntu-22.04" - compiler: "GCC-11" - - runner: "ubuntu-20.04" - qt: 6 - - runner: "ubuntu-20.04" - compiler: "GCC-12" - include: - - runner: "ubuntu-22.04" - name: "Ubuntu 22.04" - - runner: "ubuntu-20.04" - name: "Ubuntu 20.04" - runs-on: "${{ matrix.runner }}" - name: "${{ matrix.name }} (${{ matrix.compiler }}, Qt${{ matrix.qt }})" - env: - CMAKE_GENERATOR: "${{ matrix.CMAKE_GENERATOR }}" - CMAKE_GENERATOR_PLATFORM: "${{ matrix.CMAKE_GENERATOR_PLATFORM }}" - CMAKE_GENERATOR_TOOLSET: "${{ matrix.CMAKE_GENERATOR_TOOLSET }}" - CMAKE_SYSTEM_VERSION: "${{ matrix.CMAKE_SYSTEM_VERSION }}" - steps: - - name: "Clone" - uses: actions/checkout@v3 - with: - submodules: recursive - fetch-depth: 0 - - name: "Install Build Tools" - shell: bash - run: | - echo "Installing essential tools..." - sudo apt-get -qq update - sudo apt-get install build-essential checkinstall pkg-config cmake ninja-build git - # Install the appropriate compiler IFS=$'-' compiler=($(echo "${{ matrix.compiler }}")) # ToDo: Can this be done without invoking a sub-shell? echo "Installing '${compiler[0]}' version ${compiler[1]}..." @@ -359,6 +93,7 @@ jobs: echo "CMAKE_C_COMPILER=gcc-${compiler[1]}" >> "$GITHUB_ENV" echo "CMAKE_CXX_COMPILER=g++-${compiler[1]}" >> "$GITHUB_ENV" + echo "CMAKE_LINKER=gold" >> "$GITHUB_ENV" elif [[ "${compiler[0]}" == "Clang" ]]; then curl -jLo /tmp/llvm.sh "https://apt.llvm.org/llvm.sh" chmod +x /tmp/llvm.sh @@ -373,101 +108,119 @@ jobs: echo "CMAKE_C_COMPILER=clang-${compiler[1]}" >> "$GITHUB_ENV" echo "CMAKE_CXX_COMPILER=clang++-${compiler[1]}" >> "$GITHUB_ENV" + echo "CMAKE_LINKER=ld.lld-${compiler[1]}" >> "$GITHUB_ENV" else echo "Unknown Compiler" exit 1 fi - - name: "Dependency: Qt" - id: qt - shell: bash - run: | - if [[ ${{ matrix.qt }} -eq 5 ]]; then - sudo apt-get -y install -V \ - qtbase5-dev qtbase5-private-dev libqt5svg5-dev - elif [[ ${{ matrix.qt }} -eq 6 ]]; then - sudo apt-get -y install -V \ - qt6-base-dev qt6-base-private-dev libqt6svg6-dev libgles2-mesa-dev libegl1-mesa-dev libgl1-mesa-dev - fi - - name: "Dependency: Prebuilt OBS Studio Dependencies" - id: obsdeps - shell: bash - run: | - sudo apt-get -y install -V \ - libavcodec-dev libavdevice-dev libavfilter-dev libavformat-dev libavutil-dev libswresample-dev libswscale-dev \ - libcurl4-openssl-dev - - name: "Dependency: OBS Libraries (Cache)" - id: obs-cache - uses: actions/cache@v3 + - name: "Clone" + uses: "actions/checkout@v4" with: - path: "${{ github.workspace }}/build/obs" - key: "obs${{ env.obs_version }}-${{ matrix.runner }}_${{ matrix.compiler }}--${{ matrix.runner }}-${{ matrix.compiler }}-${{ env.CACHE_VERSION }}" - - name: "Dependency: OBS Libraries" - id: obs - if: ${{ steps.obs-cache.outputs.cache-hit != 'true' }} + fetch-depth: 0 + fetch-tags: true + - name: "Fetch" shell: bash run: | - # Apply patches to obs-studio - pushd "${{ github.workspace }}/third-party/obs-studio" > /dev/null - for f in ../../patches/obs-studio/*.patch; do - echo "Applying patch '${f}''..." - [ -e "$f" ] || continue - git apply "$f" - done - popd > /dev/null - - # Extra requirements by libobs on Linux. - sudo apt-get install \ - libavcodec-dev libavdevice-dev libavfilter-dev libavformat-dev libavutil-dev libswresample-dev libswscale-dev \ - libx264-dev libcurl4-openssl-dev libmbedtls-dev libgl1-mesa-dev libjansson-dev libluajit-5.1-dev python3-dev \ - libx11-dev libxcb-randr0-dev libxcb-shm0-dev libxcb-xinerama0-dev libxcomposite-dev libxinerama-dev \ - libxcb1-dev libx11-xcb-dev libxcb-xfixes0-dev swig libcmocka-dev libxss-dev libglvnd-dev libgles2-mesa \ - libgles2-mesa-dev libwayland-dev \ - libasound2-dev libfdk-aac-dev libfontconfig-dev libfreetype6-dev libjack-jackd2-dev libpulse-dev \ - libsndio-dev libspeexdsp-dev libudev-dev libv4l-dev libva-dev libvlc-dev libdrm-dev - - # Build obs-studio - cmake \ - -S "${{ github.workspace }}/third-party/obs-studio" \ - -B "${{ github.workspace }}/build/obs" \ - -G "Unix Makefiles" \ - -DCMAKE_BUILD_TYPE="Release" \ - -DCMAKE_C_COMPILER="${{ env.CMAKE_C_COMPILER }}" \ - -DCMAKE_CXX_COMPILER="${{ env.CMAKE_CXX_COMPILER }}" \ - -DCMAKE_C_FLAGS="${{ env.CMAKE_C_FLAGS }}" \ - -DCMAKE_CXX_FLAGS="${{ env.CMAKE_CXX_FLAGS }}" \ - -DCMAKE_INSTALL_PREFIX="${{ github.workspace }}/build/obs/install" \ - -DCMAKE_PREFIX_PATH="${{ github.workspace }}/build/obsdeps;${{ github.workspace }}/build/qt" \ - -DENABLE_PLUGINS=OFF \ - -DENABLE_UI=OFF \ - -DENABLE_SCRIPTING=OFF - cmake \ - --build "${{ github.workspace }}/build/obs" \ - --config Release \ - --target obs-frontend-api - cmake \ - --install "${{ github.workspace }}/build/obs" \ - --config Release \ - --component obs_libraries - - name: "Configure" + ./tools/build.sh fetch + - name: "Gather" + shell: bash + run: | + echo "OBS_VERSION=$(cd third-party/obs-studio/ && git describe --tags --long --abbrev=8 HEAD)" >> "${GITHUB_ENV}" + - name: "Patch" + shell: bash + run: | + ./tools/build.sh patch + - name: "Prerequisites" + shell: bash + run: | + ./tools/build.sh prerequisites + - name: "libOBS: Restore Cache" + id: libobs + uses: actions/cache/restore@v3 + with: + path: | + ${{ github.workspace }}/third-party/obs-studio/.deps + ${{ github.workspace }}/third-party/obs-studio/build/install + key: "${{ secrets.CACHE_VERSION }}-${{ env.CACHE_VERSION }}-${{ env.OBS_VERSION }}-${{ matrix.compiler }}-libobs" + - name: "libOBS" + if: ${{ (steps.libobs.outputs.cache-hit != 'true') }} + shell: bash + run: | + ./tools/build.sh libobs + - name: "libOBS: Save Cache" + if: ${{ (steps.libobs.outputs.cache-hit != 'true') }} + uses: actions/cache/save@v3 + with: + path: | + ${{ github.workspace }}/third-party/obs-studio/.deps + ${{ github.workspace }}/third-party/obs-studio/build/install + key: "${{ secrets.CACHE_VERSION }}-${{ env.CACHE_VERSION }}-${{ env.OBS_VERSION }}-${{ matrix.compiler }}-libobs" + - name: "Build: Restore Cache" + if: ${{ (steps.libobs.outputs.cache-hit == 'true') && (startsWith(github.ref, 'refs/heads/')) }} + uses: actions/cache/restore@v3 + with: + path: | + ${{ github.workspace }}/build + key: "${{ secrets.CACHE_VERSION }}-${{ env.CACHE_VERSION }}-${{ env.OBS_VERSION }}-${{ matrix.compiler }}-build" + - name: "Build" + shell: bash + run: | + ./tools/build.sh configure \ + --build "${{ github.workspace }}/build" \ + --install "${{ github.workspace }}/build/install" \ + --package "${{ github.workspace }}/build/package" \ + --package-name "StreamFX ${{ matrix.name }} (for OBS Studio v${{ env.OBS_VERSION }}) v" + _r=$?; if [[ $_r != 0 ]]; then exit "$_r"; fi + ./tools/build.sh build \ + --build "${{ github.workspace }}/build" + _r=$?; if [[ $_r != 0 ]]; then exit "$_r"; fi + echo "STREAMFX_VERSION=$(LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 grep -hoPe "[0-9]+\.[0-9]+\.[0-9]+(|[abc][0-9]+)-g[a-fA-F0-9]+" "${{ github.workspace }}/build/generated/version.hpp")" >> "${GITHUB_ENV}" + - name: "Build: Save Cache" + if: ${{ startsWith(github.ref, 'refs/heads/') }} + uses: actions/cache/save@v3 + with: + path: | + ${{ github.workspace }}/build + key: "${{ secrets.CACHE_VERSION }}-${{ env.CACHE_VERSION }}-${{ env.OBS_VERSION }}-${{ matrix.compiler }}-build" + - name: "Install" + shell: bash + run: | + ./tools/build.sh install \ + --build "${{ github.workspace }}/build" + - name: "Packaging: Install InnoSetup" + if: startsWith( matrix.runner, 'windows' ) + run: | + curl "-kL" "https://cdn.xaymar.com/ci/innosetup-6.2.1.exe" "-f" "--retry" "5" "-o" "inno.exe" + .\inno.exe /VERYSILENT /SP- /SUPPRESSMSGBOXES /NORESTART + - name: 'Packaging: Install Packages' continue-on-error: true + if: startsWith( matrix.runner, 'macos' ) shell: bash run: | - cmake \ - -S "${{ github.workspace }}" \ - -B "${{ github.workspace }}/build/ci" \ - -DCMAKE_C_COMPILER="${{ env.CMAKE_C_COMPILER }}" \ - -DCMAKE_CXX_COMPILER="${{ env.CMAKE_CXX_COMPILER }}" \ - -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON \ - -DCMAKE_INSTALL_PREFIX="${{ github.workspace }}/build/ci/install" \ - -DPACKAGE_NAME="streamfx-${{ env.PACKAGE_NAME }}" \ - -DPACKAGE_PREFIX="${{ github.workspace }}/build/package" \ - -Dlibobs_DIR="${{ github.workspace }}/build/obs/install" - - name: "Build: Debug" - continue-on-error: true + curl -kL https://cdn.xaymar.com/ci/Packages-1.2.10.dmg -f --retry 5 -o "Packages.dmg" + sudo hdiutil attach ./Packages.dmg + pushd /Volumes/Packages* + sudo installer -pkg ./Install\ Packages.pkg -target / + - name: "Packaging" + if: startsWith( matrix.runner, 'windows' ) + shell: cmd + run: | + "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" /V10 ".\build\installer.iss" + - name: "Packaging" + if: startsWith( matrix.runner, 'ubuntu' ) shell: bash run: | - cmake --build "build/ci" --config Debug --target StreamFX - - name: "Build: Release" + mkdir "${{ github.workspace }}/build/package" + cmake --build "${{ github.workspace }}/build" --config RelWithDebInfo --target PACKAGE + - name: "Packaging" + if: startsWith( matrix.runner, 'macos' ) shell: bash run: | - cmake --build "build/ci" --config RelWithDebInfo --target StreamFX + packagesbuild "${{ github.workspace }}/build/installer.pkgproj" + - name: "Artifacts" + uses: actions/upload-artifact@v4 + with: + name: "StreamFX ${{ matrix.name }} (for OBS Studio v${{ env.OBS_VERSION }}) v${{ env.STREAMFX_VERSION }}" + path: "${{ github.workspace }}/build/package" + if-no-files-found: error + compression-level: 0 diff --git a/.gitmodules b/.gitmodules index ece048a..05589a3 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,25 +1,51 @@ # AUTOGENERATED COPYRIGHT HEADER START -# Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +# Copyright (C) 2020-2024 Michael Fabian 'Xaymar' Dirks # AUTOGENERATED COPYRIGHT HEADER END [submodule "cmake/clang"] path = cmake/clang url = https://github.com/Xaymar/cmake-clang.git + fetchRecurseSubmodules = true + ignore = all + shallow = true [submodule "cmake/version"] path = cmake/version url = https://github.com/Xaymar/cmake-version.git + fetchRecurseSubmodules = true + ignore = all + shallow = true [submodule "third-party/nlohmann-json"] path = third-party/nlohmann-json url = https://github.com/nlohmann/json.git + fetchRecurseSubmodules = true + ignore = all + shallow = true [submodule "third-party/msvc-redist-helper"] path = third-party/msvc-redist-helper url = https://github.com/Xaymar/msvc-redist-helper.git + fetchRecurseSubmodules = true + ignore = all + shallow = true [submodule "third-party/nvidia-maxine-ar-sdk"] path = third-party/nvidia-maxine-ar-sdk url = https://github.com/NVIDIA/MAXINE-AR-SDK.git + fetchRecurseSubmodules = true + ignore = all + shallow = true [submodule "third-party/nvidia-maxine-vfx-sdk"] path = third-party/nvidia-maxine-vfx-sdk url = https://github.com/NVIDIA/MAXINE-VFX-SDK.git + fetchRecurseSubmodules = true + ignore = all + shallow = true [submodule "third-party/obs-studio"] path = third-party/obs-studio url = https://github.com/obsproject/obs-studio.git + fetchRecurseSubmodules = true + ignore = all +[submodule "third-party/nvidia-maxine-afx-sdk"] + path = third-party/nvidia-maxine-afx-sdk + url = https://github.com/NVIDIA/MAXINE-AFX-SDK.git + fetchRecurseSubmodules = true + ignore = all + shallow = true diff --git a/.mailmap b/.mailmap index 76c68b2..007409e 100644 --- a/.mailmap +++ b/.mailmap @@ -3,5 +3,6 @@ Michael Fabian 'Xaymar' Dirks Vainock <39059951+Vainock@users.noreply.github.com> Charles Fettinger Charles Fettinger +Radegast Stravinsky Radegast Stravinsky <58457062+Radegast-FFXIV@users.noreply.github.com> Carsten Braun diff --git a/BUILDING.md b/BUILDING.md new file mode 100644 index 0000000..5d48453 --- /dev/null +++ b/BUILDING.md @@ -0,0 +1,176 @@ +# Building +This document intends to guide you through the process of building StreamFX. It requires understanding of the tools used, and may require you to learn tools yourself before you can advance further in the guide. It is intended to be used by developers and contributors. + +## Required Pre-Requisites / Dependencies +- [Git](https://git-scm.com/) + - **Debian / Ubuntu** + `sudo apt install git` +- [CMake](https://cmake.org/) 3.20 (or newer) + - **Debian / Ubuntu** + `sudo apt install cmake` +- A compatible Compiler: + - **Windows** + [Visual Studio](https://visualstudio.microsoft.com/vs/) 2022 or newer + - **MacOS** + Xcode 11.x (or newer) for x86_64 + Xcode 12.x (or newer) for arm64 + - **Debian / Ubuntu** + - Essential Build Tools: + `sudo apt install build-essential pkg-config checkinstall make ninja-build` + - One of: + - GCC 12 (or newer) + `sudo apt install gcc-12 g++-12` + - [LLVM](https://releases.llvm.org/) Clang 14 (or newer) + `sudo bash -c "$(wget -O - https://apt.llvm.org/llvm.sh)"` + - One of: + - ld or gold + `sudo apt install binutils` + - [LLVM](https://releases.llvm.org/) lld + `sudo bash -c "$(wget -O - https://apt.llvm.org/llvm.sh)"` + - [mold](https://github.com/rui314/mold) + `sudo apt install mold` +- [Homebrew](https://brew.sh/) (Required, for **MacOS** only) + +## Building Bundled +The main method to build StreamFX is to first set up an OBS Studio copy and then integrate the StreamFX repository into it. It is recommended to first [Uninstall](Uninstallation) any currently installed versions of StreamFX to prevent conflicts, as OBS Studio may still attempt to load installed versions of StreamFX in addition to the one in the bundled build. + +1. Clone StreamFX recursively with submodules into a directory of your choice. + `git clone --recurse-submodules 'https://github.com/Xaymar/obs-StreamFX.git' .` +2. Navigate to the `third-party/obs-studio/UI/frontend-plugins` directory. +3. Add a symbolic link back to the StreamFX source code here. + - **Windows (Powershell)** + `New-Item -Path streamfx -ItemType SymbolicLink - Value ..\..\..\..\` + - **Windows (Batch)** + `mklink /J streamfx ..\..\..\..\` + - **Debian / Ubuntu** + `ln -s ../../../../ streamfx` +4. Open `CMakeLists.txt` in the same directory and append `add_subdirectory(streamfx)` to the end. +5. Navigate back to `third-party/obs-studio` and follow the follow the [OBS Studio build guide](https://obsproject.com/wiki/install-instructions). A short form of it is below. + 1. Check available CMake presets by running: + `cmake --list-presets` + 2. Configure for one of the available presets with the command: + - **Windows** + `cmake --preset windows-x64` + - **MacOS** + `cmake --preset macos` + - **Debian / Ubuntu (x86)** + `cmake --preset linux-x86_64` + - **Debian / Ubuntu (ARM)** + `cmake --preset linux-aarch` + 3. Open the generated IDE file of your choice and start coding. +6. Done. StreamFX is now part of the build. + +## Building Standalone +This method is primarily designed for Continuous Integration and is only used there, and as such requires a significantly more in depth experience with all used tools and projects. You are entirely on your own if you are so daring to choose this method. Here be dragons and stuff. + +### Install Prerequisites / Dependencies +- [Qt](https://www.qt.io/) 6: + - **Windows** + Handled by libobs. + - **MacOS** + Handled by libobs and the build script. + - **Debian / Ubuntu:** + `sudo apt install qt6-base-dev qt6-base-private-dev qt6-image-formats-plugins qt6-wayland libqt6svg6-dev libglx-dev libgl1-mesa-dev` +- [CURL](https://curl.se/): + - **Windows** + Handled by libobs. + - **MacOS** + Handled by libobs. + - **Debian / Ubuntu:** + `sudo apt install curl libcurl4-openssl-dev` +- [FFmpeg](https://ffmpeg.org/) (Optional, for FFmpeg component only): + - **Windows** + Handled by libobs. + - **MacOS** + Handled by libobs. + - **Debian / Ubuntu** + `sudo apt install libavcodec-dev libavdevice-dev libavfilter-dev libavformat-dev libavutil-dev libswresample-dev libswscale-dev` +- [LLVM](https://releases.llvm.org/) (Optional, for clang-format and clang-tidy integration only): + - **Windows** + Install using the Windows installer. + - **MacOS** + Install using the MacOS installer, though usually not needed. + - **Debian / Ubuntu** + `sudo bash -c "$(wget -O - https://apt.llvm.org/llvm.sh)" all` +- [InnoSetup](https://jrsoftware.org/isinfo.php) (Optional, for **Windows** installer only) + +### Steps +1. Open a `git` capable bash shell in your projects directory. (On Windows, git bash is enough). +2. Clone the project: + `git clone https://github.com/Xaymar/obs-StreamFX.git streamfx` +3. Install some required prerequisites: + `./tools/build.sh prerequisites` +4. Update submodules: + `./tools/build.sh fetch` +5. Apply patches: + `./tools/build.sh patch` +6. Build libOBS: + `./tools/build.sh libobs` +7. Build StreamFX: + `./tools/build.sh build` +8. Done. + +It is still possible to build using cmake-gui, however it is not recommended anymore. + +## CMake Options +The project is intended to be versatile and configurable, so we offer almost everything to be configured on a silver platter directly in CMake (if possible). If StreamFX detects that it is being built together with other projects, it will automatically prefix all options with `StreamFX_` to prevent collisions. + +### Generic +- `GIT` (not prefixed) + Path to the `git` binary on your system, for use with features that require git during configuration and generation. +- `VERSION` + Set or override the version of the project with a custom one. Allowed formats are: SemVer 2.0.0, CMake. + +### Code +- `ENABLE_CLANG` + Enable integration of `clang-format` and `clang-tidy` +- `CLANG_PATH` (not prefixed, only with `ENABLE_CLANG`) + Path to the `clang` installation containing `clang-format` and `clang-tidy`. Only used as a hint. +- `CLANG_FORMAT_PATH` and `CLANG_TIDY_PATH` (not prefixed) + Path to `clang-format` and `clang-tidy` that will be used. + +### Dependencies +- `LibObs_DIR` + Path to the obs-studio libraries. +- `Qt5_DIR`, `Qt6_DIR` or `Qt_DIR` (autodetect) + Path to Qt5 (OBS Studio 27.x and lower) or Qt6 (OBS Studio 28.x and higher). +- `FFmpeg_DIR` + Path to compatible FFmpeg libraries and headers. +- `CURL_DIR` + Path to compatible CURL libraries and headers. +- `AOM_DIR` + Path to compatible AOM libraries and headers. + +### Compiling +- `ENABLE_FASTMATH` + Enable fast math optimizations if the compiler supports them. This trades precision for performance, and is usually good enough anyway. +- `ENABLE_LTO` + Enable link time optimization for faster binaries in exchange for longer build times. +- `ENABLE_PROFILING` + Enable CPU and GPU profiling code, this option reduces performance drastically. +- `TARGET_*` + Specify which architecture target the generated binaries will use. + +### Components +- `COMPONENT_` + Enable the component by the given name. + +### Installing & Packaging +These options are only available in CI-Style mode. + +- `CMAKE_INSTALL_PREFIX` + The path in which installed content should be placed when building the `install` target. +- `STRUCTURE_PACKAGEMANAGER` + If enabled will install files in a layout compatible with package managers. +- `STRUCTURE_UNIFIED` + Enable to install files in a layout compatible with an OBS Studio plugin manager. +- `PACKAGE_NAME` + The name of the packaged archive, excluding the prefix, suffix and extension. +- `PACKAGE_PREFIX` + The path in which the packages should be placed. +- `PACKAGE_SUFFIX` + The suffix to attach to the name, before the file extension. If left blank will attach the current version string to the package. +- `STRUCTURE_UNIFIED` + Enable to replace the PACKAGE_ZIP target with a target that generates a single `.obs` file instead. + + diff --git a/CMakeLists.txt b/CMakeLists.txt index 186c32b..ce269f5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,12 +1,18 @@ # AUTOGENERATED COPYRIGHT HEADER START -# Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks +# Copyright (C) 2017-2024 Michael Fabian 'Xaymar' Dirks # Copyright (C) 2019 Cat Stevens # Copyright (C) 2020 Brandon Edens # Copyright (C) 2021 Christopher P Yarger # Copyright (C) 2022 Carsten Braun # Copyright (C) 2022 Romain Vigier +# Copyright (C) 2023 Isaac Nudelman # AUTOGENERATED COPYRIGHT HEADER END +# ToDo: +# - Explore RESOURCE property: https://cmake.org/cmake/help/latest/prop_tgt/RESOURCE.html +# - Explore PUBLIC_HEADER property: https://cmake.org/cmake/help/latest/prop_tgt/PUBLIC_HEADER.html + + # CMake Setup cmake_minimum_required(VERSION 3.26) list(APPEND CMAKE_MESSAGE_INDENT "[StreamFX] ") @@ -47,7 +53,7 @@ endif() # MacOSX: Require at least MacOSX 10.15 for C++17 support. if(APPLE) if(CMAKE_OSX_DEPLOYMENT_TARGET VERSION_LESS 10.15) - CacheSet(CMAKE_OSX_DEPLOYMENT_TARGET "10.15") + set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15" CACHE STRING "Minimum OSX version target" FORCE) endif() endif() @@ -227,10 +233,9 @@ if(EXISTS "${CMAKE_CURRENT_LIST_DIR}/.git") else() message(STATUS "Not a git repository, automatic version detection disabled.") endif() -version(PARSE _VERSION "${_VERSION}" REQUIRE "PATCH;TWEAK") # Allow manual overrides of the detected version. -if(NOT ("${${PREFIX}VERSION}" STREQUAL "")) +if(${PREFIX}VERSION) version(PARSE _VERSION_CFG "${${PREFIX}VERSION}" REQUIRE "PATCH;TWEAK") if("${_VERSION_CFG_BUILD}" STREQUAL "") set(_VERSION_CFG_BUILD "${_VERSION_BUILD}") @@ -245,6 +250,9 @@ if(NOT ("${${PREFIX}VERSION}" STREQUAL "")) ) endif() +# Fix up missing parts of the Version +version(PARSE _VERSION "${_VERSION}" REQUIRE "PATCH;TWEAK") + set(_VERSION_THIN "${_VERSION_MAJOR}.${_VERSION_MINOR}.${_VERSION_PATCH}") if(NOT (_VERSION_PRERELEASE STREQUAL "")) set(_VERSION_THIN "${_VERSION_THIN}${_VERSION_PRERELEASE}${_VERSION_TWEAK}") @@ -268,13 +276,13 @@ if(_VERSION_PRERELEASE STREQUAL "a") set(FEATURE_STABLE ON) elseif(_VERSION_PRERELEASE STREQUAL "b") set(FEATURE_DEPRECATED ON) - set(FEATURE_EXPERIMENTAL ON) + set(FEATURE_EXPERIMENTAL OFF) set(FEATURE_UNSTABLE ON) set(FEATURE_STABLE ON) elseif(_VERSION_PRERELEASE STREQUAL "c") set(FEATURE_DEPRECATED ON) set(FEATURE_EXPERIMENTAL OFF) - set(FEATURE_UNSTABLE ON) + set(FEATURE_UNSTABLE OFF) set(FEATURE_STABLE ON) else() set(FEATURE_DEPRECATED ON) @@ -286,50 +294,7 @@ endif() # Version override set(${PREFIX}VERSION "" CACHE STRING "Specify an override for the automatically detected version. Accepts a mixture of SemVer 2.0 and CMake Version.") -# Features -## Encoders -set(${PREFIX}ENABLE_ENCODER_FFMPEG ${FEATURE_STABLE} CACHE BOOL "Enable FFmpeg Encoder integration.") -set(${PREFIX}ENABLE_ENCODER_FFMPEG_AMF ${FEATURE_DEPRECATED} CACHE BOOL "Enable AMF Encoder in FFmpeg.") -set(${PREFIX}ENABLE_ENCODER_FFMPEG_NVENC ${FEATURE_STABLE} CACHE BOOL "Enable NVENC Encoder in FFmpeg.") -set(${PREFIX}ENABLE_ENCODER_FFMPEG_PRORES ${FEATURE_STABLE} CACHE BOOL "Enable ProRes Encoder in FFmpeg.") -set(${PREFIX}ENABLE_ENCODER_FFMPEG_DNXHR ${FEATURE_STABLE} CACHE BOOL "Enable DNXHR Encoder in FFmpeg.") -set(${PREFIX}ENABLE_ENCODER_FFMPEG_CFHD ${FEATURE_STABLE} CACHE BOOL "Enable CineForm Encoder in FFmpeg.") - -## Filters -set(${PREFIX}ENABLE_FILTER_AUTOFRAMING ${FEATURE_EXPERIMENTAL} CACHE BOOL "Enable Auto-Framing Filter") -set(${PREFIX}ENABLE_FILTER_AUTOFRAMING_NVIDIA ${FEATURE_EXPERIMENTAL} CACHE BOOL "Enable NVIDIA provider(s) Auto-Framing Filter") -set(${PREFIX}ENABLE_FILTER_BLUR ${FEATURE_UNSTABLE} CACHE BOOL "Enable Blur Filter") -set(${PREFIX}ENABLE_FILTER_COLOR_GRADE ${FEATURE_STABLE} CACHE BOOL "Enable Color Grade Filter") -set(${PREFIX}ENABLE_FILTER_DENOISING ${FEATURE_EXPERIMENTAL} CACHE BOOL "Enable Denoising filter") -set(${PREFIX}ENABLE_FILTER_DENOISING_NVIDIA ${FEATURE_EXPERIMENTAL} CACHE BOOL "Enable NVIDIA provider(s) for Denoising Filter") -set(${PREFIX}ENABLE_FILTER_DYNAMIC_MASK ${FEATURE_STABLE} CACHE BOOL "Enable Dynamic Mask Filter") -set(${PREFIX}ENABLE_FILTER_SDF_EFFECTS ${FEATURE_EXPERIMENTAL} CACHE BOOL "Enable SDF Effects Filter") -set(${PREFIX}ENABLE_FILTER_SHADER ${FEATURE_EXPERIMENTAL} CACHE BOOL "Enable Shader Filter") -set(${PREFIX}ENABLE_FILTER_TRANSFORM ${FEATURE_STABLE} CACHE BOOL "Enable Transform Filter") -set(${PREFIX}ENABLE_FILTER_UPSCALING ${FEATURE_EXPERIMENTAL} CACHE BOOL "Enable Upscaling Filter") -set(${PREFIX}ENABLE_FILTER_UPSCALING_NVIDIA ${FEATURE_EXPERIMENTAL} CACHE BOOL "Enable NVIDIA provider(s) for Upscaling Filter") -set(${PREFIX}ENABLE_FILTER_VIRTUAL_GREENSCREEN ${FEATURE_EXPERIMENTAL} CACHE BOOL "Enable Virtual Greenscreen Filter") -set(${PREFIX}ENABLE_FILTER_VIRTUAL_GREENSCREEN_NVIDIA ${FEATURE_EXPERIMENTAL} CACHE BOOL "Enable NVIDIA provider(s) for Virtual Greenscreen Filter") - -## Sources -set(${PREFIX}ENABLE_SOURCE_MIRROR ${FEATURE_DEPRECATED} CACHE BOOL "Enable Mirror Source") -set(${PREFIX}ENABLE_SOURCE_SHADER ${FEATURE_EXPERIMENTAL} CACHE BOOL "Enable Shader Source") - -## Transitions -set(${PREFIX}ENABLE_TRANSITION_SHADER ${FEATURE_EXPERIMENTAL} CACHE BOOL "Enable Shader Transition") - -## FrontEnd & UI -set(${PREFIX}ENABLE_FRONTEND ON CACHE BOOL "Enable Frontend code.") -set(${PREFIX}ENABLE_UPDATER ON CACHE BOOL "Enable automatic update checks.") - -## Code Related -if(COMMAND clang_format) - set(${PREFIX}ENABLE_CLANG OFF CACHE BOOL "Enable Clang integration for supported compilers.") -endif() -set(${PREFIX}ENABLE_PROFILING OFF CACHE BOOL "Enable CPU and GPU performance tracking, which has a non-zero overhead at all times. Do not enable this for release builds.") - -## Compile/Link Related -set(${PREFIX}ENABLE_LTO ${D_HAS_IPO} CACHE BOOL "Enable Link Time Optimization for faster and smaller binaries.") +# Compile/Link Related set(${PREFIX}ENABLE_FASTMATH ON CACHE BOOL "Enable fast math optimizations, which sacrifice precision and stability.") if(D_PLATFORM_ARCH_X86) set(${PREFIX}TARGET_X86_64_V4 OFF CACHE BOOL "Target x86-64-v4 (x86-64-v3, AVX512F, AVX512BW, AVX512CD, AVX512DQ, AVX512VL).") @@ -344,26 +309,22 @@ endif() # Installation / Packaging if(STANDALONE) - set(STRUCTURE_UNIFIED CACHE BOOL "Install for use in a Plugin Manager") if(D_PLATFORM_LINUX) set(STRUCTURE_PACKAGEMANAGER CACHE BOOL "Install for use in a Package Manager (system-wide installation)") - elseif(D_PLATFORM_MAC) - set(STRUCTURE_BUNDLE ON CACHE BOOL "Install as a Loadable Bundle (.plugin)") endif() set(PACKAGE_PREFIX "${CMAKE_BINARY_DIR}" CACHE PATH "Where to place the packages?") - set(PACKAGE_NAME "${PROJECT_NAME}" CACHE STRING "What should the package be called?") + set(PACKAGE_NAME "StreamFX ${CMAKE_SYSTEM_NAME} v" CACHE STRING "What should the package be called?") set(PACKAGE_SUFFIX "" CACHE STRING "Any suffix for the package name? (Defaults to the current version string)") endif() # Dependencies if(STANDALONE) set(libobs_DIR "" CACHE PATH "Path to libobs and obs-frontend-api") - set(Qt_DIR "" CACHE PATH "Path to Qt6 or Qt5") + set(Qt6_DIR "" CACHE PATH "Path to Qt6") set(CURL_DIR "" CACHE PATH "Path to CURL") set(FFmpeg_DIR "" CACHE PATH "Path to FFmpeg") endif() -set(AOM_DIR "" CACHE PATH "Path to AOM library") ################################################################################ # Project @@ -383,7 +344,7 @@ project( DESCRIPTION "Additional sources, filters, transitions and encoders for OBS Studio." HOMEPAGE_URL "https://streamfx.xaymar.com/" ) -set(PROJECT_IDENTIFER "com.xaymar.${PROJECT_NAME}.obs") +set(PROJECT_IDENTIFER "com.xaymar.StreamFX.obs") set(PROJECT_TITLE "StreamFX (for OBS Studio)") set(PROJECT_AUTHORS "Michael Fabian 'Xaymar' Dirks ") set(PROJECT_COPYRIGHT "2017 - 2022, Michael Fabian Dirks. All Rights Reserved") @@ -394,283 +355,45 @@ list(APPEND PROJECT_TRADEMARKS ) # Component search paths -list(APPEND CMAKE_PREFIX_PATH - "${${PREFIX}AOM_PATH}" - "${${PREFIX}OBSDEPS_PATH}" - "${${PREFIX}QT_PATH}" - "${AOM_DIR}" - "${CURL_DIR}" - "${DepsPath}" - "${FFmpeg_DIR}" - "${libobs_DIR}" - "${Qt_DIR}" - "${Qt5_DIR}" - "${Qt6_DIR}" - "${QTDIR}" -) -if(D_PLATFORM_MAC) +if(CURL_DIR) list(APPEND CMAKE_PREFIX_PATH - "${${PREFIX}AOM_PATH}/Frameworks" - "${${PREFIX}OBSDEPS_PATH}/Frameworks" - "${${PREFIX}QT_PATH}/Frameworks" - "${AOM_DIR}/Frameworks" + "${CURL_DIR}" + "${CURL_DIR}/cmake" + "${CURL_DIR}/lib/cmake" "${CURL_DIR}/Frameworks" - "${DepsPath}/Frameworks" + ) +endif() +if(FFmpeg_DIR) + list(APPEND CMAKE_PREFIX_PATH + "${FFmpeg_DIR}" + "${FFmpeg_DIR}/cmake" + "${FFmpeg_DIR}/lib/cmake" "${FFmpeg_DIR}/Frameworks" + ) +endif() +if(libobs_DIR) + list(APPEND CMAKE_PREFIX_PATH + "${libobs_DIR}" + "${libobs_DIR}/cmake" + "${libobs_DIR}/lib/cmake" "${libobs_DIR}/Frameworks" - "${Qt_DIR}/Frameworks" - "${Qt5_DIR}/Frameworks" + ) +endif() +if(Qt6_DIR) + list(APPEND CMAKE_PREFIX_PATH + "${Qt6_DIR}" + "${Qt6_DIR}/cmake" + "${Qt6_DIR}/lib/cmake" "${Qt6_DIR}/Frameworks" - "${QTDIR}/Frameworks" ) endif() +set_property(GLOBAL PROPERTY AUTOGEN_SOURCE_GROUP "User-Interface Files/Generated") + ################################################################################ -# Components +# Dependencies ################################################################################ -# Component resolving: -# 1. Check which features are enabled. For each feature, set what they require to ON. -# 2. Try and find required items. -# 3. Again check which features are enabled, if their requirements are missing, warn about it and disable them. -# TODO: Consider making this an error instead. - -macro(is_feature_enabled FEATURE OUTPUT) - set(T_ENABLED ${${PREFIX}ENABLE_${FEATURE}}) - set(T_DISABLED ${${PREFIX}DISABLE_${FEATURE}}) - if(T_ENABLED AND NOT T_DISABLED) - set(${OUTPUT} ON) - else() - # set(${PREFIX}DISABLE_${FEATURE} ON CACHE INTERNAL "" FORCE) - set(${OUTPUT} OFF) - endif() -endmacro() - -macro(set_feature_disabled FEATURE DISABLED) - set(${PREFIX}DISABLE_${FEATURE} ${DISABLED} CACHE INTERNAL "" FORCE) -endmacro() - -# Features -function(feature_encoder_ffmpeg RESOLVE) - is_feature_enabled(ENCODER_FFMPEG T_CHECK) - if(RESOLVE AND T_CHECK) - if(NOT HAVE_FFMPEG) - message(WARNING "FFmpeg Encoder requires FFmpeg. Disabling...") - set_feature_disabled(ENCODER_FFMPEG ON) - else() - # AMF - is_feature_enabled(ENCODER_FFMPEG_AMF T_CHECK) - if(T_CHECK AND D_PLATFORM_MAC) - message(WARNING "FFmpeg Encoder 'AMF' requires Windows or Linux. Disabling...") - set_feature_disabled(ENCODER_FFMPEG_AMF ON) - endif() - - # NVENC - is_feature_enabled(ENCODER_FFMPEG_NVENC T_CHECK) - if(T_CHECK AND D_PLATFORM_MAC) - message(WARNING "FFmpeg Encoder 'NVENC' requires Windows or Linux. Disabling...") - set_feature_disabled(ENCODER_FFMPEG_NVENC ON) - endif() - - # ProRes - is_feature_enabled(ENCODER_FFMPEG_PRORES T_CHECK) - - # DNxHR - is_feature_enabled(ENCODER_FFMPEG_DNXHR T_CHECK) - - # CineForm - is_feature_enabled(ENCODER_FFMPEG_CFHD T_CHECK) - endif() - elseif(T_CHECK) - set(REQUIRE_FFMPEG ON PARENT_SCOPE) - endif() -endfunction() - -function(feature_encoder_aom_av1 RESOLVE) - is_feature_enabled(ENCODER_AOM_AV1 T_CHECK) - if(RESOLVE AND T_CHECK) - if(NOT HAVE_AOM) - message(WARNING "AOM AV1 encoder missing AOM library. Disabling...") - set_feature_disabled(ENCODER_AOM_AV1 ON) - endif() - elseif(T_CHECK) - set(REQUIRE_AOM ON PARENT_SCOPE) - endif() -endfunction() - -function(feature_filter_autoframing RESOLVE) - is_feature_enabled(FILTER_AUTOFRAMING T_CHECK) - if(RESOLVE AND T_CHECK) - # Verify that the requirements for the providers are available - if(NOT HAVE_NVIDIA_AR_SDK) - message(WARNING "'NVIDIA Augmented Reality SDK' is missing. Disabling NVIDIA provider...") - set_feature_disabled(FILTER_AUTOFRAMING_NVIDIA ON) - endif() - - # Verify that we have at least one provider for Auto-Framing. - is_feature_enabled(FILTER_AUTOFRAMING_NVIDIA T_CHECK_NVIDIA) - if(NOT T_CHECK_NVIDIA) - message(WARNING "Auto-Framing has no available providers. Disabling...") - set_feature_disabled(FILTER_AUTOFRAMING ON) - endif() - elseif(T_CHECK) - set(REQUIRE_NVIDIA_AR_SDK ON PARENT_SCOPE) - set(REQUIRE_NVIDIA_CUDA ON PARENT_SCOPE) - endif() -endfunction() - -function(feature_filter_blur RESOLVE) - is_feature_enabled(FILTER_BLUR T_CHECK) -endfunction() - -function(feature_filter_color_grade RESOLVE) - is_feature_enabled(FILTER_COLOR_GRADE T_CHECK) -endfunction() - -function(feature_filter_denoising RESOLVE) - is_feature_enabled(FILTER_DENOISING T_CHECK) - if(RESOLVE AND T_CHECK) - # Verify that the requirements for the providers are available - if(NOT HAVE_NVIDIA_VFX_SDK) - message(WARNING "'NVIDIA Video Effects SDK' is missing. Disabling NVIDIA provider...") - set_feature_disabled(FILTER_DENOISING_NVIDIA ON) - endif() - - # Verify that we have at least one provider for Video Denoising. - is_feature_enabled(FILTER_DENOISING_NVIDIA T_CHECK_NVIDIA) - if(NOT T_CHECK_NVIDIA) - message(WARNING "Denoising has no available providers. Disabling...") - set_feature_disabled(FILTER_DENOISING ON) - endif() - elseif(T_CHECK) - set(REQUIRE_NVIDIA_VFX_SDK ON PARENT_SCOPE) - endif() -endfunction() - -function(feature_filter_dynamic_mask RESOLVE) - is_feature_enabled(FILTER_DYNAMIC_MASK T_CHECK) -endfunction() - -function(feature_filter_sdf_effects RESOLVE) - is_feature_enabled(FILTER_SDF_EFFECTS T_CHECK) -endfunction() - -function(feature_filter_shader RESOLVE) - is_feature_enabled(FILTER_SHADER T_CHECK) -endfunction() - -function(feature_filter_transform RESOLVE) - is_feature_enabled(FILTER_TRANSFORM T_CHECK) -endfunction() - -function(feature_filter_upscaling RESOLVE) - is_feature_enabled(FILTER_UPSCALING T_CHECK) - if(RESOLVE AND T_CHECK) - # Verify that the requirements for the providers are available - if(NOT HAVE_NVIDIA_VFX_SDK) - message(WARNING "'NVIDIA Video Effects SDK' is missing. Disabling NVIDIA provider(s)...") - set_feature_disabled(FILTER_UPSCALING_NVIDIA ON) - endif() - - # Verify that we have at least one provider for Video Super-Resolution. - is_feature_enabled(FILTER_UPSCALING_NVIDIA T_CHECK_NVIDIA) - if(NOT T_CHECK_NVIDIA) - message(WARNING "Upscaling has no available providers. Disabling...") - set_feature_disabled(FILTER_UPSCALING ON) - endif() - elseif(T_CHECK) - set(REQUIRE_NVIDIA_VFX_SDK ON PARENT_SCOPE) - endif() -endfunction() - -function(feature_filter_virtual_greenscreen RESOLVE) - is_feature_enabled(FILTER_VIRTUAL_GREENSCREEN T_CHECK) - if(RESOLVE AND T_CHECK) - # Verify that the requirements for the providers are available - if(NOT HAVE_NVIDIA_VFX_SDK) - message(WARNING "'NVIDIA Video Effects SDK' is missing. Disabling NVIDIA provider(s)...") - set_feature_disabled(FILTER_VIRTUAL_GREENSCREEN_NVIDIA ON) - endif() - - # Verify that we have at least one provider for Video Super-Resolution. - is_feature_enabled(FILTER_VIRTUAL_GREENSCREEN_NVIDIA T_CHECK_NVIDIA) - if(NOT T_CHECK_NVIDIA) - message(WARNING "Virtual Greenscreen has no available providers. Disabling...") - set_feature_disabled(FILTER_VIRTUAL_GREENSCREEN ON) - endif() - elseif(T_CHECK) - set(REQUIRE_NVIDIA_VFX_SDK ON PARENT_SCOPE) - endif() -endfunction() - -function(feature_source_mirror RESOLVE) - is_feature_enabled(SOURCE_MIRROR T_CHECK) -endfunction() - -function(feature_source_shader RESOLVE) - is_feature_enabled(SOURCE_SHADER T_CHECK) -endfunction() - -function(feature_transition_shader RESOLVE) - is_feature_enabled(TRANSITION_SHADER T_CHECK) -endfunction() - -function(feature_frontend RESOLVE) - is_feature_enabled(FRONTEND T_CHECK) - if(RESOLVE AND T_CHECK) - if(NOT (Qt6_FOUND OR Qt5_FOUND)) - message(WARNING "Front-End requires Qt. Disabling...") - set_feature_disabled(FRONTEND ON) - elseif(NOT obs-frontend-api_FOUND) - message(WARNING "Front-End requires OBS FrontEnd API. Disabling...") - set_feature_disabled(FRONTEND ON) - elseif(NOT HAVE_JSON) - message(WARNING "Front-End requires nlohmann::json. Disabling...") - set_feature_disabled(FRONTEND ON) - endif() - elseif(T_CHECK) - set(REQUIRE_QT ON PARENT_SCOPE) - set(REQUIRE_OBS_FRONTEND_API ON PARENT_SCOPE) - set(REQUIRE_JSON ON PARENT_SCOPE) - endif() -endfunction() - -function(feature_updater RESOLVE) - is_feature_enabled(UPDATER T_CHECK) - if(RESOLVE AND T_CHECK) - if(NOT CURL_FOUND) - message(WARNING "Updater requires CURL. Disabling...") - set_feature_disabled(UPDATER ON) - elseif(NOT HAVE_JSON) - message(WARNING "Updater requires nlohmann::json. Disabling...") - set_feature_disabled(UPDATER ON) - endif() - elseif(T_CHECK) - set(REQUIRE_CURL ON PARENT_SCOPE) - set(REQUIRE_JSON ON PARENT_SCOPE) - endif() -endfunction() - -# Set Requirements -feature_encoder_ffmpeg(OFF) -feature_encoder_aom_av1(OFF) -feature_filter_autoframing(OFF) -feature_filter_blur(OFF) -feature_filter_color_grade(OFF) -feature_filter_denoising(OFF) -feature_filter_dynamic_mask(OFF) -feature_filter_sdf_effects(OFF) -feature_filter_shader(OFF) -feature_filter_transform(OFF) -feature_filter_upscaling(OFF) -feature_filter_virtual_greenscreen(OFF) -feature_source_mirror(OFF) -feature_source_shader(OFF) -feature_transition_shader(OFF) -feature_frontend(OFF) -feature_updater(OFF) - -# Fulfill Requirements #- OBS: Library if(STANDALONE) find_package("libobs" REQUIRED CONFIG) @@ -695,185 +418,593 @@ if(D_PLATFORM_MAC) endif() #- OBS: Front-End API -set(obs-frontend-api_FOUND OFF) -if(REQUIRE_OBS_FRONTEND_API) - if(STANDALONE) - find_package("obs-frontend-api" CONFIG) - set(obs-frontend-api_FOUND "${obs-frontend-api_FOUND}") - elseif(TARGET obs-frontend-api) - set(obs-frontend-api_FOUND ON) - endif() - if(NOT TARGET OBS::obs-frontend-api) - add_library(OBS::obs-frontend-api ALIAS obs-frontend-api) - endif() +if(STANDALONE) + find_package("obs-frontend-api" CONFIG) + set(obs-frontend-api_FOUND "${obs-frontend-api_FOUND}") +elseif(TARGET obs-frontend-api) + set(obs-frontend-api_FOUND ON) +endif() +if(NOT TARGET OBS::obs-frontend-api) + add_library(OBS::obs-frontend-api ALIAS obs-frontend-api) endif() #- CURL -set(CURL_FOUND OFF) -if(REQUIRE_CURL) - find_package("CURL") -endif() - -#- FFmpeg -set(HAVE_FFMPEG OFF) -if(REQUIRE_FFMPEG) - find_package("FFmpeg" - COMPONENTS "avutil" "avcodec" "swscale" - ) - set(HAVE_FFMPEG ${FFmpeg_FOUND}) -endif() - -#- AOM -set(HAVE_AOM OFF) -if(REQUIRE_AOM) - if(NOT D_PLATFORM_MAC) - find_package("AOM") - set(HAVE_AOM ${AOM_FOUND}) - endif() -endif() +find_package("CURL") #- JSON -set(HAVE_JSON OFF) -if(REQUIRE_JSON) - if(NOT EXISTS "${PROJECT_SOURCE_DIR}/third-party/nlohmann-json/single_include") - message(FATAL_ERROR "Please make sure to update git submodules to their latest supported version.") - return() - else() - set(JSON_INCLUDE_DIR "${PROJECT_SOURCE_DIR}/third-party/nlohmann-json/single_include") - set(HAVE_JSON ON) - endif() +if(NOT EXISTS "${PROJECT_SOURCE_DIR}/third-party/nlohmann-json/single_include") + message(FATAL_ERROR "Please make sure to update git submodules to their latest supported version.") + return() +else() + set(JSON_INCLUDE_DIR "${PROJECT_SOURCE_DIR}/third-party/nlohmann-json/single_include") endif() -#- NVIDIA Augmented Reality SDK -set(HAVE_NVIDIA_AR_SDK OFF) -if(REQUIRE_NVIDIA_AR_SDK AND D_PLATFORM_WINDOWS) - if(EXISTS "${PROJECT_SOURCE_DIR}/third-party/nvidia-maxine-ar-sdk/version.h") - set(HAVE_NVIDIA_AR_SDK ON) - endif() +#- Qt 6 +find_package("Qt6" REQUIRED CONFIG COMPONENTS + Core + Gui + Widgets +) - if(NOT TARGET NVIDIA::AR) - add_library(NVIDIA::AR IMPORTED INTERFACE) - target_include_directories(NVIDIA::AR - INTERFACE - "${PROJECT_SOURCE_DIR}/third-party/nvidia-maxine-ar-sdk/nvar/include/" - "${PROJECT_SOURCE_DIR}/third-party/nvidia-maxine-ar-sdk/nvar/src/" +################################################################################ +# Helpers +################################################################################ +define_property(TARGET PROPERTY COMPONENT_LABEL) +define_property(TARGET PROPERTY COMPONENT_NAME) +define_property(TARGET PROPERTY COMPONENT_OPTION) +define_property(TARGET PROPERTY COMPONENT_RESOLVER) +define_property(TARGET PROPERTY COMPONENT_DEPENDS) + +function(streamfx_add_library TARGET_NAME TARGET_TYPE) + add_library(${TARGET_NAME} ${TARGET_TYPE}) + + set_target_properties(${TARGET_NAME} PROPERTIES + # Always generate position independent code. + POSITION_INDEPENDENT_CODE ON + + # Set C++ Standard and Extensions + C_STANDARD 17 + C_STANDARD_REQUIRED ON + CXX_STANDARD 20 + CXX_STANDARD_REQUIRED ON + CXX_EXTENSIONS OFF + + # Remove prefix from generated files. + PREFIX "" + IMPORT_PREFIX "" + + # Never treat warnings as errors. + COMPILE_WARNING_AS_ERROR OFF + ) + if(D_PLATFORM_MAC) + set_target_properties(${TARGET_NAME} PROPERTIES + # @rpath in installed binaries + INSTALL_RPATH "@executable_path/../Frameworks/;@loader_path/../Frameworks/;@loader_path/../Resources/" ) - endif() -endif() -#- NVIDIA Video Effects SDK -set(HAVE_NVIDIA_VFX_SDK OFF) -if(REQUIRE_NVIDIA_VFX_SDK AND D_PLATFORM_WINDOWS) - if(EXISTS "${PROJECT_SOURCE_DIR}/third-party/nvidia-maxine-vfx-sdk/version.h") - set(HAVE_NVIDIA_VFX_SDK ON) + if(STANDALONE) + set_target_properties(${TARGET_NAME} PROPERTIES + # @rpath in built binaries + BUILD_WITH_INSTALL_RPATH ON + ) + endif() endif() - if(NOT TARGET NVIDIA::VFX) - add_library(NVIDIA::VFX IMPORTED INTERFACE) - target_include_directories(NVIDIA::VFX - INTERFACE - "${PROJECT_SOURCE_DIR}/third-party/nvidia-maxine-vfx-sdk/nvvfx/include/" - "${PROJECT_SOURCE_DIR}/third-party/nvidia-maxine-vfx-sdk/nvvfx/src/" + target_compile_definitions(${TARGET_NAME} PRIVATE + __STDC_WANT_LIB_EXT1__=1 + ) + if(D_PLATFORM_WINDOWS) + target_compile_definitions(${TARGET_NAME} + PUBLIC + # Microsoft Visual C++ + _CRT_SECURE_NO_WARNINGS + _ENABLE_EXTENDED_ALIGNED_STORAGE + + PUBLIC + # windows.h + # - Disable MIN/MAX macro as this breaks a lot of code. + NOMINMAX + # - Disable IN/OUT macro as this breaks a lot of code. + NOINOUT ) endif() - set(REQUIRE_NVIDIA_CUDA ON) -endif() + if(NOT STANDALONE) + set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "plugins/StreamFX") + endif() -#- NVIDIA CUDA (Windows) -set(HAVE_NVIDIA_CUDA OFF) -if(REQUIRE_NVIDIA_CUDA AND D_PLATFORM_WINDOWS) - set(HAVE_NVIDIA_CUDA ON) -endif() + # C/C++ Compiler Adjustments + if(D_PLATFORM_WINDOWS AND ((CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") OR (CMAKE_CXX_COMPILER_ID STREQUAL "Clang"))) + # MSVC/ClangCL -#- Qt -if(REQUIRE_QT) - # Try Qt6 first... - find_package("Qt6" - COMPONENTS Core Gui Widgets - CONFIG + # - Dynamically link Microsoft C/C++ Redistributable. + target_compile_options(${TARGET_NAME} PRIVATE + $<$:/MD> + $<$:/MDd> + $<$:/MD> + $<$:/MD> + $<$:/MD> + ) + + # - Treat Warnings as Errors + target_compile_options(${TARGET_NAME} PRIVATE "/Wall") + + # - Explicitly disable treating all Warnings as Errors + target_compile_options(${TARGET_NAME} PRIVATE "/WX-") + + # - Disable useless warnings + set(DISABLED_WARNINGS + # Don't warn about unused variables, parameters, labels, functions, or typedefs. + "4100" + "4101" + "4102" + "4505" + "4514" + "5245" + "5264" + # Don't warn about unreferenced variables or parameters which are assigned/initialized. + "4189" + # Don't warn about not-explicitly-handled enumeration identifiers + "4061" + # Ignore automatic padding warnings. + "4820" + # Ignore assignment/move/copy being implicit '= delete;'. + "4623" + "4625" + "4626" + "5026" + "5027" + # Relative include paths are fine. + "4464" + # Buggy warning: printf expects string literal + "4774" + # Buggy warning: subobject initialization should be wrapped in braces + "5246" + # Ignore undefined, unused or unreferenced pre-processor macros + "4688" + # Ignore non-inlined functions + "4710" + # Ignore Spectre mitigation insertions + "5045" + # Ignore inserted padding. + "4324" + # Ignore non-standard nameless struct/union. + "4201" + # Intended behavior: Replace undefined macros with 0 + "4668" + # Ignore "does not result in a valid preprocessing token" + "5103" + ) + foreach(WARN ${DISABLED_WARNINGS}) + target_compile_options(${TARGET_NAME} PRIVATE "/wd${WARN}") + endforeach() + + # - Require enabled instruction sets. + if(D_PLATFORM_ARCH_X86) + if(${PREFIX}TARGET_X86_64_V4) + target_compile_options(${TARGET_NAME} PRIVATE "/arch:AVX512") + elseif(${PREFIX}TARGET_X86_64_V3) + target_compile_options(${TARGET_NAME} PRIVATE "/arch:AVX2") + elseif(${PREFIX}TARGET_X86_64_V2_EX) + target_compile_options(${TARGET_NAME} PRIVATE "/arch:AVX") + elseif(${PREFIX}TARGET_X86_64_V2) + target_compile_options(${TARGET_NAME} PRIVATE "/d2archSSE42") + elseif(${PREFIX}TARGET_X86_64) + #target_compile_options(${TARGET_NAME} PRIVATE "/arch:SSE2") + endif() + endif() + + # - Use fast unordered math if possible. + if(${PREFIX}ENABLE_FASTMATH) + target_compile_options(${TARGET_NAME} PRIVATE "/fp:fast") + else() + target_compile_options(${TARGET_NAME} PRIVATE "/fp:precise") + if(MSVC_VERSION GREATER 1930) + # Keep original behavior in VS2022 and up. + target_compile_options(${TARGET_NAME} PRIVATE "/fp:contract") + endif() + endif() + + # - Enable C++ exceptions with SEH exceptions. + target_compile_options(${TARGET_NAME} PRIVATE "/EHa") + + # - Enable multi-processor compiling. + target_compile_options(${TARGET_NAME} PRIVATE "/MP") + + # - Enable updated __cplusplus macro + target_compile_options(${TARGET_NAME} PRIVATE "/Zc:__cplusplus") + elseif(D_PLATFORM_LINUX AND ((CMAKE_CXX_COMPILER_ID STREQUAL "Clang") OR (CMAKE_CXX_COMPILER_ID STREQUAL "GNU"))) + # GCC/Clang + + # - Enable all warnings. + target_compile_options(${TARGET_NAME} PRIVATE "-Wall") + target_compile_options(${TARGET_NAME} PRIVATE "-Wextra") + + # - Disable useless warnings + set(DISABLED_WARNINGS + # Don't warn about unused variables, parameters, labels, functions, or typedefs. + "unused-function" + "unused-label" + "unused-local-typedefs" + "unused-parameter" + "unused-result" + "unused-const-variable" + "unused-variable" + "unused-value" + # Don't warn about unreferenced variables or parameters which are assigned/initialized. + "unused-but-set-parameter" + "unused-but-set-variable" + # Don't warn about not-explicitly-handled enumeration identifiers + "switch" + # Ignore automatic padding warnings. + "padded" + # Ignore implicit '= delete;'. + # Ignore extra arguments for printf + "format-extra-args" + # Ignore undefined, unused or unreferenced pre-processor macros + "unused-macros" + ) + foreach(WARN ${DISABLED_WARNINGS}) + target_compile_options(${TARGET_NAME} PRIVATE "-Wno-${WARN}") + endforeach() + + # - Require enabled instruction sets. + if(${PREFIX}TARGET_NATIVE) + target_compile_options(${TARGET_NAME} PRIVATE + "-march=native" + ) + elseif(D_PLATFORM_ARCH_X86) + if(${PREFIX}TARGET_X86_64_V4) + target_compile_options(${TARGET_NAME} PRIVATE + "-march=x86-64-v4" + ) + elseif(${PREFIX}TARGET_X86_64_V3) + target_compile_options(${TARGET_NAME} PRIVATE + "-march=x86-64-v3" + ) + elseif(${PREFIX}TARGET_X86_64_V2_EX) + target_compile_options(${TARGET_NAME} PRIVATE + "-march=x86-64-v2" + "-mavx" + "-mbmi2" + "-mbmi" + "-mfma" + "-mf16c" + "-mmovbe" + "-mpclmul" + "-mpopcnt" + ) + elseif(${PREFIX}TARGET_X86_64_V2) + target_compile_options(${TARGET_NAME} PRIVATE + "-march=x86-64-v2" + ) + elseif(${PREFIX}TARGET_X86_64) + target_compile_options(${TARGET_NAME} PRIVATE + "-march=x86-64" + ) + endif() + if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + target_compile_options(${TARGET_NAME} PRIVATE + "-mtune=generic" + ) + else() + target_compile_options(${TARGET_NAME} PRIVATE + "-mtune=x86-64" + ) + endif() + endif() + + # - Use fast unordered math if possible. + if(${PREFIX}ENABLE_FASTMATH) + if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + target_compile_options(${TARGET_NAME} PRIVATE + "-ffast-math" + ) + else() + target_compile_options(${TARGET_NAME} PRIVATE + "-ffp-model=fast" + ) + endif() + else() + target_compile_options(${TARGET_NAME} PRIVATE + "-ffp-model=precise" + ) + endif() + + # - Don't export by default, require attributes. + # add_compile_options("-fvisibility=hidden") + elseif(D_PLATFORM_MAC AND (CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang")) + # AppleClang + + # - Enable most useful warnings. + target_compile_options(${TARGET_NAME} PRIVATE "-Wall") + target_compile_options(${TARGET_NAME} PRIVATE "-Wextra") + + # - Disable useless warnings + set(DISABLED_WARNINGS + # Don't warn about unused variables, parameters, labels, functions, or typedefs. + "unused-function" + "unused-label" + "unused-local-typedefs" + "unused-parameter" + "unused-result" + "unused-const-variable" + "unused-variable" + "unused-value" + # Don't warn about unreferenced variables or parameters which are assigned/initialized. + "unused-but-set-parameter" + "unused-but-set-variable" + # Don't warn about not-explicitly-handled enumeration identifiers + "switch" + # Ignore automatic padding warnings. + "padded" + # Ignore implicit '= delete;'. + # Ignore extra arguments for printf + "format-extra-args" + # Ignore undefined, unused or unreferenced pre-processor macros + "unused-macros" + ) + foreach(WARN ${DISABLED_WARNINGS}) + target_compile_options(${TARGET_NAME} PRIVATE "-Wno-${WARN}") + endforeach() + + + # - Require enabled instruction sets. + if(${PREFIX}TARGET_NATIVE) + target_compile_options(${TARGET_NAME} PRIVATE + "-march=native" + ) + message(WARNING "Targeting native architecture. Binaries will not be distributable to other systems!") + endif() + + # - Use fast unordered math if possible. + # FIXME: Appears to not be supported. + + # - Don't export by default, require attributes. + # add_compile_options("-fvisibility=hidden") + endif() +endfunction() + +function(streamfx_sanitize_name TEXT _OUTPUT_NAME _OUTPUT_TARGET _OUTPUT_OPTION) + string(REGEX REPLACE "^[ \t]+" "" TEXT "${TEXT}") + string(REGEX REPLACE "[ \t]+$" "" TEXT "${TEXT}") + string(REGEX REPLACE "[^a-zA-Z0-9_-]+" "_" TEXT2 "${TEXT}") + string(REGEX REPLACE "_[_]+" "_" TEXT2 "${TEXT2}") + string(TOUPPER "${TEXT2}" TEXT3) + + set(${_OUTPUT_NAME} "${TEXT}" PARENT_SCOPE) + set(${_OUTPUT_TARGET} "${TEXT2}" PARENT_SCOPE) + set(${_OUTPUT_OPTION} "${TEXT3}" PARENT_SCOPE) +endfunction() + +function(streamfx_add_component COMPONENT_NAME) + cmake_parse_arguments(PARSE_ARGV 1 _ARG + "" + "RESOLVER" + "" ) - # If Qt6 isn't present, try Qt5 - if(NOT Qt6_FOUND) - find_package("Qt5" - COMPONENTS Core Gui Widgets - CONFIG + # Sanitize name for further use. + streamfx_sanitize_name("${COMPONENT_NAME}" COMPONENT_NAME COMPONENT_SANITIZED_NAME COMPONENT_OPTION_NAME) + set(COMPONENT_OPTION "${PREFIX}COMPONENT_${COMPONENT_OPTION_NAME}") + set(COMPONENT_OPTION "${COMPONENT_OPTION}" PARENT_SCOPE) + + # Define the necessary targets. + set(COMPONENT_TARGET "StreamFX_${COMPONENT_SANITIZED_NAME}") + set(COMPONENT_TARGET "${COMPONENT_TARGET}" PARENT_SCOPE) + set(COMPONENT_ALIAS "StreamFX::${COMPONENT_SANITIZED_NAME}") + set(COMPONENT_ALIAS "${COMPONENT_ALIAS}" PARENT_SCOPE) + + streamfx_add_library(${COMPONENT_TARGET} STATIC EXCLUDE_FROM_ALL) + add_library(${COMPONENT_ALIAS} ALIAS ${COMPONENT_TARGET}) + set_target_properties(${COMPONENT_TARGET} PROPERTIES + COMPONENT_LABEL "${COMPONENT_NAME}" + COMPONENT_NAME "${COMPONENT_ALIAS}" + COMPONENT_OPTION "${COMPONENT_OPTION}" + ) + + # Always depend on StreamFX::Core + target_link_libraries(${COMPONENT_TARGET} PUBLIC $) + + # Register the component globally. + get_target_property(_DEPENDS StreamFX COMPONENT_DEPENDS) + if(_DEPENDS) + list(APPEND _DEPENDS "${COMPONENT_SANITIZED_NAME}") + else() + set(_DEPENDS "${COMPONENT_SANITIZED_NAME}") + endif() + set_target_properties(StreamFX PROPERTIES + COMPONENT_DEPENDS "${_DEPENDS}" + ) + + # If there is a resolver function, register it. + if(_ARG_RESOLVER) + set_target_properties(${COMPONENT_TARGET} PROPERTIES + COMPONENT_RESOLVER "${_ARG_RESOLVER}" ) endif() - if(Qt6_FOUND) - message(STATUS "Using Qt6.") - elseif(Qt5_FOUND) - message(STATUS "Using Qt5.") + + # Allow disabling this component. + set(${COMPONENT_OPTION} ON CACHE BOOL "Enable the ${COMPONENT_NAME} component?") + set(${COMPONENT_OPTION}_DISABLED OFF CACHE INTERNAL "Disable the ${_NAME} component?" FORCE) + + # Add common include directories + target_include_directories(${COMPONENT_TARGET} + PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/source" + PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}/include" + ) + + # Add files in common places. + file(GLOB_RECURSE TEMPLATES FOLLOW_SYMLINKS CONFIGURE_DEPENDS "templates/*") + source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}/templates" PREFIX "Templates" FILES ${TEMPLATES}) + + file(GLOB_RECURSE DATA FOLLOW_SYMLINKS CONFIGURE_DEPENDS "data/*") + source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}/data" PREFIX "Data" FILES ${DATA}) + + file(GLOB_RECURSE SOURCE_PRIVATE FOLLOW_SYMLINKS CONFIGURE_DEPENDS "ui/*") + source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}/ui" PREFIX "User-Interface Files" FILES ${USERINTERFACE}) + + file(GLOB_RECURSE SOURCE_PRIVATE FOLLOW_SYMLINKS CONFIGURE_DEPENDS "source/*") + source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}/source" PREFIX "Private Files" FILES ${SOURCE_PRIVATE}) + file(GLOB_RECURSE UI_PRIVATE FOLLOW_SYMLINKS CONFIGURE_DEPENDS "source/ui/*") + file(GLOB_RECURSE GENERATED_PRIVATE FOLLOW_SYMLINKS CONFIGURE_DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/source/*") + source_group(TREE "${CMAKE_CURRENT_BINARY_DIR}/source" PREFIX "Private Files/Generated" FILES ${GENERATED_PRIVATE}) + + file(GLOB_RECURSE SOURCE_PUBLIC FOLLOW_SYMLINKS CONFIGURE_DEPENDS "include/*") + source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}/include" PREFIX "Public Files" FILES ${SOURCE_PUBLIC}) + file(GLOB_RECURSE UI_PUBLIC FOLLOW_SYMLINKS CONFIGURE_DEPENDS "include/ui/*") + file(GLOB_RECURSE GENERATED_PUBLIC FOLLOW_SYMLINKS CONFIGURE_DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/include/*") + source_group(TREE "${CMAKE_CURRENT_BINARY_DIR}/include" PREFIX "Public Files/Generated" FILES ${GENERATED_PUBLIC}) + + target_sources(${COMPONENT_TARGET} + PRIVATE + ${DATA} + ${TEMPLATES} + ${USERINTERFACE} + ${SOURCE_PRIVATE} + ${GENERATED_PRIVATE} + ${SOURCE_PUBLIC} + ${GENERATED_PUBLIC} + ) + + # Ignore data only files. + set_source_files_properties( + ${DATA} + ${TEMPLATES} + ${USERINTERFACE} + PROPERTIES + HEADER_FILE_ONLY ON + ) + + # Enable Qt if needed + set_target_properties(${TARGET_NAME} PROPERTIES + AUTOUIC ON + AUTOUIC_SEARCH_PATHS "${CMAKE_CURRENT_SOURCE_DIR};${CMAKE_CURRENT_SOURCE_DIR}/ui" + AUTOMOC ON + AUTORCC ON + AUTOGEN_BUILD_DIR "${CMAKE_CURRENT_BINARY_DIR}/generated" + ) + set_source_files_properties( + ${TEMPLATES} + ${SOURCE_PRIVATE} + ${GENERATED_PRIVATE} + ${SOURCE_PUBLIC} + ${GENERATED_PUBLIC} + PROPERTIES + SKIP_AUTOGEN ON + SKIP_AUTOMOC ON + SKIP_AUTORCC ON + SKIP_AUTOUIC ON + ) + set_source_files_properties( + ${UI_PRIVATE} + ${UI_PUBLIC} + PROPERTIES + SKIP_AUTOGEN OFF + SKIP_AUTOMOC OFF + SKIP_AUTORCC OFF + SKIP_AUTOUIC OFF + ) +endfunction() + +function(streamfx_has_component _NAME _OUTPUT) + streamfx_sanitize_name("${_NAME}" _NAME _TARGET _OPTION) + + set(${_OUTPUT} OFF PARENT_SCOPE) + if(TARGET "StreamFX_${_TARGET}") + set(${_OUTPUT} ON PARENT_SCOPE) endif() +endfunction() + +function(streamfx_enabled_component _NAME _OUTPUT) + streamfx_sanitize_name("${_NAME}" _NAME _TARGET _OPTION) + if(NOT TARGET "StreamFX_${_TARGET}") + message(FATAL_ERROR "Unknown component '${_NAME}'.") + endif() + + get_target_property(_OPTION "StreamFX_${_TARGET}" COMPONENT_OPTION) + set(${_OUTPUT} OFF PARENT_SCOPE) + if(${_OPTION} AND NOT (${_OPTION}_DISABLED)) + set(${_OUTPUT} ON PARENT_SCOPE) + endif() +endfunction() + +# Use this to add a dependency on another component. +function(streamfx_add_component_dependency _NAME) + cmake_parse_arguments(PARSE_ARGV 1 _ARG + "OPTIONAL" + "" + "" + ) + + streamfx_sanitize_name("${_NAME}" _NAME _TARGET _OPTION) + + set(DEPENDENCY "${_TARGET}") + if(_ARG_OPTIONAL) + list(APPEND DEPENDENCY "OPTIONAL") + endif() + string(REPLACE ";" "\\;" DEPENDENCY "${DEPENDENCY}") + + get_target_property(DEPENDS "${COMPONENT_TARGET}" COMPONENT_DEPENDS) + if(DEPENDS) + list(APPEND DEPENDS "${DEPENDENCY}") + else() + set(DEPENDS "${DEPENDENCY}") + endif() + set_target_properties("${COMPONENT_TARGET}" PROPERTIES COMPONENT_DEPENDS "${DEPENDS}") +endfunction() + +# Use this to disable a component via script. +function(streamfx_disable_component _NAME) + cmake_parse_arguments(PARSE_ARGV 1 _ARG + "" + "REASON" + "" + ) + + streamfx_sanitize_name("${_NAME}" _NAME _TARGET _OPTION) + if(NOT (_TARGET MATCHES "^StreamFX_")) + set(_TARGET "StreamFX_${_TARGET}") + endif() + + # If the component doesn't exist, skip it. + if(NOT TARGET ${_TARGET}) + message(WARNING "Not disabling invalid component '${_NAME}'.") + return() + endif() + + get_target_property(_LABEL ${_TARGET} COMPONENT_LABEL) + get_target_property(_OPTION ${_TARGET} COMPONENT_OPTION) + + CacheSet(${_OPTION}_DISABLED ON) + if(_ARG_REASON) + message(STATUS "[${_LABEL}] Disabled due to: ${_ARG_REASON}") + endif() +endfunction() + +################################################################################ +# Register Library +################################################################################ +streamfx_add_library(StreamFX MODULE) # We are a module for libOBS. + +# Ensure there is at least one file. +if(D_PLATFORM_WINDOWS) + target_sources(StreamFX + PRIVATE + "source/windll.cpp" + ) +else() + target_sources(StreamFX + PRIVATE + "/dev/null" + ) endif() -# Verify Requirements -feature_encoder_ffmpeg(ON) -feature_encoder_aom_av1(ON) -feature_filter_autoframing(ON) -feature_filter_blur(ON) -feature_filter_color_grade(ON) -feature_filter_denoising(ON) -feature_filter_dynamic_mask(ON) -feature_filter_sdf_effects(ON) -feature_filter_shader(ON) -feature_filter_transform(ON) -feature_filter_upscaling(ON) -feature_filter_virtual_greenscreen(ON) -feature_source_mirror(ON) -feature_source_shader(ON) -feature_transition_shader(ON) -feature_frontend(ON) -feature_updater(ON) - -################################################################################ -# Code -################################################################################ -set(PROJECT_DATA ) -set(PROJECT_LIBRARIES ) -set(PROJECT_LIBRARIES_DELAYED ) -set(PROJECT_INCLUDE_DIRS ) -set(PROJECT_TEMPLATES ) -set(PROJECT_PRIVATE_GENERATED ) -set(PROJECT_PRIVATE_SOURCE ) -set(PROJECT_UI ) -set(PROJECT_UI_SOURCE ) -set(PROJECT_DEFINITIONS ) -set(PROJECT_MEDIA ) - -# Data -file(GLOB_RECURSE PROJECT_DATA "data/*") - -# Media -file(GLOB_RECURSE PROJECT_MEDIA "media/*") - -# Templates -file(GLOB_RECURSE PROJECT_TEMPLATES "templates/*") - -# Configure Files -configure_file( - "templates/config.hpp.in" - "generated/config.hpp" -) -LIST(APPEND PROJECT_TEMPLATES "templates/config.hpp.in") -LIST(APPEND PROJECT_PRIVATE_GENERATED "${PROJECT_BINARY_DIR}/generated/config.hpp") - -configure_file( - "templates/version.hpp.in" - "generated/version.hpp" -) -LIST(APPEND PROJECT_TEMPLATES "templates/version.hpp.in") -LIST(APPEND PROJECT_PRIVATE_GENERATED "${PROJECT_BINARY_DIR}/generated/version.hpp") - -configure_file( - "templates/module.cpp.in" - "generated/module.cpp" -) -LIST(APPEND PROJECT_TEMPLATES "templates/module.cpp.in") -LIST(APPEND PROJECT_PRIVATE_GENERATED "${PROJECT_BINARY_DIR}/generated/module.cpp") - -if(D_PLATFORM_WINDOWS) # Windows Support +# version.rc (can't be part of a static library) +if(D_PLATFORM_WINDOWS) # Windows-exclusive + # Version Resource set(PROJECT_PRODUCT_NAME "${PROJECT_FULL_NAME}") set(PROJECT_COMPANY_NAME "${PROJECT_AUTHORS}") set(PROJECT_COPYRIGHT "${PROJECT_AUTHORS} © ${PROJECT_COPYRIGHT_YEARS}") @@ -882,670 +1013,131 @@ if(D_PLATFORM_WINDOWS) # Windows Support configure_file( "templates/windows/version.rc.in" - "generated/version.rc" + "version.rc" ) - LIST(APPEND PROJECT_PRIVATE_GENERATED "${PROJECT_BINARY_DIR}/generated/version.rc") -endif() - -# Minimum Dependencies -list(APPEND PROJECT_LIBRARIES OBS::libobs) - -# Components -if(CURL_FOUND) - list(APPEND PROJECT_PRIVATE_SOURCE - "source/util/util-curl.hpp" - "source/util/util-curl.cpp" - ) - list(APPEND PROJECT_LIBRARIES CURL::libcurl) -endif() - -if(HAVE_FFMPEG) - list(APPEND PROJECT_LIBRARIES - ${FFMPEG_LIBRARIES} - ) - list(APPEND PROJECT_INCLUDE_DIRS - ${FFMPEG_INCLUDE_DIRS} + target_sources(StreamFX PRIVATE + "${PROJECT_BINARY_DIR}/version.rc" ) endif() - -if(HAVE_JSON) - list(APPEND PROJECT_INCLUDE_DIRS ${JSON_INCLUDE_DIR}) -endif() - -if(TRUE) # OpenGL - if(NOT TARGET khronos_glad) - add_library(khronos_glad STATIC - "${PROJECT_SOURCE_DIR}/third-party/khronos/glad/src/gl.c" - ) - target_include_directories(khronos_glad - PRIVATE - "${PROJECT_SOURCE_DIR}/third-party/khronos/glad/include" - INTERFACE - "${PROJECT_SOURCE_DIR}/third-party/khronos/glad/include" - PUBLIC - "${PROJECT_SOURCE_DIR}/third-party/khronos/glad/include" - ) - if(D_PLATFORM_WINDOWS) - target_sources(khronos_glad - PRIVATE - "${PROJECT_SOURCE_DIR}/third-party/khronos/glad/src/wgl.c" - ) - elseif(D_PLATFORM_LINUX) - target_sources(khronos_glad - PRIVATE - "${PROJECT_SOURCE_DIR}/third-party/khronos/glad/src/glx.c" - ) - endif() - - # Requires for shared/static mixing. - set_property(TARGET khronos_glad PROPERTY POSITION_INDEPENDENT_CODE ON) - endif() - - list(APPEND PROJECT_LIBRARIES khronos_glad) -endif() - -if(HAVE_NVIDIA_VFX_SDK OR HAVE_NVIDIA_AR_SDK) - list(APPEND PROJECT_PRIVATE_SOURCE - "source/nvidia/cv/nvidia-cv.hpp" - "source/nvidia/cv/nvidia-cv.cpp" - "source/nvidia/cv/nvidia-cv-image.hpp" - "source/nvidia/cv/nvidia-cv-image.cpp" - "source/nvidia/cv/nvidia-cv-texture.hpp" - "source/nvidia/cv/nvidia-cv-texture.cpp" - ) -endif() - -if(HAVE_NVIDIA_AR_SDK) - list(APPEND PROJECT_PRIVATE_SOURCE - "source/nvidia/ar/nvidia-ar.hpp" - "source/nvidia/ar/nvidia-ar.cpp" - "source/nvidia/ar/nvidia-ar-feature.hpp" - "source/nvidia/ar/nvidia-ar-feature.cpp" - "source/nvidia/ar/nvidia-ar-facedetection.hpp" - "source/nvidia/ar/nvidia-ar-facedetection.cpp" - ) - list(APPEND PROJECT_LIBRARIES - NVIDIA::AR - ) -endif() - -if(HAVE_NVIDIA_VFX_SDK) - list(APPEND PROJECT_PRIVATE_SOURCE - "source/nvidia/vfx/nvidia-vfx.hpp" - "source/nvidia/vfx/nvidia-vfx.cpp" - "source/nvidia/vfx/nvidia-vfx-denoising.hpp" - "source/nvidia/vfx/nvidia-vfx-denoising.cpp" - "source/nvidia/vfx/nvidia-vfx-effect.hpp" - "source/nvidia/vfx/nvidia-vfx-effect.cpp" - "source/nvidia/vfx/nvidia-vfx-greenscreen.hpp" - "source/nvidia/vfx/nvidia-vfx-greenscreen.cpp" - "source/nvidia/vfx/nvidia-vfx-superresolution.hpp" - "source/nvidia/vfx/nvidia-vfx-superresolution.cpp" - ) - list(APPEND PROJECT_LIBRARIES - NVIDIA::VFX - ) -endif() - -if(HAVE_NVIDIA_CUDA) - list(APPEND PROJECT_PRIVATE_SOURCE - "source/nvidia/cuda/nvidia-cuda.hpp" - "source/nvidia/cuda/nvidia-cuda.cpp" - "source/nvidia/cuda/nvidia-cuda-obs.hpp" - "source/nvidia/cuda/nvidia-cuda-obs.cpp" - "source/nvidia/cuda/nvidia-cuda-context.hpp" - "source/nvidia/cuda/nvidia-cuda-context.cpp" - "source/nvidia/cuda/nvidia-cuda-gs-texture.hpp" - "source/nvidia/cuda/nvidia-cuda-gs-texture.cpp" - "source/nvidia/cuda/nvidia-cuda-memory.hpp" - "source/nvidia/cuda/nvidia-cuda-memory.cpp" - "source/nvidia/cuda/nvidia-cuda-stream.hpp" - "source/nvidia/cuda/nvidia-cuda-stream.cpp" - ) - list(APPEND PROJECT_DEFINITIONS - ENABLE_NVIDIA_CUDA - ) -endif() - -if(REQUIRE_OBS_FRONTEND_API AND obs-frontend-api_FOUND) - list(APPEND PROJECT_LIBRARIES OBS::obs-frontend-api) - list(APPEND PROJECT_UI_SOURCE - "source/obs/browser/obs-browser-panel.hpp" - ) -endif() - -if(REQUIRE_QT) - if(Qt5_FOUND) - list(APPEND PROJECT_LIBRARIES Qt5::Core Qt5::Gui Qt5::Widgets) - elseif(Qt6_FOUND) - list(APPEND PROJECT_LIBRARIES Qt6::Core Qt6::Gui Qt6::Widgets) - endif() -endif() - -################################################################################ -# Features -################################################################################ - -# Core -list(APPEND PROJECT_PRIVATE_SOURCE - "source/warning-disable.hpp" - "source/warning-enable.hpp" - "source/configuration.hpp" - "source/configuration.cpp" - "source/common.hpp" - "source/strings.hpp" - "source/plugin.hpp" - "source/plugin.cpp" - "source/util/utility.hpp" - "source/util/utility.cpp" - "source/util/util-bitmask.hpp" - "source/util/util-event.hpp" - "source/util/util-library.cpp" - "source/util/util-library.hpp" - "source/util/util-logging.cpp" - "source/util/util-logging.hpp" - "source/util/util-platform.hpp" - "source/util/util-platform.cpp" - "source/util/util-threadpool.cpp" - "source/util/util-threadpool.hpp" - "source/gfx/gfx-util.hpp" - "source/gfx/gfx-util.cpp" - "source/gfx/gfx-mipmapper.hpp" - "source/gfx/gfx-mipmapper.cpp" - "source/gfx/gfx-opengl.hpp" - "source/gfx/gfx-opengl.cpp" - "source/gfx/gfx-source-texture.hpp" - "source/gfx/gfx-source-texture.cpp" - "source/obs/gs/gs-helper.hpp" - "source/obs/gs/gs-helper.cpp" - "source/obs/gs/gs-effect.hpp" - "source/obs/gs/gs-effect.cpp" - "source/obs/gs/gs-effect-parameter.hpp" - "source/obs/gs/gs-effect-parameter.cpp" - "source/obs/gs/gs-effect-pass.hpp" - "source/obs/gs/gs-effect-pass.cpp" - "source/obs/gs/gs-effect-technique.hpp" - "source/obs/gs/gs-effect-technique.cpp" - "source/obs/gs/gs-indexbuffer.hpp" - "source/obs/gs/gs-indexbuffer.cpp" - "source/obs/gs/gs-limits.hpp" - "source/obs/gs/gs-rendertarget.hpp" - "source/obs/gs/gs-rendertarget.cpp" - "source/obs/gs/gs-sampler.hpp" - "source/obs/gs/gs-sampler.cpp" - "source/obs/gs/gs-texture.hpp" - "source/obs/gs/gs-texture.cpp" - "source/obs/gs/gs-vertex.hpp" - "source/obs/gs/gs-vertex.cpp" - "source/obs/gs/gs-vertexbuffer.hpp" - "source/obs/gs/gs-vertexbuffer.cpp" - "source/obs/obs-signal-handler.hpp" - "source/obs/obs-signal-handler.cpp" - "source/obs/obs-source-tracker.hpp" - "source/obs/obs-source-tracker.cpp" - "source/obs/obs-tools.hpp" - "source/obs/obs-tools.cpp" - - # obs_encoder_info_t, obs_encoder_t, obs_weak_encoder_t - "source/obs/obs-encoder-factory.hpp" - "source/obs/obs-encoder-factory.cpp" - - # obs_source_info_t, obs_source_t, obs_weak_source_t - "source/obs/obs-source-factory.hpp" - "source/obs/obs-source-factory.cpp" - "source/obs/obs-source.hpp" - "source/obs/obs-source.cpp" - "source/obs/obs-source-active-child.hpp" - "source/obs/obs-source-active-child.cpp" - "source/obs/obs-source-active-reference.hpp" - "source/obs/obs-source-active-reference.cpp" - "source/obs/obs-source-showing-reference.hpp" - "source/obs/obs-source-showing-reference.cpp" - "source/obs/obs-weak-source.hpp" - "source/obs/obs-weak-source.cpp" -) -list(APPEND PROJECT_DATA - "data/effects/color_conversion_rgb_hsl.effect" - "data/effects/color_conversion_rgb_hsv.effect" - "data/effects/color_conversion_rgb_yuv.effect" - "data/effects/mipgen.effect" - "data/effects/pack-unpack.effect" - "data/effects/standard.effect" - "data/effects/shared.effect" - "data/locale/en-US.ini" -) -list(APPEND PROJECT_INCLUDE_DIRS - "${PROJECT_BINARY_DIR}/generated" - "${PROJECT_SOURCE_DIR}/source" +set_target_properties(StreamFX PROPERTIES + # Version + MACHO_COMPATIBILITY_VERSION ${_VERSION_MAJOR}.${_VERSION_MINOR} + SOVERSION ${_VERSION_MAJOR}.${_VERSION_MINOR} + MACHO_CURRENT_VERSION ${PROJECT_VERSION} + VERSION ${PROJECT_VERSION} ) -# Encoder/FFmpeg -is_feature_enabled(ENCODER_FFMPEG T_CHECK) -if(T_CHECK) - list(APPEND PROJECT_PRIVATE_SOURCE - # FFmpeg - "source/ffmpeg/avframe-queue.cpp" - "source/ffmpeg/avframe-queue.hpp" - "source/ffmpeg/swscale.hpp" - "source/ffmpeg/swscale.cpp" - "source/ffmpeg/tools.hpp" - "source/ffmpeg/tools.cpp" - "source/ffmpeg/hwapi/base.hpp" - "source/ffmpeg/hwapi/base.cpp" - "source/ffmpeg/hwapi/d3d11.hpp" - "source/ffmpeg/hwapi/d3d11.cpp" - - # Encoders - "source/encoders/encoder-ffmpeg.hpp" - "source/encoders/encoder-ffmpeg.cpp" - - # Encoders/Codecs - "source/encoders/codecs/hevc.hpp" - "source/encoders/codecs/hevc.cpp" - "source/encoders/codecs/h264.hpp" - "source/encoders/codecs/h264.cpp" - "source/encoders/codecs/prores.hpp" - "source/encoders/codecs/prores.cpp" - "source/encoders/codecs/dnxhr.hpp" - "source/encoders/codecs/dnxhr.cpp" - - # Encoders/Handlers - "source/encoders/ffmpeg/handler.hpp" - "source/encoders/ffmpeg/handler.cpp" - "source/encoders/ffmpeg/debug.hpp" - "source/encoders/ffmpeg/debug.cpp" - ) - list(APPEND PROJECT_DEFINITIONS - ENABLE_ENCODER_FFMPEG - ) - - # AMF - is_feature_enabled(ENCODER_FFMPEG_AMF T_CHECK) - if(T_CHECK) - list(APPEND PROJECT_PRIVATE_SOURCE - "source/encoders/ffmpeg/amf.hpp" - "source/encoders/ffmpeg/amf.cpp" - ) - list(APPEND PROJECT_DEFINITIONS - ENABLE_ENCODER_FFMPEG_AMF - ) - endif() - - # NVENC - is_feature_enabled(ENCODER_FFMPEG_NVENC T_CHECK) - if(T_CHECK) - list(APPEND PROJECT_PRIVATE_SOURCE - "source/encoders/ffmpeg/nvenc.hpp" - "source/encoders/ffmpeg/nvenc.cpp" - ) - list(APPEND PROJECT_DEFINITIONS - ENABLE_ENCODER_FFMPEG_NVENC - ) - endif() - - # ProRES - is_feature_enabled(ENCODER_FFMPEG_PRORES T_CHECK) - if(T_CHECK) - list(APPEND PROJECT_PRIVATE_SOURCE - "source/encoders/ffmpeg/prores_aw.hpp" - "source/encoders/ffmpeg/prores_aw.cpp" - ) - list(APPEND PROJECT_DEFINITIONS - ENABLE_ENCODER_FFMPEG_PRORES - ) - endif() - - # DNxHR - is_feature_enabled(ENCODER_FFMPEG_DNXHR T_CHECK) - if(T_CHECK) - list(APPEND PROJECT_PRIVATE_SOURCE - "source/encoders/ffmpeg/dnxhd.hpp" - "source/encoders/ffmpeg/dnxhd.cpp" - ) - list(APPEND PROJECT_DEFINITIONS - ENABLE_ENCODER_FFMPEG_DNXHR - ) - endif() - - # CineForm HD - is_feature_enabled(ENCODER_FFMPEG_CFHD T_CHECK) - if(T_CHECK) - list(APPEND PROJECT_PRIVATE_SOURCE - "source/encoders/ffmpeg/cfhd.hpp" - "source/encoders/ffmpeg/cfhd.cpp" - ) - list(APPEND PROJECT_DEFINITIONS - ENABLE_ENCODER_FFMPEG_CFHD - ) - endif() -endif() - -# Filter/Auto-Framing -is_feature_enabled(FILTER_AUTOFRAMING T_CHECK) -if(T_CHECK) - list(APPEND PROJECT_PRIVATE_SOURCE - "source/filters/filter-autoframing.hpp" - "source/filters/filter-autoframing.cpp" - ) - list(APPEND PROJECT_DEFINITIONS - ENABLE_FILTER_AUTOFRAMING - ) - is_feature_enabled(FILTER_AUTOFRAMING_NVIDIA T_CHECK) - if(T_CHECK) - list(APPEND PROJECT_DEFINITIONS - ENABLE_FILTER_AUTOFRAMING_NVIDIA - ) - endif() -endif() - -# Filter/Blur -is_feature_enabled(FILTER_BLUR T_CHECK) -if(T_CHECK) - list(APPEND PROJECT_DATA - "data/effects/mask.effect" - "data/effects/blur/common.effect" - "data/effects/blur/box.effect" - "data/effects/blur/box-linear.effect" - "data/effects/blur/dual-filtering.effect" - "data/effects/blur/gaussian.effect" - "data/effects/blur/gaussian-linear.effect" - ) - list(APPEND PROJECT_PRIVATE_SOURCE - "source/gfx/blur/gfx-blur-base.hpp" - "source/gfx/blur/gfx-blur-base.cpp" - "source/gfx/blur/gfx-blur-box.hpp" - "source/gfx/blur/gfx-blur-box.cpp" - "source/gfx/blur/gfx-blur-box-linear.hpp" - "source/gfx/blur/gfx-blur-box-linear.cpp" - "source/gfx/blur/gfx-blur-dual-filtering.hpp" - "source/gfx/blur/gfx-blur-dual-filtering.cpp" - "source/gfx/blur/gfx-blur-gaussian.hpp" - "source/gfx/blur/gfx-blur-gaussian.cpp" - "source/gfx/blur/gfx-blur-gaussian-linear.hpp" - "source/gfx/blur/gfx-blur-gaussian-linear.cpp" - "source/filters/filter-blur.hpp" - "source/filters/filter-blur.cpp" - ) - list(APPEND PROJECT_DEFINITIONS - ENABLE_FILTER_BLUR - ) -endif() - -# Filter/Color Grade -is_feature_enabled(FILTER_COLOR_GRADE T_CHECK) -if(T_CHECK) - list(APPEND PROJECT_DATA - "data/effects/color-grade.effect" - ) - list(APPEND PROJECT_PRIVATE_SOURCE - "source/filters/filter-color-grade.hpp" - "source/filters/filter-color-grade.cpp" - ) - list(APPEND PROJECT_DEFINITIONS - ENABLE_FILTER_COLOR_GRADE - ) - set(REQUIRE_LUT ON) -endif() - -# Filter/Dynamic Mask -is_feature_enabled(FILTER_DYNAMIC_MASK T_CHECK) -if(T_CHECK) - list(APPEND PROJECT_DATA - "data/effects/channel-mask.effect" - ) - list(APPEND PROJECT_PRIVATE_SOURCE - "source/filters/filter-dynamic-mask.hpp" - "source/filters/filter-dynamic-mask.cpp" - ) - list(APPEND PROJECT_DEFINITIONS - ENABLE_FILTER_DYNAMIC_MASK - ) -endif() - -# Filter/SDF Effects -is_feature_enabled(FILTER_SDF_EFFECTS T_CHECK) -if(T_CHECK) - list(APPEND PROJECT_DATA - "data/effects/sdf/sdf-producer.effect" - "data/effects/sdf/sdf-consumer.effect" - ) - list(APPEND PROJECT_PRIVATE_SOURCE - "source/filters/filter-sdf-effects.hpp" - "source/filters/filter-sdf-effects.cpp" - ) - list(APPEND PROJECT_DEFINITIONS - ENABLE_FILTER_SDF_EFFECTS - ) -endif() - -# Filter/Shader -is_feature_enabled(FILTER_SHADER T_CHECK) -if(T_CHECK) - set(REQUIRE_PART_SHADER ON) - list(APPEND PROJECT_PRIVATE_SOURCE - "source/filters/filter-shader.hpp" - "source/filters/filter-shader.cpp" - ) - list(APPEND PROJECT_DEFINITIONS - ENABLE_FILTER_SHADER - ) -endif() - -# Filter/Transform -is_feature_enabled(FILTER_TRANSFORM T_CHECK) -if(T_CHECK) - list(APPEND PROJECT_PRIVATE_SOURCE - "source/filters/filter-transform.hpp" - "source/filters/filter-transform.cpp" - ) - list(APPEND PROJECT_DEFINITIONS - ENABLE_FILTER_TRANSFORM - ) -endif() - -# Filter/Denoising -is_feature_enabled(FILTER_DENOISING T_CHECK) -if(T_CHECK) - list(APPEND PROJECT_PRIVATE_SOURCE - "source/filters/filter-denoising.hpp" - "source/filters/filter-denoising.cpp" - ) - list(APPEND PROJECT_DEFINITIONS - ENABLE_FILTER_DENOISING - ) - is_feature_enabled(FILTER_DENOISING_NVIDIA T_CHECK) - if(T_CHECK) - list(APPEND PROJECT_DEFINITIONS - ENABLE_FILTER_DENOISING_NVIDIA - ) - endif() -endif() - -# Filter/Upscaling -is_feature_enabled(FILTER_UPSCALING T_CHECK) -if(T_CHECK) - list(APPEND PROJECT_PRIVATE_SOURCE - "source/filters/filter-upscaling.hpp" - "source/filters/filter-upscaling.cpp" - ) - list(APPEND PROJECT_DEFINITIONS - ENABLE_FILTER_UPSCALING - ) - is_feature_enabled(FILTER_UPSCALING_NVIDIA T_CHECK) - if(T_CHECK) - list(APPEND PROJECT_DEFINITIONS - ENABLE_FILTER_UPSCALING_NVIDIA - ) - endif() -endif() - -# Filter/Virtual Greenscreen -is_feature_enabled(FILTER_VIRTUAL_GREENSCREEN T_CHECK) -if(T_CHECK) - list(APPEND PROJECT_PRIVATE_SOURCE - "source/filters/filter-virtual-greenscreen.hpp" - "source/filters/filter-virtual-greenscreen.cpp" - ) - list(APPEND PROJECT_DEFINITIONS - ENABLE_FILTER_VIRTUAL_GREENSCREEN - ) - is_feature_enabled(FILTER_VIRTUAL_GREENSCREEN_NVIDIA T_CHECK) - if(T_CHECK) - list(APPEND PROJECT_DEFINITIONS - ENABLE_FILTER_VIRTUAL_GREENSCREEN_NVIDIA - ) - endif() -endif() - -# Source/Mirror -is_feature_enabled(SOURCE_MIRROR T_CHECK) -if(T_CHECK) - list(APPEND PROJECT_PRIVATE_SOURCE - "source/sources/source-mirror.hpp" - "source/sources/source-mirror.cpp" - ) - list(APPEND PROJECT_DEFINITIONS - ENABLE_SOURCE_MIRROR - ) -endif() - -# Source/Shader -is_feature_enabled(SOURCE_SHADER T_CHECK) -if(T_CHECK) - set(REQUIRE_PART_SHADER ON) - list(APPEND PROJECT_PRIVATE_SOURCE - "source/sources/source-shader.hpp" - "source/sources/source-shader.cpp" - ) - list(APPEND PROJECT_DEFINITIONS - ENABLE_SOURCE_SHADER - ) -endif() - -# Transition/Shader -is_feature_enabled(TRANSITION_SHADER T_CHECK) -if(T_CHECK) - set(REQUIRE_PART_SHADER ON) - list(APPEND PROJECT_PRIVATE_SOURCE - "source/transitions/transition-shader.hpp" - "source/transitions/transition-shader.cpp" - ) - list(APPEND PROJECT_DEFINITIONS - ENABLE_TRANSITION_SHADER - ) -endif() - -# Profiling -is_feature_enabled(PROFILING T_CHECK) -if(T_CHECK) - list(APPEND PROJECT_PRIVATE_SOURCE - "source/util/util-profiler.cpp" - "source/util/util-profiler.hpp" - ) - list(APPEND PROJECT_DEFINITIONS - ENABLE_PROFILING - ) -endif() - -# Updater -is_feature_enabled(UPDATER T_CHECK) -if(T_CHECK) - list(APPEND PROJECT_PRIVATE_SOURCE - "source/updater.hpp" - "source/updater.cpp" - ) - list(APPEND PROJECT_DEFINITIONS - ENABLE_UPDATER - ) -endif() - -# Frontend -is_feature_enabled(FRONTEND T_CHECK) -if(T_CHECK) - list(APPEND PROJECT_UI - "ui/streamfx.qrc" - "ui/about.ui" - "ui/about-entry.ui" - ) - list(APPEND PROJECT_UI_SOURCE - "source/ui/ui-common.hpp" - "source/ui/ui.hpp" - "source/ui/ui.cpp" - "source/ui/ui-about.hpp" - "source/ui/ui-about.cpp" - "source/ui/ui-about-entry.hpp" - "source/ui/ui-about-entry.cpp" - "source/ui/ui-obs-browser-widget.hpp" - "source/ui/ui-obs-browser-widget.cpp" - ) - list(APPEND PROJECT_INCLUDE_DIRS - "source/ui" - ) - list(APPEND PROJECT_DEFINITIONS - ENABLE_FRONTEND - ) - - is_feature_enabled(UPDATER T_CHECK) - if(T_CHECK) - list(APPEND PROJECT_UI_SOURCE - "source/ui/ui-updater.hpp" - "source/ui/ui-updater.cpp" - ) - list(APPEND PROJECT_UI - "ui/updater.ui" - ) - endif() -endif() - ################################################################################ -# Parts +# Add Core ################################################################################ +streamfx_add_library(StreamFX_Core STATIC EXCLUDE_FROM_ALL) +add_library(StreamFX::Core ALIAS StreamFX_Core) -# Shaders -if(REQUIRE_PART_SHADER) - list(APPEND PROJECT_PRIVATE_SOURCE - "source/gfx/shader/gfx-shader.hpp" - "source/gfx/shader/gfx-shader.cpp" - "source/gfx/shader/gfx-shader-param.hpp" - "source/gfx/shader/gfx-shader-param.cpp" - "source/gfx/shader/gfx-shader-param-audio.hpp" - "source/gfx/shader/gfx-shader-param-audio.cpp" - "source/gfx/shader/gfx-shader-param-basic.hpp" - "source/gfx/shader/gfx-shader-param-basic.cpp" - "source/gfx/shader/gfx-shader-param-matrix.hpp" - "source/gfx/shader/gfx-shader-param-matrix.cpp" - "source/gfx/shader/gfx-shader-param-texture.hpp" - "source/gfx/shader/gfx-shader-param-texture.cpp" - ) -endif() +# Enable Qt +set_target_properties(StreamFX_Core PROPERTIES + AUTOUIC ON + AUTOUIC_SEARCH_PATHS "${CMAKE_CURRENT_SOURCE_DIR};${CMAKE_CURRENT_SOURCE_DIR}/ui" + AUTOMOC ON + AUTORCC ON + AUTOGEN_BUILD_DIR "${CMAKE_CURRENT_BINARY_DIR}/qt" +) -# LUT -if(REQUIRE_LUT) - list(APPEND PROJECT_PRIVATE_SOURCE - "source/gfx/lut/gfx-lut.hpp" - "source/gfx/lut/gfx-lut.cpp" - "source/gfx/lut/gfx-lut-consumer.hpp" - "source/gfx/lut/gfx-lut-consumer.cpp" - "source/gfx/lut/gfx-lut-producer.hpp" - "source/gfx/lut/gfx-lut-producer.cpp" - ) - list(APPEND PROJECT_DATA - "data/effects/lut.effect" - "data/effects/lut-consumer.effect" - "data/effects/lut-producer.effect" - ) -endif() +# Default Links +target_link_libraries(StreamFX_Core + PUBLIC + CURL::libcurl + Qt6::Core + Qt6::Gui + Qt6::Widgets + OBS::obs-frontend-api + OBS::libobs +) + +# Default Include Dirs +target_include_directories(StreamFX_Core + PRIVATE + "${PROJECT_SOURCE_DIR}/source/ui" + "${PROJECT_BINARY_DIR}/qt" + PUBLIC + "${PROJECT_SOURCE_DIR}/source" + "${PROJECT_SOURCE_DIR}/include" + "${PROJECT_BINARY_DIR}/generated" + ${JSON_INCLUDE_DIR} +) + +# Templates +file(GLOB_RECURSE PROJECT_TEMPLATES FOLLOW_SYMLINKS CONFIGURE_DEPENDS "templates/*") +source_group(TREE "${PROJECT_SOURCE_DIR}/templates" PREFIX "Templates" FILES ${PROJECT_TEMPLATES}) +set_source_files_properties(${PROJECT_TEMPLATES} PROPERTIES + HEADER_FILE_ONLY ON + SKIP_AUTOGEN ON + SKIP_AUTOMOC ON + SKIP_AUTORCC ON + SKIP_AUTOUIC ON +) +target_sources(StreamFX_Core PRIVATE ${PROJECT_TEMPLATES}) + +# Generated +#- config.hpp +configure_file( + "templates/config.hpp.in" + "generated/config.hpp" +) +#- version.hpp +configure_file( + "templates/version.hpp.in" + "generated/version.hpp" +) +#- module.cpp +configure_file( + "templates/module.cpp.in" + "generated/module.cpp" +) +#- Add all generated files +file(GLOB_RECURSE PROJECT_PRIVATE_GENERATED FOLLOW_SYMLINKS CONFIGURE_DEPENDS "${PROJECT_BINARY_DIR}/generated/*") +source_group(TREE "${PROJECT_BINARY_DIR}/generated" PREFIX "Private Files/Generated" FILES ${PROJECT_PRIVATE_GENERATED}) +set_source_files_properties(${PROJECT_PRIVATE_GENERATED} PROPERTIES + SKIP_AUTOGEN ON + SKIP_AUTOMOC ON + SKIP_AUTORCC ON + SKIP_AUTOUIC ON +) +target_sources(StreamFX_Core PRIVATE ${PROJECT_PRIVATE_GENERATED}) + +# Source +file(GLOB_RECURSE PROJECT_PRIVATE FOLLOW_SYMLINKS CONFIGURE_DEPENDS "${PROJECT_SOURCE_DIR}/source/*") +list(REMOVE_ITEM PROJECT_PRIVATE "${PROJECT_SOURCE_DIR}/source/windll.cpp") # Only used by StreamFX, not part of StreamFX::Core +source_group(TREE "${PROJECT_SOURCE_DIR}/source" PREFIX "Private Files" FILES ${PROJECT_PRIVATE}) +set_source_files_properties(${PROJECT_PRIVATE} PROPERTIES + SKIP_AUTOGEN ON + SKIP_AUTOMOC ON + SKIP_AUTORCC ON + SKIP_AUTOUIC ON +) +target_sources(StreamFX_Core PRIVATE ${PROJECT_PRIVATE}) + +# User-Interface Source +file(GLOB_RECURSE PROJECT_PRIVATE_UI FOLLOW_SYMLINKS CONFIGURE_DEPENDS "${PROJECT_SOURCE_DIR}/source/ui/*") +list(APPEND PROJECT_PRIVATE_UI "${PROJECT_SOURCE_DIR}/source/obs/browser/obs-browser-panel.hpp") +set_source_files_properties(${PROJECT_PRIVATE_UI} PROPERTIES + SKIP_AUTOGEN OFF + SKIP_AUTOMOC OFF + SKIP_AUTORCC OFF + SKIP_AUTOUIC OFF +) + +# User-Interface Definitions +file(GLOB_RECURSE PROJECT_UI FOLLOW_SYMLINKS CONFIGURE_DEPENDS "${PROJECT_SOURCE_DIR}/ui/*") +source_group(TREE "${PROJECT_SOURCE_DIR}/ui" PREFIX "User-Interface Files" FILES ${PROJECT_UI}) +set_source_files_properties(${PROJECT_UI} PROPERTIES + HEADER_FILE_ONLY ON +) +target_sources(StreamFX_Core PRIVATE ${PROJECT_UI}) # Windows if(D_PLATFORM_WINDOWS) - list(APPEND PROJECT_PRIVATE_SOURCE - "source/windll.cpp" - ) - list(APPEND PROJECT_LIBRARIES - Delayimp.lib - ) # Disable/Enable a ton of things. - list(APPEND PROJECT_DEFINITIONS + target_compile_definitions(StreamFX_Core PRIVATE # Microsoft Visual C++ _CRT_SECURE_NO_WARNINGS _ENABLE_EXTENDED_ALIGNED_STORAGE @@ -1595,404 +1187,276 @@ if(D_PLATFORM_WINDOWS) ) endif() -# GCC before 9.0, Clang before 9.0 -if((CMAKE_C_COMPILER_ID STREQUAL "GNU") - OR (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") - OR (CMAKE_C_COMPILER_ID STREQUAL "Clang") - OR (CMAKE_CXX_COMPILER_ID STREQUAL "Clang")) - if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS 9.0) - list(APPEND PROJECT_LIBRARIES - "stdc++fs" +# OpenGL via GLAD +if(NOT TARGET StreamFX::GLAD) + add_library(StreamFX_GLAD STATIC + "${PROJECT_SOURCE_DIR}/third-party/khronos/glad/src/gl.c" + ) + target_include_directories(StreamFX_GLAD + PUBLIC + "${PROJECT_SOURCE_DIR}/third-party/khronos/glad/include" + ) + if(D_PLATFORM_WINDOWS) + target_sources(StreamFX_GLAD + PRIVATE + "${PROJECT_SOURCE_DIR}/third-party/khronos/glad/src/wgl.c" + ) + elseif(D_PLATFORM_LINUX) + target_sources(StreamFX_GLAD + PRIVATE + "${PROJECT_SOURCE_DIR}/third-party/khronos/glad/src/glx.c" ) endif() + + # Requires for shared/static mixing. + set_property(TARGET StreamFX_GLAD PROPERTY POSITION_INDEPENDENT_CODE ON) + + add_library(StreamFX::GLAD ALIAS StreamFX_GLAD) endif() -################################################################################ -# Register Library -################################################################################ +target_link_libraries(StreamFX_Core PUBLIC StreamFX::GLAD) -# Combine all variables that matter. -set(PROJECT_FILES - # Always exist - ${PROJECT_DATA} - ${PROJECT_PRIVATE_GENERATED} - ${PROJECT_PRIVATE_SOURCE} - ${PROJECT_TEMPLATES} - # UI-only (empty if not enabled) - ${PROJECT_UI} - ${PROJECT_UI_SOURCE} - # Media - ${PROJECT_MEDIA} -) +################################################################################ +# Components +################################################################################ +# As the above monolithic approach started to show its weaknesses, it was time +# for an improved system which suffers under less issues. This new component +# system should address the main necessary parts, -# Set source groups for IDE generators. +#- Registration +file(GLOB COMPONENTS RELATIVE ${PROJECT_SOURCE_DIR} CONFIGURE_DEPENDS ${PROJECT_SOURCE_DIR}/components/*) +foreach(COMPONENT ${COMPONENTS}) + # Ignore directories with no CMakeLists.txt + if(EXISTS "${PROJECT_SOURCE_DIR}/${COMPONENT}/CMakeLists.txt") + add_subdirectory(${COMPONENT} EXCLUDE_FROM_ALL) + endif() +endforeach() + +get_target_property(_UNRESOLVED StreamFX COMPONENT_DEPENDS) +set(_RESOLVED "") +set(_DISABLED "") + +#- Cleanup +list(REMOVE_DUPLICATES _UNRESOLVED) +foreach(_ENTITY ${_UNRESOLVED}) + # Remove any invalid entries. + if(NOT TARGET "StreamFX_${_ENTITY}") + message(WARNING "Encountered invalid component '${_ENTITY}', removing...") + list(REMOVE_ITEM _UNRESOLVED "${_ENTITY}") + continue() + endif() +endforeach() + +#- Resolve Dependencies in a loop +set(_UNRESOLVED_LOOP 0) +while(_UNRESOLVED) + MATH(EXPR _UNRESOLVED_LOOP "${_UNRESOLVED_LOOP}+1") + if(_UNRESOLVED_LOOP GREATER_EQUAL 10) + message(FATAL_ERROR "Infinite loop while resolving components: ${_UNRESOLVED}") + endif() + + # Attempt to resolve while there are still unresolved entries. + foreach(_ENTITY ${_UNRESOLVED}) + set(RENTITY "StreamFX_${_ENTITY}") + + get_target_property(_ALIAS "${RENTITY}" COMPONENT_NAME) + get_target_property(_LABEL "${RENTITY}" COMPONENT_LABEL) + get_target_property(_OPTION "${RENTITY}" COMPONENT_OPTION) + get_target_property(_DEPENDS "${RENTITY}" COMPONENT_DEPENDS) + get_target_property(_RESOLVER "${RENTITY}" COMPONENT_RESOLVER) + + # Remove any that have been disabled. + if(NOT ${_OPTION}) + message(STATUS "[${_LABEL}] Disabled by user.") + list(REMOVE_ITEM _UNRESOLVED "${_ENTITY}") + list(APPEND _DISABLED "${_ENTITY}") + continue() + elseif(${_OPTION}_DISABLED) # Test for pre-resolve disabling. + message(STATUS "[${_LABEL}] Disabled by build script.") + list(REMOVE_ITEM _UNRESOLVED "${_ENTITY}") + list(APPEND _DISABLED "${_ENTITY}") + continue() + endif() + + # Check if all dependencies are resolved. + set(_HAS_UNRESOLVED_DEPENDS OFF) + set(_HAS_DISABLED_DEPENDS OFF) + set(_HAS_UNRESOLVED_OPTIONAL_DEPENDS OFF) + if(_DEPENDS) + foreach(_DEPEND ${_DEPENDS}) + list(GET _DEPEND 0 _ENTITY2) + set(RENTITY2 "StreamFX_${_ENTITY2}") + + get_target_property(_ENTITY2_LABEL "${RENTITY2}" COMPONENT_LABEL) + + if(NOT ("OPTIONAL" IN_LIST _DEPEND)) + if("${_ENTITY2}" IN_LIST _DISABLED) + message("[${_LABEL}] Required dependency '${_ENTITY2_LABEL}' is disabled.") + set(_HAS_DISABLED_DEPENDS ON) + endif() + if("${_ENTITY2}" IN_LIST _UNRESOLVED) + set(_HAS_UNRESOLVED_DEPENDS ON) + endif() + else() + if("${_ENTITY2}" IN_LIST _UNRESOLVED) + set(_HAS_UNRESOLVED_OPTIONAL_DEPENDS ON) + endif() + endif() + endforeach() + list(JOIN _DEPENDS ", " _DEPENDS) + else() + set(_DEPENDS "") + endif() + if(_HAS_DISABLED_DEPENDS) # A required dependency is disabled, so disable this entry. + message(STATUS "[${_LABEL}] Disabled by dependency.") + list(REMOVE_ITEM _UNRESOLVED "${_ENTITY}") + list(APPEND _DISABLED "${_ENTITY}") + streamfx_disable_component(${_ENTITY}) + continue() + elseif(_HAS_UNRESOLVED_DEPENDS) + continue() + elseif(_HAS_UNRESOLVED_OPTIONAL_DEPENDS AND (_UNRESOLVED_LOOP LESS 8)) + # Temporarily skip this element while there are still remaining loops. + continue() + endif() + + # Call Resolver function + if(_RESOLVER) + set(COMPONENT_TARGET "${RENTITY}") + set(COMPONENT_ALIAS "${_ALIAS}") + set(COMPONENT_OPTION "${_OPTION}") + cmake_language(CALL ${_RESOLVER}) + endif() + if(${_OPTION}_DISABLED) # Test for resolve disabling. + message(STATUS "[${_LABEL}] Disabled by resolver.") + list(REMOVE_ITEM _UNRESOLVED "${_ENTITY}") + list(APPEND _DISABLED "${_ENTITY}") + continue() + endif() + + # Finally, if everything went well, we now have a resolved entity. + list(REMOVE_ITEM _UNRESOLVED "${_ENTITY}") + list(APPEND _RESOLVED "${_ENTITY}") + message(STATUS "[${_LABEL}] Enabled. Depends: ${_DEPENDS}") + endforeach() +endwhile() + +#- Linking +foreach(_ENTITY ${_RESOLVED}) + set(RENTITY "StreamFX_${_ENTITY}") + get_target_property(_DEPENDS "${RENTITY}" COMPONENT_DEPENDS) + + # Finally if everything is correct, do things. + if(_DEPENDS) + foreach(_DEPEND ${_DEPENDS}) + list(GET _DEPEND 0 _ENTITY2) + set(RENTITY2 "StreamFX_${_ENTITY2}") + + streamfx_enabled_component(${_ENTITY2} T_CHECK) + if(T_CHECK) + target_link_libraries(${RENTITY} PUBLIC $) + endif() + endforeach() + endif() + + target_link_libraries(StreamFX PUBLIC $) +endforeach() + +target_link_libraries(StreamFX PUBLIC $) + +################################################################################ +# Resources +################################################################################ +# This needs to be handled separately, as CMake does not propagate Resources +# between dependencies. This feels like a bug in CMake, but it is better than +# not doing anything at all. Might eventually need to revise this when we start +# generating data files. + +# Data +file(GLOB_RECURSE PROJECT_DATA "data/*") source_group(TREE "${PROJECT_SOURCE_DIR}/data" PREFIX "Data" FILES ${PROJECT_DATA}) -source_group(TREE "${PROJECT_SOURCE_DIR}/source" PREFIX "Source" FILES ${PROJECT_PRIVATE_SOURCE} ${PROJECT_UI_SOURCE}) -source_group(TREE "${PROJECT_BINARY_DIR}/generated" PREFIX "Source" FILES ${PROJECT_PRIVATE_GENERATED}) -source_group(TREE "${PROJECT_SOURCE_DIR}/templates" PREFIX "Templates" FILES ${PROJECT_TEMPLATES}) -source_group(TREE "${PROJECT_SOURCE_DIR}/ui" PREFIX "User Interface" FILES ${PROJECT_UI}) -source_group(TREE "${PROJECT_SOURCE_DIR}/media" PREFIX "Media" FILES ${PROJECT_MEDIA}) +if(D_PLATFORM_MAC) + foreach(FILE IN LISTS PROJECT_DATA) # Data location + cmake_path(ABSOLUTE_PATH FILE OUTPUT_VARIABLE FILE_PATH) + cmake_path(RELATIVE_PATH FILE_PATH BASE_DIRECTORY "${PROJECT_SOURCE_DIR}/data/") + cmake_path(GET FILE_PATH PARENT_PATH FILE_PATH) + set_source_files_properties("${FILE}" PROPERTIES + MACOSX_PACKAGE_LOCATION "Resources/${FILE_PATH}" + ) + endforeach() +endif() -# Prevent unwanted files from being built as source. -set_source_files_properties(${PROJECT_DATA} ${PROJECT_TEMPLATES} ${PROJECT_UI} ${PROJECT_MEDIA} PROPERTIES +# Media +file(GLOB_RECURSE PROJECT_MEDIA "media/*") +source_group(TREE "${PROJECT_SOURCE_DIR}/media" PREFIX "Media" FILES ${PROJECT_MEDIA}) +if(D_PLATFORM_MAC) + #- Bundle Icon location + set_source_files_properties("media/icon.png" PROPERTIES + MACOSX_PACKAGE_LOCATION "Resources" + ) +endif() + +# Add all Resources to StreamFX +set_source_files_properties(${PROJECT_DATA} ${PROJECT_MEDIA} PROPERTIES HEADER_FILE_ONLY ON ) +target_sources(StreamFX PRIVATE ${PROJECT_DATA} ${PROJECT_MEDIA}) -# Prevent non-UI files from being Qt'd -if(Qt5_Found OR Qt6_FOUND) - set_source_files_properties(${PROJECT_DATA} ${PROJECT_TEMPLATES} ${PROJECT_MEDIA} ${PROJECT_PRIVATE_GENERATED} ${PROJECT_PRIVATE_SOURCE} PROPERTIES - SKIP_AUTOGEN ON - SKIP_AUTOMOC ON - SKIP_AUTORCC ON - SKIP_AUTOUIC ON - ) -endif() +################################################################################ +# Installation +################################################################################ -# Register the library -add_library(${PROJECT_NAME} MODULE ${PROJECT_FILES}) # We are a module for libOBS. -target_link_libraries(${PROJECT_NAME} PRIVATE ${PROJECT_LIBRARIES}) -target_include_directories(${PROJECT_NAME} PRIVATE ${PROJECT_INCLUDE_DIRS}) -target_compile_definitions(${PROJECT_NAME} PRIVATE ${PROJECT_DEFINITIONS}) - -# Always generate position independent code. -set_target_properties(${PROJECT_NAME} PROPERTIES - POSITION_INDEPENDENT_CODE ON -) - -# Set C++ Standard and Extensions -set_target_properties(${PROJECT_NAME} PROPERTIES - C_STANDARD 17 - C_STANDARD_REQUIRED ON - CXX_STANDARD 17 - CXX_STANDARD_REQUIRED ON - CXX_EXTENSIONS OFF -) -target_compile_definitions(${PROJECT_NAME} PRIVATE - __STDC_WANT_LIB_EXT1__=1 -) - -# Link-Time/Interprocedural Optimization -if(${PREFIX}ENABLE_LTO) - set_target_properties(${PROJECT_NAME} PROPERTIES - INTERPROCEDURAL_OPTIMIZATION ON - ) -endif() - -# C/C++ Compiler Adjustments -if(D_PLATFORM_WINDOWS AND ((CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") OR (CMAKE_CXX_COMPILER_ID STREQUAL "Clang"))) - # MSVC/ClangCL - message(STATUS "Applying custom flags for MSVC style build.") - - # - Dynamically link Microsoft C/C++ Redistributable. - target_compile_options(${PROJECT_NAME} PRIVATE - $<$:/MD> - $<$:/MDd> - $<$:/MD> - $<$:/MD> - $<$:/MD> - ) - - # - Treat Warnings as Errors - target_compile_options(${PROJECT_NAME} PRIVATE "/Wall") - - # - Disable useless warnings - set(DISABLED_WARNINGS - # Don't warn about unused variables, parameters, labels, functions, or typedefs. - "4100" - "4101" - "4102" - "4505" - "4514" - "5245" - # Don't warn about unreferenced variables or parameters which are assigned/initialized. - "4189" - # Don't warn about not-explicitly-handled enumeration identifiers - "4061" - # Ignore automatic padding warnings. - "4820" - # Ignore assignment/move/copy being implicit '= delete;'. - "4623" - "4625" - "4626" - "5026" - "5027" - # Relative include paths are fine. - "4464" - # Buggy warning: printf expects string literal - "4774" - # Buggy warning: subobject initialization should be wrapped in braces - "5246" - # Ignore undefined, unused or unreferenced pre-processor macros - "4688" - # Ignore non-inlined functions - "4710" - # Ignore Spectre mitigation insertions - "5045" - # Ignore inserted padding. - "4324" - ) - foreach(WARN ${DISABLED_WARNINGS}) - target_compile_options(${PROJECT_NAME} PRIVATE "/wd${WARN}") - endforeach() - - # - Require enabled instruction sets. - if(D_PLATFORM_ARCH_X86) - if(${PREFIX}TARGET_X86_64_V4) - target_compile_options(${PROJECT_NAME} PRIVATE "/arch:AVX512") - message(STATUS "Targeting x86-64-v4.") - elseif(${PREFIX}TARGET_X86_64_V3) - target_compile_options(${PROJECT_NAME} PRIVATE "/arch:AVX2") - message(STATUS "Targeting x86-64-v3.") - elseif(${PREFIX}TARGET_X86_64_V2_EX) - target_compile_options(${PROJECT_NAME} PRIVATE "/arch:AVX") - message(STATUS "Targeting extended x86-64-v2.") - elseif(${PREFIX}TARGET_X86_64_V2) - target_compile_options(${PROJECT_NAME} PRIVATE "/d2archSSE42") - message(STATUS "Targeting x86-64-v2.") - elseif(${PREFIX}TARGET_X86_64) - #target_compile_options(${PROJECT_NAME} PRIVATE "/arch:SSE2") - message(STATUS "Targeting x86-64.") - endif() - endif() - - # - Use fast unordered math if possible. - if(${PREFIX}ENABLE_FASTMATH) - target_compile_options(${PROJECT_NAME} PRIVATE "/fp:fast") - else() - target_compile_options(${PROJECT_NAME} PRIVATE "/fp:precise") - if(MSVC_VERSION GREATER 1930) - # Keep original behavior in VS2022 and up. - target_compile_options(${PROJECT_NAME} PRIVATE "/fp:contract") - endif() - endif() - - # - Disable incremental builds - target_compile_options(${PROJECT_NAME} PRIVATE "/INCREMENTAL:NO") - - # - Enable C++ exceptions with SEH exceptions. - target_compile_options(${PROJECT_NAME} PRIVATE "/EHa") - - # - Enable multi-processor compiling. - target_compile_options(${PROJECT_NAME} PRIVATE "/MP") - - # - Enable updated __cplusplus macro - target_compile_options(${PROJECT_NAME} PRIVATE "/Zc:__cplusplus") - - # - Generic Optimizations for Release/RelWithDebInfo/MinSizeRel - set(FLAGS - "/OPT:REF" - "/OPT:ICF=3" - "/GL" - "/Gy" - "/GF" - "/Ox" - "/Ob3" - ) - foreach(FLAG ${FLAGS}) - target_compile_options(${PROJECT_NAME} PRIVATE "$<$:${FLAG}>") - endforeach() -elseif(D_PLATFORM_LINUX AND ((CMAKE_CXX_COMPILER_ID STREQUAL "Clang") OR (CMAKE_CXX_COMPILER_ID STREQUAL "GNU"))) - # GCC/Clang - message(STATUS "Applying custom flags for GCC/Clang style build.") - - # - Enable all warnings. - target_compile_options(${PROJECT_NAME} PRIVATE "-Wall") - target_compile_options(${PROJECT_NAME} PRIVATE "-Wextra") - - # - Disable useless warnings - set(DISABLED_WARNINGS - # Don't warn about unused variables, parameters, labels, functions, or typedefs. - "unused-function" - "unused-label" - "unused-local-typedefs" - "unused-parameter" - "unused-result" - "unused-const-variable" - "unused-variable" - "unused-value" - # Don't warn about unreferenced variables or parameters which are assigned/initialized. - "unused-but-set-parameter" - "unused-but-set-variable" - # Don't warn about not-explicitly-handled enumeration identifiers - "switch" - # Ignore automatic padding warnings. - "padded" - # Ignore implicit '= delete;'. - # Ignore extra arguments for printf - "format-extra-args" - # Ignore undefined, unused or unreferenced pre-processor macros - "unused-macros" - ) - foreach(WARN ${DISABLED_WARNINGS}) - target_compile_options(${PROJECT_NAME} PRIVATE "-Wno-${WARN}") - endforeach() - - # - Require enabled instruction sets. - if(${PREFIX}TARGET_NATIVE) - target_compile_options(${PROJECT_NAME} PRIVATE - "-march=native" +if(STANDALONE) + if(D_PLATFORM_WINDOWS) + install( + TARGETS StreamFX + RUNTIME DESTINATION "obs-plugins/${D_PLATFORM_BITS}bit/" COMPONENT StreamFX + LIBRARY DESTINATION "obs-plugins/${D_PLATFORM_BITS}bit/" COMPONENT StreamFX ) - message(WARNING "Targeting native architecture. Binaries will not be distributable to other systems!") - elseif(D_PLATFORM_ARCH_X86) - if(${PREFIX}TARGET_X86_64_V4) - target_compile_options(${PROJECT_NAME} PRIVATE - "-march=x86-64-v4" + install( + DIRECTORY "data/" + DESTINATION "data/obs-plugins/StreamFX/" + ) + if(MSVC) + install( + FILES $ + DESTINATION "obs-plugins/${D_PLATFORM_BITS}bit/" + OPTIONAL ) - message(STATUS "Targeting x86-64-v4.") - elseif(${PREFIX}TARGET_X86_64_V3) - target_compile_options(${PROJECT_NAME} PRIVATE - "-march=x86-64-v3" - ) - message(STATUS "Targeting x86-64-v3.") - elseif(${PREFIX}TARGET_X86_64_V2_EX) - target_compile_options(${PROJECT_NAME} PRIVATE - "-march=x86-64-v2" - "-mavx" - "-mbmi2" - "-mbmi" - "-mfma" - "-mf16c" - "-mmovbe" - "-mpclmul" - "-mpopcnt" - ) - message(STATUS "Targeting extended x86-64-v2.") - elseif(${PREFIX}TARGET_X86_64_V2) - target_compile_options(${PROJECT_NAME} PRIVATE - "-march=x86-64-v2" - ) - message(STATUS "Targeting x86-64-v2.") - elseif(${PREFIX}TARGET_X86_64) - target_compile_options(${PROJECT_NAME} PRIVATE - "-march=x86-64" - ) - message(STATUS "Targeting x86-64.") endif() - if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") - target_compile_options(${PROJECT_NAME} PRIVATE - "-mtune=generic" + elseif(D_PLATFORM_LINUX) + if(STRUCTURE_PACKAGEMANAGER) + install( + TARGETS StreamFX + RUNTIME DESTINATION "lib/obs-plugins/" COMPONENT StreamFX + LIBRARY DESTINATION "lib/obs-plugins/" COMPONENT StreamFX + PERMISSIONS WORLD_EXECUTE;WORLD_READ;OWNER_EXECUTE;OWNER_READ;OWNER_WRITE;GROUP_EXECUTE;GROUP_READ;GROUP_WRITE + ) + install( + DIRECTORY "data/" + DESTINATION "share/obs/obs-plugins/StreamFX" + COMPONENT StreamFX + FILE_PERMISSIONS WORLD_EXECUTE;WORLD_READ;OWNER_EXECUTE;OWNER_READ;OWNER_WRITE;GROUP_EXECUTE;GROUP_READ;GROUP_WRITE + DIRECTORY_PERMISSIONS WORLD_EXECUTE;WORLD_READ;OWNER_EXECUTE;OWNER_READ;OWNER_WRITE;GROUP_EXECUTE;GROUP_READ;GROUP_WRITE ) else() - target_compile_options(${PROJECT_NAME} PRIVATE - "-mtune=x86-64" + install( + TARGETS StreamFX + RUNTIME DESTINATION "plugins/StreamFX/bin/${D_PLATFORM_BITS}bit/" COMPONENT StreamFX + LIBRARY DESTINATION "plugins/StreamFX/bin/${D_PLATFORM_BITS}bit/" COMPONENT StreamFX + PERMISSIONS WORLD_EXECUTE;WORLD_READ;OWNER_EXECUTE;OWNER_READ;OWNER_WRITE;GROUP_EXECUTE;GROUP_READ;GROUP_WRITE + ) + install( + DIRECTORY "data/" + DESTINATION "plugins/StreamFX/data/" + COMPONENT StreamFX + FILE_PERMISSIONS WORLD_EXECUTE;WORLD_READ;OWNER_EXECUTE;OWNER_READ;OWNER_WRITE;GROUP_EXECUTE;GROUP_READ;GROUP_WRITE + DIRECTORY_PERMISSIONS WORLD_EXECUTE;WORLD_READ;OWNER_EXECUTE;OWNER_READ;OWNER_WRITE;GROUP_EXECUTE;GROUP_READ;GROUP_WRITE ) endif() - endif() - - # - Use fast unordered math if possible. - if(${PREFIX}ENABLE_FASTMATH) - if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") - target_compile_options(${PROJECT_NAME} PRIVATE - "-ffast-math" - ) - else() - target_compile_options(${PROJECT_NAME} PRIVATE - "-ffp-model=fast" - ) - endif() - else() - target_compile_options(${PROJECT_NAME} PRIVATE - "-ffp-model=precise" - ) - endif() - - # - Don't export by default, require attributes. - # add_compile_options("-fvisibility=hidden") -elseif(D_PLATFORM_MAC AND (CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang")) - # AppleClang - message(STATUS "Applying custom flags for AppleClang style build.") - - # - Enable most useful warnings. - target_compile_options(${PROJECT_NAME} PRIVATE "-Wall") - target_compile_options(${PROJECT_NAME} PRIVATE "-Wextra") - - # - Require enabled instruction sets. - if(${PREFIX}TARGET_NATIVE) - target_compile_options(${PROJECT_NAME} PRIVATE - "-march=native" - ) - message(WARNING "Targeting native architecture. Binaries will not be distributable to other systems!") - endif() - - # - Use fast unordered math if possible. - # FIXME: Appears to not be supported. - - # - Don't export by default, require attributes. - # add_compile_options("-fvisibility=hidden") -endif() - -# Remove prefix on other platforms. -set_target_properties(${PROJECT_NAME} PROPERTIES - PREFIX "" - IMPORT_PREFIX "" -) - -# Set file version -set_target_properties(${PROJECT_NAME} PROPERTIES - MACHO_COMPATIBILITY_VERSION ${_VERSION_MAJOR}.${_VERSION_MINOR} - MACHO_CURRENT_VERSION ${PROJECT_VERSION} - SOVERSION ${_VERSION_MAJOR}.${_VERSION_MINOR} - VERSION ${PROJECT_VERSION} -) - -# Enable Qt if needed -if(Qt5_FOUND OR Qt6_FOUND) - set_target_properties(${PROJECT_NAME} PROPERTIES - AUTOUIC ON - AUTOUIC_SEARCH_PATHS "${PROJECT_SOURCE_DIR};${PROJECT_SOURCE_DIR}/ui" - AUTOMOC ON - AUTORCC ON - AUTOGEN_BUILD_DIR "${PROJECT_BINARY_DIR}/generated" - ) -endif() - -# Windows exclusive changes -if(D_PLATFORM_WINDOWS) - foreach(DELAYLOAD ${PROJECT_LIBRARIES_DELAYED}) - get_target_property(_lf ${PROJECT_NAME} LINK_FLAGS) - if(NOT _lf) - set(_lf "") - endif() - set_target_properties(${PROJECT_NAME} PROPERTIES LINK_FLAGS "${_lf} /DELAYLOAD:${DELAYLOAD}") - add_link_options("/DELAYLOAD:${DELAYLOAD}") - endforeach() -endif() - -# MacOS exclusive Changes -if(D_PLATFORM_MAC) - set_target_properties(${PROJECT_NAME} PROPERTIES - # No automatic code signing in XCode - XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "" - XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED "NO" - # @rpath in installed binaries - INSTALL_RPATH "@executable_path/../Frameworks/;@loader_path/../Frameworks/;@loader_path/../Resources/" - ) - if(STANDALONE) - set_target_properties(${PROJECT_NAME} PROPERTIES - # @rpath in built binaries - BUILD_WITH_INSTALL_RPATH ON - ) - endif() - - # Bundle exporting - if(STRUCTURE_BUNDLE) - # Proper location for resources in Bundles - foreach(FILE IN LISTS PROJECT_DATA) - cmake_path(ABSOLUTE_PATH FILE OUTPUT_VARIABLE FILE_PATH) - cmake_path(RELATIVE_PATH FILE_PATH BASE_DIRECTORY "${PROJECT_SOURCE_DIR}/data/") - cmake_path(GET FILE_PATH PARENT_PATH FILE_PATH) - set_source_files_properties("${FILE}" PROPERTIES - MACOSX_PACKAGE_LOCATION "Resources/${FILE_PATH}" - ) - endforeach() - - # Bundle Icon - set_source_files_properties("media/icon.png" PROPERTIES - MACOSX_PACKAGE_LOCATION "Resources" - ) - + elseif(D_PLATFORM_MAC) # Bundle Information - set(MACOSX_BUNDLE_BUNDLE_NAME "${PROJECT_NAME}") + set(MACOSX_BUNDLE_BUNDLE_NAME "StreamFX") set(MACOSX_BUNDLE_BUNDLE_VERSION "${PROJECT_VERSION}") set(MACOSX_BUNDLE_COPYRIGHT "${PROJECT_COPYRIGHT}") set(MACOSX_BUNDLE_GUI_IDENTIFIER "${PROJECT_IDENTIFER}") @@ -2000,201 +1464,33 @@ if(D_PLATFORM_MAC) set(MACOSX_BUNDLE_VERSION_STRING "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}") set(MACOSX_BUNDLE_LONG_VERSION_STRING "${_VERSION}") set(MACOSX_BUNDLE_SHORT_VERSION_STRING "${PROJECT_VERSION}") - set_target_properties(${PROJECT_NAME} PROPERTIES + set_target_properties(StreamFX PROPERTIES BUNDLE ON BUNDLE_EXTENSION "plugin" - OUTPUT_NAME ${PROJECT_NAME} + OUTPUT_NAME StreamFX MACOSX_BUNDLE_INFO_PLIST "${PROJECT_SOURCE_DIR}/templates/macos/Info.plist.in" XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "${MACOSX_BUNDLE_GUI_IDENTIFIER}" ) - endif() -endif() - -################################################################################ -# Extra Tools -################################################################################ - -# Clang -is_feature_enabled(CLANG T_CHECK) -if(T_CHECK) - generate_compile_commands_json( - TARGETS ${PROJECT_NAME} - ) - clang_tidy( - TARGETS ${PROJECT_NAME} - VERSION 14.0.0 - ) - clang_format( - TARGETS ${PROJECT_NAME} - DEPENDENCY - VERSION 14.0.0 - ) -endif() - -################################################################################ -# Installation -################################################################################ - -if(STANDALONE) - if(STRUCTURE_UNIFIED) - install( - DIRECTORY "data/" - DESTINATION "data/" - FILE_PERMISSIONS WORLD_EXECUTE;WORLD_READ;OWNER_EXECUTE;OWNER_READ;OWNER_WRITE;GROUP_EXECUTE;GROUP_READ;GROUP_WRITE - DIRECTORY_PERMISSIONS WORLD_EXECUTE;WORLD_READ;OWNER_EXECUTE;OWNER_READ;OWNER_WRITE;GROUP_EXECUTE;GROUP_READ;GROUP_WRITE - ) - - if(D_PLATFORM_WINDOWS) - install( - TARGETS ${PROJECT_NAME} - RUNTIME DESTINATION "bin/windows-${D_PLATFORM_INSTR}-${D_PLATFORM_BITS}/" COMPONENT StreamFX - LIBRARY DESTINATION "bin/windows-${D_PLATFORM_INSTR}-${D_PLATFORM_BITS}/" COMPONENT StreamFX - PERMISSIONS WORLD_EXECUTE;WORLD_READ;OWNER_EXECUTE;OWNER_READ;OWNER_WRITE;GROUP_EXECUTE;GROUP_READ;GROUP_WRITE - ) - if(MSVC) - install( - FILES $ - DESTINATION "bin/windows-${D_PLATFORM_INSTR}-${D_PLATFORM_BITS}/" - COMPONENT StreamFX - OPTIONAL - ) - endif() - - # Dependency: AOM - if(HAVE_AOM AND AOM_BINARY AND D_PLATFORM_WINDOWS) - install( - FILES ${AOM_BINARY} - DESTINATION "data/" COMPONENT StreamFX - ) - endif() - elseif(D_PLATFORM_LINUX) - install( - TARGETS ${PROJECT_NAME} - RUNTIME DESTINATION "bin/linux-${D_PLATFORM_INSTR}-${D_PLATFORM_BITS}/" COMPONENT StreamFX - LIBRARY DESTINATION "bin/linux-${D_PLATFORM_INSTR}-${D_PLATFORM_BITS}/" COMPONENT StreamFX - PERMISSIONS WORLD_EXECUTE;WORLD_READ;OWNER_EXECUTE;OWNER_READ;OWNER_WRITE;GROUP_EXECUTE;GROUP_READ;GROUP_WRITE - ) - elseif(D_PLATFORM_MAC) - install( - TARGETS ${PROJECT_NAME} - RUNTIME DESTINATION "bin/mac-${D_PLATFORM_INSTR}-${D_PLATFORM_BITS}/" COMPONENT StreamFX - LIBRARY DESTINATION "bin/mac-${D_PLATFORM_INSTR}-${D_PLATFORM_BITS}/" COMPONENT StreamFX - PERMISSIONS WORLD_EXECUTE;WORLD_READ;OWNER_EXECUTE;OWNER_READ;OWNER_WRITE;GROUP_EXECUTE;GROUP_READ;GROUP_WRITE - ) - endif() install( - FILES LICENSE - DESTINATION "LICENSE" - COMPONENT StreamFX + TARGETS StreamFX + RUNTIME DESTINATION "." COMPONENT StreamFX + LIBRARY DESTINATION "." COMPONENT StreamFX + BUNDLE DESTINATION "." COMPONENT StreamFX + PERMISSIONS WORLD_EXECUTE;WORLD_READ;OWNER_EXECUTE;OWNER_READ;OWNER_WRITE;GROUP_EXECUTE;GROUP_READ;GROUP_WRITE ) - install( - FILES icon.png - DESTINATION "icon.png" - COMPONENT StreamFX - ) - elseif(D_PLATFORM_WINDOWS) - install( - TARGETS ${PROJECT_NAME} - RUNTIME DESTINATION "obs-plugins/${D_PLATFORM_BITS}bit/" COMPONENT StreamFX - LIBRARY DESTINATION "obs-plugins/${D_PLATFORM_BITS}bit/" COMPONENT StreamFX - ) - install( - DIRECTORY "data/" - DESTINATION "data/obs-plugins/${PROJECT_NAME}/" - ) - if(MSVC) - install( - FILES $ - DESTINATION "obs-plugins/${D_PLATFORM_BITS}bit/" - OPTIONAL - ) - endif() - - # Dependency: AOM - if(HAVE_AOM AND AOM_BINARY) - install( - FILES "${AOM_BINARY}" - DESTINATION "data/obs-plugins/${PROJECT_NAME}/" COMPONENT StreamFX - ) - endif() - elseif(D_PLATFORM_LINUX) - if(STRUCTURE_PACKAGEMANAGER) - install( - TARGETS ${PROJECT_NAME} - RUNTIME DESTINATION "lib/obs-plugins/" COMPONENT StreamFX - LIBRARY DESTINATION "lib/obs-plugins/" COMPONENT StreamFX - PERMISSIONS WORLD_EXECUTE;WORLD_READ;OWNER_EXECUTE;OWNER_READ;OWNER_WRITE;GROUP_EXECUTE;GROUP_READ;GROUP_WRITE - ) - install( - DIRECTORY "data/" - DESTINATION "share/obs/obs-plugins/${PROJECT_NAME}" - COMPONENT StreamFX - FILE_PERMISSIONS WORLD_EXECUTE;WORLD_READ;OWNER_EXECUTE;OWNER_READ;OWNER_WRITE;GROUP_EXECUTE;GROUP_READ;GROUP_WRITE - DIRECTORY_PERMISSIONS WORLD_EXECUTE;WORLD_READ;OWNER_EXECUTE;OWNER_READ;OWNER_WRITE;GROUP_EXECUTE;GROUP_READ;GROUP_WRITE - ) - else() - install( - TARGETS ${PROJECT_NAME} - RUNTIME DESTINATION "plugins/${PROJECT_NAME}/bin/${D_PLATFORM_BITS}bit/" COMPONENT StreamFX - LIBRARY DESTINATION "plugins/${PROJECT_NAME}/bin/${D_PLATFORM_BITS}bit/" COMPONENT StreamFX - PERMISSIONS WORLD_EXECUTE;WORLD_READ;OWNER_EXECUTE;OWNER_READ;OWNER_WRITE;GROUP_EXECUTE;GROUP_READ;GROUP_WRITE - ) - install( - DIRECTORY "data/" - DESTINATION "plugins/${PROJECT_NAME}/data/" - COMPONENT StreamFX - FILE_PERMISSIONS WORLD_EXECUTE;WORLD_READ;OWNER_EXECUTE;OWNER_READ;OWNER_WRITE;GROUP_EXECUTE;GROUP_READ;GROUP_WRITE - DIRECTORY_PERMISSIONS WORLD_EXECUTE;WORLD_READ;OWNER_EXECUTE;OWNER_READ;OWNER_WRITE;GROUP_EXECUTE;GROUP_READ;GROUP_WRITE - ) - endif() - elseif(D_PLATFORM_MAC) - if(STRUCTURE_BUNDLE) - install( - TARGETS ${PROJECT_NAME} - RUNTIME DESTINATION "." COMPONENT StreamFX - LIBRARY DESTINATION "." COMPONENT StreamFX - BUNDLE DESTINATION "." COMPONENT StreamFX - PERMISSIONS WORLD_EXECUTE;WORLD_READ;OWNER_EXECUTE;OWNER_READ;OWNER_WRITE;GROUP_EXECUTE;GROUP_READ;GROUP_WRITE - ) - else() - install( - TARGETS ${PROJECT_NAME} - RUNTIME DESTINATION "${PROJECT_NAME}/bin/" COMPONENT StreamFX - LIBRARY DESTINATION "${PROJECT_NAME}/bin/" COMPONENT StreamFX - PERMISSIONS WORLD_EXECUTE;WORLD_READ;OWNER_EXECUTE;OWNER_READ;OWNER_WRITE;GROUP_EXECUTE;GROUP_READ;GROUP_WRITE - ) - install( - DIRECTORY "data/" - DESTINATION "${PROJECT_NAME}/data/" - COMPONENT StreamFX - FILE_PERMISSIONS WORLD_EXECUTE;WORLD_READ;OWNER_EXECUTE;OWNER_READ;OWNER_WRITE;GROUP_EXECUTE;GROUP_READ;GROUP_WRITE - DIRECTORY_PERMISSIONS WORLD_EXECUTE;WORLD_READ;OWNER_EXECUTE;OWNER_READ;OWNER_WRITE;GROUP_EXECUTE;GROUP_READ;GROUP_WRITE - ) - endif() endif() else() - if(COMMAND setup_plugin_target) - setup_plugin_target(${PROJECT_NAME}) + if(COMMAND set_target_properties_obs) + # The previous way no longer works, so we're just doing this now. + set_property(GLOBAL APPEND PROPERTY OBS_MODULES_ENABLED StreamFX) + _target_install_obs(StreamFX DESTINATION "${OBS_PLUGIN_DESTINATION}") + target_install_resources(StreamFX) + elseif(COMMAND setup_plugin_target) + setup_plugin_target(StreamFX) # Seems like we lost the ability to customize which directoy resources are in, and instead are forced to use '/data'. - - if(HAVE_AOM AND AOM_BINARY) # Dependency: AOM - add_target_resource(${PROJECT_NAME} "${AOM_BINARY}" "obs-plugins/${PROJECT_NAME}") - endif() elseif(COMMAND install_obs_plugin_with_data) - install_obs_plugin_with_data(${PROJECT_NAME} data) - - if(HAVE_AOM AND AOM_BINARY) # Dependency: AOM - install( - FILES "${AOM_BINARY}" - DESTINATION "${OBS_DATA_DESTINATION}/obs-plugins/${PROJECT_NAME}" - ) - add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD - COMMAND "${CMAKE_COMMAND}" -E copy - "${AOM_BINARY}" - "${OBS_DATA_DESTINATION}/obs-plugins/${PROJECT_NAME}" - VERBATIM) - endif() + install_obs_plugin_with_data(StreamFX data) endif() endif() @@ -2209,61 +1505,40 @@ if(STANDALONE) else() set(_PACKAGE_SUFFIX_OVERRIDE "${PACKAGE_SUFFIX}") endif() - set(_PACKAGE_FULL_NAME "${PACKAGE_PREFIX}/${PACKAGE_NAME}-${_PACKAGE_SUFFIX_OVERRIDE}") + set(_PACKAGE_FULL_NAME "${PACKAGE_PREFIX}/${PACKAGE_NAME}${_PACKAGE_SUFFIX_OVERRIDE}") - if(STRUCTURE_UNIFIED) - add_custom_target( - PACKAGE_ZIP - ${CMAKE_COMMAND} -E tar cfv "${_PACKAGE_FULL_NAME}.obs" --format=zip -- - "." - WORKING_DIRECTORY "${CMAKE_INSTALL_PREFIX}" + if(D_PLATFORM_WINDOWS) + ## Installer (InnoSetup) + get_filename_component(ISS_FILES_DIR "${CMAKE_INSTALL_PREFIX}" ABSOLUTE) + file(TO_NATIVE_PATH "${ISS_FILES_DIR}" ISS_FILES_DIR) + + get_filename_component(ISS_PACKAGE_DIR "${PACKAGE_PREFIX}" ABSOLUTE) + file(TO_NATIVE_PATH "${ISS_PACKAGE_DIR}" ISS_PACKAGE_DIR) + + get_filename_component(ISS_SOURCE_DIR "${PROJECT_SOURCE_DIR}" ABSOLUTE) + file(TO_NATIVE_PATH "${ISS_SOURCE_DIR}" ISS_SOURCE_DIR) + + get_filename_component(ISS_MSVCHELPER_PATH "${msvc-redist-helper_BUILD_DIR}" ABSOLUTE) + file(TO_NATIVE_PATH "${ISS_MSVCHELPER_PATH}" ISS_MSVCHELPER_PATH) + + configure_file( + "templates/windows/installer.iss.in" + "installer.iss" ) - else() - add_custom_target( - PACKAGE_7Z - ${CMAKE_COMMAND} -E tar cfv "${_PACKAGE_FULL_NAME}.7z" --format=7zip -- - "." - WORKING_DIRECTORY "${CMAKE_INSTALL_PREFIX}" + elseif(D_PLATFORM_MAC) + # .pkg Installer + set(PACKAGES_PATH_NAME "StreamFX") + set(PACKAGES_PATH_NAME "${PACKAGES_PATH_NAME}.plugin") + configure_file( + "templates/macos/installer.pkgproj.in" + "installer.pkgproj" ) + elseif(D_PLATFORM_LINUX) add_custom_target( - PACKAGE_ZIP - ${CMAKE_COMMAND} -E tar cfv "${_PACKAGE_FULL_NAME}.zip" --format=zip -- - "." + PACKAGE + COMMAND ${CMAKE_COMMAND} -E tar cfv "${_PACKAGE_FULL_NAME}.7z" --format=7zip -- "." WORKING_DIRECTORY "${CMAKE_INSTALL_PREFIX}" + VERBATIM ) - - # Windows - if(D_PLATFORM_WINDOWS) - ## Installer (InnoSetup) - get_filename_component(ISS_FILES_DIR "${CMAKE_INSTALL_PREFIX}" ABSOLUTE) - file(TO_NATIVE_PATH "${ISS_FILES_DIR}" ISS_FILES_DIR) - - get_filename_component(ISS_PACKAGE_DIR "${PACKAGE_PREFIX}" ABSOLUTE) - file(TO_NATIVE_PATH "${ISS_PACKAGE_DIR}" ISS_PACKAGE_DIR) - - get_filename_component(ISS_SOURCE_DIR "${PROJECT_SOURCE_DIR}" ABSOLUTE) - file(TO_NATIVE_PATH "${ISS_SOURCE_DIR}" ISS_SOURCE_DIR) - - get_filename_component(ISS_MSVCHELPER_PATH "${msvc-redist-helper_BUILD_DIR}" ABSOLUTE) - file(TO_NATIVE_PATH "${ISS_MSVCHELPER_PATH}" ISS_MSVCHELPER_PATH) - - configure_file( - "templates/windows/installer.iss.in" - "installer.iss" - ) - endif() - - # Apple MacOS - if(D_PLATFORM_MAC) - # .pkg Installer - set(PACKAGES_PATH_NAME "${PROJECT_NAME}") - if(STRUCTURE_BUNDLE) - set(PACKAGES_PATH_NAME "${PACKAGES_PATH_NAME}.plugin") - endif() - configure_file( - "templates/macos/installer.pkgproj.in" - "installer.pkgproj" - ) - endif() endif() endif() diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 45f2ddc..7dabee5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,17 +1,18 @@ # Contributing -This document goes over how you (and/or your organization) are expected to contribute. These guidelines are softly enforced and sometimes not required. +This document intends to teach you the proper way to contribute to the project as a set of guidelines. While they aren't always enforced, your chances of your code being accepted are significantly higher when you follow these. For smaller changes, we might opt to squash your changes to apply the guidelines below to your contribution. -## Localization -We use Crowdin to handle translations into many languages, and you can join the [StreamFX project on Crowdin](https://crowdin.com/project/obs-stream-effects) if you are interested in improving the translations to your native tongue. As Crowdin handles all other languages, Pull Requests therefore should only include changes to `en-US.ini`. +

Repository & Commits

-## Commit Guidelines -Commits should focus on a single change such as formatting, fixing a bug, a warning across the code, and similar things. This means that you should not include a fix to color format handling in a commit that implements a new encoder, or include a fix to a bug with a fix to a warning. +As this is a rather large project, we have certain rules to follow when contributing via git. ### Linear History -This project prefers the linear history of `git rebase` and forbids merge commits. This allows all branches to be a single line back to the root, unless viewed as a whole where it becomes a tree. If you are working on a branch for a feature, bug or other thing, you should know how to rebase back onto the main branch before making a pull request. +We follow the paradigm of linear history which forbids branches from being merged, thus changes made on branches are `git rebase`d back onto the root. This simplifies the code history significantly, but makes reverting changes more difficult. -### Commit Message & Title -We require a commit message format like this: +❌ `git merge` +✅ `git rebase` + +### Commits +A commit should be containing a single change, even if it spans multiple units, and has the following format: ``` prefix: short description @@ -19,35 +20,60 @@ prefix: short description optional long description ``` -The `short description` should be no longer than 80 characters, excluding the `prefix: ` part. The `optional long description` should be present if the change is not immediately obvious - however it does not replace proper documentation. +The short description should be no longer than 120 characters and focus on the important things. The long description is optional, but should be included for larger changes. -#### The correct `prefix` -Depending on where the file is that you ended up modifying, or if you modified multiple files at once, the prefix changes. Take a look at the list to understand which directories cause which prefix: +#### The appropriate `prefix` -- `/CMakeLists.txt`, `/cmake` -> `cmake` -- `/.github/workflows` -> `ci` -- `/data/locale`, `/crowdin.yml` -> `locale` -- `/data/examples` -> `examples` -- `/data` -> `data` (if not part of another prefix) -- `/media` -> `media` -- `/source`, `/include` -> `code` -- `/templates` -> `templates` (or merge with `cmake`) -- `/third-party` -> `third-party` -- `/patches` -> `patches` -- `/tools` -> `tools` -- `/ui` -> `ui` (if not part of a `code` change) -- Most other files -> `project` + + + + + + + + + + + + + + + + + + + + + + + + + + +
Path(s)PrefixExample
+ data/locale + locale + data/locale/en-US.ini -> locale +
components/namename + components/shader -> shader +
+ source
+ templates
+ data
+ ui +
core + ui/main.ui -> core +
Anything elseOmit the prefix
-If multiple locations match, they should be alphabetically sorted and separated by `, `. A change to both `ui` and `code` will as such result in a prefix of `code, ui`. If a `code` change only affects a single file, or multiple files with a common parent file, the prefix should be the path of the file, like shown in the following examples: +If multiple match, apply the prefix that changes the most files. If all are equal, alphabetically sort the prefixes and list comma separated. -- `/source/encoders/encoder-ffmpeg` -> `encoder/ffmpeg` -- `/source/filters/filter-shader` -> `filter/shader` -- `/source/encoders/handlers/handler`, `/source/encoders/encoder-ffmpeg` -> `encoder/ffmpeg` +
-## Coding Guidelines + +

Coding

### Documentation -Documentation should be present in areas where it would save time to new developers, and in areas where an API is defined. This means that you should not provide documentation for things like `1 + 1`, but for things like the following: +The short form of the this part is **Code != Documentation**. Documentation is what you intend your Code to do, while Code is what it actually does. If your Code mismatches the Documentation, it is time to fix the Code, unless the change is a new addition in terms of behavior or functionality. Note that by this we don't mean to document things like `1 + 1` but instead things like the following: ```c++ int32_t idepth = static_cast(depth); @@ -58,14 +84,18 @@ int32_t container_size = static_cast(pow(2l, (idepth + (idepth / 2)))); ```c++ class magic_class { - void do_magic_thing(float magic_number); + void do_magic_thing(float magic_number) { + // Lots and lots of SIMD code that does a magic thing... + } } ``` -Both of these examples would be much easier to understand if they had proper documentation, and save hours if not even days of delving into code. Documentation is about saving time to new developers, and can't be replaced by code. Code is not Documentation! +Documenting what a block of Code does not only helps you, it also helps other contributors understand what this Code is supposed to do. While you may be able to read your own Code (at least for now), there is no guarantee that either you or someone else will be able to read it in the future. Not only that, but it makes spotting mistakes and fixing them easier, since we have Documentation to tell us what it is supposed to do! ### Naming & Casing -All long-term objects should have a descriptive name, which can be used by other developers to know what it is for. Temporary objects should also have some information, but do not necessarily follow the same rules. +The project isn't too strict about variable naming as well as casing, but we do prefer a universal style across all code. While this may appear as removing your individuality from the code, it ultimately serves the purpose of making it easier to jump from one block of code to the other, without having to guess at what this code now does. + +Additionally we prefer it when things are named by what they either do or what they contain, instead of having the entire alphabet spelled out in different arrangements. While it is fine to have chaos in your own Code for your private or hobby projects, it is not fine to submit such code to other projects. #### Macros - Casing: ELEPHANT_CASE @@ -249,6 +279,16 @@ Special rules for `class` #### Members All class members must be `private` and only accessible through get-/setters. The setter of a member should also validate if the setting is within an allowed range, and throw exceptions if an error occurs. If there is no better option, it is allowed to delay validation until a common function is called. -## Building -Please read [the guide on the wiki](https://github.com/Xaymar/obs-StreamFX/wiki/Building) for building the project. +
+

Localization

+ +We use Crowdin to handle translations into many languages, and you can join the [StreamFX project on Crowdin](https://crowdin.com/project/obs-stream-effects) if you are interested in improving the translations to your native tongue. As Crowdin handles all other languages, Pull Requests therefore should only include changes to `en-US.ini`. + +
+ +## Further Resources +- A guide on how to build the project is in BUILDING.MD. +- A no bullshit guide to `git`: https://rogerdudler.github.io/git-guide/ + - Remember, `git` has help pages for all commands - run `git --help`. + - ... or use visual clients, like TortoiseGit, Github Desktop, SourceTree, and similar. It's what I do. diff --git a/README.adoc b/README.adoc new file mode 100644 index 0000000..4aa5704 --- /dev/null +++ b/README.adoc @@ -0,0 +1,57 @@ +== image:https://raw.githubusercontent.com/Xaymar/obs-StreamFX/master/media/logo.png[alt="StreamFX"] +Upgrade your setup with several modern sources, filters, transitions and encoders using StreamFX! With several performant and flexible features, you will discover new ways to build your scenes, better ways to encode your content, and take your stream to the next level. Create cool new scenes with 3D effects, add glow or shadow, or blur out content - endless choices, and all of it at your fingertips. + +++++ +

+ +More Information
+CI Status +Crowdin Status + +

+++++ + +=== Support the development of StreamFX! +++++ + +Patreon + +++++ +Maintaining a project like StreamFX requires time and money, of which both are in short supply. If you use any feature of StreamFX, please consider supporting StreamFX via link:https://patreon.com/xaymar[Patreon]. Even as little as 1€ per month matters a lot, plus you get a number of benefits! + +=== License +Licensed under link:https://github.com/Xaymar/obs-StreamFX/blob/root/LICENSE[GPLv3 (or later), see LICENSE]. Additional works included are: + +[options="header"] +|================= +|Work |License |Author(s) + +|link:https://gen.glad.sh/[GLAD] +|link:https://github.com/Dav1dde/glad/blob/glad2/LICENSE[MIT License] +|link:https://github.com/Dav1dde/glad/graphs/contributors?type=a[Dav1dde, madebr, BtbN, and more] + +|link:https://github.com/nlohmann/json[JSON for Modern C++] +|link:https://github.com/nlohmann/json/blob/develop/LICENSE.MIT[MIT License] +|link:https://github.com/nlohmann/json/graphs/contributors?type=a[nlohmann, ChrisKtiching, nickaein, and more] + +|link:https://github.com/NVIDIA/MAXINE-AFX-SDK[NVIDIA Maxine Audio Effects SDK] +|link:https://github.com/NVIDIA/MAXINE-AFX-SDK/blob/master/LICENSE[MIT License] +|link:https://nvidia.com/[NVIDIA Corporation] + +|link:https://github.com/NVIDIA/MAXINE-AR-SDK[NVIDIA Maxine Augmented Reality SDK] +|link:https://github.com/NVIDIA/MAXINE-Ar-SDK/blob/master/LICENSE[MIT License] +|link:https://nvidia.com/[NVIDIA Corporation] + +|link:https://github.com/NVIDIA/MAXINE-VFX-SDK[NVIDIA Maxine Video Effects SDK] +|link:https://github.com/NVIDIA/MAXINE-VFX-SDK/blob/master/LICENSE[MIT License] +|link:https://nvidia.com/[NVIDIA Corporation] + +|link:https://github.com/obsproject/obs-studio[Open Broadcaster Software Studio] +|link:https://github.com/obsproject/obs-studio/blob/master/COPYING[GPL-2.0 (or later)] +|link:https://github.com/obsproject/obs-studio/graphs/contributors?type=a[jp9000, computerquip, and more] + +|link:https://www.qt.io/[Qt 6.x] +|link:https://www.qt.io/download-open-source[(L)GPL-3.0 (or later)] +|link:https://www.qt.io/[The Qt Company], and open source contributors + +|================= diff --git a/cmake/modules/FindFFmpeg.cmake b/cmake/modules/FindFFmpeg.cmake index 75ed158..99c7121 100644 --- a/cmake/modules/FindFFmpeg.cmake +++ b/cmake/modules/FindFFmpeg.cmake @@ -1,157 +1,293 @@ # AUTOGENERATED COPYRIGHT HEADER START -# Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +# Copyright (C) 2020-2024 Michael Fabian 'Xaymar' Dirks # AUTOGENERATED COPYRIGHT HEADER END # # This module defines the following variables: # -# FFMPEG_FOUND - All required components and the core library were found -# FFMPEG_INCLUDE_DIRS - Combined list of all components include dirs -# FFMPEG_LIBRARIES - Combined list of all componenets libraries -# FFMPEG_VERSION_STRING - Version of the first component requested +# FFmpeg_FOUND - All required components and the core library were found +# FFmpeg_INCLUDE_DIRS - Combined list of all components include dirs +# FFmpeg_LIBRARIES - Combined list of all componenets libraries +# FFmpeg_VERSION_STRING - Version of the first component requested # # For each requested component the following variables are defined: # -# FFMPEG__FOUND - The component was found -# FFMPEG__INCLUDE_DIRS - The components include dirs -# FFMPEG__LIBRARIES - The components libraries -# FFMPEG__VERSION_STRING - The components version string -# FFMPEG__VERSION_MAJOR - The components major version -# FFMPEG__VERSION_MINOR - The components minor version -# FFMPEG__VERSION_MICRO - The components micro version +# FFmpeg__FOUND - The component was found +# FFmpeg__INCLUDE_DIRS - The components include dirs +# FFmpeg__LIBRARIES - The components libraries +# FFmpeg__VERSION_STRING - The components version string +# FFmpeg__VERSION_MAJOR - The components major version +# FFmpeg__VERSION_MINOR - The components minor version +# FFmpeg__VERSION_MICRO - The components micro version # # is the uppercase name of the component +cmake_minimum_required(VERSION 3.26.0...3.28.1) +include(FindPackageHandleStandardArgs) +include(CMakeParseArguments) find_package(PkgConfig QUIET) -if(CMAKE_SIZEOF_VOID_P EQUAL 8) - set(_lib_suffix 64) -else() - set(_lib_suffix 32) -endif() +function(find_ffmpeg_component) + cmake_parse_arguments( + _ARGS "" "COMPONENT;OUTPUT" "" ${ARGN} + ) -function(find_ffmpeg_library component header) - string(TOUPPER "${component}" component_u) - set(FFMPEG_${component_u}_FOUND FALSE PARENT_SCOPE) - set(FFmpeg_${component}_FOUND FALSE PARENT_SCOPE) + string(TOLOWER "${_ARGS_COMPONENT}" lcomponent) + # Do nothing if the target already exists. + if(TARGET FFmpeg::${lcomponent}) + set(FFmpeg_${lcomponent}_FOUND TRUE PARENT_SCOPE) + return() + endif() + set(FFmpeg_${lcomponent}_FOUND OFF PARENT_SCOPE) + + # pkg-config if(PKG_CONFIG_FOUND) - pkg_check_modules(PC_FFMPEG_${component} QUIET lib${component}) + pkg_check_modules(PC_FFmpeg_${lcomponent} QUIET lib${lcomponent}) endif() - find_path(FFMPEG_${component}_INCLUDE_DIR + # Find headers + find_path(FFmpeg_${lcomponent}_INCLUDE_DIR NAMES - "lib${component}/${header}" "lib${component}/version.h" + "${lcomponent}.h" + "lib${lcomponent}.h" HINTS - ENV FFmpegPath${_lib_suffix} - ENV FFmpegPath - ENV DepsPath${_lib_suffix} - ENV DepsPath - ${FFmpegPath${_lib_suffix}} - ${FFmpegPath} - ${DepsPath${_lib_suffix}} - ${DepsPath} - ${PC_FFMPEG_${component}_INCLUDE_DIRS} - PATHS - /usr/include /usr/local/include /opt/local/include /sw/include - PATH_SUFFIXES ffmpeg libav include) - - find_library(FFMPEG_${component}_LIBRARY - NAMES - "${component}" "lib${component}" - HINTS - ENV FFmpegPath${_lib_suffix} - ENV FFmpegPath - ENV DepsPath${_lib_suffix} - ENV DepsPath - ${FFmpegPath${_lib_suffix}} - ${FFmpegPath} - ${DepsPath${_lib_suffix}} - ${DepsPath} - ${PC_FFMPEG_${component}_LIBRARY_DIRS} - PATHS - /usr/lib /usr/local/lib /opt/local/lib /sw/lib + ${PC_FFmpeg_${lcomponent}_INCLUDE_DIRS} + ${FFmpeg_DIR} PATH_SUFFIXES - lib${_lib_suffix} lib - libs${_lib_suffix} libs - bin${_lib_suffix} bin - ../lib${_lib_suffix} ../lib - ../libs${_lib_suffix} ../libs - ../bin${_lib_suffix} ../bin) + "include/lib${lcomponent}" + "lib${lcomponent}" - set(FFMPEG_${component_u}_INCLUDE_DIRS ${FFMPEG_${component}_INCLUDE_DIR} PARENT_SCOPE) - set(FFMPEG_${component_u}_LIBRARIES ${FFMPEG_${component}_LIBRARY} PARENT_SCOPE) + "include/${lcomponent}" + "${lcomponent}" - mark_as_advanced(FFMPEG_${component}_INCLUDE_DIR FFMPEG_${component}_LIBRARY) + "include" + DOC "${lcomponent}: Path to include directory" + ) + mark_as_advanced(FFmpeg_${lcomponent}_INCLUDE_DIR) - if(FFMPEG_${component}_INCLUDE_DIR AND FFMPEG_${component}_LIBRARY) - set(FFMPEG_${component_u}_FOUND TRUE PARENT_SCOPE) - set(FFmpeg_${component}_FOUND TRUE PARENT_SCOPE) + # Find library + math(EXPR LIBSUFFIX "8*${CMAKE_SIZEOF_VOID_P}") + if(CMAKE_SIZEOF_VOID_P EQUAL 8) + set(FIND_LIBRARY_USE_LIB64_PATHS ON) + set(FIND_LIBRARY_USE_LIB32_PATHS OFF) + set(FIND_LIBRARY_USE_LIBX32_PATHS OFF) + else() + set(FIND_LIBRARY_USE_LIB64_PATHS OFF) + set(FIND_LIBRARY_USE_LIB32_PATHS ON) + set(FIND_LIBRARY_USE_LIBX32_PATHS OFF) + endif() + find_library(FFmpeg_${lcomponent}_LIBRARY + NAMES + "${lcomponent}" + "lib${lcomponent}" + HINTS + ${PC_FFmpeg_${lcomponent}_LIBRARY_DIRS} + ${FFmpeg_DIR} + PATHS + "/lib${LIBSUFFIX}" + "/lib" - list(APPEND FFMPEG_INCLUDE_DIRS ${FFMPEG_${component}_INCLUDE_DIR}) - list(REMOVE_DUPLICATES FFMPEG_INCLUDE_DIRS) - set(FFMPEG_INCLUDE_DIRS "${FFMPEG_INCLUDE_DIRS}" PARENT_SCOPE) + "/usr/lib${LIBSUFFIX}" + "/usr/lib" - list(APPEND FFMPEG_LIBRARIES ${FFMPEG_${component}_LIBRARY}) - list(REMOVE_DUPLICATES FFMPEG_LIBRARIES) - set(FFMPEG_LIBRARIES "${FFMPEG_LIBRARIES}" PARENT_SCOPE) + "/usr/local/lib${LIBSUFFIX}" + "/usr/local/lib" + PATH_SUFFIXES + "lib${LIBSUFFIX}/lib${lcomponent}" + "lib/lib${lcomponent}" + "lib${lcomponent}" - set(FFMPEG_${component_u}_VERSION_STRING "unknown" PARENT_SCOPE) - set(_vfile "${FFMPEG_${component}_INCLUDE_DIR}/lib${component}/version.h") + "lib${LIBSUFFIX}/${lcomponent}" + "lib/${lcomponent}" + "${lcomponent}" + DOC "${lcomponent}: Path to library file" + ) + mark_as_advanced(FFmpeg_${lcomponent}_LIBRARY) - if(EXISTS "${_vfile}") - file(STRINGS "${_vfile}" _version_parse REGEX "^.*VERSION_(MAJOR|MINOR|MICRO)[ \t]+[0-9]+[ \t]*$") - string(REGEX REPLACE ".*VERSION_MAJOR[ \t]+([0-9]+).*" "\\1" _major "${_version_parse}") - string(REGEX REPLACE ".*VERSION_MINOR[ \t]+([0-9]+).*" "\\1" _minor "${_version_parse}") - string(REGEX REPLACE ".*VERSION_MICRO[ \t]+([0-9]+).*" "\\1" _micro "${_version_parse}") + if((FFmpeg_${lcomponent}_LIBRARY) AND (FFmpeg_${lcomponent}_INCLUDE_DIR)) + # The include path should be the parent directory. + cmake_path(GET FFmpeg_${lcomponent}_INCLUDE_DIR PARENT_PATH _INCLUDE_DIR) - set(FFMPEG_${component_u}_VERSION_MAJOR "${_major}" PARENT_SCOPE) - set(FFMPEG_${component_u}_VERSION_MINOR "${_minor}" PARENT_SCOPE) - set(FFMPEG_${component_u}_VERSION_MICRO "${_micro}" PARENT_SCOPE) - - set(FFMPEG_${component_u}_VERSION_STRING "${_major}.${_minor}.${_micro}" PARENT_SCOPE) + # Detect the library version. + set(_VERSION "") + if((EXISTS "${FFmpeg_${lcomponent}_INCLUDE_DIR}/version_major.h") AND (EXISTS "${FFmpeg_${lcomponent}_INCLUDE_DIR}/version.h")) + # Parse version_major.h: + # - #define LIB${lcomponent}_VERSION_MAJOR 58 + # and version.h: + # - #define LIB${lcomponent}_VERSION_MINOR 3 + # - #define LIB${lcomponent}_VERSION_MICRO 100 + file(STRINGS "${FFmpeg_${lcomponent}_INCLUDE_DIR}/version_major.h" _FILE_VERSION REGEX "^.*_VERSION_(MAJOR|MINOR|MICRO)[ \t]+[1-9]+[0-9]*.*$") + file(STRINGS "${FFmpeg_${lcomponent}_INCLUDE_DIR}/version.h" _FILE_VERSION_MINOR REGEX "^.*_VERSION_(MAJOR|MINOR|MICRO)[ \t]+[1-9]+[0-9]*.*$") + list(APPEND _FILE_VERSION ${_FILE_VERSION_MINOR}) + elseif((EXISTS "${FFmpeg_${lcomponent}_INCLUDE_DIR}/version.h")) + # Parse version.h: + # - #define LIB${lcomponent}_VERSION_MAJOR 58 + # - #define LIB${lcomponent}_VERSION_MINOR 3 + # - #define LIB${lcomponent}_VERSION_MICRO 100 + file(STRINGS "${FFmpeg_${lcomponent}_INCLUDE_DIR}/version.h" _FILE_VERSION REGEX "^.*_VERSION_(MAJOR|MINOR|MICRO)[ \t]+[1-9]+[0-9]*.*$") else() - message(STATUS "Failed parsing FFmpeg ${component} version") + message(WARNING "${lcomponent}: No version header found, defaulting to 0.0.0.") + set(FFmpeg_${lcomponent}_VERSION_MAJOR "0") + set(FFmpeg_${lcomponent}_VERSION_MINOR "0") + set(FFmpeg_${lcomponent}_VERSION_PATCH "0") endif() + foreach(_FILE_VERSION_PART IN LISTS _FILE_VERSION) + if(_FILE_VERSION_PART MATCHES "^.*MAJOR[ \t]+([1-9]+[0-9]*).*$") + set(FFmpeg_${lcomponent}_VERSION_MAJOR ${CMAKE_MATCH_1}) + elseif(_FILE_VERSION_PART MATCHES "^.*MINOR[ \t]+([1-9]+[0-9]*).*$") + set(FFmpeg_${lcomponent}_VERSION_MINOR ${CMAKE_MATCH_1}) + elseif(_FILE_VERSION_PART MATCHES "^.*MICRO[ \t]+([1-9]+[0-9]*).*$") + set(FFmpeg_${lcomponent}_VERSION_PATCH ${CMAKE_MATCH_1}) + endif() + endforeach() + set(FFmpeg_${lcomponent}_VERSION "${FFmpeg_${lcomponent}_VERSION_MAJOR}.${FFmpeg_${lcomponent}_VERSION_MINOR}.${FFmpeg_${lcomponent}_VERSION_PATCH}") + set(FFmpeg_${lcomponent}_VERSION "${FFmpeg_${lcomponent}_VERSION}" PARENT_SCOPE) + + # If possible (shared/module library), find the binary file for it. + find_file(FFmpeg_${lcomponent}_BINARY + NAMES + "${lcomponent}${CMAKE_SHARED_LIBRARY_SUFFIX}.${FFmpeg_${lcomponent}_VERSION_MAJOR}.${FFmpeg_${lcomponent}_VERSION_MINOR}" + "lib${lcomponent}${CMAKE_SHARED_LIBRARY_SUFFIX}.${FFmpeg_${lcomponent}_VERSION_MAJOR}.${FFmpeg_${lcomponent}_VERSION_MINOR}" + + "${lcomponent}${CMAKE_SHARED_LIBRARY_SUFFIX}.${FFmpeg_${lcomponent}_VERSION_MAJOR}" + "lib${lcomponent}${CMAKE_SHARED_LIBRARY_SUFFIX}.${FFmpeg_${lcomponent}_VERSION_MAJOR}" + + "${lcomponent}-${FFmpeg_${lcomponent}_VERSION_MAJOR}${CMAKE_SHARED_LIBRARY_SUFFIX}" + "lib${lcomponent}-${FFmpeg_${lcomponent}_VERSION_MAJOR}${CMAKE_SHARED_LIBRARY_SUFFIX}" + + "${lcomponent}${CMAKE_SHARED_LIBRARY_SUFFIX}" + "lib${lcomponent}${CMAKE_SHARED_LIBRARY_SUFFIX}" + HINTS + ${PC_FFmpeg_${lcomponent}_LIBRARY_DIRS} + ${FFmpeg_DIR} + PATHS + "/bin${LIBSUFFIX}" + "/bin" + + "/usr/bin${LIBSUFFIX}" + "/usr/bin" + + "/usr/local/bin${LIBSUFFIX}" + "/usr/local/bin" + + "/lib${LIBSUFFIX}" + "/lib" + + "/usr/lib${LIBSUFFIX}" + "/usr/lib" + + "/usr/local/lib${LIBSUFFIX}" + "/usr/local/lib" + PATH_SUFFIXES + "bin${LIBSUFFIX}/lib${lcomponent}" + "bin/lib${lcomponent}" + "lib${lcomponent}" + + "bin${LIBSUFFIX}/${lcomponent}" + "bin/${lcomponent}" + "${lcomponent}" + + "bin" + DOC "${lcomponent}: Path to binary file (optional)" + ) + if(FFmpeg_${lcomponent}_BINARY) + set(_TARGET_TYPE SHARED) + else() + set(_TARGET_TYPE UNKNOWN) + endif() + + # ToDo: Detect if static or shared. Usually should be shared, due to FFmpeg's size. + add_library(FFmpeg::${lcomponent} ${_TARGET_TYPE} IMPORTED) + target_include_directories(FFmpeg::${lcomponent} + INTERFACE "${_INCLUDE_DIR}" + ) + set_target_properties(FFmpeg::${lcomponent} PROPERTIES + VERSION ${FFmpeg_${lcomponent}_VERSION} + SOVERSION ${FFmpeg_${lcomponent}_VERSION} + ) + if(APPLE OR UNIX) # Unix/Mac, Unix/Linux + set_target_properties(FFmpeg::${lcomponent} PROPERTIES + IMPORTED_LOCATION "${FFmpeg_${lcomponent}_LIBRARY}" + ) + if(FFmpeg_${lcomponent}_BINARY) + set_target_properties(FFmpeg::${lcomponent} PROPERTIES + LIBRARY_OUTPUT_NAME "${FFmpeg_${lcomponent}_BINARY}" + ) + endif() + else() # Windows + set_target_properties(FFmpeg::${lcomponent} PROPERTIES + IMPORTED_IMPLIB "${FFmpeg_${lcomponent}_LIBRARY}" + ) + if(FFmpeg_${lcomponent}_BINARY) + set_target_properties(FFmpeg::${lcomponent} PROPERTIES + IMPORTED_LOCATION "${FFmpeg_${lcomponent}_BINARY}" + LIBRARY_OUTPUT_NAME "${FFmpeg_${lcomponent}_BINARY}" + ) + endif() + endif() + + set(FFmpeg_${lcomponent}_FOUND ON PARENT_SCOPE) + message(STATUS "${lcomponent}: Found v${FFmpeg_${lcomponent}_VERSION}") endif() endfunction() -set(FFMPEG_INCLUDE_DIRS) -set(FFMPEG_LIBRARIES) +# Components +# - avcodec: libavcodec, avcodec.h +# - avdevice: libavdevice, avdevice.h +# - avfilter: libavfilter, avfilter.h +# - avformat: libavformat, avformat.h +# - avutil: libavutil, avutil.h +# - swresample: libswresample, swresample.h +# - swscale: libswscale, swscale.h +set(_COMPONENTS + "avcodec" + "avformat" + "avfilter" + "swresample" + "swscale" + "avdevice" + "avutil" +) + +set(FFmpeg_LIBRARIES) +set(FFmpeg_VERSION "0.0.0") if(NOT FFmpeg_FIND_COMPONENTS) - message(FATAL_ERROR "No FFmpeg components requested") + message(WARNING "No specific component requested, defaulting to everything") + set(FFmpeg_FIND_COMPONENT ${_COMPONENTS}) endif() -list(GET FFmpeg_FIND_COMPONENTS 0 _first_comp) -string(TOUPPER "${_first_comp}" _first_comp) +set(FFmpeg_FOUND ON) +foreach(component IN LISTS FFmpeg_FIND_COMPONENTS) + string(TOUPPER "${component}" lcomponent) + string(TOLOWER "${component}" lcomponent) -foreach(component ${FFmpeg_FIND_COMPONENTS}) - if(component STREQUAL "avcodec") - find_ffmpeg_library("${component}" "avcodec.h") - elseif(component STREQUAL "avdevice") - find_ffmpeg_library("${component}" "avdevice.h") - elseif(component STREQUAL "avfilter") - find_ffmpeg_library("${component}" "avfilter.h") - elseif(component STREQUAL "avformat") - find_ffmpeg_library("${component}" "avformat.h") - elseif(component STREQUAL "avresample") - find_ffmpeg_library("${component}" "avresample.h") - elseif(component STREQUAL "avutil") - find_ffmpeg_library("${component}" "avutil.h") - elseif(component STREQUAL "postproc") - find_ffmpeg_library("${component}" "postprocess.h") - elseif(component STREQUAL "swresample") - find_ffmpeg_library("${component}" "swresample.h") - elseif(component STREQUAL "swscale") - find_ffmpeg_library("${component}" "swscale.h") - else() - message(FATAL_ERROR "Unknown FFmpeg component requested: ${component}") + # Error out if an invalid component is requested. + if(NOT lcomponent IN_LIST _COMPONENTS) + message(FATAL_ERROR "Requested component '${lcomponent}' is unknown to us.") + endif() + + find_ffmpeg_component(COMPONENT ${lcomponent}) + if(NOT TARGET FFmpeg::${lcomponent}) + if(FFmpeg_FIND_REQUIRED_${lcomponent}) + set(FFmpeg_FOUND OFF) + endif() + endif() +endforeach() + +# Set version based on priority list. +foreach(component IN LISTS _COMPONENTS) + string(TOUPPER "${lcomponent}" lcomponent) + string(TOLOWER "${lcomponent}" lcomponent) + + if(TARGET FFmpeg::${lcomponent}) + set(FFmpeg_VERSION ${FFmpeg_${lcomponent}_VERSION}) + break() endif() endforeach() -include(FindPackageHandleStandardArgs) find_package_handle_standard_args(FFmpeg - FOUND_VAR FFMPEG_FOUND - REQUIRED_VARS FFMPEG_${_first_comp}_LIBRARIES FFMPEG_${_first_comp}_INCLUDE_DIRS - VERSION_VAR FFMPEG_${_first_comp}_VERSION_STRING - HANDLE_COMPONENTS) + FOUND_VAR FFmpeg_FOUND + HANDLE_COMPONENTS + NAME_MISMATCHED +) diff --git a/components/autoframing/CMakeLists.txt b/components/autoframing/CMakeLists.txt new file mode 100644 index 0000000..80b2a35 --- /dev/null +++ b/components/autoframing/CMakeLists.txt @@ -0,0 +1,23 @@ +# AUTOGENERATED COPYRIGHT HEADER START +# Copyright (C) 2023 Michael Fabian 'Xaymar' Dirks +# AUTOGENERATED COPYRIGHT HEADER END + +cmake_minimum_required(VERSION 3.26) +project("AutoFraming") +list(APPEND CMAKE_MESSAGE_INDENT "[${PROJECT_NAME}] ") + +streamfx_add_component("Auto-Framing" + RESOLVER streamfx_auto_framing_resolver +) +streamfx_add_component_dependency("NVIDIA" OPTIONAL) + +function(streamfx_auto_framing_resolver) + # Providers + #- NVIDIA + streamfx_enabled_component("NVIDIA" T_CHECK) + if(T_CHECK) + target_compile_definitions(${COMPONENT_TARGET} PRIVATE + PRIVATE ENABLE_NVIDIA + ) + endif() +endfunction() diff --git a/components/autoframing/source/filters/filter-autoframing.cpp b/components/autoframing/source/filters/filter-autoframing.cpp new file mode 100644 index 0000000..7d67ed9 --- /dev/null +++ b/components/autoframing/source/filters/filter-autoframing.cpp @@ -0,0 +1,1278 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2021-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2022 lainon +// AUTOGENERATED COPYRIGHT HEADER END + +#include "filter-autoframing.hpp" +#include "obs/gs/gs-helper.hpp" +#include "util/util-logging.hpp" + +#ifdef _DEBUG +#define ST_PREFIX "<%s> " +#define D_LOG_ERROR(x, ...) P_LOG_ERROR(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_WARNING(x, ...) P_LOG_WARN(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_INFO(x, ...) P_LOG_INFO(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_DEBUG(x, ...) P_LOG_DEBUG(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#else +#define ST_PREFIX " " +#define D_LOG_ERROR(...) P_LOG_ERROR(ST_PREFIX __VA_ARGS__) +#define D_LOG_WARNING(...) P_LOG_WARN(ST_PREFIX __VA_ARGS__) +#define D_LOG_INFO(...) P_LOG_INFO(ST_PREFIX __VA_ARGS__) +#define D_LOG_DEBUG(...) P_LOG_DEBUG(ST_PREFIX __VA_ARGS__) +#endif + +// Auto-Framing is the process of tracking important information inside of a group of video or +// audio samples, and then automatically cutting away all the unnecessary parts. In our case, we +// will focus on video only as the audio field is already covered by other solutions, like Noise +// Gate, Denoising, etc. The implementation will rely on the Provider system, so varying +// functionality should be expected from all providers. Some providers may only offer a way to +// track a single face, others will allow groups, yet others will allow even non-humans to be +// tracked. +// +// The goal is to provide Auto-Framing for single person streams ('Solo') as well as group streams +// ('Group'), though the latter will only be available if the provider supports it. In 'Solo' mode +// the filter will perfectly frame a single person, and no more than that. In 'Group' mode, it will +// combine all important elements into a single frame, and track that instead. In the future, we +// might want to offer a third mode to give each tracked face a separate frame however this may +// exceed the intended complexity of this feature entirely. + +/** Settings + * Framing + * Mode: How should things be tracked? + * Solo: Frame only a single face. + * Group: Frame many faces, group all into single frame. + * Padding: How many pixels/much % of tracked are should be kept + * Aspect Ratio: What Aspect Ratio should the framed output have? + * Stability: How stable is the framing against changes of tracked elements? + * + * Motion + * Motion Prediction: How much should we attempt to predict where tracked elements move? + * Smoothing: How much should the position between tracking attempts + * + * Advanced + * Provider: What provider should be used? + * Frequency: How often should we track? Every frame, every 2nd frame, etc. + */ + +#define ST_I18N "Filter.AutoFraming" + +#define ST_I18N_TRACKING ST_I18N ".Tracking" +#define ST_KEY_TRACKING_MODE "Tracking.Mode" +#define ST_I18N_TRACKING_MODE ST_I18N_TRACKING ".Mode" +#define ST_I18N_FRAMING_MODE_SOLO ST_I18N_TRACKING_MODE ".Solo" +#define ST_I18N_FRAMING_MODE_GROUP ST_I18N_TRACKING_MODE ".Group" +#define ST_KEY_TRACKING_FREQUENCY "Tracking.Frequency" +#define ST_I18N_TRACKING_FREQUENCY ST_I18N_TRACKING ".Frequency" + +#define ST_I18N_MOTION ST_I18N ".Motion" +#define ST_KEY_MOTION_PREDICTION "Motion.Prediction" +#define ST_I18N_MOTION_PREDICTION ST_I18N_MOTION ".Prediction" +#define ST_KEY_MOTION_SMOOTHING "Motion.Smoothing" +#define ST_I18N_MOTION_SMOOTHING ST_I18N_MOTION ".Smoothing" + +#define ST_I18N_FRAMING ST_I18N ".Framing" +#define ST_KEY_FRAMING_STABILITY "Framing.Stability" +#define ST_I18N_FRAMING_STABILITY ST_I18N_FRAMING ".Stability" +#define ST_KEY_FRAMING_PADDING "Framing.Padding" +#define ST_I18N_FRAMING_PADDING ST_I18N_FRAMING ".Padding" +#define ST_KEY_FRAMING_OFFSET "Framing.Offset" +#define ST_I18N_FRAMING_OFFSET ST_I18N_FRAMING ".Offset" +#define ST_KEY_FRAMING_ASPECTRATIO "Framing.AspectRatio" +#define ST_I18N_FRAMING_ASPECTRATIO ST_I18N_FRAMING ".AspectRatio" + +#define ST_KEY_ADVANCED_PROVIDER "Provider" +#define ST_I18N_ADVANCED_PROVIDER ST_I18N ".Provider" +#define ST_I18N_ADVANCED_PROVIDER_NVIDIA_FACEDETECTION ST_I18N_ADVANCED_PROVIDER ".NVIDIA.FaceDetection" + +#define ST_KALMAN_EEC 1.0f + +using streamfx::filter::autoframing::autoframing_factory; +using streamfx::filter::autoframing::autoframing_instance; +using streamfx::filter::autoframing::tracking_provider; + +static constexpr std::string_view HELP_URL = "https://github.com/Xaymar/obs-StreamFX/wiki/Filter-Auto-Framing"; + +static tracking_provider provider_priority[] = { + tracking_provider::NVIDIA_FACEDETECTION, +}; + +inline std::pair parse_text_as_size(const char* text) +{ + double_t v = 0; + if (sscanf(text, "%lf", &v) == 1) { + const char* prc_chr = strrchr(text, '%'); + if (prc_chr && (*prc_chr == '%')) { + return {true, v / 100.0}; + } else { + return {false, v}; + } + } else { + return {true, 1.0}; + } +} + +const char* streamfx::filter::autoframing::cstring(tracking_provider provider) +{ + switch (provider) { + case tracking_provider::INVALID: + return "N/A"; + case tracking_provider::AUTOMATIC: + return D_TRANSLATE(S_STATE_AUTOMATIC); + case tracking_provider::NVIDIA_FACEDETECTION: + return D_TRANSLATE(ST_I18N_ADVANCED_PROVIDER_NVIDIA_FACEDETECTION); + default: + throw std::runtime_error("Missing Conversion Entry"); + } +} + +std::string streamfx::filter::autoframing::string(tracking_provider provider) +{ + return cstring(provider); +} + +autoframing_instance::~autoframing_instance() +{ + D_LOG_DEBUG("Finalizing... (Addr: 0x%" PRIuPTR ")", this); + + { // Unload the underlying effect ASAP. + std::unique_lock ul(_provider_lock); + + // De-queue the underlying task. + if (_provider_task) { + streamfx::util::threadpool::threadpool::instance()->pop(_provider_task); + _provider_task->await_completion(); + _provider_task.reset(); + } + + // TODO: Make this asynchronous. + switch (_provider) { +#ifdef ENABLE_NVIDIA + case tracking_provider::NVIDIA_FACEDETECTION: + nvar_facedetection_unload(); + break; +#endif + default: + break; + } + } +} + +autoframing_instance::autoframing_instance(obs_data_t* data, obs_source_t* self) + : source_instance(data, self), + + _dirty(true), _size(1, 1), _out_size(1, 1), + + _gfx_debug(), _standard_effect(), _input(), _vb(), + + _provider(tracking_provider::INVALID), _provider_ui(tracking_provider::INVALID), _provider_ready(false), _provider_lock(), _provider_task(), + + _track_mode(tracking_mode::SOLO), _track_frequency(1), + + _motion_smoothing(0.0), _motion_smoothing_kalman_pnc(1.), _motion_smoothing_kalman_mnc(1.), _motion_prediction(0.0), + + _frame_stability(0.), _frame_stability_kalman(1.), _frame_padding_prc(), _frame_padding(), _frame_offset_prc(), _frame_offset(), _frame_aspect_ratio(0.0), + + _track_frequency_counter(0), _tracked_elements(), _predicted_elements(), + + _frame_pos_x({1., 1., 1., 1.}), _frame_pos_y({1., 1., 1., 1.}), _frame_pos({0, 0}), _frame_size({1, 1}), + + _debug(false) +{ + D_LOG_DEBUG("Initializating... (Addr: 0x%" PRIuPTR ")", this); + + { + ::streamfx::obs::gs::context gctx; + + // Get debug renderer. + _gfx_debug = ::streamfx::gfx::util::get(); + + // Create the render target for the input buffering. + _input = std::make_shared<::streamfx::obs::gs::texrender>(GS_RGBA_UNORM, GS_ZS_NONE); + _input->render(1, 1); // Preallocate the RT on the driver and GPU. + + // Load the required effect. + _standard_effect = std::make_shared<::streamfx::obs::gs::effect>(::streamfx::data_file_path("effects/standard.effect")); + + // Create the Vertex Buffer for rendering. + _vb = std::make_shared<::streamfx::obs::gs::vertexbuffer>(uint32_t{4}, uint8_t{1}); + vec3_set(_vb->at(0).position, 0, 0, 0); + vec3_set(_vb->at(1).position, 1, 0, 0); + vec3_set(_vb->at(2).position, 0, 1, 0); + vec3_set(_vb->at(3).position, 1, 1, 0); + _vb->update(true); + } + + if (data) { + load(data); + } +} + +void autoframing_instance::load(obs_data_t* data) +{ + // Update from passed data. + update(data); +} + +void autoframing_instance::migrate(obs_data_t* data, uint64_t version) +{ + if (version < STREAMFX_MAKE_VERSION(0, 11, 0, 0)) { + obs_data_unset_user_value(data, "ROI.Zoom"); + obs_data_unset_user_value(data, "ROI.Offset.X"); + obs_data_unset_user_value(data, "ROI.Offset.Y"); + obs_data_unset_user_value(data, "ROI.Stability"); + } +} + +void autoframing_instance::update(obs_data_t* data) +{ + // Tracking + _track_mode = static_cast(obs_data_get_int(data, ST_KEY_TRACKING_MODE)); + { + if (const char* text = obs_data_get_string(data, ST_KEY_TRACKING_FREQUENCY); text != nullptr) { + float value = 0.; + if (sscanf(text, "%f", &value) == 1) { + if (const char* seconds = strchr(text, 's'); seconds == nullptr) { + value = 1.f / value; // Hz -> seconds + } else { + // No-op + } + } + _track_frequency = value; + } + } + _track_frequency_counter = 0; + + // Motion + _motion_prediction = static_cast(obs_data_get_double(data, ST_KEY_MOTION_PREDICTION)) / 100.f; + _motion_smoothing = static_cast(obs_data_get_double(data, ST_KEY_MOTION_SMOOTHING)) / 100.f; + _motion_smoothing_kalman_pnc = streamfx::util::math::lerp(1.0f, 0.00001f, _motion_smoothing); + _motion_smoothing_kalman_mnc = streamfx::util::math::lerp(0.001f, 1000.0f, _motion_smoothing); + for (auto kv : _predicted_elements) { + // Regenerate filters. + kv.second->filter_pos_x = {_frame_stability_kalman, _motion_smoothing_kalman_mnc, ST_KALMAN_EEC, kv.second->filter_pos_x.get()}; + kv.second->filter_pos_y = {_frame_stability_kalman, _motion_smoothing_kalman_mnc, ST_KALMAN_EEC, kv.second->filter_pos_y.get()}; + } + + // Framing + { // Smoothing + _frame_stability = static_cast(obs_data_get_double(data, ST_KEY_FRAMING_STABILITY)) / 100.f; + _frame_stability_kalman = streamfx::util::math::lerp(1.0f, 0.00001f, _frame_stability); + + _frame_pos_x = {_frame_stability_kalman, 1.0f, ST_KALMAN_EEC, _frame_pos_x.get()}; + _frame_pos_y = {_frame_stability_kalman, 1.0f, ST_KALMAN_EEC, _frame_pos_y.get()}; + _frame_size_x = {_frame_stability_kalman, 1.0f, ST_KALMAN_EEC, _frame_size_x.get()}; + _frame_size_y = {_frame_stability_kalman, 1.0f, ST_KALMAN_EEC, _frame_size_y.get()}; + } + { // Padding + if (const char* text = obs_data_get_string(data, ST_KEY_FRAMING_PADDING ".X"); text != nullptr) { + float value = 0.; + if (sscanf(text, "%f", &value) == 1) { + if (const char* percent = strchr(text, '%'); percent != nullptr) { + // Flip sign, percent is negative. + value = -(value / 100.f); + _frame_padding_prc[0] = true; + } else { + _frame_padding_prc[0] = false; + } + } + _frame_padding.x = value; + } + if (const char* text = obs_data_get_string(data, ST_KEY_FRAMING_PADDING ".Y"); text != nullptr) { + float value = 0.; + if (sscanf(text, "%f", &value) == 1) { + if (const char* percent = strchr(text, '%'); percent != nullptr) { + // Flip sign, percent is negative. + value = -(value / 100.f); + _frame_padding_prc[1] = true; + } else { + _frame_padding_prc[1] = false; + } + } + _frame_padding.y = value; + } + } + { // Offset + if (const char* text = obs_data_get_string(data, ST_KEY_FRAMING_OFFSET ".X"); text != nullptr) { + float value = 0.; + if (sscanf(text, "%f", &value) == 1) { + if (const char* percent = strchr(text, '%'); percent != nullptr) { + // Flip sign, percent is negative. + value = -(value / 100.f); + _frame_offset_prc[0] = true; + } else { + _frame_offset_prc[0] = false; + } + } + _frame_offset.x = value; + } + if (const char* text = obs_data_get_string(data, ST_KEY_FRAMING_OFFSET ".Y"); text != nullptr) { + float value = 0.; + if (sscanf(text, "%f", &value) == 1) { + if (const char* percent = strchr(text, '%'); percent != nullptr) { + // Flip sign, percent is negative. + value = -(value / 100.f); + _frame_offset_prc[1] = true; + } else { + _frame_offset_prc[1] = false; + } + } + _frame_offset.y = value; + } + } + { // Aspect Ratio + _frame_aspect_ratio = static_cast(_size.first) / static_cast(_size.second); + if (const char* text = obs_data_get_string(data, ST_KEY_FRAMING_ASPECTRATIO); text != nullptr) { + if (const char* percent = strchr(text, ':'); percent != nullptr) { + float left = 0.; + float right = 0.; + if ((sscanf(text, "%f", &left) == 1) && (sscanf(percent + 1, "%f", &right) == 1)) { + _frame_aspect_ratio = left / right; + } else { + _frame_aspect_ratio = 0.0; + } + } else { + float value = 0.; + if (sscanf(text, "%f", &value) == 1) { + _frame_aspect_ratio = value; + } else { + _frame_aspect_ratio = 0.0; + } + } + } + } + + // Advanced / Provider + { // Check if the user changed which Denoising provider we use. + auto provider = static_cast(obs_data_get_int(data, ST_KEY_ADVANCED_PROVIDER)); + if (provider == tracking_provider::AUTOMATIC) { + provider = autoframing_factory::instance()->find_ideal_provider(); + } + + // Check if the provider was changed, and if so switch. + if (provider != _provider) { + _provider_ui = provider; + switch_provider(provider); + } + + if (_provider_ready) { + std::unique_lock ul(_provider_lock); + + switch (_provider) { +#ifdef ENABLE_NVIDIA + case tracking_provider::NVIDIA_FACEDETECTION: + nvar_facedetection_update(data); + break; +#endif + default: + break; + } + } + } + + _debug = obs_data_get_bool(data, "Debug"); +} + +void streamfx::filter::autoframing::autoframing_instance::properties(obs_properties_t* properties) +{ + switch (_provider_ui) { +#ifdef ENABLE_NVIDIA + case tracking_provider::NVIDIA_FACEDETECTION: + nvar_facedetection_properties(properties); + break; +#endif + default: + break; + } +} + +uint32_t autoframing_instance::get_width() +{ + if (_debug) { + return std::max(_size.first, 1); + } + return std::max(_out_size.first, 1); +} + +uint32_t autoframing_instance::get_height() +{ + if (_debug) { + return std::max(_size.second, 1); + } + return std::max(_out_size.second, 1); +} + +void autoframing_instance::video_tick(float seconds) +{ + auto target = obs_filter_get_target(_self); + auto width = obs_source_get_base_width(target); + auto height = obs_source_get_base_height(target); + _size = {width, height}; + + { // Calculate output size for aspect ratio. + _out_size = _size; + if (_frame_aspect_ratio > 0.0) { + if (width > height) { + _out_size.first = static_cast(std::lroundf(static_cast(_out_size.second) * _frame_aspect_ratio), 0, std::numeric_limits::max()); + } else { + _out_size.second = static_cast(std::lroundf(static_cast(_out_size.first) * _frame_aspect_ratio), 0, std::numeric_limits::max()); + } + } + } + + // Update tracking. + tracking_tick(seconds); + + // Mark the effect as dirty. + _dirty = true; +} + +void autoframing_instance::video_render(gs_effect_t* effect) +{ + auto parent = obs_filter_get_parent(_self); + auto target = obs_filter_get_target(_self); + auto width = obs_source_get_base_width(target); + auto height = obs_source_get_base_height(target); + vec4 blank = vec4{0, 0, 0, 0}; + + // Ensure we have the bare minimum of valid information. + target = target ? target : parent; + effect = effect ? effect : obs_get_base_effect(OBS_EFFECT_DEFAULT); + + // Skip the filter if: + // - The Provider isn't ready yet. + // - We don't have a target. + // - The width/height of the next filter in the chain is empty. + if (!_provider_ready || !target || (width == 0) || (height == 0)) { + obs_source_skip_video_filter(_self); + return; + } + +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + ::streamfx::obs::gs::debug_marker profiler0{::streamfx::obs::gs::debug_color_source, "StreamFX Auto-Framing"}; + ::streamfx::obs::gs::debug_marker profiler0_0{::streamfx::obs::gs::debug_color_gray, "'%s' on '%s'", obs_source_get_name(_self), obs_source_get_name(parent)}; +#endif + + if (_dirty) { + // Capture the input. + if (obs_source_process_filter_begin(_self, GS_RGBA, OBS_ALLOW_DIRECT_RENDERING)) { + auto op = _input->render(width, height); + + // Set correct projection matrix. + gs_ortho(0, static_cast(width), 0, static_cast(height), 0, 1); + + // Clear the buffer + gs_clear(GS_CLEAR_COLOR | GS_CLEAR_DEPTH, &blank, 0, 0); + + // Set GPU state + gs_blend_state_push(); + gs_enable_color(true, true, true, true); + gs_enable_blending(false); + gs_enable_depth_test(false); + gs_enable_stencil_test(false); + gs_set_cull_mode(GS_NEITHER); + + // Render + bool srgb = gs_framebuffer_srgb_enabled(); + gs_enable_framebuffer_srgb(gs_get_linear_srgb()); + obs_source_process_filter_end(_self, obs_get_base_effect(OBS_EFFECT_DEFAULT), width, height); + gs_enable_framebuffer_srgb(srgb); + + // Reset GPU state + gs_blend_state_pop(); + } else { + obs_source_skip_video_filter(_self); + return; + } + + // Lock & Process the captured input with the provider. + if (_track_frequency_counter >= _track_frequency) { + _track_frequency_counter = 0; + + std::unique_lock ul(_provider_lock); + switch (_provider) { +#ifdef ENABLE_NVIDIA + case tracking_provider::NVIDIA_FACEDETECTION: + nvar_facedetection_process(); + break; +#endif + default: + obs_source_skip_video_filter(_self); + return; + } + } + + _dirty = false; + } + + { // Draw the result for the next filter to use. +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + ::streamfx::obs::gs::debug_marker profiler1{::streamfx::obs::gs::debug_color_render, "Render"}; +#endif + + if (_debug) { // Debug Mode + gs_effect_set_texture(gs_effect_get_param_by_name(effect, "image"), _input->get_object()); + while (gs_effect_loop(effect, "Draw")) { + gs_draw_sprite(nullptr, 0, _size.first, _size.second); + } + + for (auto kv : _predicted_elements) { + // Tracked Area (Red) + _gfx_debug->draw_rectangle(kv.first->pos.x - kv.first->size.x / 2.f, kv.first->pos.y - kv.first->size.y / 2.f, kv.first->size.x, kv.first->size.y, true, 0x7E0000FF); + + // Velocity Arrow (Black) + _gfx_debug->draw_arrow(kv.first->pos.x, kv.first->pos.y, kv.first->pos.x + kv.first->vel.x, kv.first->pos.y + kv.first->vel.y, 0., 0x7E000000); + + // Predicted Area (Orange) + _gfx_debug->draw_rectangle(kv.second->mp_pos.x - kv.first->size.x / 2.f, kv.second->mp_pos.y - kv.first->size.y / 2.f, kv.first->size.x, kv.first->size.y, true, 0x7E007EFF); + + // Filtered Area (Yellow) + _gfx_debug->draw_rectangle(kv.second->filter_pos_x.get() - kv.first->size.x / 2.f, kv.second->filter_pos_y.get() - kv.first->size.y / 2.f, kv.first->size.x, kv.first->size.y, true, 0x7E00FFFF); + + // Offset Filtered Area (Blue) + _gfx_debug->draw_rectangle(kv.second->offset_pos.x - kv.first->size.x / 2.f, kv.second->offset_pos.y - kv.first->size.y / 2.f, kv.first->size.x, kv.first->size.y, true, 0x7EFF0000); + + // Padded Offset Filtered Area (Cyan) + _gfx_debug->draw_rectangle(kv.second->offset_pos.x - kv.second->pad_size.x / 2.f, kv.second->offset_pos.y - kv.second->pad_size.y / 2.f, kv.second->pad_size.x, kv.second->pad_size.y, true, 0x7EFFFF00); + + // Aspect-Ratio-Corrected Padded Offset Filtered Area (Green) + _gfx_debug->draw_rectangle(kv.second->offset_pos.x - kv.second->aspected_size.x / 2.f, kv.second->offset_pos.y - kv.second->aspected_size.y / 2.f, kv.second->aspected_size.x, kv.second->aspected_size.y, true, 0x7E00FF00); + } + + // Final Region (White) + _gfx_debug->draw_rectangle(_frame_pos.x - _frame_size.x / 2.f, _frame_pos.y - _frame_size.y / 2.f, _frame_size.x, _frame_size.y, true, 0x7EFFFFFF); + } else { + float x0 = (_frame_pos.x - _frame_size.x / 2.f) / static_cast(_size.first); + float x1 = (_frame_pos.x + _frame_size.x / 2.f) / static_cast(_size.first); + float y0 = (_frame_pos.y - _frame_size.y / 2.f) / static_cast(_size.second); + float y1 = (_frame_pos.y + _frame_size.y / 2.f) / static_cast(_size.second); + + { + auto v = _vb->at(0); + vec3_set(v.position, 0., 0., 0.); + v.uv[0]->x = x0; + v.uv[0]->y = y0; + } + { + auto v = _vb->at(1); + vec3_set(v.position, static_cast(_out_size.first), 0., 0.); + v.uv[0]->x = x1; + v.uv[0]->y = y0; + } + { + auto v = _vb->at(2); + vec3_set(v.position, 0., static_cast(_out_size.second), 0.); + v.uv[0]->x = x0; + v.uv[0]->y = y1; + } + { + auto v = _vb->at(3); + vec3_set(v.position, static_cast(_out_size.first), static_cast(_out_size.second), 0.); + v.uv[0]->x = x1; + v.uv[0]->y = y1; + } + + gs_load_vertexbuffer(_vb->update(true)); + if (!effect) { + if (_standard_effect->has_parameter("InputA", ::streamfx::obs::gs::effect_parameter::type::Texture)) { + _standard_effect->get_parameter("InputA").set_texture(_input->get_texture()); + } + + while (gs_effect_loop(_standard_effect->get_object(), "Texture")) { + gs_draw(GS_TRISTRIP, 0, 4); + } + } else { + gs_effect_set_texture(gs_effect_get_param_by_name(effect, "image"), *_input); + + while (gs_effect_loop(effect, "Draw")) { + gs_draw(GS_TRISTRIP, 0, 4); + } + } + gs_load_vertexbuffer(nullptr); + } + } +} + +void streamfx::filter::autoframing::autoframing_instance::tracking_tick(float seconds) +{ + { // Increase the age of all elements, and kill off any that are "too old". + float threshold = (0.5f * (1.f / (1.f - _track_frequency))); + + auto iter = _tracked_elements.begin(); + while (iter != _tracked_elements.end()) { + // Increment the age by the tick duration. + (*iter)->age += seconds; + + // If the age exceeds the threshold, remove it. + if ((*iter)->age >= threshold) { + if (iter == _tracked_elements.begin()) { + // Erase iter, then reset to start. + _predicted_elements.erase(*iter); + _tracked_elements.erase(iter); + iter = _tracked_elements.begin(); + } else { + // Copy, then advance before erasing. + auto iter2 = iter; + iter++; + _predicted_elements.erase(*iter2); + _tracked_elements.erase(iter2); + } + } else { + // Move ahead. + iter++; + } + } + } + + for (auto trck : _tracked_elements) { // Updated predicted elements + std::shared_ptr pred; + + // Find the corresponding prediction element. + auto iter = _predicted_elements.find(trck); + if (iter == _predicted_elements.end()) { + pred = std::make_shared(); + _predicted_elements.insert_or_assign(trck, pred); + pred->filter_pos_x = {_motion_smoothing_kalman_pnc, _motion_smoothing_kalman_mnc, ST_KALMAN_EEC, trck->pos.x}; + pred->filter_pos_y = {_motion_smoothing_kalman_pnc, _motion_smoothing_kalman_mnc, ST_KALMAN_EEC, trck->pos.y}; + } else { + pred = iter->second; + } + + // Calculate absolute velocity. + vec2 vel; + vec2_copy(&vel, &trck->vel); + vec2_mulf(&vel, &vel, _motion_prediction); + vec2_mulf(&vel, &vel, seconds); + + // Calculate predicted position. + vec2 pos; + if (trck->age > seconds) { + vec2_copy(&pos, &pred->mp_pos); + } else { + vec2_copy(&pos, &trck->pos); + } + vec2_add(&pos, &pos, &vel); + vec2_copy(&pred->mp_pos, &pos); + + // Update filtered position. + pred->filter_pos_x.filter(pred->mp_pos.x); + pred->filter_pos_y.filter(pred->mp_pos.y); + + // Update offset position. + vec2_set(&pred->offset_pos, pred->filter_pos_x.get(), pred->filter_pos_y.get()); + if (_frame_offset_prc[0]) { // % + pred->offset_pos.x += trck->size.x * (-_frame_offset.x); + } else { // Pixels + pred->offset_pos.x += _frame_offset.x; + } + if (_frame_offset_prc[1]) { // % + pred->offset_pos.y += trck->size.y * (-_frame_offset.y); + } else { // Pixels + pred->offset_pos.y += _frame_offset.y; + } + + // Calculate padded area. + vec2_copy(&pred->pad_size, &trck->size); + if (_frame_padding_prc[0]) { // % + pred->pad_size.x += trck->size.x * (-_frame_padding.x) * 2.f; + } else { // Pixels + pred->pad_size.x += _frame_padding.x * 2.f; + } + if (_frame_padding_prc[1]) { // % + pred->pad_size.y += trck->size.y * (-_frame_padding.y) * 2.f; + } else { // Pixels + pred->pad_size.y += _frame_padding.y * 2.f; + } + + // Adjust to match aspect ratio (width / height). + vec2_copy(&pred->aspected_size, &pred->pad_size); + if (_frame_aspect_ratio > 0.0) { + if ((pred->aspected_size.x / pred->aspected_size.y) >= _frame_aspect_ratio) { // Ours > Target + pred->aspected_size.y = pred->aspected_size.x / _frame_aspect_ratio; + } else { // Target > Ours + pred->aspected_size.x = pred->aspected_size.y * _frame_aspect_ratio; + } + } + } + + { // Find final frame. + bool need_filter = true; + if (_predicted_elements.size() > 0) { + if (_track_mode == tracking_mode::SOLO) { + auto kv = _predicted_elements.rbegin(); + + _frame_pos_x.filter(kv->second->offset_pos.x); + _frame_pos_y.filter(kv->second->offset_pos.y); + + vec2_set(&_frame_pos, _frame_pos_x.get(), _frame_pos_y.get()); + vec2_copy(&_frame_size, &kv->second->aspected_size); + + need_filter = false; + } else { + vec2 min; + vec2 max; + + vec2_set(&min, std::numeric_limits::max(), std::numeric_limits::max()); + vec2_set(&max, 0., 0.); + + for (auto kv : _predicted_elements) { + vec2 size; + vec2 low; + vec2 high; + + vec2_copy(&size, &kv.second->aspected_size); + vec2_mulf(&size, &size, .5f); + + vec2_copy(&low, &kv.second->offset_pos); + vec2_copy(&high, &kv.second->offset_pos); + + vec2_sub(&low, &low, &size); + vec2_add(&high, &high, &size); + + if (low.x < min.x) { + min.x = low.x; + } + if (low.y < min.y) { + min.y = low.y; + } + if (high.x > max.x) { + max.x = high.x; + } + if (high.y > max.y) { + max.y = high.y; + } + } + + // Calculate center. + vec2 center; + vec2_add(¢er, &min, &max); + vec2_divf(¢er, ¢er, 2.f); + + // Assign center. + _frame_pos_x.filter(center.x); + _frame_pos_y.filter(center.y); + + // Calculate size. + vec2 size; + vec2_copy(&size, &max); + vec2_sub(&size, &size, &min); + _frame_size_x.filter(size.x); + _frame_size_y.filter(size.y); + } + } else { + _frame_pos_x.filter(static_cast(_size.first) / 2.f); + _frame_pos_y.filter(static_cast(_size.second) / 2.f); + _frame_size_x.filter(static_cast(_size.first)); + _frame_size_y.filter(static_cast(_size.second)); + } + + // Grab filtered data if needed, otherwise stick with direct data. + if (need_filter) { + vec2_set(&_frame_pos, _frame_pos_x.get(), _frame_pos_y.get()); + vec2_set(&_frame_size, _frame_size_x.get(), _frame_size_y.get()); + } + + { // Aspect Ratio correction is a three step process: + float aspect = _frame_aspect_ratio > 0. ? _frame_aspect_ratio : (static_cast(_size.first) / static_cast(_size.second)); + + { // 1. Adjust aspect ratio so that all elements end up contained. + float frame_aspect = _frame_size.x / _frame_size.y; + if (aspect < frame_aspect) { + _frame_size.y = _frame_size.x / aspect; + } else { + _frame_size.x = _frame_size.y * aspect; + } + } + + // 2. Limit the size of the frame to the allowed region, and adjust it so it's inside the frame. + // This will move the center, which might not be a wanted side effect. + vec4 rect; + rect.x = std::clamp(_frame_pos.x - _frame_size.x / 2.f, 0.f, static_cast(_size.first)); + rect.z = std::clamp(_frame_pos.x + _frame_size.x / 2.f, 0.f, static_cast(_size.first)); + rect.y = std::clamp(_frame_pos.y - _frame_size.y / 2.f, 0.f, static_cast(_size.second)); + rect.w = std::clamp(_frame_pos.y + _frame_size.y / 2.f, 0.f, static_cast(_size.second)); + _frame_pos.x = (rect.x + rect.z) / 2.f; + _frame_pos.y = (rect.y + rect.w) / 2.f; + _frame_size.x = (rect.z - rect.x); + _frame_size.y = (rect.w - rect.y); + + { // 3. Adjust the aspect ratio so that it matches the expected output aspect ratio. + float frame_aspect = _frame_size.x / _frame_size.y; + if (aspect < frame_aspect) { + _frame_size.x = _frame_size.y * aspect; + } else { + _frame_size.y = _frame_size.x / aspect; + } + } + } + } + + // Increment tracking counter. + _track_frequency_counter += seconds; +} + +struct switch_provider_data_t { + tracking_provider provider; +}; + +void streamfx::filter::autoframing::autoframing_instance::switch_provider(tracking_provider provider) +{ + std::unique_lock ul(_provider_lock); + + // Safeguard against calls made from unlocked memory. + if (provider == _provider) { + return; + } + + // This doesn't work correctly. + // - Need to allow multiple switches at once because OBS is weird. + // - Doesn't guarantee that the task is properly killed off. + + // Log information. + D_LOG_INFO("Instance '%s' is switching provider from '%s' to '%s'.", obs_source_get_name(_self), cstring(_provider), cstring(provider)); + + // If there is an ongoing task to switch provider, cancel it. + if (_provider_task) { + // De-queue it. + streamfx::util::threadpool::threadpool::instance()->pop(_provider_task); + + // Await the death of the task itself. + _provider_task->await_completion(); + + // Clear any memory associated with it. + _provider_task.reset(); + } + + // Build data to pass into the task. + auto spd = std::make_shared(); + spd->provider = _provider; + _provider = provider; + + // Then spawn a new task to switch provider. + _provider_task = streamfx::util::threadpool::threadpool::instance()->push(std::bind(&autoframing_instance::task_switch_provider, this, std::placeholders::_1), spd); +} + +void streamfx::filter::autoframing::autoframing_instance::task_switch_provider(util::threadpool::task_data_t data) +{ + std::shared_ptr spd = std::static_pointer_cast(data); + + // Mark the provider as no longer ready. + _provider_ready = false; + + // Lock the provider from being used. + std::unique_lock ul(_provider_lock); + + try { + // Unload the previous provider. + switch (spd->provider) { +#ifdef ENABLE_NVIDIA + case tracking_provider::NVIDIA_FACEDETECTION: + nvar_facedetection_unload(); + break; +#endif + default: + break; + } + + // Load the new provider. + switch (_provider) { +#ifdef ENABLE_NVIDIA + case tracking_provider::NVIDIA_FACEDETECTION: + nvar_facedetection_load(); + break; +#endif + default: + break; + } + + // Log information. + D_LOG_INFO("Instance '%s' switched provider from '%s' to '%s'.", obs_source_get_name(_self), cstring(spd->provider), cstring(_provider)); + + _provider_ready = true; + } catch (std::exception const& ex) { + // Log information. + D_LOG_ERROR("Instance '%s' failed switching provider with error: %s", obs_source_get_name(_self), ex.what()); + } +} + +#ifdef ENABLE_NVIDIA +void streamfx::filter::autoframing::autoframing_instance::nvar_facedetection_load() +{ + _nvidia_fx = std::make_shared<::streamfx::nvidia::ar::facedetection>(); +} + +void streamfx::filter::autoframing::autoframing_instance::nvar_facedetection_unload() +{ + _nvidia_fx.reset(); +} + +void streamfx::filter::autoframing::autoframing_instance::nvar_facedetection_process() +{ + if (!_nvidia_fx) { + return; + } + + // Frames may not move more than this distance. + float max_dst = sqrtf(static_cast(_size.first * _size.first) + static_cast(_size.second * _size.second)) * 0.667f; + max_dst *= 1.f / (1.f - _track_frequency); // Fine-tune this? + + // Process the current frame (if requested). + _nvidia_fx->process(_input->get_texture()); + + // If there are tracked faces, merge them with the tracked elements. + if (auto edx = _nvidia_fx->count(); edx > 0) { + for (size_t idx = 0; idx < edx; idx++) { + float confidence = 0.; + auto rect = _nvidia_fx->at(idx, confidence); + + // Skip elements that have not enough confidence of being a face. + // TODO: Make the threshold configurable. + if (confidence < .5) { + continue; + } + + // Calculate centered position. + vec2 pos; + pos.x = rect.x + (rect.z / 2.f); + pos.y = rect.y + (rect.w / 2.f); + + // Try and find a match in the current list of tracked elements. + std::shared_ptr match; + float match_dst = max_dst; + for (const auto& el : _tracked_elements) { + // Skip "fresh" elements. + if (el->age < 0.00001) { + continue; + } + + // Check if the distance is within acceptable bounds. + float dst = vec2_dist(&pos, &el->pos); + if ((dst < match_dst) && (dst < max_dst)) { + match_dst = dst; + match = el; + } + } + + // Do we have a match? + if (!match) { + // No, so create a new one. + match = std::make_shared(); + + // Insert it. + _tracked_elements.push_back(match); + + // Update information. + vec2_copy(&match->pos, &pos); + vec2_set(&match->size, rect.z, rect.w); + vec2_set(&match->vel, 0., 0.); + match->age = 0.; + } else { + // Reset the age to 0. + match->age = 0.; + + // Calculate the velocity between changes. + vec2 vel; + vec2_sub(&vel, &pos, &match->pos); + + // Update information. + vec2_copy(&match->pos, &pos); + vec2_set(&match->size, rect.z, rect.w); + vec2_copy(&match->vel, &vel); + match->age = 0.; + } + } + } +} + +void streamfx::filter::autoframing::autoframing_instance::nvar_facedetection_properties(obs_properties_t* props) {} + +void streamfx::filter::autoframing::autoframing_instance::nvar_facedetection_update(obs_data_t* data) +{ + if (!_nvidia_fx) { + return; + } + + switch (_track_mode) { + case tracking_mode::SOLO: + _nvidia_fx->set_tracking_limit(1); + break; + case tracking_mode::GROUP: + _nvidia_fx->set_tracking_limit(_nvidia_fx->tracking_limit_range().second); + break; + } +} + +#endif + +autoframing_factory::autoframing_factory() +{ + bool any_available = false; + + // 1. Try and load any configured providers. +#ifdef ENABLE_NVIDIA + try { + // Load CVImage and Video Effects SDK. + _nvcuda = ::streamfx::nvidia::cuda::obs::get(); + _nvcvi = ::streamfx::nvidia::cv::cv::get(); + _nvar = ::streamfx::nvidia::ar::ar::get(); + _nvidia_available = true; + any_available |= _nvidia_available; + } catch (const std::exception& ex) { + _nvidia_available = false; + _nvar.reset(); + _nvcvi.reset(); + _nvcuda.reset(); + D_LOG_WARNING("Failed to make NVIDIA providers available due to error: %s", ex.what()); + } catch (...) { + _nvidia_available = false; + _nvar.reset(); + _nvcvi.reset(); + _nvcuda.reset(); + D_LOG_WARNING("Failed to make NVIDIA providers available with unknown error.", nullptr); + } +#endif + + // 2. Check if any of them managed to load at all. + if (!any_available) { + D_LOG_ERROR("All supported providers failed to initialize, disabling effect.", 0); + return; + } + + // Register initial source. + _info.id = S_PREFIX "filter-autoframing"; + _info.type = OBS_SOURCE_TYPE_FILTER; + _info.output_flags = OBS_SOURCE_VIDEO; + + support_size(true); + finish_setup(); + + // Register proxy identifiers. + register_proxy("streamfx-filter-nvidia-face-tracking"); + register_proxy("streamfx-nvidia-face-tracking"); +} + +autoframing_factory::~autoframing_factory() {} + +const char* autoframing_factory::get_name() +{ + return D_TRANSLATE(ST_I18N); +} + +void autoframing_factory::get_defaults2(obs_data_t* data) +{ + // Tracking + obs_data_set_default_int(data, ST_KEY_TRACKING_MODE, static_cast(tracking_mode::SOLO)); + obs_data_set_default_string(data, ST_KEY_TRACKING_FREQUENCY, "20 Hz"); + + // Motion + obs_data_set_default_double(data, ST_KEY_MOTION_SMOOTHING, 33.333); + obs_data_set_default_double(data, ST_KEY_MOTION_PREDICTION, 200.0); + + // Framing + obs_data_set_default_double(data, ST_KEY_FRAMING_STABILITY, 10.0); + obs_data_set_default_string(data, ST_KEY_FRAMING_PADDING ".X", "33.333 %"); + obs_data_set_default_string(data, ST_KEY_FRAMING_PADDING ".Y", "33.333 %"); + obs_data_set_default_string(data, ST_KEY_FRAMING_OFFSET ".X", " 0.00 %"); + obs_data_set_default_string(data, ST_KEY_FRAMING_OFFSET ".Y", "-7.50 %"); + obs_data_set_default_string(data, ST_KEY_FRAMING_ASPECTRATIO, ""); + + // Advanced + obs_data_set_default_int(data, ST_KEY_ADVANCED_PROVIDER, static_cast(tracking_provider::AUTOMATIC)); + obs_data_set_default_bool(data, "Debug", false); +} + +static bool modified_provider(obs_properties_t* props, obs_property_t*, obs_data_t* settings) noexcept +{ + try { + return true; + } catch (const std::exception& ex) { + DLOG_ERROR("Unexpected exception in function '%s': %s.", __FUNCTION_NAME__, ex.what()); + return false; + } catch (...) { + DLOG_ERROR("Unexpected exception in function '%s'.", __FUNCTION_NAME__); + return false; + } +} + +obs_properties_t* autoframing_factory::get_properties2(autoframing_instance* data) +{ + obs_properties_t* pr = obs_properties_create(); + + { + obs_properties_add_button2(pr, S_MANUAL_OPEN, D_TRANSLATE(S_MANUAL_OPEN), autoframing_factory::on_manual_open, nullptr); + } + + { + auto grp = obs_properties_create(); + obs_properties_add_group(pr, ST_I18N_TRACKING, D_TRANSLATE(ST_I18N_TRACKING), OBS_GROUP_NORMAL, grp); + + { + auto p = obs_properties_add_list(grp, ST_KEY_TRACKING_MODE, D_TRANSLATE(ST_I18N_TRACKING_MODE), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); + obs_property_set_modified_callback(p, modified_provider); + obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_FRAMING_MODE_SOLO), static_cast(tracking_mode::SOLO)); + obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_FRAMING_MODE_GROUP), static_cast(tracking_mode::GROUP)); + } + + { + auto p = obs_properties_add_text(grp, ST_KEY_TRACKING_FREQUENCY, D_TRANSLATE(ST_I18N_TRACKING_FREQUENCY), OBS_TEXT_DEFAULT); + } + } + + { + auto grp = obs_properties_create(); + obs_properties_add_group(pr, ST_I18N_MOTION, D_TRANSLATE(ST_I18N_MOTION), OBS_GROUP_NORMAL, grp); + + { + auto p = obs_properties_add_float_slider(grp, ST_KEY_MOTION_SMOOTHING, D_TRANSLATE(ST_I18N_MOTION_SMOOTHING), 0.0, 100.0, 0.01); + obs_property_float_set_suffix(p, " %"); + } + + { + auto p = obs_properties_add_float_slider(grp, ST_KEY_MOTION_PREDICTION, D_TRANSLATE(ST_I18N_MOTION_PREDICTION), 0.0, 500.0, 0.01); + obs_property_float_set_suffix(p, " %"); + } + } + + { + auto grp = obs_properties_create(); + obs_properties_add_group(pr, ST_I18N_FRAMING, D_TRANSLATE(ST_I18N_FRAMING), OBS_GROUP_NORMAL, grp); + + { + auto p = obs_properties_add_float_slider(grp, ST_KEY_FRAMING_STABILITY, D_TRANSLATE(ST_I18N_FRAMING_STABILITY), 0.0, 100.0, 0.01); + obs_property_float_set_suffix(p, " %"); + } + + { + auto grp2_ = obs_properties_create(); + obs_properties_add_group(grp, ST_KEY_FRAMING_PADDING, D_TRANSLATE(ST_I18N_FRAMING_PADDING), OBS_GROUP_NORMAL, grp2_); + + { + auto p = obs_properties_add_text(grp2_, ST_KEY_FRAMING_PADDING ".X", "X", OBS_TEXT_DEFAULT); + } + { + auto p = obs_properties_add_text(grp2_, ST_KEY_FRAMING_PADDING ".Y", "Y", OBS_TEXT_DEFAULT); + } + } + + { + auto grp2_ = obs_properties_create(); + obs_properties_add_group(grp, ST_KEY_FRAMING_OFFSET, D_TRANSLATE(ST_I18N_FRAMING_OFFSET), OBS_GROUP_NORMAL, grp2_); + + { + auto p = obs_properties_add_text(grp2_, ST_KEY_FRAMING_OFFSET ".X", "X", OBS_TEXT_DEFAULT); + } + { + auto p = obs_properties_add_text(grp2_, ST_KEY_FRAMING_OFFSET ".Y", "Y", OBS_TEXT_DEFAULT); + } + } + + { + auto p = obs_properties_add_list(grp, ST_KEY_FRAMING_ASPECTRATIO, D_TRANSLATE(ST_I18N_FRAMING_ASPECTRATIO), OBS_COMBO_TYPE_EDITABLE, OBS_COMBO_FORMAT_STRING); + obs_property_list_add_string(p, "None", ""); + obs_property_list_add_string(p, "1:1", "1:1"); + + obs_property_list_add_string(p, "3:2", "3:2"); + obs_property_list_add_string(p, "2:3", "2:3"); + + obs_property_list_add_string(p, "4:3", "4:3"); + obs_property_list_add_string(p, "3:4", "3:4"); + + obs_property_list_add_string(p, "5:4", "5:4"); + obs_property_list_add_string(p, "4:5", "4:5"); + + obs_property_list_add_string(p, "16:9", "16:9"); + obs_property_list_add_string(p, "9:16", "9:16"); + + obs_property_list_add_string(p, "16:10", "16:10"); + obs_property_list_add_string(p, "10:16", "10:16"); + + obs_property_list_add_string(p, "21:9", "21:9"); + obs_property_list_add_string(p, "9:21", "9:21"); + + obs_property_list_add_string(p, "21:10", "21:10"); + obs_property_list_add_string(p, "10:21", "10:21"); + + obs_property_list_add_string(p, "32:9", "32:9"); + obs_property_list_add_string(p, "9:32", "9:32"); + + obs_property_list_add_string(p, "32:10", "32:10"); + obs_property_list_add_string(p, "10:32", "10:32"); + } + } + + if (data) { + data->properties(pr); + } + + { // Advanced Settings + auto grp = obs_properties_create(); + obs_properties_add_group(pr, S_ADVANCED, D_TRANSLATE(S_ADVANCED), OBS_GROUP_NORMAL, grp); + + { + auto p = obs_properties_add_list(grp, ST_KEY_ADVANCED_PROVIDER, D_TRANSLATE(ST_I18N_ADVANCED_PROVIDER), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); + obs_property_set_modified_callback(p, modified_provider); + obs_property_list_add_int(p, D_TRANSLATE(S_STATE_AUTOMATIC), static_cast(tracking_provider::AUTOMATIC)); +#ifdef ENABLE_NVIDIA + obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_ADVANCED_PROVIDER_NVIDIA_FACEDETECTION), static_cast(tracking_provider::NVIDIA_FACEDETECTION)); +#endif + } + + obs_properties_add_bool(grp, "Debug", "Debug"); + } + + return pr; +} + +bool streamfx::filter::autoframing::autoframing_factory::on_manual_open(obs_properties_t* props, obs_property_t* property, void* data) +{ + streamfx::open_url(HELP_URL); + return false; +} + +bool streamfx::filter::autoframing::autoframing_factory::is_provider_available(tracking_provider provider) +{ + switch (provider) { +#ifdef ENABLE_NVIDIA + case tracking_provider::NVIDIA_FACEDETECTION: + return _nvidia_available; +#endif + default: + return false; + } +} + +tracking_provider streamfx::filter::autoframing::autoframing_factory::find_ideal_provider() +{ + for (auto v : provider_priority) { + if (is_provider_available(v)) { + return v; + break; + } + } + return tracking_provider::INVALID; +} + +std::shared_ptr autoframing_factory::instance() +{ + static std::weak_ptr winst; + static std::mutex mtx; + + std::unique_lock lock(mtx); + auto instance = winst.lock(); + if (!instance) { + instance = std::shared_ptr(new autoframing_factory()); + winst = instance; + } + return instance; +} + +static std::shared_ptr loader_instance; + +static auto loader = streamfx::component( + "autoframing", + []() { // Initializer + loader_instance = autoframing_factory::instance(); + }, + []() { // Finalizer + loader_instance.reset(); + }, + {}); diff --git a/components/autoframing/source/filters/filter-autoframing.hpp b/components/autoframing/source/filters/filter-autoframing.hpp new file mode 100644 index 0000000..3dc8435 --- /dev/null +++ b/components/autoframing/source/filters/filter-autoframing.hpp @@ -0,0 +1,174 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2021-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "gfx/gfx-util.hpp" +#include "obs/gs/gs-texrender.hpp" +#include "obs/gs/gs-texture.hpp" +#include "obs/gs/gs-vertexbuffer.hpp" +#include "obs/obs-source-factory.hpp" +#include "plugin.hpp" +#include "util/util-threadpool.hpp" +#include "util/utility.hpp" + +#include "warning-disable.hpp" +#include +#include +#include +#include +#include "warning-enable.hpp" + +#ifdef ENABLE_NVIDIA +#include "nvidia/ar/nvidia-ar-facedetection.hpp" +#endif + +namespace streamfx::filter::autoframing { + + enum class tracking_mode : int64_t { + SOLO = 0, + GROUP = 1, + }; + + enum class tracking_provider : int64_t { + INVALID = -1, + AUTOMATIC = 0, + NVIDIA_FACEDETECTION = 1, + }; + + const char* cstring(tracking_provider provider); + + std::string string(tracking_provider provider); + + class autoframing_instance : public obs::source_instance { + struct track_el { + float age; + vec2 pos; + vec2 size; + vec2 vel; + }; + + struct pred_el { + // Motion-Predicted Position + vec2 mp_pos; + + // Filtered Position + streamfx::util::math::kalman1D filter_pos_x; + streamfx::util::math::kalman1D filter_pos_y; + + // Offset Filtered Position + vec2 offset_pos; + + // Padded Area + vec2 pad_size; + + // Aspect-Ratio-Corrected Padded Area + vec2 aspected_size; + }; + + bool _dirty; + std::pair _size; + std::pair _out_size; + + std::shared_ptr<::streamfx::gfx::util> _gfx_debug; + std::shared_ptr<::streamfx::obs::gs::effect> _standard_effect; + std::shared_ptr<::streamfx::obs::gs::texrender> _input; + std::shared_ptr<::streamfx::obs::gs::vertexbuffer> _vb; + + tracking_provider _provider; + tracking_provider _provider_ui; + std::atomic _provider_ready; + std::mutex _provider_lock; + std::shared_ptr _provider_task; + +#ifdef ENABLE_NVIDIA + std::shared_ptr<::streamfx::nvidia::ar::facedetection> _nvidia_fx; +#endif + + tracking_mode _track_mode; + float _track_frequency; + + float _motion_smoothing; + float _motion_smoothing_kalman_pnc; + float _motion_smoothing_kalman_mnc; + float _motion_prediction; + + float _frame_stability; + float _frame_stability_kalman; + bool _frame_padding_prc[2]; + vec2 _frame_padding; + bool _frame_offset_prc[2]; + vec2 _frame_offset; + float _frame_aspect_ratio; + + float _track_frequency_counter; + std::list> _tracked_elements; + std::map, std::shared_ptr> _predicted_elements; + + streamfx::util::math::kalman1D _frame_pos_x; + streamfx::util::math::kalman1D _frame_pos_y; + vec2 _frame_pos; + streamfx::util::math::kalman1D _frame_size_x; + streamfx::util::math::kalman1D _frame_size_y; + vec2 _frame_size; + + bool _debug; + + public: + ~autoframing_instance(); + autoframing_instance(obs_data_t* settings, obs_source_t* self); + + void load(obs_data_t* data) override; + void migrate(obs_data_t* data, uint64_t version) override; + void update(obs_data_t* data) override; + void properties(obs_properties_t* properties); + + uint32_t get_width() override; + uint32_t get_height() override; + + virtual void video_tick(float seconds) override; + virtual void video_render(gs_effect_t* effect) override; + + private: + void tracking_tick(float seconds); + + void switch_provider(tracking_provider provider); + void task_switch_provider(util::threadpool::task_data_t data); + +#ifdef ENABLE_NVIDIA + void nvar_facedetection_load(); + void nvar_facedetection_unload(); + void nvar_facedetection_process(); + void nvar_facedetection_properties(obs_properties_t* props); + void nvar_facedetection_update(obs_data_t* data); +#endif + }; + + class autoframing_factory : public obs::source_factory { +#ifdef ENABLE_NVIDIA + bool _nvidia_available; + std::shared_ptr<::streamfx::nvidia::cuda::obs> _nvcuda; + std::shared_ptr<::streamfx::nvidia::cv::cv> _nvcvi; + std::shared_ptr<::streamfx::nvidia::ar::ar> _nvar; +#endif + + public: + autoframing_factory(); + ~autoframing_factory() override; + + const char* get_name() override; + + void get_defaults2(obs_data_t* data) override; + obs_properties_t* get_properties2(autoframing_instance* data) override; + + static bool on_manual_open(obs_properties_t* props, obs_property_t* property, void* data); + + bool is_provider_available(tracking_provider); + tracking_provider find_ideal_provider(); + + public: // Singleton + static void initialize(); + static void finalize(); + static std::shared_ptr instance(); + }; +} // namespace streamfx::filter::autoframing diff --git a/components/blur/CMakeLists.txt b/components/blur/CMakeLists.txt new file mode 100644 index 0000000..849a281 --- /dev/null +++ b/components/blur/CMakeLists.txt @@ -0,0 +1,9 @@ +# AUTOGENERATED COPYRIGHT HEADER START +# Copyright (C) 2023 Michael Fabian 'Xaymar' Dirks +# AUTOGENERATED COPYRIGHT HEADER END + +cmake_minimum_required(VERSION 3.26) +project("Blur") +list(APPEND CMAKE_MESSAGE_INDENT "[${PROJECT_NAME}] ") + +streamfx_add_component("Blur") diff --git a/components/blur/source/filter/filter-blur.cpp b/components/blur/source/filter/filter-blur.cpp new file mode 100644 index 0000000..eb332f1 --- /dev/null +++ b/components/blur/source/filter/filter-blur.cpp @@ -0,0 +1,879 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2019 Cat Stevens +// AUTOGENERATED COPYRIGHT HEADER END + +#include "filter-blur.hpp" +#include "strings.hpp" +#include "gfx/blur/gfx-blur-box-linear.hpp" +#include "gfx/blur/gfx-blur-box.hpp" +#include "gfx/blur/gfx-blur-dual-filtering.hpp" +#include "gfx/blur/gfx-blur-gaussian-linear.hpp" +#include "gfx/blur/gfx-blur-gaussian.hpp" +#include "obs/gs/gs-helper.hpp" +#include "obs/obs-source-tracker.hpp" +#include "util/util-logging.hpp" + +#include "warning-disable.hpp" +#include +#include +#include +#include +#include +#include "warning-enable.hpp" + +// OBS +#include "warning-disable.hpp" +#include +#include +#include +#include +#include "warning-enable.hpp" + +#ifdef _DEBUG +#define ST_PREFIX "<%s> " +#define D_LOG_ERROR(x, ...) P_LOG_ERROR(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_WARNING(x, ...) P_LOG_WARN(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_INFO(x, ...) P_LOG_INFO(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_DEBUG(x, ...) P_LOG_DEBUG(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#else +#define ST_PREFIX " " +#define D_LOG_ERROR(...) P_LOG_ERROR(ST_PREFIX __VA_ARGS__) +#define D_LOG_WARNING(...) P_LOG_WARN(ST_PREFIX __VA_ARGS__) +#define D_LOG_INFO(...) P_LOG_INFO(ST_PREFIX __VA_ARGS__) +#define D_LOG_DEBUG(...) P_LOG_DEBUG(ST_PREFIX __VA_ARGS__) +#endif + +// Translation Strings +#define ST_I18N "Filter.Blur" + +#define ST_I18N_TYPE "Filter.Blur.Type" +#define ST_KEY_TYPE "Filter.Blur.Type" +#define ST_I18N_SUBTYPE "Filter.Blur.SubType" +#define ST_KEY_SUBTYPE "Filter.Blur.SubType" +#define ST_I18N_SIZE "Filter.Blur.Size" +#define ST_KEY_SIZE "Filter.Blur.Size" +#define ST_I18N_ANGLE "Filter.Blur.Angle" +#define ST_KEY_ANGLE "Filter.Blur.Angle" +#define ST_CENTER "Filter.Blur.Center" +#define ST_I18N_CENTER_X "Filter.Blur.Center.X" +#define ST_KEY_CENTER_X "Filter.Blur.Center.X" +#define ST_I18N_CENTER_Y "Filter.Blur.Center.Y" +#define ST_KEY_CENTER_Y "Filter.Blur.Center.Y" +#define ST_I18N_STEPSCALE "Filter.Blur.StepScale" +#define ST_KEY_STEPSCALE "Filter.Blur.StepScale" +#define ST_I18N_STEPSCALE_X "Filter.Blur.StepScale.X" +#define ST_KEY_STEPSCALE_X "Filter.Blur.StepScale.X" +#define ST_I18N_STEPSCALE_Y "Filter.Blur.StepScale.Y" +#define ST_KEY_STEPSCALE_Y "Filter.Blur.StepScale.Y" +#define ST_I18N_MASK "Filter.Blur.Mask" +#define ST_KEY_MASK "Filter.Blur.Mask" +#define ST_I18N_MASK_TYPE "Filter.Blur.Mask.Type" +#define ST_KEY_MASK_TYPE "Filter.Blur.Mask.Type" +#define ST_I18N_MASK_TYPE_REGION "Filter.Blur.Mask.Type.Region" +#define ST_I18N_MASK_TYPE_IMAGE "Filter.Blur.Mask.Type.Image" +#define ST_I18N_MASK_TYPE_SOURCE "Filter.Blur.Mask.Type.Source" +#define ST_I18N_MASK_REGION_LEFT "Filter.Blur.Mask.Region.Left" +#define ST_KEY_MASK_REGION_LEFT "Filter.Blur.Mask.Region.Left" +#define ST_I18N_MASK_REGION_RIGHT "Filter.Blur.Mask.Region.Right" +#define ST_KEY_MASK_REGION_RIGHT "Filter.Blur.Mask.Region.Right" +#define ST_I18N_MASK_REGION_TOP "Filter.Blur.Mask.Region.Top" +#define ST_KEY_MASK_REGION_TOP "Filter.Blur.Mask.Region.Top" +#define ST_I18N_MASK_REGION_BOTTOM "Filter.Blur.Mask.Region.Bottom" +#define ST_KEY_MASK_REGION_BOTTOM "Filter.Blur.Mask.Region.Bottom" +#define ST_I18N_MASK_REGION_FEATHER "Filter.Blur.Mask.Region.Feather" +#define ST_KEY_MASK_REGION_FEATHER "Filter.Blur.Mask.Region.Feather" +#define ST_I18N_MASK_REGION_FEATHER_SHIFT "Filter.Blur.Mask.Region.Feather.Shift" +#define ST_KEY_MASK_REGION_FEATHER_SHIFT "Filter.Blur.Mask.Region.Feather.Shift" +#define ST_I18N_MASK_REGION_INVERT "Filter.Blur.Mask.Region.Invert" +#define ST_KEY_MASK_REGION_INVERT "Filter.Blur.Mask.Region.Invert" +#define ST_I18N_MASK_IMAGE "Filter.Blur.Mask.Image" +#define ST_KEY_MASK_IMAGE "Filter.Blur.Mask.Image" +#define ST_I18N_MASK_SOURCE "Filter.Blur.Mask.Source" +#define ST_KEY_MASK_SOURCE "Filter.Blur.Mask.Source" +#define ST_I18N_MASK_COLOR "Filter.Blur.Mask.Color" +#define ST_KEY_MASK_COLOR "Filter.Blur.Mask.Color" +#define ST_I18N_MASK_ALPHA "Filter.Blur.Mask.Alpha" +#define ST_KEY_MASK_ALPHA "Filter.Blur.Mask.Alpha" +#define ST_I18N_MASK_MULTIPLIER "Filter.Blur.Mask.Multiplier" +#define ST_KEY_MASK_MULTIPLIER "Filter.Blur.Mask.Multiplier" + +using namespace streamfx::filter::blur; + +static constexpr std::string_view HELP_URL = "https://github.com/Xaymar/obs-StreamFX/wiki/Filter-Blur"; + +struct local_blur_type_t { + std::function<::streamfx::gfx::blur::ifactory&()> fn; + const char* name; +}; +struct local_blur_subtype_t { + ::streamfx::gfx::blur::type type; + const char* name; +}; + +static std::map list_of_types = { + {"box", {&::streamfx::gfx::blur::box_factory::get, S_BLUR_TYPE_BOX}}, {"box_linear", {&::streamfx::gfx::blur::box_linear_factory::get, S_BLUR_TYPE_BOX_LINEAR}}, {"gaussian", {&::streamfx::gfx::blur::gaussian_factory::get, S_BLUR_TYPE_GAUSSIAN}}, {"gaussian_linear", {&::streamfx::gfx::blur::gaussian_linear_factory::get, S_BLUR_TYPE_GAUSSIAN_LINEAR}}, {"dual_filtering", {&::streamfx::gfx::blur::dual_filtering_factory::get, S_BLUR_TYPE_DUALFILTERING}}, +}; +static std::map list_of_subtypes = { + {"area", {::streamfx::gfx::blur::type::Area, S_BLUR_SUBTYPE_AREA}}, + {"directional", {::streamfx::gfx::blur::type::Directional, S_BLUR_SUBTYPE_DIRECTIONAL}}, + {"rotational", {::streamfx::gfx::blur::type::Rotational, S_BLUR_SUBTYPE_ROTATIONAL}}, + {"zoom", {::streamfx::gfx::blur::type::Zoom, S_BLUR_SUBTYPE_ZOOM}}, +}; + +blur_instance::blur_instance(obs_data_t* settings, obs_source_t* self) : obs::source_instance(settings, self), _gfx_util(::streamfx::gfx::util::get()), _source_rendered(false), _output_rendered(false) +{ + { + auto gctx = streamfx::obs::gs::context(); + + // Create RenderTargets + this->_source_rt = std::make_shared(GS_RGBA, GS_ZS_NONE); + this->_output_rt = std::make_shared(GS_RGBA, GS_ZS_NONE); + + // Load Effects + { + auto file = streamfx::data_file_path("effects/mask.effect"); + try { + _effect_mask = streamfx::obs::gs::effect::create(file); + } catch (std::exception& ex) { + DLOG_ERROR("Error loading '%s': %s", file.generic_u8string().c_str(), ex.what()); + } + } + } + + update(settings); +} + +blur_instance::~blur_instance() {} + +bool blur_instance::apply_mask_parameters(streamfx::obs::gs::effect effect, gs_texture_t* original_texture, gs_texture_t* blurred_texture) +{ + if (effect.has_parameter("image_orig")) { + effect.get_parameter("image_orig").set_texture(original_texture); + } + if (effect.has_parameter("image_blur")) { + effect.get_parameter("image_blur").set_texture(blurred_texture); + } + + // Region + if (_mask.type == mask_type::Region) { + if (effect.has_parameter("mask_region_left")) { + effect.get_parameter("mask_region_left").set_float(_mask.region.left); + } + if (effect.has_parameter("mask_region_right")) { + effect.get_parameter("mask_region_right").set_float(_mask.region.right); + } + if (effect.has_parameter("mask_region_top")) { + effect.get_parameter("mask_region_top").set_float(_mask.region.top); + } + if (effect.has_parameter("mask_region_bottom")) { + effect.get_parameter("mask_region_bottom").set_float(_mask.region.bottom); + } + if (effect.has_parameter("mask_region_feather")) { + effect.get_parameter("mask_region_feather").set_float(_mask.region.feather); + } + if (effect.has_parameter("mask_region_feather_shift")) { + effect.get_parameter("mask_region_feather_shift").set_float(_mask.region.feather_shift); + } + } + + // Image + if (_mask.type == mask_type::Image) { + if (effect.has_parameter("mask_image")) { + if (_mask.image.texture) { + effect.get_parameter("mask_image").set_texture(_mask.image.texture); + } else { + effect.get_parameter("mask_image").set_texture(nullptr); + } + } + } + + // Source + if (_mask.type == mask_type::Source) { + if (effect.has_parameter("mask_image")) { + if (_mask.source.texture) { + effect.get_parameter("mask_image").set_texture(_mask.source.texture); + } else { + effect.get_parameter("mask_image").set_texture(nullptr); + } + } + } + + // Shared + if (effect.has_parameter("mask_color")) { + effect.get_parameter("mask_color").set_float4(_mask.color.r, _mask.color.g, _mask.color.b, _mask.color.a); + } + if (effect.has_parameter("mask_multiplier")) { + effect.get_parameter("mask_multiplier").set_float(_mask.multiplier); + } + + return true; +} + +void blur_instance::load(obs_data_t* settings) +{ + update(settings); +} + +void blur_instance::migrate(obs_data_t* settings, uint64_t version) +{ + // Now we use a fall-through switch to gradually upgrade each known version change. + switch (version) { + case 0: + /// Blur Type + int64_t old_blur = obs_data_get_int(settings, "Filter.Blur.Type"); + if (old_blur == 0) { // Box + obs_data_set_string(settings, ST_KEY_TYPE, "box"); + } else if (old_blur == 1) { // Gaussian + obs_data_set_string(settings, ST_KEY_TYPE, "gaussian"); + } else if (old_blur == 2) { // Bilateral, no longer included. + obs_data_set_string(settings, ST_KEY_TYPE, "box"); + } else if (old_blur == 3) { // Box Linear + obs_data_set_string(settings, ST_KEY_TYPE, "box_linear"); + } else if (old_blur == 4) { // Gaussian Linear + obs_data_set_string(settings, ST_KEY_TYPE, "gaussian_linear"); + } else { + obs_data_set_string(settings, ST_KEY_TYPE, "box"); + } + obs_data_unset_user_value(settings, "Filter.Blur.Type"); + + /// Directional Blur + bool directional = obs_data_get_bool(settings, "Filter.Blur.Directional"); + if (directional) { + obs_data_set_string(settings, ST_KEY_SUBTYPE, "directional"); + } else { + obs_data_set_string(settings, ST_KEY_SUBTYPE, "area"); + } + obs_data_unset_user_value(settings, "Filter.Blur.Directional"); + + /// Directional Blur Angle + double_t angle = obs_data_get_double(settings, "Filter.Blur.Directional.Angle"); + obs_data_set_double(settings, ST_KEY_ANGLE, angle); + obs_data_unset_user_value(settings, "Filter.Blur.Directional.Angle"); + } +} + +void blur_instance::update(obs_data_t* settings) +{ + { // Blur Type + const char* blur_type = obs_data_get_string(settings, ST_KEY_TYPE); + const char* blur_subtype = obs_data_get_string(settings, ST_KEY_SUBTYPE); + const char* last_blur_type = obs_data_get_string(settings, ST_KEY_TYPE ".last"); + + auto type_found = list_of_types.find(blur_type); + if (type_found != list_of_types.end()) { + auto subtype_found = list_of_subtypes.find(blur_subtype); + if (subtype_found != list_of_subtypes.end()) { + if ((strcmp(last_blur_type, blur_type) != 0) || (_blur->get_type() != subtype_found->second.type)) { + if (type_found->second.fn().is_type_supported(subtype_found->second.type)) { + _blur = type_found->second.fn().create(subtype_found->second.type); + } + } + } + } + } + + { // Blur Parameters + this->_blur_size = obs_data_get_double(settings, ST_KEY_SIZE); + this->_blur_angle = obs_data_get_double(settings, ST_KEY_ANGLE); + this->_blur_center.first = obs_data_get_double(settings, ST_KEY_CENTER_X) / 100.0; + this->_blur_center.second = obs_data_get_double(settings, ST_KEY_CENTER_Y) / 100.0; + + // Scaling + this->_blur_step_scaling = obs_data_get_bool(settings, ST_KEY_STEPSCALE); + this->_blur_step_scale.first = obs_data_get_double(settings, ST_KEY_STEPSCALE_X) / 100.0; + this->_blur_step_scale.second = obs_data_get_double(settings, ST_KEY_STEPSCALE_Y) / 100.0; + } + + { // Masking + _mask.enabled = obs_data_get_bool(settings, ST_KEY_MASK); + if (_mask.enabled) { + _mask.type = static_cast(obs_data_get_int(settings, ST_KEY_MASK_TYPE)); + switch (_mask.type) { + case mask_type::Region: + _mask.region.left = float(obs_data_get_double(settings, ST_KEY_MASK_REGION_LEFT) / 100.0); + _mask.region.top = float(obs_data_get_double(settings, ST_KEY_MASK_REGION_TOP) / 100.0); + _mask.region.right = 1.0f - float(obs_data_get_double(settings, ST_KEY_MASK_REGION_RIGHT) / 100.0); + _mask.region.bottom = 1.0f - float(obs_data_get_double(settings, ST_KEY_MASK_REGION_BOTTOM) / 100.0); + _mask.region.feather = float(obs_data_get_double(settings, ST_KEY_MASK_REGION_FEATHER) / 100.0); + _mask.region.feather_shift = float(obs_data_get_double(settings, ST_KEY_MASK_REGION_FEATHER_SHIFT) / 100.0); + _mask.region.invert = obs_data_get_bool(settings, ST_KEY_MASK_REGION_INVERT); + break; + case mask_type::Image: + _mask.image.path = obs_data_get_string(settings, ST_KEY_MASK_IMAGE); + break; + case mask_type::Source: + _mask.source.name = obs_data_get_string(settings, ST_KEY_MASK_SOURCE); + break; + } + if ((_mask.type == mask_type::Image) || (_mask.type == mask_type::Source)) { + uint32_t color = static_cast(obs_data_get_int(settings, ST_KEY_MASK_COLOR)); + _mask.color.r = static_cast((color >> 0) & 0xFF) / 255.0f; + _mask.color.g = static_cast((color >> 8) & 0xFF) / 255.0f; + _mask.color.b = static_cast((color >> 16) & 0xFF) / 255.0f; + _mask.color.a = static_cast(obs_data_get_double(settings, ST_KEY_MASK_ALPHA)); + _mask.multiplier = float(obs_data_get_double(settings, ST_KEY_MASK_MULTIPLIER)); + } + } + } +} + +void blur_instance::video_tick(float) +{ + // Blur + if (_blur) { + _blur->set_size(_blur_size); + if (_blur_step_scaling) { + _blur->set_step_scale(_blur_step_scale.first, _blur_step_scale.second); + } else { + _blur->set_step_scale(1.0, 1.0); + } + if ((_blur->get_type() == ::streamfx::gfx::blur::type::Directional) || (_blur->get_type() == ::streamfx::gfx::blur::type::Rotational)) { + auto obj = std::dynamic_pointer_cast<::streamfx::gfx::blur::base_angle>(_blur); + obj->set_angle(_blur_angle); + } + if ((_blur->get_type() == ::streamfx::gfx::blur::type::Zoom) || (_blur->get_type() == ::streamfx::gfx::blur::type::Rotational)) { + auto obj = std::dynamic_pointer_cast<::streamfx::gfx::blur::base_center>(_blur); + obj->set_center(_blur_center.first, _blur_center.second); + } + } + + // Load Mask + if (_mask.type == mask_type::Image) { + if (_mask.image.path_old != _mask.image.path) { + try { + _mask.image.texture = std::make_shared(_mask.image.path); + _mask.image.path_old = _mask.image.path; + } catch (...) { + DLOG_ERROR(" Instance '%s' failed to load image '%s'.", obs_source_get_name(_self), _mask.image.path.c_str()); + } + } + } else if (_mask.type == mask_type::Source) { + if (_mask.source.name_old != _mask.source.name) { + try { + _mask.source.source_texture = std::make_shared(::streamfx::obs::source{_mask.source.name}, ::streamfx::obs::source{_self, false}); + _mask.source.is_scene = (obs_scene_from_source(_mask.source.source_texture->get_object()) != nullptr); + _mask.source.name_old = _mask.source.name; + } catch (...) { + DLOG_ERROR(" Instance '%s' failed to grab source '%s'.", obs_source_get_name(_self), _mask.source.name.c_str()); + } + } + } + + _source_rendered = false; + _output_rendered = false; +} + +void blur_instance::video_render(gs_effect_t* effect) +{ + obs_source_t* parent = obs_filter_get_parent(this->_self); + obs_source_t* target = obs_filter_get_target(this->_self); + gs_effect_t* defaultEffect = obs_get_base_effect(obs_base_effect::OBS_EFFECT_DEFAULT); + uint32_t baseW = obs_source_get_base_width(target); + uint32_t baseH = obs_source_get_base_height(target); + + // Verify that we can actually run first. + if (!target || !parent || !this->_self || !this->_blur || (baseW == 0) || (baseH == 0)) { + obs_source_skip_video_filter(this->_self); + return; + } + +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + streamfx::obs::gs::debug_marker gdmp{streamfx::obs::gs::debug_color_source, "Blur '%s'", obs_source_get_name(_self)}; +#endif + + if (!_source_rendered) { + // Source To Texture + { +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + streamfx::obs::gs::debug_marker gdm{streamfx::obs::gs::debug_color_cache, "Cache"}; +#endif + + if (obs_source_process_filter_begin(this->_self, GS_RGBA, OBS_ALLOW_DIRECT_RENDERING)) { + { + auto op = this->_source_rt->render(baseW, baseH); + + gs_blend_state_push(); + gs_reset_blend_state(); + gs_enable_blending(false); + gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO); + + gs_set_cull_mode(GS_NEITHER); + gs_enable_color(true, true, true, true); + + gs_enable_depth_test(false); + gs_depth_function(GS_ALWAYS); + + gs_enable_stencil_test(false); + gs_enable_stencil_write(false); + gs_stencil_function(GS_STENCIL_BOTH, GS_ALWAYS); + gs_stencil_op(GS_STENCIL_BOTH, GS_KEEP, GS_KEEP, GS_KEEP); + + // Orthographic Camera and clear RenderTarget. + gs_ortho(0, static_cast(baseW), 0, static_cast(baseH), -1., 1.); + //gs_clear(GS_CLEAR_COLOR | GS_CLEAR_DEPTH, &black, 0, 0); + + // Render + obs_source_process_filter_end(this->_self, defaultEffect, baseW, baseH); + + gs_blend_state_pop(); + } + + _source_texture = this->_source_rt->get_texture(); + if (!_source_texture) { + obs_source_skip_video_filter(this->_self); + return; + } + } else { + obs_source_skip_video_filter(this->_self); + return; + } + } + + _source_rendered = true; + } + + if (!_output_rendered) { + { +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + streamfx::obs::gs::debug_marker gdm{streamfx::obs::gs::debug_color_convert, "Blur"}; +#endif + + _blur->set_input(_source_texture); + _output_texture = _blur->render(); + } + + // Mask + if (_mask.enabled) { +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + streamfx::obs::gs::debug_marker gdm{streamfx::obs::gs::debug_color_convert, "Mask"}; +#endif + + gs_blend_state_push(); + gs_reset_blend_state(); + gs_enable_color(true, true, true, true); + gs_enable_blending(false); + gs_enable_depth_test(false); + gs_enable_stencil_test(false); + gs_enable_stencil_write(false); + gs_set_cull_mode(GS_NEITHER); + gs_depth_function(GS_ALWAYS); + gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO); + gs_stencil_function(GS_STENCIL_BOTH, GS_ALWAYS); + gs_stencil_op(GS_STENCIL_BOTH, GS_ZERO, GS_ZERO, GS_ZERO); + + std::string technique = ""; + switch (this->_mask.type) { + case mask_type::Region: + if (this->_mask.region.feather > std::numeric_limits::epsilon()) { + if (this->_mask.region.invert) { + technique = "RegionFeatherInverted"; + } else { + technique = "RegionFeather"; + } + } else { + if (this->_mask.region.invert) { + technique = "RegionInverted"; + } else { + technique = "Region"; + } + } + break; + case mask_type::Image: + case mask_type::Source: + technique = "Image"; + break; + } + + if (_mask.source.source_texture) { + uint32_t source_width = obs_source_get_width(this->_mask.source.source_texture->get_object()); + uint32_t source_height = obs_source_get_height(this->_mask.source.source_texture->get_object()); + + if (source_width == 0) { + source_width = baseW; + } + if (source_height == 0) { + source_height = baseH; + } + if (this->_mask.source.is_scene) { + obs_video_info ovi; + if (obs_get_video_info(&ovi)) { + source_width = ovi.base_width; + source_height = ovi.base_height; + } + } + +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + streamfx::obs::gs::debug_marker gdm{streamfx::obs::gs::debug_color_capture, "Capture '%s'", obs_source_get_name(_mask.source.source_texture->get_object())}; +#endif + + this->_mask.source.texture = this->_mask.source.source_texture->render(source_width, source_height); + } + + apply_mask_parameters(_effect_mask, _source_texture->get_object(), _output_texture->get_object()); + + try { + auto op = this->_output_rt->render(baseW, baseH); + gs_ortho(0, 1, 0, 1, -1, 1); + + // Render + while (gs_effect_loop(_effect_mask.get_object(), technique.c_str())) { + _gfx_util->draw_fullscreen_triangle(); + } + } catch (const std::exception&) { + gs_blend_state_pop(); + obs_source_skip_video_filter(this->_self); + return; + } + gs_blend_state_pop(); + + if (!(_output_texture = this->_output_rt->get_texture())) { + obs_source_skip_video_filter(this->_self); + return; + } + } + + _output_rendered = true; + } + + // Draw source + { +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + streamfx::obs::gs::debug_marker gdm{streamfx::obs::gs::debug_color_render, "Render"}; +#endif + + // It is important that we do not modify the blend state here, as it is set correctly by OBS + gs_set_cull_mode(GS_NEITHER); + gs_enable_color(true, true, true, true); + gs_enable_depth_test(false); + gs_depth_function(GS_ALWAYS); + gs_enable_stencil_test(false); + gs_enable_stencil_write(false); + gs_stencil_function(GS_STENCIL_BOTH, GS_ALWAYS); + gs_stencil_op(GS_STENCIL_BOTH, GS_ZERO, GS_ZERO, GS_ZERO); + + gs_effect_t* finalEffect = effect ? effect : defaultEffect; + const char* technique = "Draw"; + + gs_eparam_t* param = gs_effect_get_param_by_name(finalEffect, "image"); + if (!param) { + DLOG_ERROR(" Failed to set image param.", obs_source_get_name(this->_self)); + obs_source_skip_video_filter(_self); + return; + } else { + gs_effect_set_texture(param, *_output_texture); + } + while (gs_effect_loop(finalEffect, technique)) { + gs_draw_sprite(*_output_texture, 0, baseW, baseH); + } + } +} + +blur_factory::blur_factory() +{ + _info.id = S_PREFIX "filter-blur"; + _info.type = OBS_SOURCE_TYPE_FILTER; + _info.output_flags = OBS_SOURCE_VIDEO; + + support_size(false); + finish_setup(); + register_proxy("obs-stream-effects-filter-blur"); +} + +blur_factory::~blur_factory() {} + +const char* blur_factory::get_name() +{ + return D_TRANSLATE(ST_I18N); +} + +void blur_factory::get_defaults2(obs_data_t* settings) +{ + // Type, Subtype + obs_data_set_default_string(settings, ST_KEY_TYPE, "box"); + obs_data_set_default_string(settings, ST_KEY_SUBTYPE, "area"); + + // Parameters + obs_data_set_default_int(settings, ST_KEY_SIZE, 5); + obs_data_set_default_double(settings, ST_KEY_ANGLE, 0.); + obs_data_set_default_double(settings, ST_KEY_CENTER_X, 50.); + obs_data_set_default_double(settings, ST_KEY_CENTER_Y, 50.); + obs_data_set_default_bool(settings, ST_KEY_STEPSCALE, false); + obs_data_set_default_double(settings, ST_KEY_STEPSCALE_X, 1.); + obs_data_set_default_double(settings, ST_KEY_STEPSCALE_Y, 1.); + + // Masking + obs_data_set_default_bool(settings, ST_KEY_MASK, false); + obs_data_set_default_int(settings, ST_KEY_MASK_TYPE, static_cast(mask_type::Region)); + obs_data_set_default_double(settings, ST_KEY_MASK_REGION_LEFT, 0.0); + obs_data_set_default_double(settings, ST_KEY_MASK_REGION_RIGHT, 0.0); + obs_data_set_default_double(settings, ST_KEY_MASK_REGION_TOP, 0.0); + obs_data_set_default_double(settings, ST_KEY_MASK_REGION_BOTTOM, 0.0); + obs_data_set_default_double(settings, ST_KEY_MASK_REGION_FEATHER, 0.0); + obs_data_set_default_double(settings, ST_KEY_MASK_REGION_FEATHER_SHIFT, 0.0); + obs_data_set_default_bool(settings, ST_KEY_MASK_REGION_INVERT, false); + obs_data_set_default_string(settings, ST_KEY_MASK_IMAGE, reinterpret_cast(streamfx::data_file_path("white.png").generic_u8string().c_str())); + obs_data_set_default_string(settings, ST_KEY_MASK_SOURCE, ""); + obs_data_set_default_int(settings, ST_KEY_MASK_COLOR, 0xFFFFFFFFull); + obs_data_set_default_double(settings, ST_KEY_MASK_MULTIPLIER, 1.0); +} + +bool modified_properties(void*, obs_properties_t* props, obs_property* prop, obs_data_t* settings) noexcept +{ + try { + obs_property_t* p; + const char* propname = obs_property_name(prop); + const char* vtype = obs_data_get_string(settings, ST_KEY_TYPE); + const char* vsubtype = obs_data_get_string(settings, ST_KEY_SUBTYPE); + + // Find new Type + auto type_found = list_of_types.find(vtype); + if (type_found == list_of_types.end()) { + return false; + } + + // Find new Subtype + auto subtype_found = list_of_subtypes.find(vsubtype); + if (subtype_found == list_of_subtypes.end()) { + return false; + } + + // Blur Type + if (strcmp(propname, ST_KEY_TYPE) == 0) { + obs_property_t* prop_subtype = obs_properties_get(props, ST_KEY_SUBTYPE); + + /// Disable unsupported items. + std::size_t subvalue_idx = 0; + for (std::size_t idx = 0, edx = obs_property_list_item_count(prop_subtype); idx < edx; idx++) { + const char* subtype = obs_property_list_item_string(prop_subtype, idx); + bool disabled = false; + + auto subtype_found_idx = list_of_subtypes.find(subtype); + if (subtype_found_idx != list_of_subtypes.end()) { + disabled = !type_found->second.fn().is_type_supported(subtype_found_idx->second.type); + } else { + disabled = true; + } + + obs_property_list_item_disable(prop_subtype, idx, disabled); + if (strcmp(subtype, vsubtype) == 0) { + subvalue_idx = idx; + } + } + + /// Ensure that there is a valid item selected. + if (obs_property_list_item_disabled(prop_subtype, subvalue_idx)) { + for (std::size_t idx = 0, edx = obs_property_list_item_count(prop_subtype); idx < edx; idx++) { + if (!obs_property_list_item_disabled(prop_subtype, idx)) { + obs_data_set_string(settings, ST_KEY_SUBTYPE, obs_property_list_item_string(prop_subtype, idx)); + + // Find new Subtype + auto subtype_found2 = list_of_subtypes.find(vsubtype); + if (subtype_found2 == list_of_subtypes.end()) { + subtype_found = list_of_subtypes.end(); + } else { + subtype_found = subtype_found2; + } + + break; + } + } + } + } + + // Blur Sub-Type + { + bool has_angle_support = (subtype_found->second.type == ::streamfx::gfx::blur::type::Directional) || (subtype_found->second.type == ::streamfx::gfx::blur::type::Rotational); + bool has_center_support = (subtype_found->second.type == ::streamfx::gfx::blur::type::Rotational) || (subtype_found->second.type == ::streamfx::gfx::blur::type::Zoom); + bool has_stepscale_support = type_found->second.fn().is_step_scale_supported(subtype_found->second.type); + bool show_scaling = obs_data_get_bool(settings, ST_KEY_STEPSCALE) && has_stepscale_support; + + /// Size + p = obs_properties_get(props, ST_KEY_SIZE); + obs_property_float_set_limits(p, type_found->second.fn().get_min_size(subtype_found->second.type), type_found->second.fn().get_max_size(subtype_found->second.type), type_found->second.fn().get_step_size(subtype_found->second.type)); + + /// Angle + p = obs_properties_get(props, ST_KEY_ANGLE); + obs_property_set_visible(p, has_angle_support); + obs_property_float_set_limits(p, type_found->second.fn().get_min_angle(subtype_found->second.type), type_found->second.fn().get_max_angle(subtype_found->second.type), type_found->second.fn().get_step_angle(subtype_found->second.type)); + + /// Center, Radius + obs_property_set_visible(obs_properties_get(props, ST_KEY_CENTER_X), has_center_support); + obs_property_set_visible(obs_properties_get(props, ST_KEY_CENTER_Y), has_center_support); + + /// Step Scaling + obs_property_set_visible(obs_properties_get(props, ST_KEY_STEPSCALE), has_stepscale_support); + p = obs_properties_get(props, ST_KEY_STEPSCALE_X); + obs_property_set_visible(p, show_scaling); + obs_property_float_set_limits(p, type_found->second.fn().get_min_step_scale_x(subtype_found->second.type), type_found->second.fn().get_max_step_scale_x(subtype_found->second.type), type_found->second.fn().get_step_step_scale_x(subtype_found->second.type)); + p = obs_properties_get(props, ST_KEY_STEPSCALE_Y); + obs_property_set_visible(p, show_scaling); + obs_property_float_set_limits(p, type_found->second.fn().get_min_step_scale_x(subtype_found->second.type), type_found->second.fn().get_max_step_scale_x(subtype_found->second.type), type_found->second.fn().get_step_step_scale_x(subtype_found->second.type)); + } + + { // Masking + using namespace ::streamfx::gfx::blur; + bool show_mask = obs_data_get_bool(settings, ST_KEY_MASK); + mask_type mtype = static_cast(obs_data_get_int(settings, ST_KEY_MASK_TYPE)); + bool show_region = (mtype == mask_type::Region) && show_mask; + bool show_image = (mtype == mask_type::Image) && show_mask; + bool show_source = (mtype == mask_type::Source) && show_mask; + obs_property_set_visible(obs_properties_get(props, ST_KEY_MASK_TYPE), show_mask); + obs_property_set_visible(obs_properties_get(props, ST_KEY_MASK_REGION_LEFT), show_region); + obs_property_set_visible(obs_properties_get(props, ST_KEY_MASK_REGION_TOP), show_region); + obs_property_set_visible(obs_properties_get(props, ST_KEY_MASK_REGION_RIGHT), show_region); + obs_property_set_visible(obs_properties_get(props, ST_KEY_MASK_REGION_BOTTOM), show_region); + obs_property_set_visible(obs_properties_get(props, ST_KEY_MASK_REGION_FEATHER), show_region); + obs_property_set_visible(obs_properties_get(props, ST_KEY_MASK_REGION_FEATHER_SHIFT), show_region); + obs_property_set_visible(obs_properties_get(props, ST_KEY_MASK_REGION_INVERT), show_region); + obs_property_set_visible(obs_properties_get(props, ST_KEY_MASK_IMAGE), show_image); + obs_property_set_visible(obs_properties_get(props, ST_KEY_MASK_SOURCE), show_source); + obs_property_set_visible(obs_properties_get(props, ST_KEY_MASK_COLOR), show_image || show_source); + obs_property_set_visible(obs_properties_get(props, ST_KEY_MASK_ALPHA), show_image || show_source); + obs_property_set_visible(obs_properties_get(props, ST_KEY_MASK_MULTIPLIER), show_image || show_source); + } + + return true; + } catch (...) { + DLOG_ERROR("Unexpected exception in modified_properties callback."); + return false; + } +} + +obs_properties_t* blur_factory::get_properties2(blur_instance* data) +{ + obs_properties_t* pr = obs_properties_create(); + obs_property_t* p = NULL; + + { + obs_properties_add_button2(pr, S_MANUAL_OPEN, D_TRANSLATE(S_MANUAL_OPEN), streamfx::filter::blur::blur_factory::on_manual_open, nullptr); + } + + // Blur Type and Sub-Type + { + p = obs_properties_add_list(pr, ST_KEY_TYPE, D_TRANSLATE(ST_I18N_TYPE), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + obs_property_set_modified_callback2(p, modified_properties, this); + obs_property_list_add_string(p, D_TRANSLATE(S_BLUR_TYPE_BOX), "box"); + obs_property_list_add_string(p, D_TRANSLATE(S_BLUR_TYPE_BOX_LINEAR), "box_linear"); + obs_property_list_add_string(p, D_TRANSLATE(S_BLUR_TYPE_GAUSSIAN), "gaussian"); + obs_property_list_add_string(p, D_TRANSLATE(S_BLUR_TYPE_GAUSSIAN_LINEAR), "gaussian_linear"); + obs_property_list_add_string(p, D_TRANSLATE(S_BLUR_TYPE_DUALFILTERING), "dual_filtering"); + + p = obs_properties_add_list(pr, ST_KEY_SUBTYPE, D_TRANSLATE(ST_I18N_SUBTYPE), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + obs_property_set_modified_callback2(p, modified_properties, this); + obs_property_list_add_string(p, D_TRANSLATE(S_BLUR_SUBTYPE_AREA), "area"); + obs_property_list_add_string(p, D_TRANSLATE(S_BLUR_SUBTYPE_DIRECTIONAL), "directional"); + obs_property_list_add_string(p, D_TRANSLATE(S_BLUR_SUBTYPE_ROTATIONAL), "rotational"); + obs_property_list_add_string(p, D_TRANSLATE(S_BLUR_SUBTYPE_ZOOM), "zoom"); + } + + // Blur Parameters + { + p = obs_properties_add_float_slider(pr, ST_KEY_SIZE, D_TRANSLATE(ST_I18N_SIZE), 1, 32767, 1); + p = obs_properties_add_float_slider(pr, ST_KEY_ANGLE, D_TRANSLATE(ST_I18N_ANGLE), -180.0, 180.0, 0.01); + p = obs_properties_add_float_slider(pr, ST_KEY_CENTER_X, D_TRANSLATE(ST_I18N_CENTER_X), 0.00, 100.0, 0.01); + p = obs_properties_add_float_slider(pr, ST_KEY_CENTER_Y, D_TRANSLATE(ST_I18N_CENTER_Y), 0.00, 100.0, 0.01); + + p = obs_properties_add_bool(pr, ST_KEY_STEPSCALE, D_TRANSLATE(ST_I18N_STEPSCALE)); + obs_property_set_modified_callback2(p, modified_properties, this); + p = obs_properties_add_float_slider(pr, ST_KEY_STEPSCALE_X, D_TRANSLATE(ST_I18N_STEPSCALE_X), 0.0, 1000.0, 0.01); + p = obs_properties_add_float_slider(pr, ST_KEY_STEPSCALE_Y, D_TRANSLATE(ST_I18N_STEPSCALE_Y), 0.0, 1000.0, 0.01); + } + + // Masking + { + p = obs_properties_add_bool(pr, ST_KEY_MASK, D_TRANSLATE(ST_I18N_MASK)); + obs_property_set_modified_callback2(p, modified_properties, this); + p = obs_properties_add_list(pr, ST_KEY_MASK_TYPE, D_TRANSLATE(ST_I18N_MASK_TYPE), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); + obs_property_set_modified_callback2(p, modified_properties, this); + obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_MASK_TYPE_REGION), static_cast(mask_type::Region)); + obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_MASK_TYPE_IMAGE), static_cast(mask_type::Image)); + obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_MASK_TYPE_SOURCE), static_cast(mask_type::Source)); + /// Region + p = obs_properties_add_float_slider(pr, ST_KEY_MASK_REGION_LEFT, D_TRANSLATE(ST_I18N_MASK_REGION_LEFT), 0.0, 100.0, 0.01); + p = obs_properties_add_float_slider(pr, ST_KEY_MASK_REGION_TOP, D_TRANSLATE(ST_I18N_MASK_REGION_TOP), 0.0, 100.0, 0.01); + p = obs_properties_add_float_slider(pr, ST_KEY_MASK_REGION_RIGHT, D_TRANSLATE(ST_I18N_MASK_REGION_RIGHT), 0.0, 100.0, 0.01); + p = obs_properties_add_float_slider(pr, ST_KEY_MASK_REGION_BOTTOM, D_TRANSLATE(ST_I18N_MASK_REGION_BOTTOM), 0.0, 100.0, 0.01); + p = obs_properties_add_float_slider(pr, ST_KEY_MASK_REGION_FEATHER, D_TRANSLATE(ST_I18N_MASK_REGION_FEATHER), 0.0, 50.0, 0.01); + p = obs_properties_add_float_slider(pr, ST_KEY_MASK_REGION_FEATHER_SHIFT, D_TRANSLATE(ST_I18N_MASK_REGION_FEATHER_SHIFT), -100.0, 100.0, 0.01); + p = obs_properties_add_bool(pr, ST_KEY_MASK_REGION_INVERT, D_TRANSLATE(ST_I18N_MASK_REGION_INVERT)); + /// Image + { + std::string filter = translate_string("%s (%s);;* (*.*)", D_TRANSLATE(S_FILETYPE_IMAGES), S_FILEFILTERS_TEXTURE); + _translation_cache.push_back(filter); + p = obs_properties_add_path(pr, ST_KEY_MASK_IMAGE, D_TRANSLATE(ST_I18N_MASK_IMAGE), OBS_PATH_FILE, _translation_cache.back().c_str(), nullptr); + } + /// Source + p = obs_properties_add_list(pr, ST_KEY_MASK_SOURCE, D_TRANSLATE(ST_I18N_MASK_SOURCE), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + obs_property_list_add_string(p, "", ""); + obs::source_tracker::instance()->enumerate( + [&p](std::string name, ::streamfx::obs::source) { + obs_property_list_add_string(p, std::string(name + " (Source)").c_str(), name.c_str()); + return false; + }, + obs::source_tracker::filter_video_sources); + obs::source_tracker::instance()->enumerate( + [&p](std::string name, ::streamfx::obs::source) { + obs_property_list_add_string(p, std::string(name + " (Scene)").c_str(), name.c_str()); + return false; + }, + obs::source_tracker::filter_scenes); + + /// Shared + p = obs_properties_add_color(pr, ST_KEY_MASK_COLOR, D_TRANSLATE(ST_I18N_MASK_COLOR)); + p = obs_properties_add_float_slider(pr, ST_KEY_MASK_ALPHA, D_TRANSLATE(ST_I18N_MASK_ALPHA), 0.0, 100.0, 0.1); + p = obs_properties_add_float_slider(pr, ST_KEY_MASK_MULTIPLIER, D_TRANSLATE(ST_I18N_MASK_MULTIPLIER), 0.0, 10.0, 0.01); + } + + return pr; +} + +std::string blur_factory::translate_string(const char* format, ...) +{ + va_list vargs; + va_start(vargs, format); + std::vector buffer(2048); + std::size_t len = static_cast(vsnprintf(buffer.data(), buffer.size(), format, vargs)); + va_end(vargs); + return std::string(buffer.data(), buffer.data() + len); +} + +bool blur_factory::on_manual_open(obs_properties_t* props, obs_property_t* property, void* data) +{ + try { + streamfx::open_url(HELP_URL); + return false; + } catch (const std::exception& ex) { + D_LOG_ERROR("Failed to open manual due to error: %s", ex.what()); + return false; + } catch (...) { + D_LOG_ERROR("Failed to open manual due to unknown error.", ""); + return false; + } +} + +std::shared_ptr blur_factory::instance() +{ + static std::weak_ptr winst; + static std::mutex mtx; + + std::unique_lock lock(mtx); + auto instance = winst.lock(); + if (!instance) { + instance = std::shared_ptr(new blur_factory()); + winst = instance; + } + return instance; +} + +static std::shared_ptr loader_instance; + +static auto loader = streamfx::component( + "blur", + []() { // Initializer + loader_instance = blur_factory::instance(); + }, + []() { // Finalizer + loader_instance.reset(); + }, + {"core::threadpool", "core::source_tracker"}); diff --git a/components/blur/source/filter/filter-blur.hpp b/components/blur/source/filter/filter-blur.hpp new file mode 100644 index 0000000..68a7fcb --- /dev/null +++ b/components/blur/source/filter/filter-blur.hpp @@ -0,0 +1,123 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "common.hpp" +#include "gfx/blur/gfx-blur-base.hpp" +#include "gfx/gfx-source-texture.hpp" +#include "gfx/gfx-util.hpp" +#include "obs/gs/gs-effect.hpp" +#include "obs/gs/gs-helper.hpp" +#include "obs/gs/gs-texrender.hpp" +#include "obs/gs/gs-texture.hpp" +#include "obs/obs-source-factory.hpp" + +#include "warning-disable.hpp" +#include +#include +#include +#include +#include "warning-enable.hpp" + +namespace streamfx::filter::blur { + enum class mask_type : int64_t { + Region, + Image, + Source, + }; + + class blur_instance : public obs::source_instance { + // Effects + streamfx::obs::gs::effect _effect_mask; + std::shared_ptr _gfx_util; + + // Input + std::shared_ptr _source_rt; + std::shared_ptr _source_texture; + bool _source_rendered; + + // Rendering + std::shared_ptr _output_texture; + std::shared_ptr _output_rt; + bool _output_rendered; + + // Blur + std::shared_ptr<::streamfx::gfx::blur::base> _blur; + double_t _blur_size; + double_t _blur_angle; + std::pair _blur_center; + bool _blur_step_scaling; + std::pair _blur_step_scale; + + // Masking + struct { + bool enabled; + mask_type type; + struct { + float left; + float top; + float right; + float bottom; + float feather; + float feather_shift; + bool invert; + } region; + struct { + std::string path; + std::string path_old; + std::shared_ptr texture; + } image; + struct { + std::string name_old; + std::string name; + bool is_scene; + std::shared_ptr source_texture; + std::shared_ptr texture; + } source; + struct { + float r; + float g; + float b; + float a; + } color; + float multiplier; + } _mask; + + public: + blur_instance(obs_data_t* settings, obs_source_t* self); + ~blur_instance(); + + public: + virtual void load(obs_data_t* settings) override; + virtual void migrate(obs_data_t* settings, uint64_t version) override; + virtual void update(obs_data_t* settings) override; + + virtual void video_tick(float time) override; + virtual void video_render(gs_effect_t* effect) override; + + private: + bool apply_mask_parameters(streamfx::obs::gs::effect effect, gs_texture_t* original_texture, gs_texture_t* blurred_texture); + }; + + class blur_factory : public obs::source_factory { + std::vector _translation_cache; + + public: + blur_factory(); + virtual ~blur_factory(); + + virtual const char* get_name() override; + + virtual void get_defaults2(obs_data_t* settings) override; + + virtual obs_properties_t* get_properties2(filter::blur::blur_instance* data) override; + + std::string translate_string(const char* format, ...); + + static bool on_manual_open(obs_properties_t* props, obs_property_t* property, void* data); + + public: // Singleton + static std::shared_ptr instance(); + }; +} // namespace streamfx::filter::blur diff --git a/components/blur/source/gfx/blur/gfx-blur-base.cpp b/components/blur/source/gfx/blur/gfx-blur-base.cpp new file mode 100644 index 0000000..eace51a --- /dev/null +++ b/components/blur/source/gfx/blur/gfx-blur-base.cpp @@ -0,0 +1,57 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#include "gfx-blur-base.hpp" + +#include "warning-disable.hpp" +#include +#include "warning-enable.hpp" + +void streamfx::gfx::blur::base::set_step_scale_x(double_t v) +{ + this->set_step_scale(v, this->get_step_scale_y()); +} + +void streamfx::gfx::blur::base::set_step_scale_y(double_t v) +{ + this->set_step_scale(this->get_step_scale_x(), v); +} + +double_t streamfx::gfx::blur::base::get_step_scale_x() +{ + double_t x, y; + this->get_step_scale(x, y); + return x; +} + +double_t streamfx::gfx::blur::base::get_step_scale_y() +{ + double_t x, y; + this->get_step_scale(x, y); + return y; +} + +void streamfx::gfx::blur::base_center::set_center_x(double_t v) +{ + this->set_center(v, this->get_center_y()); +} + +void streamfx::gfx::blur::base_center::set_center_y(double_t v) +{ + this->set_center(this->get_center_x(), v); +} + +double_t streamfx::gfx::blur::base_center::get_center_x() +{ + double_t x, y; + this->get_center(x, y); + return x; +} + +double_t streamfx::gfx::blur::base_center::get_center_y() +{ + double_t x, y; + this->get_center(x, y); + return y; +} diff --git a/components/blur/source/gfx/blur/gfx-blur-base.hpp b/components/blur/source/gfx/blur/gfx-blur-base.hpp new file mode 100644 index 0000000..2028864 --- /dev/null +++ b/components/blur/source/gfx/blur/gfx-blur-base.hpp @@ -0,0 +1,109 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "common.hpp" +#include "obs/gs/gs-texture.hpp" + +namespace streamfx::gfx { + namespace blur { + enum class type : int64_t { + Invalid = -1, + Area, + Directional, + Rotational, + Zoom, + }; + + class base { + public: + virtual ~base() {} + + virtual void set_input(std::shared_ptr<::streamfx::obs::gs::texture> texture) = 0; + + virtual ::streamfx::gfx::blur::type get_type() = 0; + + virtual double_t get_size() = 0; + + virtual void set_size(double_t width) = 0; + + virtual void set_step_scale(double_t x, double_t y) = 0; + + virtual void set_step_scale_x(double_t v); + + virtual void set_step_scale_y(double_t v); + + virtual void get_step_scale(double_t& x, double_t& y) = 0; + + virtual double_t get_step_scale_x(); + + virtual double_t get_step_scale_y(); + + virtual std::shared_ptr<::streamfx::obs::gs::texture> render() = 0; + + virtual std::shared_ptr<::streamfx::obs::gs::texture> get() = 0; + }; + + class base_angle { + public: + virtual ~base_angle() {} + + virtual double_t get_angle() = 0; + + virtual void set_angle(double_t angle) = 0; + }; + + class base_center { + public: + virtual ~base_center() {} + + virtual void set_center(double_t x, double_t y) = 0; + + virtual void set_center_x(double_t v); + + virtual void set_center_y(double_t v); + + virtual void get_center(double_t& x, double_t& y) = 0; + + virtual double_t get_center_x(); + + virtual double_t get_center_y(); + }; + + class ifactory { + public: + virtual ~ifactory() {} + + virtual bool is_type_supported(::streamfx::gfx::blur::type type) = 0; + + virtual std::shared_ptr<::streamfx::gfx::blur::base> create(::streamfx::gfx::blur::type type) = 0; + + virtual double_t get_min_size(::streamfx::gfx::blur::type type) = 0; + + virtual double_t get_step_size(::streamfx::gfx::blur::type type) = 0; + + virtual double_t get_max_size(::streamfx::gfx::blur::type type) = 0; + + virtual double_t get_min_angle(::streamfx::gfx::blur::type type) = 0; + + virtual double_t get_step_angle(::streamfx::gfx::blur::type type) = 0; + + virtual double_t get_max_angle(::streamfx::gfx::blur::type type) = 0; + + virtual bool is_step_scale_supported(::streamfx::gfx::blur::type type) = 0; + + virtual double_t get_min_step_scale_x(::streamfx::gfx::blur::type type) = 0; + + virtual double_t get_step_step_scale_x(::streamfx::gfx::blur::type type) = 0; + + virtual double_t get_max_step_scale_x(::streamfx::gfx::blur::type type) = 0; + + virtual double_t get_min_step_scale_y(::streamfx::gfx::blur::type type) = 0; + + virtual double_t get_step_step_scale_y(::streamfx::gfx::blur::type type) = 0; + + virtual double_t get_max_step_scale_y(::streamfx::gfx::blur::type type) = 0; + }; + } // namespace blur +} // namespace streamfx::gfx diff --git a/components/blur/source/gfx/blur/gfx-blur-box-linear.cpp b/components/blur/source/gfx/blur/gfx-blur-box-linear.cpp new file mode 100644 index 0000000..4964443 --- /dev/null +++ b/components/blur/source/gfx/blur/gfx-blur-box-linear.cpp @@ -0,0 +1,366 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2022 lainon +// AUTOGENERATED COPYRIGHT HEADER END + +#include "gfx-blur-box-linear.hpp" +#include "common.hpp" +#include "obs/gs/gs-helper.hpp" +#include "plugin.hpp" + +#include "warning-disable.hpp" +#include +#include +#include +#include "warning-enable.hpp" + +#define ST_MAX_BLUR_SIZE 128 // Also change this in box-linear.effect if modified. + +streamfx::gfx::blur::box_linear_data::box_linear_data() : _gfx_util(::streamfx::gfx::util::get()) +{ + auto gctx = streamfx::obs::gs::context(); + { + auto file = streamfx::data_file_path("effects/blur/box-linear.effect"); + try { + _effect = streamfx::obs::gs::effect::create(file); + } catch (const std::exception& ex) { + DLOG_ERROR("Error loading '%s': %s", file.generic_u8string().c_str(), ex.what()); + } + } +} + +streamfx::gfx::blur::box_linear_data::~box_linear_data() +{ + auto gctx = streamfx::obs::gs::context(); + _effect.reset(); +} + +std::shared_ptr streamfx::gfx::blur::box_linear_data::get_gfx_util() +{ + return _gfx_util; +} + +streamfx::obs::gs::effect streamfx::gfx::blur::box_linear_data::get_effect() +{ + return _effect; +} + +streamfx::gfx::blur::box_linear_factory::box_linear_factory() {} + +streamfx::gfx::blur::box_linear_factory::~box_linear_factory() {} + +bool streamfx::gfx::blur::box_linear_factory::is_type_supported(::streamfx::gfx::blur::type type) +{ + switch (type) { + case ::streamfx::gfx::blur::type::Area: + case ::streamfx::gfx::blur::type::Directional: + return true; + default: + return false; + } +} + +std::shared_ptr<::streamfx::gfx::blur::base> streamfx::gfx::blur::box_linear_factory::create(::streamfx::gfx::blur::type type) +{ + switch (type) { + case ::streamfx::gfx::blur::type::Area: + return std::make_shared<::streamfx::gfx::blur::box_linear>(); + case ::streamfx::gfx::blur::type::Directional: + return std::make_shared<::streamfx::gfx::blur::box_linear_directional>(); + default: + throw std::runtime_error("Invalid type."); + } +} + +double_t streamfx::gfx::blur::box_linear_factory::get_min_size(::streamfx::gfx::blur::type) +{ + return double_t(1.0); +} + +double_t streamfx::gfx::blur::box_linear_factory::get_step_size(::streamfx::gfx::blur::type) +{ + return double_t(1.0); +} + +double_t streamfx::gfx::blur::box_linear_factory::get_max_size(::streamfx::gfx::blur::type) +{ + return double_t(ST_MAX_BLUR_SIZE); +} + +double_t streamfx::gfx::blur::box_linear_factory::get_min_angle(::streamfx::gfx::blur::type v) +{ + switch (v) { + case ::streamfx::gfx::blur::type::Directional: + case ::streamfx::gfx::blur::type::Rotational: + return -180.0; + default: + return 0; + } +} + +double_t streamfx::gfx::blur::box_linear_factory::get_step_angle(::streamfx::gfx::blur::type) +{ + return double_t(0.01); +} + +double_t streamfx::gfx::blur::box_linear_factory::get_max_angle(::streamfx::gfx::blur::type v) +{ + switch (v) { + case ::streamfx::gfx::blur::type::Directional: + case ::streamfx::gfx::blur::type::Rotational: + return 180.0; + default: + return 0; + } +} + +bool streamfx::gfx::blur::box_linear_factory::is_step_scale_supported(::streamfx::gfx::blur::type v) +{ + switch (v) { + case ::streamfx::gfx::blur::type::Area: + case ::streamfx::gfx::blur::type::Zoom: + case ::streamfx::gfx::blur::type::Directional: + return true; + default: + return false; + } +} + +double_t streamfx::gfx::blur::box_linear_factory::get_min_step_scale_x(::streamfx::gfx::blur::type) +{ + return double_t(0.01); +} + +double_t streamfx::gfx::blur::box_linear_factory::get_step_step_scale_x(::streamfx::gfx::blur::type) +{ + return double_t(0.01); +} + +double_t streamfx::gfx::blur::box_linear_factory::get_max_step_scale_x(::streamfx::gfx::blur::type) +{ + return double_t(1000.0); +} + +double_t streamfx::gfx::blur::box_linear_factory::get_min_step_scale_y(::streamfx::gfx::blur::type) +{ + return double_t(0.01); +} + +double_t streamfx::gfx::blur::box_linear_factory::get_step_step_scale_y(::streamfx::gfx::blur::type) +{ + return double_t(0.01); +} + +double_t streamfx::gfx::blur::box_linear_factory::get_max_step_scale_y(::streamfx::gfx::blur::type) +{ + return double_t(1000.0); +} + +std::shared_ptr<::streamfx::gfx::blur::box_linear_data> streamfx::gfx::blur::box_linear_factory::data() +{ + std::unique_lock ulock(_data_lock); + std::shared_ptr<::streamfx::gfx::blur::box_linear_data> data = _data.lock(); + if (!data) { + data = std::make_shared<::streamfx::gfx::blur::box_linear_data>(); + _data = data; + } + return data; +} + +::streamfx::gfx::blur::box_linear_factory& streamfx::gfx::blur::box_linear_factory::get() +{ + static ::streamfx::gfx::blur::box_linear_factory instance; + return instance; +} + +streamfx::gfx::blur::box_linear::box_linear() : _data(::streamfx::gfx::blur::box_linear_factory::get().data()), _size(1.), _step_scale({1., 1.}) +{ + _rendertarget = std::make_shared<::streamfx::obs::gs::texrender>(GS_RGBA, GS_ZS_NONE); + _rendertarget2 = std::make_shared<::streamfx::obs::gs::texrender>(GS_RGBA, GS_ZS_NONE); +} + +streamfx::gfx::blur::box_linear::~box_linear() {} + +void streamfx::gfx::blur::box_linear::set_input(std::shared_ptr<::streamfx::obs::gs::texture> texture) +{ + _input_texture = std::move(texture); +} + +::streamfx::gfx::blur::type streamfx::gfx::blur::box_linear::get_type() +{ + return ::streamfx::gfx::blur::type::Area; +} + +double_t streamfx::gfx::blur::box_linear::get_size() +{ + return _size; +} + +void streamfx::gfx::blur::box_linear::set_size(double_t width) +{ + _size = width; + if (_size < 1.0) { + _size = 1.0; + } + if (_size > ST_MAX_BLUR_SIZE) { + _size = ST_MAX_BLUR_SIZE; + } +} + +void streamfx::gfx::blur::box_linear::set_step_scale(double_t x, double_t y) +{ + _step_scale = {x, y}; +} + +void streamfx::gfx::blur::box_linear::get_step_scale(double_t& x, double_t& y) +{ + x = _step_scale.first; + y = _step_scale.second; +} + +double_t streamfx::gfx::blur::box_linear::get_step_scale_x() +{ + return _step_scale.first; +} + +double_t streamfx::gfx::blur::box_linear::get_step_scale_y() +{ + return _step_scale.second; +} + +std::shared_ptr<::streamfx::obs::gs::texture> streamfx::gfx::blur::box_linear::render() +{ + auto gctx = streamfx::obs::gs::context(); + +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + auto gdmp = streamfx::obs::gs::debug_marker(streamfx::obs::gs::debug_color_azure_radiance, "Box Linear Blur"); +#endif + + float width = float(_input_texture->width()); + float height = float(_input_texture->height()); + + gs_set_cull_mode(GS_NEITHER); + gs_enable_color(true, true, true, true); + gs_enable_depth_test(false); + gs_depth_function(GS_ALWAYS); + gs_blend_state_push(); + gs_reset_blend_state(); + gs_enable_blending(false); + gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO); + gs_enable_stencil_test(false); + gs_enable_stencil_write(false); + gs_stencil_function(GS_STENCIL_BOTH, GS_ALWAYS); + gs_stencil_op(GS_STENCIL_BOTH, GS_ZERO, GS_ZERO, GS_ZERO); + + // Two Pass Blur + streamfx::obs::gs::effect effect = _data->get_effect(); + if (effect) { + // Pass 1 + effect.get_parameter("pImage").set_texture(_input_texture); + effect.get_parameter("pImageTexel").set_float2(float(1.f / width), 0.f); + effect.get_parameter("pStepScale").set_float2(float(_step_scale.first), float(_step_scale.second)); + effect.get_parameter("pSize").set_float(float(_size)); + effect.get_parameter("pSizeInverseMul").set_float(float(1.0f / (float(_size) * 2.0f + 1.0f))); + + { +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + auto gdm = streamfx::obs::gs::debug_marker(streamfx::obs::gs::debug_color_azure_radiance, "Horizontal"); +#endif + + auto op = _rendertarget2->render(uint32_t(width), uint32_t(height)); + gs_ortho(0, 1., 0, 1., 0, 1.); + while (gs_effect_loop(effect.get_object(), "Draw")) { + _data->get_gfx_util()->draw_fullscreen_triangle(); + } + } + + // Pass 2 + effect.get_parameter("pImage").set_texture(_rendertarget2->get_texture()); + effect.get_parameter("pImageTexel").set_float2(0., float(1.f / height)); + + { +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + auto gdm = streamfx::obs::gs::debug_marker(streamfx::obs::gs::debug_color_azure_radiance, "Vertical"); +#endif + + auto op = _rendertarget->render(uint32_t(width), uint32_t(height)); + gs_ortho(0, 1., 0, 1., 0, 1.); + while (gs_effect_loop(effect.get_object(), "Draw")) { + _data->get_gfx_util()->draw_fullscreen_triangle(); + } + } + } + + gs_blend_state_pop(); + + return _rendertarget->get_texture(); +} + +std::shared_ptr<::streamfx::obs::gs::texture> streamfx::gfx::blur::box_linear::get() +{ + return _rendertarget->get_texture(); +} + +streamfx::gfx::blur::box_linear_directional::box_linear_directional() : _angle(0) {} + +::streamfx::gfx::blur::type streamfx::gfx::blur::box_linear_directional::get_type() +{ + return ::streamfx::gfx::blur::type::Directional; +} + +double_t streamfx::gfx::blur::box_linear_directional::get_angle() +{ + return D_RAD_TO_DEG(_angle); +} + +void streamfx::gfx::blur::box_linear_directional::set_angle(double_t angle) +{ + _angle = D_DEG_TO_RAD(angle); +} + +std::shared_ptr<::streamfx::obs::gs::texture> streamfx::gfx::blur::box_linear_directional::render() +{ + auto gctx = streamfx::obs::gs::context(); + +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + auto gdmp = streamfx::obs::gs::debug_marker(streamfx::obs::gs::debug_color_azure_radiance, "Box Linear Directional Blur"); +#endif + + float width = float(_input_texture->width()); + float height = float(_input_texture->height()); + + gs_blend_state_push(); + gs_reset_blend_state(); + gs_enable_color(true, true, true, true); + gs_enable_blending(false); + gs_enable_depth_test(false); + gs_enable_stencil_test(false); + gs_enable_stencil_write(false); + gs_set_cull_mode(GS_NEITHER); + gs_depth_function(GS_ALWAYS); + gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO); + gs_stencil_function(GS_STENCIL_BOTH, GS_ALWAYS); + gs_stencil_op(GS_STENCIL_BOTH, GS_ZERO, GS_ZERO, GS_ZERO); + + // One Pass Blur + streamfx::obs::gs::effect effect = _data->get_effect(); + if (effect) { + effect.get_parameter("pImage").set_texture(_input_texture); + effect.get_parameter("pImageTexel").set_float2(float(1. / width * cos(_angle)), float(1.f / height * sin(_angle))); + effect.get_parameter("pStepScale").set_float2(float(_step_scale.first), float(_step_scale.second)); + effect.get_parameter("pSize").set_float(float(_size)); + effect.get_parameter("pSizeInverseMul").set_float(float(1.0f / (float(_size) * 2.0f + 1.0f))); + + { + auto op = _rendertarget->render(uint32_t(width), uint32_t(height)); + gs_ortho(0, 1., 0, 1., 0, 1.); + while (gs_effect_loop(effect.get_object(), "Draw")) { + _data->get_gfx_util()->draw_fullscreen_triangle(); + } + } + } + + gs_blend_state_pop(); + + return _rendertarget->get_texture(); +} diff --git a/components/blur/source/gfx/blur/gfx-blur-box-linear.hpp b/components/blur/source/gfx/blur/gfx-blur-box-linear.hpp new file mode 100644 index 0000000..f832924 --- /dev/null +++ b/components/blur/source/gfx/blur/gfx-blur-box-linear.hpp @@ -0,0 +1,122 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "common.hpp" +#include "gfx-blur-base.hpp" +#include "gfx/gfx-util.hpp" +#include "obs/gs/gs-effect.hpp" +#include "obs/gs/gs-texrender.hpp" +#include "obs/gs/gs-texture.hpp" + +#include "warning-disable.hpp" +#include +#include "warning-enable.hpp" + +namespace streamfx::gfx { + namespace blur { + class box_linear_data { + streamfx::obs::gs::effect _effect; + std::shared_ptr _gfx_util; + + public: + box_linear_data(); + virtual ~box_linear_data(); + + std::shared_ptr get_gfx_util(); + + streamfx::obs::gs::effect get_effect(); + }; + + class box_linear_factory : public ::streamfx::gfx::blur::ifactory { + std::mutex _data_lock; + std::weak_ptr<::streamfx::gfx::blur::box_linear_data> _data; + + public: + box_linear_factory(); + virtual ~box_linear_factory() override; + + virtual bool is_type_supported(::streamfx::gfx::blur::type type) override; + + virtual std::shared_ptr<::streamfx::gfx::blur::base> create(::streamfx::gfx::blur::type type) override; + + virtual double_t get_min_size(::streamfx::gfx::blur::type type) override; + + virtual double_t get_step_size(::streamfx::gfx::blur::type type) override; + + virtual double_t get_max_size(::streamfx::gfx::blur::type type) override; + + virtual double_t get_min_angle(::streamfx::gfx::blur::type type) override; + + virtual double_t get_step_angle(::streamfx::gfx::blur::type type) override; + + virtual double_t get_max_angle(::streamfx::gfx::blur::type type) override; + + virtual bool is_step_scale_supported(::streamfx::gfx::blur::type type) override; + + virtual double_t get_min_step_scale_x(::streamfx::gfx::blur::type type) override; + + virtual double_t get_step_step_scale_x(::streamfx::gfx::blur::type type) override; + + virtual double_t get_max_step_scale_x(::streamfx::gfx::blur::type type) override; + + virtual double_t get_min_step_scale_y(::streamfx::gfx::blur::type type) override; + + virtual double_t get_step_step_scale_y(::streamfx::gfx::blur::type type) override; + + virtual double_t get_max_step_scale_y(::streamfx::gfx::blur::type type) override; + + std::shared_ptr<::streamfx::gfx::blur::box_linear_data> data(); + + public: // Singleton + static ::streamfx::gfx::blur::box_linear_factory& get(); + }; + + class box_linear : public ::streamfx::gfx::blur::base { + protected: + std::shared_ptr<::streamfx::gfx::blur::box_linear_data> _data; + + double_t _size; + std::pair _step_scale; + std::shared_ptr<::streamfx::obs::gs::texture> _input_texture; + std::shared_ptr<::streamfx::obs::gs::texrender> _rendertarget; + + private: + std::shared_ptr<::streamfx::obs::gs::texrender> _rendertarget2; + + public: + box_linear(); + virtual ~box_linear() override; + + virtual void set_input(std::shared_ptr<::streamfx::obs::gs::texture> texture) override; + + virtual ::streamfx::gfx::blur::type get_type() override; + + virtual double_t get_size() override; + virtual void set_size(double_t width) override; + + virtual void set_step_scale(double_t x, double_t y) override; + virtual void get_step_scale(double_t& x, double_t& y) override; + virtual double_t get_step_scale_x() override; + virtual double_t get_step_scale_y() override; + + virtual std::shared_ptr<::streamfx::obs::gs::texture> render() override; + virtual std::shared_ptr<::streamfx::obs::gs::texture> get() override; + }; + + class box_linear_directional : public ::streamfx::gfx::blur::box_linear, public ::streamfx::gfx::blur::base_angle { + double_t _angle; + + public: + box_linear_directional(); + + virtual ::streamfx::gfx::blur::type get_type() override; + + virtual double_t get_angle() override; + virtual void set_angle(double_t angle) override; + + virtual std::shared_ptr<::streamfx::obs::gs::texture> render() override; + }; + } // namespace blur +} // namespace streamfx::gfx diff --git a/components/blur/source/gfx/blur/gfx-blur-box.cpp b/components/blur/source/gfx/blur/gfx-blur-box.cpp new file mode 100644 index 0000000..4112bbf --- /dev/null +++ b/components/blur/source/gfx/blur/gfx-blur-box.cpp @@ -0,0 +1,517 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2022 lainon +// AUTOGENERATED COPYRIGHT HEADER END + +#include "gfx-blur-box.hpp" +#include "common.hpp" +#include "obs/gs/gs-helper.hpp" +#include "plugin.hpp" + +#include "warning-disable.hpp" +#include +#include +#include +#include "warning-enable.hpp" + +#define ST_MAX_BLUR_SIZE 128 // Also change this in box.effect if modified. + +streamfx::gfx::blur::box_data::box_data() : _gfx_util(::streamfx::gfx::util::get()) +{ + auto gctx = streamfx::obs::gs::context(); + { + auto file = streamfx::data_file_path("effects/blur/box.effect"); + try { + _effect = streamfx::obs::gs::effect::create(file); + } catch (const std::exception& ex) { + DLOG_ERROR("Error loading '%s': %s", file.generic_u8string().c_str(), ex.what()); + } + } +} + +streamfx::gfx::blur::box_data::~box_data() +{ + auto gctx = streamfx::obs::gs::context(); + _effect.reset(); +} + +std::shared_ptr streamfx::gfx::blur::box_data::get_gfx_util() +{ + return _gfx_util; +} + +streamfx::obs::gs::effect streamfx::gfx::blur::box_data::get_effect() +{ + return _effect; +} + +streamfx::gfx::blur::box_factory::box_factory() {} + +streamfx::gfx::blur::box_factory::~box_factory() {} + +bool streamfx::gfx::blur::box_factory::is_type_supported(::streamfx::gfx::blur::type type) +{ + switch (type) { + case ::streamfx::gfx::blur::type::Area: + return true; + case ::streamfx::gfx::blur::type::Directional: + return true; + case ::streamfx::gfx::blur::type::Rotational: + return true; + case ::streamfx::gfx::blur::type::Zoom: + return true; + default: + return false; + } +} + +std::shared_ptr<::streamfx::gfx::blur::base> streamfx::gfx::blur::box_factory::create(::streamfx::gfx::blur::type type) +{ + switch (type) { + case ::streamfx::gfx::blur::type::Area: + return std::make_shared<::streamfx::gfx::blur::box>(); + case ::streamfx::gfx::blur::type::Directional: + return std::static_pointer_cast<::streamfx::gfx::blur::box>(std::make_shared<::streamfx::gfx::blur::box_directional>()); + case ::streamfx::gfx::blur::type::Rotational: + return std::make_shared<::streamfx::gfx::blur::box_rotational>(); + case ::streamfx::gfx::blur::type::Zoom: + return std::make_shared<::streamfx::gfx::blur::box_zoom>(); + default: + throw std::runtime_error("Invalid type."); + } +} + +double_t streamfx::gfx::blur::box_factory::get_min_size(::streamfx::gfx::blur::type) +{ + return double_t(1.0); +} + +double_t streamfx::gfx::blur::box_factory::get_step_size(::streamfx::gfx::blur::type) +{ + return double_t(1.0); +} + +double_t streamfx::gfx::blur::box_factory::get_max_size(::streamfx::gfx::blur::type) +{ + return double_t(ST_MAX_BLUR_SIZE); +} + +double_t streamfx::gfx::blur::box_factory::get_min_angle(::streamfx::gfx::blur::type v) +{ + switch (v) { + case ::streamfx::gfx::blur::type::Directional: + case ::streamfx::gfx::blur::type::Rotational: + return -180.0; + default: + return 0; + } +} + +double_t streamfx::gfx::blur::box_factory::get_step_angle(::streamfx::gfx::blur::type) +{ + return double_t(0.01); +} + +double_t streamfx::gfx::blur::box_factory::get_max_angle(::streamfx::gfx::blur::type v) +{ + switch (v) { + case ::streamfx::gfx::blur::type::Directional: + case ::streamfx::gfx::blur::type::Rotational: + return 180.0; + default: + return 0; + } +} + +bool streamfx::gfx::blur::box_factory::is_step_scale_supported(::streamfx::gfx::blur::type v) +{ + switch (v) { + case ::streamfx::gfx::blur::type::Area: + case ::streamfx::gfx::blur::type::Zoom: + case ::streamfx::gfx::blur::type::Directional: + return true; + default: + return false; + } +} + +double_t streamfx::gfx::blur::box_factory::get_min_step_scale_x(::streamfx::gfx::blur::type) +{ + return double_t(0.01); +} + +double_t streamfx::gfx::blur::box_factory::get_step_step_scale_x(::streamfx::gfx::blur::type) +{ + return double_t(0.01); +} + +double_t streamfx::gfx::blur::box_factory::get_max_step_scale_x(::streamfx::gfx::blur::type) +{ + return double_t(1000.0); +} + +double_t streamfx::gfx::blur::box_factory::get_min_step_scale_y(::streamfx::gfx::blur::type) +{ + return double_t(0.01); +} + +double_t streamfx::gfx::blur::box_factory::get_step_step_scale_y(::streamfx::gfx::blur::type) +{ + return double_t(0.01); +} + +double_t streamfx::gfx::blur::box_factory::get_max_step_scale_y(::streamfx::gfx::blur::type) +{ + return double_t(1000.0); +} + +std::shared_ptr<::streamfx::gfx::blur::box_data> streamfx::gfx::blur::box_factory::data() +{ + std::unique_lock ulock(_data_lock); + std::shared_ptr<::streamfx::gfx::blur::box_data> data = _data.lock(); + if (!data) { + data = std::make_shared<::streamfx::gfx::blur::box_data>(); + _data = data; + } + return data; +} + +::streamfx::gfx::blur::box_factory& streamfx::gfx::blur::box_factory::get() +{ + static ::streamfx::gfx::blur::box_factory instance; + return instance; +} + +streamfx::gfx::blur::box::box() : _data(::streamfx::gfx::blur::box_factory::get().data()), _size(1.), _step_scale({1., 1.}) +{ + auto gctx = streamfx::obs::gs::context(); + _rendertarget = std::make_shared<::streamfx::obs::gs::texrender>(GS_RGBA, GS_ZS_NONE); + _rendertarget2 = std::make_shared<::streamfx::obs::gs::texrender>(GS_RGBA, GS_ZS_NONE); +} + +streamfx::gfx::blur::box::~box() {} + +void streamfx::gfx::blur::box::set_input(std::shared_ptr<::streamfx::obs::gs::texture> texture) +{ + _input_texture = std::move(texture); +} + +::streamfx::gfx::blur::type streamfx::gfx::blur::box::get_type() +{ + return ::streamfx::gfx::blur::type::Area; +} + +double_t streamfx::gfx::blur::box::get_size() +{ + return _size; +} + +void streamfx::gfx::blur::box::set_size(double_t width) +{ + _size = width; + if (_size < 1.0) { + _size = 1.0; + } + if (_size > ST_MAX_BLUR_SIZE) { + _size = ST_MAX_BLUR_SIZE; + } +} + +void streamfx::gfx::blur::box::set_step_scale(double_t x, double_t y) +{ + _step_scale = {x, y}; +} + +void streamfx::gfx::blur::box::get_step_scale(double_t& x, double_t& y) +{ + x = _step_scale.first; + y = _step_scale.second; +} + +double_t streamfx::gfx::blur::box::get_step_scale_x() +{ + return _step_scale.first; +} + +double_t streamfx::gfx::blur::box::get_step_scale_y() +{ + return _step_scale.second; +} + +std::shared_ptr<::streamfx::obs::gs::texture> streamfx::gfx::blur::box::render() +{ + auto gctx = streamfx::obs::gs::context(); + +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + auto gdmp = streamfx::obs::gs::debug_marker(streamfx::obs::gs::debug_color_azure_radiance, "Box Blur"); +#endif + + float width = float(_input_texture->width()); + float height = float(_input_texture->height()); + + gs_set_cull_mode(GS_NEITHER); + gs_enable_color(true, true, true, true); + gs_enable_depth_test(false); + gs_depth_function(GS_ALWAYS); + gs_blend_state_push(); + gs_reset_blend_state(); + gs_enable_blending(false); + gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO); + gs_enable_stencil_test(false); + gs_enable_stencil_write(false); + gs_stencil_function(GS_STENCIL_BOTH, GS_ALWAYS); + gs_stencil_op(GS_STENCIL_BOTH, GS_ZERO, GS_ZERO, GS_ZERO); + + // Two Pass Blur + streamfx::obs::gs::effect effect = _data->get_effect(); + if (effect) { + // Pass 1 + effect.get_parameter("pImage").set_texture(_input_texture); + effect.get_parameter("pImageTexel").set_float2(float(1.f / width), 0.f); + effect.get_parameter("pStepScale").set_float2(float(_step_scale.first), float(_step_scale.second)); + effect.get_parameter("pSize").set_float(float(_size)); + effect.get_parameter("pSizeInverseMul").set_float(float(1.0f / (float(_size) * 2.0f + 1.0f))); + + { +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + auto gdm = streamfx::obs::gs::debug_marker(streamfx::obs::gs::debug_color_azure_radiance, "Horizontal"); +#endif + + auto op = _rendertarget2->render(uint32_t(width), uint32_t(height)); + gs_ortho(0, 1., 0, 1., 0, 1.); + while (gs_effect_loop(effect.get_object(), "Draw")) { + _data->get_gfx_util()->draw_fullscreen_triangle(); + } + } + + // Pass 2 + effect.get_parameter("pImage").set_texture(_rendertarget2->get_texture()); + effect.get_parameter("pImageTexel").set_float2(0.f, float(1.f / height)); + + { +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + auto gdm = streamfx::obs::gs::debug_marker(streamfx::obs::gs::debug_color_azure_radiance, "Vertical"); +#endif + + auto op = _rendertarget->render(uint32_t(width), uint32_t(height)); + gs_ortho(0, 1., 0, 1., 0, 1.); + while (gs_effect_loop(effect.get_object(), "Draw")) { + _data->get_gfx_util()->draw_fullscreen_triangle(); + } + } + } + + gs_blend_state_pop(); + + return _rendertarget->get_texture(); +} + +std::shared_ptr<::streamfx::obs::gs::texture> streamfx::gfx::blur::box::get() +{ + return _rendertarget->get_texture(); +} + +streamfx::gfx::blur::box_directional::box_directional() : _angle(0) {} + +::streamfx::gfx::blur::type streamfx::gfx::blur::box_directional::get_type() +{ + return ::streamfx::gfx::blur::type::Directional; +} + +double_t streamfx::gfx::blur::box_directional::get_angle() +{ + return D_RAD_TO_DEG(_angle); +} + +void streamfx::gfx::blur::box_directional::set_angle(double_t angle) +{ + _angle = D_DEG_TO_RAD(angle); +} + +std::shared_ptr<::streamfx::obs::gs::texture> streamfx::gfx::blur::box_directional::render() +{ + auto gctx = streamfx::obs::gs::context(); + +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + auto gdmp = streamfx::obs::gs::debug_marker(streamfx::obs::gs::debug_color_azure_radiance, "Box Directional Blur"); +#endif + + float width = float(_input_texture->width()); + float height = float(_input_texture->height()); + + gs_blend_state_push(); + gs_reset_blend_state(); + gs_enable_color(true, true, true, true); + gs_enable_blending(false); + gs_enable_depth_test(false); + gs_enable_stencil_test(false); + gs_enable_stencil_write(false); + gs_set_cull_mode(GS_NEITHER); + gs_depth_function(GS_ALWAYS); + gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO); + gs_stencil_function(GS_STENCIL_BOTH, GS_ALWAYS); + gs_stencil_op(GS_STENCIL_BOTH, GS_ZERO, GS_ZERO, GS_ZERO); + + // One Pass Blur + streamfx::obs::gs::effect effect = _data->get_effect(); + if (effect) { + effect.get_parameter("pImage").set_texture(_input_texture); + effect.get_parameter("pImageTexel").set_float2(float(1. / width * cos(_angle)), float(1.f / height * sin(_angle))); + effect.get_parameter("pStepScale").set_float2(float(_step_scale.first), float(_step_scale.second)); + effect.get_parameter("pSize").set_float(float(_size)); + effect.get_parameter("pSizeInverseMul").set_float(float(1.0f / (float(_size) * 2.0f + 1.0f))); + + { + auto op = _rendertarget->render(uint32_t(width), uint32_t(height)); + gs_ortho(0, 1., 0, 1., 0, 1.); + while (gs_effect_loop(effect.get_object(), "Draw")) { + _data->get_gfx_util()->draw_fullscreen_triangle(); + } + } + } + + gs_blend_state_pop(); + + return _rendertarget->get_texture(); +} + +::streamfx::gfx::blur::type streamfx::gfx::blur::box_rotational::get_type() +{ + return ::streamfx::gfx::blur::type::Rotational; +} + +void streamfx::gfx::blur::box_rotational::set_center(double_t x, double_t y) +{ + _center.first = x; + _center.second = y; +} + +void streamfx::gfx::blur::box_rotational::get_center(double_t& x, double_t& y) +{ + x = _center.first; + y = _center.second; +} + +double_t streamfx::gfx::blur::box_rotational::get_angle() +{ + return D_RAD_TO_DEG(_angle); +} + +void streamfx::gfx::blur::box_rotational::set_angle(double_t angle) +{ + _angle = D_DEG_TO_RAD(angle); +} + +std::shared_ptr<::streamfx::obs::gs::texture> streamfx::gfx::blur::box_rotational::render() +{ + auto gctx = streamfx::obs::gs::context(); + +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + auto gdmp = streamfx::obs::gs::debug_marker(streamfx::obs::gs::debug_color_azure_radiance, "Box Rotational Blur"); +#endif + + float width = float(_input_texture->width()); + float height = float(_input_texture->height()); + + gs_blend_state_push(); + gs_reset_blend_state(); + gs_enable_color(true, true, true, true); + gs_enable_blending(false); + gs_enable_depth_test(false); + gs_enable_stencil_test(false); + gs_enable_stencil_write(false); + gs_set_cull_mode(GS_NEITHER); + gs_depth_function(GS_ALWAYS); + gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO); + gs_stencil_function(GS_STENCIL_BOTH, GS_ALWAYS); + gs_stencil_op(GS_STENCIL_BOTH, GS_ZERO, GS_ZERO, GS_ZERO); + + // One Pass Blur + streamfx::obs::gs::effect effect = _data->get_effect(); + if (effect) { + effect.get_parameter("pImage").set_texture(_input_texture); + effect.get_parameter("pImageTexel").set_float2(float(1.f / width), float(1.f / height)); + effect.get_parameter("pStepScale").set_float2(float(_step_scale.first), float(_step_scale.second)); + effect.get_parameter("pSize").set_float(float(_size)); + effect.get_parameter("pSizeInverseMul").set_float(float(1.0f / (float(_size) * 2.0f + 1.0f))); + effect.get_parameter("pAngle").set_float(float(_angle / _size)); + effect.get_parameter("pCenter").set_float2(float(_center.first), float(_center.second)); + + { + auto op = _rendertarget->render(uint32_t(width), uint32_t(height)); + gs_ortho(0, 1., 0, 1., 0, 1.); + while (gs_effect_loop(effect.get_object(), "Rotate")) { + _data->get_gfx_util()->draw_fullscreen_triangle(); + } + } + } + + gs_blend_state_pop(); + + return _rendertarget->get_texture(); +} + +::streamfx::gfx::blur::type streamfx::gfx::blur::box_zoom::get_type() +{ + return ::streamfx::gfx::blur::type::Zoom; +} + +void streamfx::gfx::blur::box_zoom::set_center(double_t x, double_t y) +{ + _center.first = x; + _center.second = y; +} + +void streamfx::gfx::blur::box_zoom::get_center(double_t& x, double_t& y) +{ + x = _center.first; + y = _center.second; +} + +std::shared_ptr<::streamfx::obs::gs::texture> streamfx::gfx::blur::box_zoom::render() +{ + auto gctx = streamfx::obs::gs::context(); + +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + auto gdmp = streamfx::obs::gs::debug_marker(streamfx::obs::gs::debug_color_azure_radiance, "Box Zoom Blur"); +#endif + + float width = float(_input_texture->width()); + float height = float(_input_texture->height()); + + gs_blend_state_push(); + gs_reset_blend_state(); + gs_enable_color(true, true, true, true); + gs_enable_blending(false); + gs_enable_depth_test(false); + gs_enable_stencil_test(false); + gs_enable_stencil_write(false); + gs_set_cull_mode(GS_NEITHER); + gs_depth_function(GS_ALWAYS); + gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO); + gs_stencil_function(GS_STENCIL_BOTH, GS_ALWAYS); + gs_stencil_op(GS_STENCIL_BOTH, GS_ZERO, GS_ZERO, GS_ZERO); + + // One Pass Blur + streamfx::obs::gs::effect effect = _data->get_effect(); + if (effect) { + effect.get_parameter("pImage").set_texture(_input_texture); + effect.get_parameter("pImageTexel").set_float2(float(1.f / width), float(1.f / height)); + effect.get_parameter("pStepScale").set_float2(float(_step_scale.first), float(_step_scale.second)); + effect.get_parameter("pSize").set_float(float(_size)); + effect.get_parameter("pSizeInverseMul").set_float(float(1.0f / (float(_size) * 2.0f + 1.0f))); + effect.get_parameter("pCenter").set_float2(float(_center.first), float(_center.second)); + + { + auto op = _rendertarget->render(uint32_t(width), uint32_t(height)); + gs_ortho(0, 1., 0, 1., 0, 1.); + while (gs_effect_loop(effect.get_object(), "Zoom")) { + _data->get_gfx_util()->draw_fullscreen_triangle(); + } + } + } + + gs_blend_state_pop(); + + return _rendertarget->get_texture(); +} diff --git a/components/blur/source/gfx/blur/gfx-blur-box.hpp b/components/blur/source/gfx/blur/gfx-blur-box.hpp new file mode 100644 index 0000000..34fdae9 --- /dev/null +++ b/components/blur/source/gfx/blur/gfx-blur-box.hpp @@ -0,0 +1,150 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "common.hpp" +#include "gfx-blur-base.hpp" +#include "gfx/gfx-util.hpp" +#include "obs/gs/gs-effect.hpp" +#include "obs/gs/gs-texrender.hpp" +#include "obs/gs/gs-texture.hpp" + +#include "warning-disable.hpp" +#include +#include "warning-enable.hpp" + +namespace streamfx::gfx { + namespace blur { + class box_data { + streamfx::obs::gs::effect _effect; + std::shared_ptr _gfx_util; + + public: + box_data(); + virtual ~box_data(); + + std::shared_ptr get_gfx_util(); + + streamfx::obs::gs::effect get_effect(); + }; + + class box_factory : public ::streamfx::gfx::blur::ifactory { + std::mutex _data_lock; + std::weak_ptr<::streamfx::gfx::blur::box_data> _data; + + public: + box_factory(); + virtual ~box_factory() override; + + virtual bool is_type_supported(::streamfx::gfx::blur::type type) override; + + virtual std::shared_ptr<::streamfx::gfx::blur::base> create(::streamfx::gfx::blur::type type) override; + + virtual double_t get_min_size(::streamfx::gfx::blur::type type) override; + + virtual double_t get_step_size(::streamfx::gfx::blur::type type) override; + + virtual double_t get_max_size(::streamfx::gfx::blur::type type) override; + + virtual double_t get_min_angle(::streamfx::gfx::blur::type type) override; + + virtual double_t get_step_angle(::streamfx::gfx::blur::type type) override; + + virtual double_t get_max_angle(::streamfx::gfx::blur::type type) override; + + virtual bool is_step_scale_supported(::streamfx::gfx::blur::type type) override; + + virtual double_t get_min_step_scale_x(::streamfx::gfx::blur::type type) override; + + virtual double_t get_step_step_scale_x(::streamfx::gfx::blur::type type) override; + + virtual double_t get_max_step_scale_x(::streamfx::gfx::blur::type type) override; + + virtual double_t get_min_step_scale_y(::streamfx::gfx::blur::type type) override; + + virtual double_t get_step_step_scale_y(::streamfx::gfx::blur::type type) override; + + virtual double_t get_max_step_scale_y(::streamfx::gfx::blur::type type) override; + + std::shared_ptr<::streamfx::gfx::blur::box_data> data(); + + public: // Singleton + static ::streamfx::gfx::blur::box_factory& get(); + }; + + class box : public ::streamfx::gfx::blur::base { + protected: + std::shared_ptr<::streamfx::gfx::blur::box_data> _data; + + double_t _size; + std::pair _step_scale; + std::shared_ptr<::streamfx::obs::gs::texture> _input_texture; + std::shared_ptr<::streamfx::obs::gs::texrender> _rendertarget; + + private: + std::shared_ptr<::streamfx::obs::gs::texrender> _rendertarget2; + + public: + box(); + virtual ~box() override; + + virtual void set_input(std::shared_ptr<::streamfx::obs::gs::texture> texture) override; + + virtual ::streamfx::gfx::blur::type get_type() override; + + virtual double_t get_size() override; + virtual void set_size(double_t width) override; + + virtual void set_step_scale(double_t x, double_t y) override; + virtual void get_step_scale(double_t& x, double_t& y) override; + virtual double_t get_step_scale_x() override; + virtual double_t get_step_scale_y() override; + + virtual std::shared_ptr<::streamfx::obs::gs::texture> render() override; + virtual std::shared_ptr<::streamfx::obs::gs::texture> get() override; + }; + + class box_directional : public ::streamfx::gfx::blur::box, public ::streamfx::gfx::blur::base_angle { + double_t _angle; + + public: + box_directional(); + + virtual ::streamfx::gfx::blur::type get_type() override; + + virtual double_t get_angle() override; + virtual void set_angle(double_t angle) override; + + virtual std::shared_ptr<::streamfx::obs::gs::texture> render() override; + }; + + class box_rotational : public ::streamfx::gfx::blur::box, public ::streamfx::gfx::blur::base_angle, public ::streamfx::gfx::blur::base_center { + std::pair _center; + double_t _angle; + + public: + virtual ::streamfx::gfx::blur::type get_type() override; + + virtual void set_center(double_t x, double_t y) override; + virtual void get_center(double_t& x, double_t& y) override; + + virtual double_t get_angle() override; + virtual void set_angle(double_t angle) override; + + virtual std::shared_ptr<::streamfx::obs::gs::texture> render() override; + }; + + class box_zoom : public ::streamfx::gfx::blur::box, public ::streamfx::gfx::blur::base_center { + std::pair _center; + + public: + virtual ::streamfx::gfx::blur::type get_type() override; + + virtual void set_center(double_t x, double_t y) override; + virtual void get_center(double_t& x, double_t& y) override; + + virtual std::shared_ptr<::streamfx::obs::gs::texture> render() override; + }; + } // namespace blur +} // namespace streamfx::gfx diff --git a/components/blur/source/gfx/blur/gfx-blur-dual-filtering.cpp b/components/blur/source/gfx/blur/gfx-blur-dual-filtering.cpp new file mode 100644 index 0000000..980e396 --- /dev/null +++ b/components/blur/source/gfx/blur/gfx-blur-dual-filtering.cpp @@ -0,0 +1,316 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2022 lainon +// AUTOGENERATED COPYRIGHT HEADER END + +#include "gfx-blur-dual-filtering.hpp" +#include "common.hpp" +#include "obs/gs/gs-helper.hpp" +#include "plugin.hpp" + +#include "warning-disable.hpp" +#include +#include +#include "warning-enable.hpp" + +// Dual Filtering Blur +// +// This type of Blur uses downsampling and upsampling and clever math. That makes it less +// controllable compared to other blur, but it can still be worked with. The distance for +// sampling texels has to be adjusted to match the correct value so that lower levels of +// blur than 2^n are possible. +// +// That means that for a blur size of: +// 0: No Iterations, straight copy. +// 1: 1 Iteration (2x), Arm Size 2, Offset Scale 1.0 +// 2: 2 Iteration (4x), Arm Size 3, Offset Scale 0.5 +// 3: 2 Iteration (4x), Arm Size 4, Offset Scale 1.0 +// 4: 3 Iteration (8x), Arm Size 5, Offset Scale 0.25 +// 5: 3 Iteration (8x), Arm Size 6, Offset Scale 0.5 +// 6: 3 Iteration (8x), Arm Size 7, Offset Scale 0.75 +// 7: 3 Iteration (8x), Arm Size 8, Offset Scale 1.0 +// ... + +#define ST_MAX_LEVELS 16 + +streamfx::gfx::blur::dual_filtering_data::dual_filtering_data() : _gfx_util(::streamfx::gfx::util::get()) +{ + auto gctx = streamfx::obs::gs::context(); + { + auto file = streamfx::data_file_path("effects/blur/dual-filtering.effect"); + try { + _effect = streamfx::obs::gs::effect::create(file); + } catch (const std::exception& ex) { + DLOG_ERROR("Error loading '%s': %s", file.generic_u8string().c_str(), ex.what()); + } + } +} + +streamfx::gfx::blur::dual_filtering_data::~dual_filtering_data() +{ + auto gctx = streamfx::obs::gs::context(); + _effect.reset(); +} + +std::shared_ptr streamfx::gfx::blur::dual_filtering_data::get_gfx_util() +{ + return _gfx_util; +} + +streamfx::obs::gs::effect streamfx::gfx::blur::dual_filtering_data::get_effect() +{ + return _effect; +} + +streamfx::gfx::blur::dual_filtering_factory::dual_filtering_factory() {} + +streamfx::gfx::blur::dual_filtering_factory::~dual_filtering_factory() {} + +bool streamfx::gfx::blur::dual_filtering_factory::is_type_supported(::streamfx::gfx::blur::type type) +{ + switch (type) { + case ::streamfx::gfx::blur::type::Area: + return true; + default: + return false; + } +} + +std::shared_ptr<::streamfx::gfx::blur::base> streamfx::gfx::blur::dual_filtering_factory::create(::streamfx::gfx::blur::type type) +{ + switch (type) { + case ::streamfx::gfx::blur::type::Area: + return std::make_shared<::streamfx::gfx::blur::dual_filtering>(); + default: + throw std::runtime_error("Invalid type."); + } +} + +double_t streamfx::gfx::blur::dual_filtering_factory::get_min_size(::streamfx::gfx::blur::type) +{ + return double_t(1.); +} + +double_t streamfx::gfx::blur::dual_filtering_factory::get_step_size(::streamfx::gfx::blur::type) +{ + return double_t(1.); +} + +double_t streamfx::gfx::blur::dual_filtering_factory::get_max_size(::streamfx::gfx::blur::type) +{ + return double_t(ST_MAX_LEVELS); +} + +double_t streamfx::gfx::blur::dual_filtering_factory::get_min_angle(::streamfx::gfx::blur::type) +{ + return double_t(0); +} + +double_t streamfx::gfx::blur::dual_filtering_factory::get_step_angle(::streamfx::gfx::blur::type) +{ + return double_t(0); +} + +double_t streamfx::gfx::blur::dual_filtering_factory::get_max_angle(::streamfx::gfx::blur::type) +{ + return double_t(0); +} + +bool streamfx::gfx::blur::dual_filtering_factory::is_step_scale_supported(::streamfx::gfx::blur::type) +{ + return false; +} + +double_t streamfx::gfx::blur::dual_filtering_factory::get_min_step_scale_x(::streamfx::gfx::blur::type) +{ + return double_t(0); +} + +double_t streamfx::gfx::blur::dual_filtering_factory::get_step_step_scale_x(::streamfx::gfx::blur::type) +{ + return double_t(0); +} + +double_t streamfx::gfx::blur::dual_filtering_factory::get_max_step_scale_x(::streamfx::gfx::blur::type) +{ + return double_t(0); +} + +double_t streamfx::gfx::blur::dual_filtering_factory::get_min_step_scale_y(::streamfx::gfx::blur::type) +{ + return double_t(0); +} + +double_t streamfx::gfx::blur::dual_filtering_factory::get_step_step_scale_y(::streamfx::gfx::blur::type) +{ + return double_t(0); +} + +double_t streamfx::gfx::blur::dual_filtering_factory::get_max_step_scale_y(::streamfx::gfx::blur::type) +{ + return double_t(0); +} + +std::shared_ptr<::streamfx::gfx::blur::dual_filtering_data> streamfx::gfx::blur::dual_filtering_factory::data() +{ + std::unique_lock ulock(_data_lock); + std::shared_ptr<::streamfx::gfx::blur::dual_filtering_data> data = _data.lock(); + if (!data) { + data = std::make_shared<::streamfx::gfx::blur::dual_filtering_data>(); + _data = data; + } + return data; +} + +::streamfx::gfx::blur::dual_filtering_factory& streamfx::gfx::blur::dual_filtering_factory::get() +{ + static ::streamfx::gfx::blur::dual_filtering_factory instance; + return instance; +} + +streamfx::gfx::blur::dual_filtering::dual_filtering() : _data(::streamfx::gfx::blur::dual_filtering_factory::get().data()), _size(0), _iterations(0) +{ + auto gctx = streamfx::obs::gs::context(); + _rts.resize(ST_MAX_LEVELS + 1); + for (std::size_t n = 0; n <= ST_MAX_LEVELS; n++) { + gs_color_format cf = GS_RGBA; +#if 0 + cf = GS_RGBA16F; +#elif 0 + cf = GS_RGBA32F; +#endif + _rts[n] = std::make_shared(cf, GS_ZS_NONE); + } +} + +streamfx::gfx::blur::dual_filtering::~dual_filtering() {} + +void streamfx::gfx::blur::dual_filtering::set_input(std::shared_ptr<::streamfx::obs::gs::texture> texture) +{ + _input_texture = std::move(texture); +} + +::streamfx::gfx::blur::type streamfx::gfx::blur::dual_filtering::get_type() +{ + return ::streamfx::gfx::blur::type::Area; +} + +double_t streamfx::gfx::blur::dual_filtering::get_size() +{ + return _size; +} + +void streamfx::gfx::blur::dual_filtering::set_size(double_t width) +{ + _size = width; + _iterations = std::clamp(static_cast(round(width)), 0, ST_MAX_LEVELS); +} + +void streamfx::gfx::blur::dual_filtering::set_step_scale(double_t, double_t) {} + +void streamfx::gfx::blur::dual_filtering::get_step_scale(double_t&, double_t&) {} + +std::shared_ptr<::streamfx::obs::gs::texture> streamfx::gfx::blur::dual_filtering::render() +{ + auto gctx = streamfx::obs::gs::context(); + +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + auto gdmp = streamfx::obs::gs::debug_marker(streamfx::obs::gs::debug_color_azure_radiance, "Dual-Filtering Blur"); +#endif + + auto effect = _data->get_effect(); + if (!effect) { + return _input_texture; + } + + gs_blend_state_push(); + gs_reset_blend_state(); + gs_enable_color(true, true, true, true); + gs_enable_blending(false); + gs_enable_depth_test(false); + gs_enable_stencil_test(false); + gs_enable_stencil_write(false); + gs_set_cull_mode(GS_NEITHER); + gs_depth_function(GS_ALWAYS); + gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO); + gs_stencil_function(GS_STENCIL_BOTH, GS_ALWAYS); + gs_stencil_op(GS_STENCIL_BOTH, GS_ZERO, GS_ZERO, GS_ZERO); + + uint32_t width = _input_texture->width(); + uint32_t height = _input_texture->height(); + size_t iterations = _iterations; + + // Downsample + for (std::size_t n = 1; n <= iterations; n++) { +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + auto gdm = streamfx::obs::gs::debug_marker(streamfx::obs::gs::debug_color_azure_radiance, "Down %" PRIuMAX, n); +#endif + + // Select Texture + std::shared_ptr tex; + if (n > 1) { + tex = _rts[n - 1]->get_texture(); + } else { // Idx 0 is a simply considered as a straight copy of the original and not rendered to. + tex = _input_texture; + } + + // Reduce Size + uint32_t owidth = width >> n; + uint32_t oheight = height >> n; + if ((owidth == 0) || (oheight == 0)) { + iterations = n - 1; + break; + } + + // Apply + effect.get_parameter("pImage").set_texture(tex); + effect.get_parameter("pImageSize").set_float2(static_cast(owidth), static_cast(oheight)); + effect.get_parameter("pImageTexel").set_float2(0.5f / static_cast(owidth), 0.5f / static_cast(oheight)); + + { + auto op = _rts[n]->render(owidth, oheight); + gs_ortho(0., 1., 0., 1., 0., 1.); + while (gs_effect_loop(effect.get_object(), "Down")) { + _data->get_gfx_util()->draw_fullscreen_triangle(); + } + } + } + + // Upsample + for (std::size_t n = iterations; n > 0; n--) { +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + auto gdm = streamfx::obs::gs::debug_marker(streamfx::obs::gs::debug_color_azure_radiance, "Up %" PRIuMAX, n); +#endif + + // Select Texture + std::shared_ptr tex = _rts[n]->get_texture(); + + // Get Size + uint32_t iwidth = tex->width(); + uint32_t iheight = tex->height(); + uint32_t owidth = width >> (n - 1); + uint32_t oheight = height >> (n - 1); + + // Apply + effect.get_parameter("pImage").set_texture(tex); + effect.get_parameter("pImageSize").set_float2(static_cast(iwidth), static_cast(iheight)); + effect.get_parameter("pImageTexel").set_float2(0.5f / static_cast(iwidth), 0.5f / static_cast(iheight)); + + { + auto op = _rts[n - 1]->render(owidth, oheight); + gs_ortho(0., 1., 0., 1., 0., 1.); + while (gs_effect_loop(effect.get_object(), "Up")) { + _data->get_gfx_util()->draw_fullscreen_triangle(); + } + } + } + + gs_blend_state_pop(); + + return _rts[0]->get_texture(); +} + +std::shared_ptr<::streamfx::obs::gs::texture> streamfx::gfx::blur::dual_filtering::get() +{ + return _rts[0]->get_texture(); +} diff --git a/components/blur/source/gfx/blur/gfx-blur-dual-filtering.hpp b/components/blur/source/gfx/blur/gfx-blur-dual-filtering.hpp new file mode 100644 index 0000000..7f61948 --- /dev/null +++ b/components/blur/source/gfx/blur/gfx-blur-dual-filtering.hpp @@ -0,0 +1,108 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "common.hpp" +#include "gfx-blur-base.hpp" +#include "gfx/gfx-util.hpp" +#include "obs/gs/gs-effect.hpp" +#include "obs/gs/gs-texrender.hpp" +#include "obs/gs/gs-texture.hpp" + +#include "warning-disable.hpp" +#include +#include +#include "warning-enable.hpp" + +namespace streamfx::gfx { + namespace blur { + class dual_filtering_data { + streamfx::obs::gs::effect _effect; + std::shared_ptr _gfx_util; + + public: + dual_filtering_data(); + virtual ~dual_filtering_data(); + + std::shared_ptr get_gfx_util(); + + streamfx::obs::gs::effect get_effect(); + }; + + class dual_filtering_factory : public ::streamfx::gfx::blur::ifactory { + std::mutex _data_lock; + std::weak_ptr<::streamfx::gfx::blur::dual_filtering_data> _data; + + public: + dual_filtering_factory(); + virtual ~dual_filtering_factory() override; + + virtual bool is_type_supported(::streamfx::gfx::blur::type type) override; + + virtual std::shared_ptr<::streamfx::gfx::blur::base> create(::streamfx::gfx::blur::type type) override; + + virtual double_t get_min_size(::streamfx::gfx::blur::type type) override; + + virtual double_t get_step_size(::streamfx::gfx::blur::type type) override; + + virtual double_t get_max_size(::streamfx::gfx::blur::type type) override; + + virtual double_t get_min_angle(::streamfx::gfx::blur::type type) override; + + virtual double_t get_step_angle(::streamfx::gfx::blur::type type) override; + + virtual double_t get_max_angle(::streamfx::gfx::blur::type type) override; + + virtual bool is_step_scale_supported(::streamfx::gfx::blur::type type) override; + + virtual double_t get_min_step_scale_x(::streamfx::gfx::blur::type type) override; + + virtual double_t get_step_step_scale_x(::streamfx::gfx::blur::type type) override; + + virtual double_t get_max_step_scale_x(::streamfx::gfx::blur::type type) override; + + virtual double_t get_min_step_scale_y(::streamfx::gfx::blur::type type) override; + + virtual double_t get_step_step_scale_y(::streamfx::gfx::blur::type type) override; + + virtual double_t get_max_step_scale_y(::streamfx::gfx::blur::type type) override; + + std::shared_ptr<::streamfx::gfx::blur::dual_filtering_data> data(); + + public: // Singleton + static ::streamfx::gfx::blur::dual_filtering_factory& get(); + }; + + class dual_filtering : public ::streamfx::gfx::blur::base { + std::shared_ptr<::streamfx::gfx::blur::dual_filtering_data> _data; + + double_t _size; + std::size_t _iterations; + + std::shared_ptr _input_texture; + + std::vector> _rts; + + public: + dual_filtering(); + virtual ~dual_filtering() override; + + virtual void set_input(std::shared_ptr<::streamfx::obs::gs::texture> texture) override; + + virtual ::streamfx::gfx::blur::type get_type() override; + + virtual double_t get_size() override; + + virtual void set_size(double_t width) override; + + virtual void set_step_scale(double_t x, double_t y) override; + + virtual void get_step_scale(double_t& x, double_t& y) override; + + virtual std::shared_ptr<::streamfx::obs::gs::texture> render() override; + + virtual std::shared_ptr<::streamfx::obs::gs::texture> get() override; + }; + } // namespace blur +} // namespace streamfx::gfx diff --git a/components/blur/source/gfx/blur/gfx-blur-gaussian-linear.cpp b/components/blur/source/gfx/blur/gfx-blur-gaussian-linear.cpp new file mode 100644 index 0000000..6ecf286 --- /dev/null +++ b/components/blur/source/gfx/blur/gfx-blur-gaussian-linear.cpp @@ -0,0 +1,435 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2022 lainon +// AUTOGENERATED COPYRIGHT HEADER END + +#include "gfx-blur-gaussian-linear.hpp" +#include "common.hpp" +#include "obs/gs/gs-helper.hpp" + +#include "warning-disable.hpp" +#include +#include "warning-enable.hpp" + +// FIXME: This breaks when MAX_KERNEL_SIZE is changed, due to the way the Gaussian +// function first goes up at the point, and then once we pass the critical point +// will go down again and it is not handled well. This is a pretty basic +// approximation anyway at the moment. +#define ST_MAX_KERNEL_SIZE 128 +#define ST_MAX_BLUR_SIZE (ST_MAX_KERNEL_SIZE - 1) +#define ST_SEARCH_DENSITY double_t(1. / 500.) +#define ST_SEARCH_THRESHOLD double_t(1. / (ST_MAX_KERNEL_SIZE * 5)) +#define ST_SEARCH_EXTENSION 1 +#define ST_SEARCH_RANGE ST_MAX_KERNEL_SIZE * 2 + +streamfx::gfx::blur::gaussian_linear_data::gaussian_linear_data() : _gfx_util(::streamfx::gfx::util::get()) +{ + { + auto gctx = streamfx::obs::gs::context(); + + { + auto file = streamfx::data_file_path("effects/blur/gaussian-linear.effect"); + try { + _effect = streamfx::obs::gs::effect::create(file); + } catch (const std::exception& ex) { + DLOG_ERROR("Error loading '%s': %s", file.generic_u8string().c_str(), ex.what()); + } + } + } + + // Precalculate Kernels + for (std::size_t kernel_size = 1; kernel_size <= ST_MAX_BLUR_SIZE; kernel_size++) { + std::vector kernel_math(ST_MAX_KERNEL_SIZE); + std::vector kernel_data(ST_MAX_KERNEL_SIZE); + double_t actual_width = 1.; + + // Find actual kernel width. + for (double_t h = ST_SEARCH_DENSITY; h < ST_SEARCH_RANGE; h += ST_SEARCH_DENSITY) { + if (streamfx::util::math::gaussian(double_t(kernel_size + ST_SEARCH_EXTENSION), h) > ST_SEARCH_THRESHOLD) { + actual_width = h; + break; + } + } + + // Calculate and normalize + double_t sum = 0; + for (std::size_t p = 0; p <= kernel_size; p++) { + kernel_math[p] = streamfx::util::math::gaussian(double_t(p), actual_width); + sum += kernel_math[p] * (p > 0 ? 2 : 1); + } + + // Normalize to fill the entire 0..1 range over the width. + double_t inverse_sum = 1.0 / sum; + for (std::size_t p = 0; p <= kernel_size; p++) { + kernel_data.at(p) = float(kernel_math[p] * inverse_sum); + } + + _kernels.push_back(std::move(kernel_data)); + } +} + +streamfx::gfx::blur::gaussian_linear_data::~gaussian_linear_data() +{ + _effect.reset(); +} + +streamfx::obs::gs::effect streamfx::gfx::blur::gaussian_linear_data::get_effect() +{ + return _effect; +} + +std::vector const& streamfx::gfx::blur::gaussian_linear_data::get_kernel(std::size_t width) +{ + if (width < 1) + width = 1; + if (width > ST_MAX_BLUR_SIZE) + width = ST_MAX_BLUR_SIZE; + width -= 1; + return _kernels[width]; +} + +std::shared_ptr streamfx::gfx::blur::gaussian_linear_data::get_gfx_util() +{ + return _gfx_util; +} + +streamfx::gfx::blur::gaussian_linear_factory::gaussian_linear_factory() {} + +streamfx::gfx::blur::gaussian_linear_factory::~gaussian_linear_factory() {} + +bool streamfx::gfx::blur::gaussian_linear_factory::is_type_supported(::streamfx::gfx::blur::type v) +{ + switch (v) { + case ::streamfx::gfx::blur::type::Area: + case ::streamfx::gfx::blur::type::Directional: + return true; + default: + return false; + } +} + +std::shared_ptr<::streamfx::gfx::blur::base> streamfx::gfx::blur::gaussian_linear_factory::create(::streamfx::gfx::blur::type v) +{ + switch (v) { + case ::streamfx::gfx::blur::type::Area: + return std::make_shared<::streamfx::gfx::blur::gaussian_linear>(); + case ::streamfx::gfx::blur::type::Directional: + return std::static_pointer_cast<::streamfx::gfx::blur::gaussian_linear>(std::make_shared<::streamfx::gfx::blur::gaussian_linear_directional>()); + default: + throw std::runtime_error("Invalid type."); + } +} + +double_t streamfx::gfx::blur::gaussian_linear_factory::get_min_size(::streamfx::gfx::blur::type) +{ + return double_t(1.0); +} + +double_t streamfx::gfx::blur::gaussian_linear_factory::get_step_size(::streamfx::gfx::blur::type) +{ + return double_t(1.0); +} + +double_t streamfx::gfx::blur::gaussian_linear_factory::get_max_size(::streamfx::gfx::blur::type) +{ + return double_t(ST_MAX_BLUR_SIZE); +} + +double_t streamfx::gfx::blur::gaussian_linear_factory::get_min_angle(::streamfx::gfx::blur::type v) +{ + switch (v) { + case ::streamfx::gfx::blur::type::Directional: + case ::streamfx::gfx::blur::type::Rotational: + return -180.0; + default: + return 0; + } +} + +double_t streamfx::gfx::blur::gaussian_linear_factory::get_step_angle(::streamfx::gfx::blur::type) +{ + return double_t(0.01); +} + +double_t streamfx::gfx::blur::gaussian_linear_factory::get_max_angle(::streamfx::gfx::blur::type v) +{ + switch (v) { + case ::streamfx::gfx::blur::type::Directional: + case ::streamfx::gfx::blur::type::Rotational: + return 180.0; + default: + return 0; + } +} + +bool streamfx::gfx::blur::gaussian_linear_factory::is_step_scale_supported(::streamfx::gfx::blur::type v) +{ + switch (v) { + case ::streamfx::gfx::blur::type::Area: + case ::streamfx::gfx::blur::type::Zoom: + case ::streamfx::gfx::blur::type::Directional: + return true; + default: + return false; + } +} + +double_t streamfx::gfx::blur::gaussian_linear_factory::get_min_step_scale_x(::streamfx::gfx::blur::type) +{ + return double_t(0.01); +} + +double_t streamfx::gfx::blur::gaussian_linear_factory::get_step_step_scale_x(::streamfx::gfx::blur::type) +{ + return double_t(0.01); +} + +double_t streamfx::gfx::blur::gaussian_linear_factory::get_max_step_scale_x(::streamfx::gfx::blur::type) +{ + return double_t(1000.0); +} + +double_t streamfx::gfx::blur::gaussian_linear_factory::get_min_step_scale_y(::streamfx::gfx::blur::type) +{ + return double_t(0.01); +} + +double_t streamfx::gfx::blur::gaussian_linear_factory::get_step_step_scale_y(::streamfx::gfx::blur::type) +{ + return double_t(0.01); +} + +double_t streamfx::gfx::blur::gaussian_linear_factory::get_max_step_scale_y(::streamfx::gfx::blur::type) +{ + return double_t(1000.0); +} + +std::shared_ptr<::streamfx::gfx::blur::gaussian_linear_data> streamfx::gfx::blur::gaussian_linear_factory::data() +{ + std::unique_lock ulock(_data_lock); + std::shared_ptr<::streamfx::gfx::blur::gaussian_linear_data> data = _data.lock(); + if (!data) { + data = std::make_shared<::streamfx::gfx::blur::gaussian_linear_data>(); + _data = data; + } + return data; +} + +::streamfx::gfx::blur::gaussian_linear_factory& streamfx::gfx::blur::gaussian_linear_factory::get() +{ + static ::streamfx::gfx::blur::gaussian_linear_factory instance; + return instance; +} + +streamfx::gfx::blur::gaussian_linear::gaussian_linear() : _data(::streamfx::gfx::blur::gaussian_linear_factory::get().data()), _size(1.), _step_scale({1., 1.}) +{ + auto gctx = streamfx::obs::gs::context(); + + _rendertarget = std::make_shared(GS_RGBA, GS_ZS_NONE); + _rendertarget2 = std::make_shared(GS_RGBA, GS_ZS_NONE); +} + +streamfx::gfx::blur::gaussian_linear::~gaussian_linear() {} + +void streamfx::gfx::blur::gaussian_linear::set_input(std::shared_ptr<::streamfx::obs::gs::texture> texture) +{ + _input_texture = std::move(texture); +} + +::streamfx::gfx::blur::type streamfx::gfx::blur::gaussian_linear::get_type() +{ + return ::streamfx::gfx::blur::type::Area; +} + +double_t streamfx::gfx::blur::gaussian_linear::get_size() +{ + return _size; +} + +void streamfx::gfx::blur::gaussian_linear::set_size(double_t width) +{ + if (width < 1.) + width = 1.; + if (width > ST_MAX_BLUR_SIZE) + width = ST_MAX_BLUR_SIZE; + _size = width; +} + +void streamfx::gfx::blur::gaussian_linear::set_step_scale(double_t x, double_t y) +{ + _step_scale.first = x; + _step_scale.second = y; +} + +void streamfx::gfx::blur::gaussian_linear::get_step_scale(double_t& x, double_t& y) +{ + x = _step_scale.first; + y = _step_scale.second; +} + +double_t streamfx::gfx::blur::gaussian_linear::get_step_scale_x() +{ + return _step_scale.first; +} + +double_t streamfx::gfx::blur::gaussian_linear::get_step_scale_y() +{ + return _step_scale.second; +} + +std::shared_ptr<::streamfx::obs::gs::texture> streamfx::gfx::blur::gaussian_linear::render() +{ + auto gctx = streamfx::obs::gs::context(); + +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + auto gdmp = streamfx::obs::gs::debug_marker(streamfx::obs::gs::debug_color_azure_radiance, "Gaussian Linear Blur"); +#endif + + streamfx::obs::gs::effect effect = _data->get_effect(); + auto kernel = _data->get_kernel(size_t(_size)); + + if (!effect || ((_step_scale.first + _step_scale.second) < std::numeric_limits::epsilon())) { + return _input_texture; + } + + float width = float(_input_texture->width()); + float height = float(_input_texture->height()); + + // Setup + gs_set_cull_mode(GS_NEITHER); + gs_enable_color(true, true, true, true); + gs_enable_depth_test(false); + gs_depth_function(GS_ALWAYS); + gs_blend_state_push(); + gs_reset_blend_state(); + gs_enable_blending(false); + gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO); + gs_enable_stencil_test(false); + gs_enable_stencil_write(false); + gs_stencil_function(GS_STENCIL_BOTH, GS_ALWAYS); + gs_stencil_op(GS_STENCIL_BOTH, GS_ZERO, GS_ZERO, GS_ZERO); + + effect.get_parameter("pImage").set_texture(_input_texture); + effect.get_parameter("pStepScale").set_float2(float(_step_scale.first), float(_step_scale.second)); + effect.get_parameter("pSize").set_float(float(_size)); + effect.get_parameter("pKernel").set_value(kernel.data(), ST_MAX_KERNEL_SIZE); + + // First Pass + if (_step_scale.first > std::numeric_limits::epsilon()) { + effect.get_parameter("pImageTexel").set_float2(float(1.f / width), 0.f); + + { +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + auto gdm = streamfx::obs::gs::debug_marker(streamfx::obs::gs::debug_color_azure_radiance, "Horizontal"); +#endif + + auto op = _rendertarget2->render(uint32_t(width), uint32_t(height)); + gs_ortho(0, 1., 0, 1., 0, 1.); + while (gs_effect_loop(effect.get_object(), "Draw")) { + _data->get_gfx_util()->draw_fullscreen_triangle(); + } + } + + std::swap(_rendertarget, _rendertarget2); + effect.get_parameter("pImage").set_texture(_rendertarget->get_texture()); + } + + // Second Pass + if (_step_scale.second > std::numeric_limits::epsilon()) { + effect.get_parameter("pImageTexel").set_float2(0.f, float(1.f / height)); + + { +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + auto gdm = streamfx::obs::gs::debug_marker(streamfx::obs::gs::debug_color_azure_radiance, "Vertical"); +#endif + + auto op = _rendertarget2->render(uint32_t(width), uint32_t(height)); + gs_ortho(0, 1., 0, 1., 0, 1.); + while (gs_effect_loop(effect.get_object(), "Draw")) { + _data->get_gfx_util()->draw_fullscreen_triangle(); + } + } + + std::swap(_rendertarget, _rendertarget2); + } + + gs_blend_state_pop(); + + return this->get(); +} + +std::shared_ptr<::streamfx::obs::gs::texture> streamfx::gfx::blur::gaussian_linear::get() +{ + return _rendertarget->get_texture(); +} + +streamfx::gfx::blur::gaussian_linear_directional::gaussian_linear_directional() : _angle(0.) {} + +streamfx::gfx::blur::gaussian_linear_directional::~gaussian_linear_directional() {} + +::streamfx::gfx::blur::type streamfx::gfx::blur::gaussian_linear_directional::get_type() +{ + return ::streamfx::gfx::blur::type::Directional; +} + +double_t streamfx::gfx::blur::gaussian_linear_directional::get_angle() +{ + return D_RAD_TO_DEG(_angle); +} + +void streamfx::gfx::blur::gaussian_linear_directional::set_angle(double_t angle) +{ + _angle = D_DEG_TO_RAD(angle); +} + +std::shared_ptr<::streamfx::obs::gs::texture> streamfx::gfx::blur::gaussian_linear_directional::render() +{ + auto gctx = streamfx::obs::gs::context(); + +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + auto gdmp = streamfx::obs::gs::debug_marker(streamfx::obs::gs::debug_color_azure_radiance, "Gaussian Linear Directional Blur"); +#endif + + streamfx::obs::gs::effect effect = _data->get_effect(); + auto kernel = _data->get_kernel(size_t(_size)); + + if (!effect || ((_step_scale.first + _step_scale.second) < std::numeric_limits::epsilon())) { + return _input_texture; + } + + float width = float(_input_texture->width()); + float height = float(_input_texture->height()); + + // Setup + gs_set_cull_mode(GS_NEITHER); + gs_enable_color(true, true, true, true); + gs_enable_depth_test(false); + gs_depth_function(GS_ALWAYS); + gs_blend_state_push(); + gs_reset_blend_state(); + gs_enable_blending(false); + gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO); + gs_enable_stencil_test(false); + gs_enable_stencil_write(false); + gs_stencil_function(GS_STENCIL_BOTH, GS_ALWAYS); + gs_stencil_op(GS_STENCIL_BOTH, GS_ZERO, GS_ZERO, GS_ZERO); + + effect.get_parameter("pImage").set_texture(_input_texture); + effect.get_parameter("pImageTexel").set_float2(float(1.f / width * cos(_angle)), float(1.f / height * sin(_angle))); + effect.get_parameter("pStepScale").set_float2(float(_step_scale.first), float(_step_scale.second)); + effect.get_parameter("pSize").set_float(float(_size)); + effect.get_parameter("pKernel").set_value(kernel.data(), ST_MAX_KERNEL_SIZE); + + // First Pass + { + auto op = _rendertarget->render(uint32_t(width), uint32_t(height)); + gs_ortho(0, 1., 0, 1., 0, 1.); + while (gs_effect_loop(effect.get_object(), "Draw")) { + _data->get_gfx_util()->draw_fullscreen_triangle(); + } + } + + gs_blend_state_pop(); + + return this->get(); +} diff --git a/components/blur/source/gfx/blur/gfx-blur-gaussian-linear.hpp b/components/blur/source/gfx/blur/gfx-blur-gaussian-linear.hpp new file mode 100644 index 0000000..d5dbcc2 --- /dev/null +++ b/components/blur/source/gfx/blur/gfx-blur-gaussian-linear.hpp @@ -0,0 +1,133 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "common.hpp" +#include "gfx-blur-base.hpp" +#include "gfx/gfx-util.hpp" +#include "obs/gs/gs-effect.hpp" +#include "obs/gs/gs-texrender.hpp" +#include "obs/gs/gs-texture.hpp" + +#include "warning-disable.hpp" +#include +#include +#include "warning-enable.hpp" + +namespace streamfx::gfx { + namespace blur { + class gaussian_linear_data { + streamfx::obs::gs::effect _effect; + std::shared_ptr _gfx_util; + std::vector> _kernels; + + public: + gaussian_linear_data(); + virtual ~gaussian_linear_data(); + + std::shared_ptr get_gfx_util(); + + streamfx::obs::gs::effect get_effect(); + + std::vector const& get_kernel(std::size_t width); + }; + + class gaussian_linear_factory : public ::streamfx::gfx::blur::ifactory { + std::mutex _data_lock; + std::weak_ptr<::streamfx::gfx::blur::gaussian_linear_data> _data; + + public: + gaussian_linear_factory(); + virtual ~gaussian_linear_factory() override; + + virtual bool is_type_supported(::streamfx::gfx::blur::type type) override; + + virtual std::shared_ptr<::streamfx::gfx::blur::base> create(::streamfx::gfx::blur::type type) override; + + virtual double_t get_min_size(::streamfx::gfx::blur::type type) override; + + virtual double_t get_step_size(::streamfx::gfx::blur::type type) override; + + virtual double_t get_max_size(::streamfx::gfx::blur::type type) override; + + virtual double_t get_min_angle(::streamfx::gfx::blur::type type) override; + + virtual double_t get_step_angle(::streamfx::gfx::blur::type type) override; + + virtual double_t get_max_angle(::streamfx::gfx::blur::type type) override; + + virtual bool is_step_scale_supported(::streamfx::gfx::blur::type type) override; + + virtual double_t get_min_step_scale_x(::streamfx::gfx::blur::type type) override; + + virtual double_t get_step_step_scale_x(::streamfx::gfx::blur::type type) override; + + virtual double_t get_max_step_scale_x(::streamfx::gfx::blur::type type) override; + + virtual double_t get_min_step_scale_y(::streamfx::gfx::blur::type type) override; + + virtual double_t get_step_step_scale_y(::streamfx::gfx::blur::type type) override; + + virtual double_t get_max_step_scale_y(::streamfx::gfx::blur::type type) override; + + std::shared_ptr<::streamfx::gfx::blur::gaussian_linear_data> data(); + + public: // Singleton + static ::streamfx::gfx::blur::gaussian_linear_factory& get(); + }; + + class gaussian_linear : public ::streamfx::gfx::blur::base { + protected: + std::shared_ptr<::streamfx::gfx::blur::gaussian_linear_data> _data; + + double_t _size; + std::pair _step_scale; + std::shared_ptr<::streamfx::obs::gs::texture> _input_texture; + std::shared_ptr<::streamfx::obs::gs::texrender> _rendertarget; + + private: + std::shared_ptr<::streamfx::obs::gs::texrender> _rendertarget2; + + public: + gaussian_linear(); + virtual ~gaussian_linear() override; + + virtual void set_input(std::shared_ptr<::streamfx::obs::gs::texture> texture) override; + + virtual ::streamfx::gfx::blur::type get_type() override; + + virtual double_t get_size() override; + + virtual void set_size(double_t width) override; + + virtual void set_step_scale(double_t x, double_t y) override; + + virtual void get_step_scale(double_t& x, double_t& y) override; + + virtual double_t get_step_scale_x() override; + + virtual double_t get_step_scale_y() override; + + virtual std::shared_ptr<::streamfx::obs::gs::texture> render() override; + + virtual std::shared_ptr<::streamfx::obs::gs::texture> get() override; + }; + + class gaussian_linear_directional : public ::streamfx::gfx::blur::gaussian_linear, public ::streamfx::gfx::blur::base_angle { + double_t _angle; + + public: + gaussian_linear_directional(); + virtual ~gaussian_linear_directional() override; + + virtual ::streamfx::gfx::blur::type get_type() override; + + virtual double_t get_angle() override; + + virtual void set_angle(double_t angle) override; + + virtual std::shared_ptr<::streamfx::obs::gs::texture> render() override; + }; + } // namespace blur +} // namespace streamfx::gfx diff --git a/components/blur/source/gfx/blur/gfx-blur-gaussian.cpp b/components/blur/source/gfx/blur/gfx-blur-gaussian.cpp new file mode 100644 index 0000000..5a0d19c --- /dev/null +++ b/components/blur/source/gfx/blur/gfx-blur-gaussian.cpp @@ -0,0 +1,614 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2022 lainon +// AUTOGENERATED COPYRIGHT HEADER END + +#include "gfx-blur-gaussian.hpp" +#include "common.hpp" +#include "gfx/gfx-util.hpp" +#include "obs/gs/gs-helper.hpp" +#include "plugin.hpp" + +#include "warning-disable.hpp" +#include +#include +#include "warning-enable.hpp" + +// TODO: It may be possible to optimize to run much faster: https://rastergrid.com/blog/2010/09/efficient-gaussian-blur-with-linear-sampling/ + +#define ST_KERNEL_SIZE 128u +#define ST_OVERSAMPLE_MULTIPLIER 2 +#define ST_MAX_BLUR_SIZE ST_KERNEL_SIZE / ST_OVERSAMPLE_MULTIPLIER + +streamfx::gfx::blur::gaussian_data::gaussian_data() : _gfx_util(::streamfx::gfx::util::get()) +{ + using namespace streamfx::util; + + std::array kernel_dbl; + std::vector kernel(ST_KERNEL_SIZE); + + { + auto gctx = streamfx::obs::gs::context(); + + { + auto file = streamfx::data_file_path("effects/blur/gaussian.effect"); + try { + _effect = streamfx::obs::gs::effect::create(file); + } catch (const std::exception& ex) { + DLOG_ERROR("Error loading '%s': %s", file.generic_u8string().c_str(), ex.what()); + } + } + } + + //#define ST_USE_PASCAL_TRIANGLE + + // Pre-calculate Kernel Information for all Kernel sizes + for (size_t size = 1; size <= ST_MAX_BLUR_SIZE; size++) { +#ifdef ST_USE_PASCAL_TRIANGLE + // The Pascal Triangle can be used to generate Gaussian Kernels, which is + // significantly faster than doing the same task with searching. It is also + // much more accurate at the same time, so it is a 2-in-1 solution. + + // Generate the required row and sum. + size_t offset = size; + size_t row = size * 2; + auto triangle = math::pascal_triangle(row); + double sum = pow(2, row); + + // Convert all integers to floats. + double accum = 0.; + for (size_t idx = offset; idx < std::min(triangle.size(), ST_KERNEL_SIZE); idx++) { + double v = static_cast(triangle[idx]) / sum; + kernel_dbl[idx - offset] = v; + // Accumulator needed as we end up with float inaccuracies above a certain threshold. + accum += v * (idx > offset ? 2 : 1); + } + + // Rescale all values back into useful ranges. + accum = 1. / accum; + for (size_t idx = offset; idx < ST_KERNEL_SIZE; idx++) { + kernel[idx - offset] = kernel_dbl[idx - offset] * accum; + } +#else + size_t oversample = size * ST_OVERSAMPLE_MULTIPLIER; + + // Generate initial weights and calculate a total from them. + double total = 0.; + for (size_t idx = 0; (idx < oversample) && (idx < ST_KERNEL_SIZE); idx++) { + kernel_dbl[idx] = math::gaussian(static_cast(idx), static_cast(size)); + total += kernel_dbl[idx] * (idx > 0 ? 2 : 1); + } + + // Scale the weights according to the total gathered, and convert to float. + for (size_t idx = 0; (idx < oversample) && (idx < ST_KERNEL_SIZE); idx++) { + kernel_dbl[idx] /= total; + kernel[idx] = static_cast(kernel_dbl[idx]); + } + +#endif + + // Store Kernel + _kernels.insert_or_assign(size, kernel); + } +} + +streamfx::gfx::blur::gaussian_data::~gaussian_data() +{ + auto gctx = streamfx::obs::gs::context(); + _effect.reset(); +} + +streamfx::obs::gs::effect streamfx::gfx::blur::gaussian_data::get_effect() +{ + return _effect; +} + +std::shared_ptr streamfx::gfx::blur::gaussian_data::get_gfx_util() +{ + return _gfx_util; +} + +std::vector const& streamfx::gfx::blur::gaussian_data::get_kernel(std::size_t width) +{ + width = std::clamp(width, 1, ST_MAX_BLUR_SIZE); + return _kernels.at(width); +} + +streamfx::gfx::blur::gaussian_factory::gaussian_factory() {} + +streamfx::gfx::blur::gaussian_factory::~gaussian_factory() {} + +bool streamfx::gfx::blur::gaussian_factory::is_type_supported(::streamfx::gfx::blur::type v) +{ + switch (v) { + case ::streamfx::gfx::blur::type::Area: + return true; + case ::streamfx::gfx::blur::type::Directional: + return true; + case ::streamfx::gfx::blur::type::Rotational: + return true; + case ::streamfx::gfx::blur::type::Zoom: + return true; + default: + return false; + } +} + +std::shared_ptr<::streamfx::gfx::blur::base> streamfx::gfx::blur::gaussian_factory::create(::streamfx::gfx::blur::type v) +{ + switch (v) { + case ::streamfx::gfx::blur::type::Area: + return std::make_shared<::streamfx::gfx::blur::gaussian>(); + case ::streamfx::gfx::blur::type::Directional: + return std::static_pointer_cast<::streamfx::gfx::blur::gaussian>(std::make_shared<::streamfx::gfx::blur::gaussian_directional>()); + case ::streamfx::gfx::blur::type::Rotational: + return std::make_shared<::streamfx::gfx::blur::gaussian_rotational>(); + case ::streamfx::gfx::blur::type::Zoom: + return std::make_shared<::streamfx::gfx::blur::gaussian_zoom>(); + default: + throw std::runtime_error("Invalid type."); + } +} + +double_t streamfx::gfx::blur::gaussian_factory::get_min_size(::streamfx::gfx::blur::type) +{ + return double_t(1.0); +} + +double_t streamfx::gfx::blur::gaussian_factory::get_step_size(::streamfx::gfx::blur::type) +{ + return double_t(1.0); +} + +double_t streamfx::gfx::blur::gaussian_factory::get_max_size(::streamfx::gfx::blur::type) +{ + return double_t(ST_MAX_BLUR_SIZE); +} + +double_t streamfx::gfx::blur::gaussian_factory::get_min_angle(::streamfx::gfx::blur::type v) +{ + switch (v) { + case ::streamfx::gfx::blur::type::Directional: + case ::streamfx::gfx::blur::type::Rotational: + return -180.0; + default: + return 0; + } +} + +double_t streamfx::gfx::blur::gaussian_factory::get_step_angle(::streamfx::gfx::blur::type) +{ + return double_t(0.01); +} + +double_t streamfx::gfx::blur::gaussian_factory::get_max_angle(::streamfx::gfx::blur::type v) +{ + switch (v) { + case ::streamfx::gfx::blur::type::Directional: + case ::streamfx::gfx::blur::type::Rotational: + return 180.0; + default: + return 0; + } +} + +bool streamfx::gfx::blur::gaussian_factory::is_step_scale_supported(::streamfx::gfx::blur::type v) +{ + switch (v) { + case ::streamfx::gfx::blur::type::Area: + case ::streamfx::gfx::blur::type::Zoom: + case ::streamfx::gfx::blur::type::Directional: + return true; + default: + return false; + } +} + +double_t streamfx::gfx::blur::gaussian_factory::get_min_step_scale_x(::streamfx::gfx::blur::type) +{ + return double_t(0.01); +} + +double_t streamfx::gfx::blur::gaussian_factory::get_step_step_scale_x(::streamfx::gfx::blur::type) +{ + return double_t(0.01); +} + +double_t streamfx::gfx::blur::gaussian_factory::get_max_step_scale_x(::streamfx::gfx::blur::type) +{ + return double_t(1000.0); +} + +double_t streamfx::gfx::blur::gaussian_factory::get_min_step_scale_y(::streamfx::gfx::blur::type) +{ + return double_t(0.01); +} + +double_t streamfx::gfx::blur::gaussian_factory::get_step_step_scale_y(::streamfx::gfx::blur::type) +{ + return double_t(0.01); +} + +double_t streamfx::gfx::blur::gaussian_factory::get_max_step_scale_y(::streamfx::gfx::blur::type) +{ + return double_t(1000.0); +} + +std::shared_ptr<::streamfx::gfx::blur::gaussian_data> streamfx::gfx::blur::gaussian_factory::data() +{ + std::unique_lock ulock(_data_lock); + std::shared_ptr<::streamfx::gfx::blur::gaussian_data> data = _data.lock(); + if (!data) { + data = std::make_shared<::streamfx::gfx::blur::gaussian_data>(); + _data = data; + } + return data; +} + +::streamfx::gfx::blur::gaussian_factory& streamfx::gfx::blur::gaussian_factory::get() +{ + static ::streamfx::gfx::blur::gaussian_factory instance; + return instance; +} + +streamfx::gfx::blur::gaussian::gaussian() : _data(::streamfx::gfx::blur::gaussian_factory::get().data()), _size(1.), _step_scale({1., 1.}) +{ + auto gctx = streamfx::obs::gs::context(); + _rendertarget = std::make_shared(GS_RGBA, GS_ZS_NONE); + _rendertarget2 = std::make_shared(GS_RGBA, GS_ZS_NONE); +} + +streamfx::gfx::blur::gaussian::~gaussian() {} + +void streamfx::gfx::blur::gaussian::set_input(std::shared_ptr<::streamfx::obs::gs::texture> texture) +{ + _input_texture = std::move(texture); +} + +::streamfx::gfx::blur::type streamfx::gfx::blur::gaussian::get_type() +{ + return ::streamfx::gfx::blur::type::Area; +} + +double_t streamfx::gfx::blur::gaussian::get_size() +{ + return _size; +} + +void streamfx::gfx::blur::gaussian::set_size(double_t width) +{ + if (width < 1.) + width = 1.; + if (width > ST_MAX_BLUR_SIZE) + width = ST_MAX_BLUR_SIZE; + _size = width; +} + +void streamfx::gfx::blur::gaussian::set_step_scale(double_t x, double_t y) +{ + _step_scale.first = x; + _step_scale.second = y; +} + +void streamfx::gfx::blur::gaussian::get_step_scale(double_t& x, double_t& y) +{ + x = _step_scale.first; + y = _step_scale.second; +} + +double_t streamfx::gfx::blur::gaussian::get_step_scale_x() +{ + return _step_scale.first; +} + +double_t streamfx::gfx::blur::gaussian::get_step_scale_y() +{ + return _step_scale.second; +} + +std::shared_ptr<::streamfx::obs::gs::texture> streamfx::gfx::blur::gaussian::render() +{ + auto gctx = streamfx::obs::gs::context(); + +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + auto gdmp = streamfx::obs::gs::debug_marker(streamfx::obs::gs::debug_color_azure_radiance, "Gaussian Blur"); +#endif + + streamfx::obs::gs::effect effect = _data->get_effect(); + + if (!effect || ((_step_scale.first + _step_scale.second) < std::numeric_limits::epsilon())) { + return _input_texture; + } + + auto kernel = _data->get_kernel(size_t(_size)); + float width = float(_input_texture->width()); + float height = float(_input_texture->height()); + + // Setup + gs_set_cull_mode(GS_NEITHER); + gs_enable_color(true, true, true, true); + gs_enable_depth_test(false); + gs_depth_function(GS_ALWAYS); + gs_blend_state_push(); + gs_reset_blend_state(); + gs_enable_blending(false); + gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO); + gs_enable_stencil_test(false); + gs_enable_stencil_write(false); + gs_stencil_function(GS_STENCIL_BOTH, GS_ALWAYS); + gs_stencil_op(GS_STENCIL_BOTH, GS_ZERO, GS_ZERO, GS_ZERO); + + effect.get_parameter("pStepScale").set_float2(float(_step_scale.first), float(_step_scale.second)); + effect.get_parameter("pSize").set_float(float(_size * ST_OVERSAMPLE_MULTIPLIER)); + effect.get_parameter("pKernel").set_value(kernel.data(), ST_KERNEL_SIZE); + + // First Pass + if (_step_scale.first > std::numeric_limits::epsilon()) { + effect.get_parameter("pImage").set_texture(_input_texture); + effect.get_parameter("pImageTexel").set_float2(float(1.f / width), 0.f); + + { +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + auto gdm = streamfx::obs::gs::debug_marker(streamfx::obs::gs::debug_color_azure_radiance, "Horizontal"); +#endif + + auto op = _rendertarget2->render(uint32_t(width), uint32_t(height)); + gs_ortho(0, 1., 0, 1., 0, 1.); + while (gs_effect_loop(effect.get_object(), "Draw")) { + _data->get_gfx_util()->draw_fullscreen_triangle(); + } + } + + std::swap(_rendertarget, _rendertarget2); + } + + // Second Pass + if (_step_scale.second > std::numeric_limits::epsilon()) { + effect.get_parameter("pImage").set_texture(_rendertarget->get_texture()); + effect.get_parameter("pImageTexel").set_float2(0.f, float(1.f / height)); + + { +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + auto gdm = streamfx::obs::gs::debug_marker(streamfx::obs::gs::debug_color_azure_radiance, "Vertical"); +#endif + + auto op = _rendertarget2->render(uint32_t(width), uint32_t(height)); + gs_ortho(0, 1., 0, 1., 0, 1.); + while (gs_effect_loop(effect.get_object(), "Draw")) { + _data->get_gfx_util()->draw_fullscreen_triangle(); + } + } + + std::swap(_rendertarget, _rendertarget2); + } + + gs_blend_state_pop(); + + return this->get(); +} + +std::shared_ptr<::streamfx::obs::gs::texture> streamfx::gfx::blur::gaussian::get() +{ + return _rendertarget->get_texture(); +} + +streamfx::gfx::blur::gaussian_directional::gaussian_directional() : m_angle(0.) {} + +streamfx::gfx::blur::gaussian_directional::~gaussian_directional() {} + +::streamfx::gfx::blur::type streamfx::gfx::blur::gaussian_directional::get_type() +{ + return ::streamfx::gfx::blur::type::Directional; +} + +double_t streamfx::gfx::blur::gaussian_directional::get_angle() +{ + return D_RAD_TO_DEG(m_angle); +} + +void streamfx::gfx::blur::gaussian_directional::set_angle(double_t angle) +{ + m_angle = D_DEG_TO_RAD(angle); +} + +std::shared_ptr<::streamfx::obs::gs::texture> streamfx::gfx::blur::gaussian_directional::render() +{ + auto gctx = streamfx::obs::gs::context(); + +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + auto gdmp = streamfx::obs::gs::debug_marker(streamfx::obs::gs::debug_color_azure_radiance, "Gaussian Directional Blur"); +#endif + + streamfx::obs::gs::effect effect = _data->get_effect(); + + if (!effect || ((_step_scale.first + _step_scale.second) < std::numeric_limits::epsilon())) { + return _input_texture; + } + + auto kernel = _data->get_kernel(size_t(_size)); + float width = float(_input_texture->width()); + float height = float(_input_texture->height()); + + // Setup + gs_set_cull_mode(GS_NEITHER); + gs_enable_color(true, true, true, true); + gs_enable_depth_test(false); + gs_depth_function(GS_ALWAYS); + gs_blend_state_push(); + gs_reset_blend_state(); + gs_enable_blending(false); + gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO); + gs_enable_stencil_test(false); + gs_enable_stencil_write(false); + gs_stencil_function(GS_STENCIL_BOTH, GS_ALWAYS); + gs_stencil_op(GS_STENCIL_BOTH, GS_ZERO, GS_ZERO, GS_ZERO); + + effect.get_parameter("pImage").set_texture(_input_texture); + effect.get_parameter("pImageTexel").set_float2(float(1.f / width * cos(m_angle)), float(1.f / height * sin(m_angle))); + effect.get_parameter("pStepScale").set_float2(float(_step_scale.first), float(_step_scale.second)); + effect.get_parameter("pSize").set_float(float(_size * ST_OVERSAMPLE_MULTIPLIER)); + effect.get_parameter("pKernel").set_value(kernel.data(), ST_KERNEL_SIZE); + + { + auto op = _rendertarget->render(uint32_t(width), uint32_t(height)); + gs_ortho(0, 1., 0, 1., 0, 1.); + while (gs_effect_loop(effect.get_object(), "Draw")) { + _data->get_gfx_util()->draw_fullscreen_triangle(); + } + } + + gs_blend_state_pop(); + + return this->get(); +} + +::streamfx::gfx::blur::type streamfx::gfx::blur::gaussian_rotational::get_type() +{ + return ::streamfx::gfx::blur::type::Rotational; +} + +std::shared_ptr<::streamfx::obs::gs::texture> streamfx::gfx::blur::gaussian_rotational::render() +{ + auto gctx = streamfx::obs::gs::context(); + +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + auto gdmp = streamfx::obs::gs::debug_marker(streamfx::obs::gs::debug_color_azure_radiance, "Gaussian Rotational Blur"); +#endif + + streamfx::obs::gs::effect effect = _data->get_effect(); + + if (!effect || ((_step_scale.first + _step_scale.second) < std::numeric_limits::epsilon())) { + return _input_texture; + } + + auto kernel = _data->get_kernel(size_t(_size)); + float width = float(_input_texture->width()); + float height = float(_input_texture->height()); + + // Setup + gs_set_cull_mode(GS_NEITHER); + gs_enable_color(true, true, true, true); + gs_enable_depth_test(false); + gs_depth_function(GS_ALWAYS); + gs_blend_state_push(); + gs_reset_blend_state(); + gs_enable_blending(false); + gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO); + gs_enable_stencil_test(false); + gs_enable_stencil_write(false); + gs_stencil_function(GS_STENCIL_BOTH, GS_ALWAYS); + gs_stencil_op(GS_STENCIL_BOTH, GS_ZERO, GS_ZERO, GS_ZERO); + + effect.get_parameter("pImage").set_texture(_input_texture); + effect.get_parameter("pImageTexel").set_float2(float(1.f / width), float(1.f / height)); + effect.get_parameter("pStepScale").set_float2(float(_step_scale.first), float(_step_scale.second)); + effect.get_parameter("pSize").set_float(float(_size * ST_OVERSAMPLE_MULTIPLIER)); + effect.get_parameter("pAngle").set_float(float(m_angle / _size)); + effect.get_parameter("pCenter").set_float2(float(m_center.first), float(m_center.second)); + effect.get_parameter("pKernel").set_value(kernel.data(), ST_KERNEL_SIZE); + + // First Pass + { + auto op = _rendertarget->render(uint32_t(width), uint32_t(height)); + gs_ortho(0, 1., 0, 1., 0, 1.); + while (gs_effect_loop(effect.get_object(), "Rotate")) { + _data->get_gfx_util()->draw_fullscreen_triangle(); + } + } + + gs_blend_state_pop(); + + return this->get(); +} + +void streamfx::gfx::blur::gaussian_rotational::set_center(double_t x, double_t y) +{ + m_center.first = x; + m_center.second = y; +} + +void streamfx::gfx::blur::gaussian_rotational::get_center(double_t& x, double_t& y) +{ + x = m_center.first; + y = m_center.second; +} + +double_t streamfx::gfx::blur::gaussian_rotational::get_angle() +{ + return double_t(D_RAD_TO_DEG(m_angle)); +} + +void streamfx::gfx::blur::gaussian_rotational::set_angle(double_t angle) +{ + m_angle = D_DEG_TO_RAD(angle); +} + +::streamfx::gfx::blur::type streamfx::gfx::blur::gaussian_zoom::get_type() +{ + return ::streamfx::gfx::blur::type::Zoom; +} + +std::shared_ptr<::streamfx::obs::gs::texture> streamfx::gfx::blur::gaussian_zoom::render() +{ + auto gctx = streamfx::obs::gs::context(); + +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + auto gdmp = streamfx::obs::gs::debug_marker(streamfx::obs::gs::debug_color_azure_radiance, "Gaussian Zoom Blur"); +#endif + + streamfx::obs::gs::effect effect = _data->get_effect(); + auto kernel = _data->get_kernel(size_t(_size)); + + if (!effect || ((_step_scale.first + _step_scale.second) < std::numeric_limits::epsilon())) { + return _input_texture; + } + + float width = float(_input_texture->width()); + float height = float(_input_texture->height()); + + // Setup + gs_set_cull_mode(GS_NEITHER); + gs_enable_color(true, true, true, true); + gs_enable_depth_test(false); + gs_depth_function(GS_ALWAYS); + gs_blend_state_push(); + gs_reset_blend_state(); + gs_enable_blending(false); + gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO); + gs_enable_stencil_test(false); + gs_enable_stencil_write(false); + gs_stencil_function(GS_STENCIL_BOTH, GS_ALWAYS); + gs_stencil_op(GS_STENCIL_BOTH, GS_ZERO, GS_ZERO, GS_ZERO); + + effect.get_parameter("pImage").set_texture(_input_texture); + effect.get_parameter("pImageTexel").set_float2(float(1.f / width), float(1.f / height)); + effect.get_parameter("pStepScale").set_float2(float(_step_scale.first), float(_step_scale.second)); + effect.get_parameter("pSize").set_float(float(_size)); + effect.get_parameter("pCenter").set_float2(float(m_center.first), float(m_center.second)); + effect.get_parameter("pKernel").set_value(kernel.data(), ST_KERNEL_SIZE); + + // First Pass + { + auto op = _rendertarget->render(uint32_t(width), uint32_t(height)); + gs_ortho(0, 1., 0, 1., 0, 1.); + while (gs_effect_loop(effect.get_object(), "Zoom")) { + _data->get_gfx_util()->draw_fullscreen_triangle(); + } + } + + gs_blend_state_pop(); + + return this->get(); +} + +void streamfx::gfx::blur::gaussian_zoom::set_center(double_t x, double_t y) +{ + m_center.first = x; + m_center.second = y; +} + +void streamfx::gfx::blur::gaussian_zoom::get_center(double_t& x, double_t& y) +{ + x = m_center.first; + y = m_center.second; +} diff --git a/components/blur/source/gfx/blur/gfx-blur-gaussian.hpp b/components/blur/source/gfx/blur/gfx-blur-gaussian.hpp new file mode 100644 index 0000000..614c3ce --- /dev/null +++ b/components/blur/source/gfx/blur/gfx-blur-gaussian.hpp @@ -0,0 +1,160 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "common.hpp" +#include "gfx-blur-base.hpp" +#include "gfx/gfx-util.hpp" +#include "obs/gs/gs-effect.hpp" +#include "obs/gs/gs-texrender.hpp" +#include "obs/gs/gs-texture.hpp" + +#include "warning-disable.hpp" +#include +#include +#include "warning-enable.hpp" + +namespace streamfx::gfx { + namespace blur { + class gaussian_data { + streamfx::obs::gs::effect _effect; + std::shared_ptr _gfx_util; + std::map> _kernels; + + public: + gaussian_data(); + virtual ~gaussian_data(); + + streamfx::obs::gs::effect get_effect(); + + std::shared_ptr get_gfx_util(); + + std::vector const& get_kernel(std::size_t width); + }; + + class gaussian_factory : public ::streamfx::gfx::blur::ifactory { + std::mutex _data_lock; + std::weak_ptr<::streamfx::gfx::blur::gaussian_data> _data; + + public: + gaussian_factory(); + virtual ~gaussian_factory() override; + + virtual bool is_type_supported(::streamfx::gfx::blur::type type) override; + + virtual std::shared_ptr<::streamfx::gfx::blur::base> create(::streamfx::gfx::blur::type type) override; + + virtual double_t get_min_size(::streamfx::gfx::blur::type type) override; + + virtual double_t get_step_size(::streamfx::gfx::blur::type type) override; + + virtual double_t get_max_size(::streamfx::gfx::blur::type type) override; + + virtual double_t get_min_angle(::streamfx::gfx::blur::type type) override; + + virtual double_t get_step_angle(::streamfx::gfx::blur::type type) override; + + virtual double_t get_max_angle(::streamfx::gfx::blur::type type) override; + + virtual bool is_step_scale_supported(::streamfx::gfx::blur::type type) override; + + virtual double_t get_min_step_scale_x(::streamfx::gfx::blur::type type) override; + + virtual double_t get_step_step_scale_x(::streamfx::gfx::blur::type type) override; + + virtual double_t get_max_step_scale_x(::streamfx::gfx::blur::type type) override; + + virtual double_t get_min_step_scale_y(::streamfx::gfx::blur::type type) override; + + virtual double_t get_step_step_scale_y(::streamfx::gfx::blur::type type) override; + + virtual double_t get_max_step_scale_y(::streamfx::gfx::blur::type type) override; + + std::shared_ptr<::streamfx::gfx::blur::gaussian_data> data(); + + public: // Singleton + static ::streamfx::gfx::blur::gaussian_factory& get(); + }; + + class gaussian : public ::streamfx::gfx::blur::base { + protected: + std::shared_ptr<::streamfx::gfx::blur::gaussian_data> _data; + + double_t _size; + std::pair _step_scale; + std::shared_ptr<::streamfx::obs::gs::texture> _input_texture; + std::shared_ptr<::streamfx::obs::gs::texrender> _rendertarget; + + private: + std::shared_ptr<::streamfx::obs::gs::texrender> _rendertarget2; + + public: + gaussian(); + virtual ~gaussian() override; + + virtual void set_input(std::shared_ptr<::streamfx::obs::gs::texture> texture) override; + + virtual ::streamfx::gfx::blur::type get_type() override; + + virtual double_t get_size() override; + + virtual void set_size(double_t width) override; + + virtual void set_step_scale(double_t x, double_t y) override; + + virtual void get_step_scale(double_t& x, double_t& y) override; + + virtual double_t get_step_scale_x() override; + + virtual double_t get_step_scale_y() override; + + virtual std::shared_ptr<::streamfx::obs::gs::texture> render() override; + + virtual std::shared_ptr<::streamfx::obs::gs::texture> get() override; + }; + + class gaussian_directional : public ::streamfx::gfx::blur::gaussian, public ::streamfx::gfx::blur::base_angle { + double_t m_angle; + + public: + gaussian_directional(); + virtual ~gaussian_directional() override; + + virtual ::streamfx::gfx::blur::type get_type() override; + + virtual double_t get_angle() override; + virtual void set_angle(double_t angle) override; + + virtual std::shared_ptr<::streamfx::obs::gs::texture> render() override; + }; + + class gaussian_rotational : public ::streamfx::gfx::blur::gaussian, public ::streamfx::gfx::blur::base_angle, public ::streamfx::gfx::blur::base_center { + std::pair m_center; + double_t m_angle; + + public: + virtual ::streamfx::gfx::blur::type get_type() override; + + virtual void set_center(double_t x, double_t y) override; + virtual void get_center(double_t& x, double_t& y) override; + + virtual double_t get_angle() override; + virtual void set_angle(double_t angle) override; + + virtual std::shared_ptr<::streamfx::obs::gs::texture> render() override; + }; + + class gaussian_zoom : public ::streamfx::gfx::blur::gaussian, public ::streamfx::gfx::blur::base_center { + std::pair m_center; + + public: + virtual ::streamfx::gfx::blur::type get_type() override; + + virtual void set_center(double_t x, double_t y) override; + virtual void get_center(double_t& x, double_t& y) override; + + virtual std::shared_ptr<::streamfx::obs::gs::texture> render() override; + }; + } // namespace blur +} // namespace streamfx::gfx diff --git a/components/color-grade/CMakeLists.txt b/components/color-grade/CMakeLists.txt new file mode 100644 index 0000000..2af2928 --- /dev/null +++ b/components/color-grade/CMakeLists.txt @@ -0,0 +1,9 @@ +# AUTOGENERATED COPYRIGHT HEADER START +# Copyright (C) 2023 Michael Fabian 'Xaymar' Dirks +# AUTOGENERATED COPYRIGHT HEADER END + +cmake_minimum_required(VERSION 3.26) +project("ColorGrade") +list(APPEND CMAKE_MESSAGE_INDENT "[${PROJECT_NAME}] ") + +streamfx_add_component("Color Grade") diff --git a/components/color-grade/source/filters/filter-color-grade.cpp b/components/color-grade/source/filters/filter-color-grade.cpp new file mode 100644 index 0000000..8725bdc --- /dev/null +++ b/components/color-grade/source/filters/filter-color-grade.cpp @@ -0,0 +1,855 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#include "filter-color-grade.hpp" +#include "strings.hpp" +#include "gfx/gfx-util.hpp" +#include "obs/gs/gs-helper.hpp" +#include "util/util-logging.hpp" + +#include "warning-disable.hpp" +#include +#include "warning-enable.hpp" + +// OBS +#include "warning-disable.hpp" +extern "C" { +#include +#include +#include +} +#include "warning-enable.hpp" + +#ifdef _DEBUG +#define ST_PREFIX "<%s> " +#define D_LOG_ERROR(x, ...) P_LOG_ERROR(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_WARNING(x, ...) P_LOG_WARN(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_INFO(x, ...) P_LOG_INFO(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_DEBUG(x, ...) P_LOG_DEBUG(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#else +#define ST_PREFIX " " +#define D_LOG_ERROR(...) P_LOG_ERROR(ST_PREFIX __VA_ARGS__) +#define D_LOG_WARNING(...) P_LOG_WARN(ST_PREFIX __VA_ARGS__) +#define D_LOG_INFO(...) P_LOG_INFO(ST_PREFIX __VA_ARGS__) +#define D_LOG_DEBUG(...) P_LOG_DEBUG(ST_PREFIX __VA_ARGS__) +#endif + +#define ST_I18N "Filter.ColorGrade" +// Lift +#define ST_KEY_LIFT "Filter.ColorGrade.Lift" +#define ST_I18N_LIFT ST_I18N ".Lift" +#define ST_KEY_LIFT_(x) ST_KEY_LIFT "." x +#define ST_I18N_LIFT_(x) ST_I18N_LIFT "." x +// Gamma +#define ST_KEY_GAMMA "Filter.ColorGrade.Gamma" +#define ST_I18N_GAMMA ST_I18N ".Gamma" +#define ST_KEY_GAMMA_(x) ST_KEY_GAMMA "." x +#define ST_I18N_GAMMA_(x) ST_I18N_GAMMA "." x +// Gain +#define ST_KEY_GAIN "Filter.ColorGrade.Gain" +#define ST_I18N_GAIN ST_I18N ".Gain" +#define ST_KEY_GAIN_(x) ST_KEY_GAIN "." x +#define ST_I18N_GAIN_(x) ST_I18N_GAIN "." x +// Offset +#define ST_KEY_OFFSET "Filter.ColorGrade.Offset" +#define ST_I18N_OFFSET ST_I18N ".Offset" +#define ST_KEY_OFFSET_(x) ST_KEY_OFFSET "." x +#define ST_I18N_OFFSET_(x) ST_I18N_OFFSET "." x +// Tint +#define ST_KEY_TINT "Filter.ColorGrade.Tint" +#define ST_I18N_TINT ST_I18N ".Tint" +#define ST_KEY_TINT_DETECTION ST_KEY_TINT ".Detection" +#define ST_I18N_TINT_DETECTION ST_I18N_TINT ".Detection" +#define ST_I18N_TINT_DETECTION_(x) ST_I18N_TINT_DETECTION "." x +#define ST_KEY_TINT_MODE ST_KEY_TINT ".Mode" +#define ST_I18N_TINT_MODE ST_I18N_TINT ".Mode" +#define ST_I18N_TINT_MODE_(x) ST_I18N_TINT_MODE "." x +#define ST_KEY_TINT_EXPONENT ST_KEY_TINT ".Exponent" +#define ST_I18N_TINT_EXPONENT ST_I18N_TINT ".Exponent" +#define ST_KEY_TINT_(x, y) ST_KEY_TINT "." x "." y +#define ST_I18N_TINT_(x, y) ST_I18N_TINT "." x "." y +// Color Correction +#define ST_KEY_CORRECTION "Filter.ColorGrade.Correction" +#define ST_KEY_CORRECTION_(x) ST_KEY_CORRECTION "." x +#define ST_I18N_CORRECTION ST_I18N ".Correction" +#define ST_I18N_CORRECTION_(x) ST_I18N_CORRECTION "." x +// Render Mode +#define ST_KEY_RENDERMODE "Filter.ColorGrade.RenderMode" +#define ST_I18N_RENDERMODE ST_I18N ".RenderMode" +#define ST_I18N_RENDERMODE_DIRECT ST_I18N_RENDERMODE ".Direct" +#define ST_I18N_RENDERMODE_LUT_2BIT ST_I18N_RENDERMODE ".LUT.2Bit" +#define ST_I18N_RENDERMODE_LUT_4BIT ST_I18N_RENDERMODE ".LUT.4Bit" +#define ST_I18N_RENDERMODE_LUT_6BIT ST_I18N_RENDERMODE ".LUT.6Bit" +#define ST_I18N_RENDERMODE_LUT_8BIT ST_I18N_RENDERMODE ".LUT.8Bit" +#define ST_I18N_RENDERMODE_LUT_10BIT ST_I18N_RENDERMODE ".LUT.10Bit" + +#define ST_RED "Red" +#define ST_GREEN "Green" +#define ST_BLUE "Blue" +#define ST_ALL "All" +#define ST_HUE "Hue" +#define ST_SATURATION "Saturation" +#define ST_LIGHTNESS "Lightness" +#define ST_CONTRAST "Contrast" +#define ST_TONE_LOW "Shadow" +#define ST_TONE_MID "Midtone" +#define ST_TONE_HIGH "Highlight" +#define ST_DETECTION_HSV "HSV" +#define ST_DETECTION_HSL "HSL" +#define ST_DETECTION_YUV_SDR "YUV.SDR" +#define ST_MODE_LINEAR "Linear" +#define ST_MODE_EXP "Exp" +#define ST_MODE_EXP2 "Exp2" +#define ST_MODE_LOG "Log" +#define ST_MODE_LOG10 "Log10" + +using namespace streamfx::filter::color_grade; + +static constexpr std::string_view HELP_URL = "https://github.com/Xaymar/obs-StreamFX/wiki/Filter-Color-Grade"; + +// TODO: Figure out a way to merge _lut_rt, _lut_texture, _rt_source, _rt_grad, _tex_source, _tex_grade, _source_updated and _grade_updated. +// Seriously this is too much GPU space wasted on unused trash. + +color_grade_instance::~color_grade_instance() {} + +color_grade_instance::color_grade_instance(obs_data_t* data, obs_source_t* self) : obs::source_instance(data, self), _effect(), _gfx_util(::streamfx::gfx::util::get()), _lift(), _gamma(), _gain(), _offset(), _tint_detection(), _tint_luma(), _tint_exponent(), _tint_low(), _tint_mid(), _tint_hig(), _correction(), _lut_enabled(true), _lut_depth(), _ccache_rt(), _ccache_texture(), _ccache_fresh(false), _lut_initialized(false), _lut_dirty(true), _lut_producer(), _lut_consumer(), _lut_rt(), _lut_texture(), _cache_rt(), _cache_texture(), _cache_fresh(false) +{ + { + auto gctx = streamfx::obs::gs::context(); + + // Load the color grading effect. + { + auto file = streamfx::data_file_path("effects/color-grade.effect"); + try { + _effect = streamfx::obs::gs::effect::create(file); + } catch (std::exception& ex) { + D_LOG_ERROR("Error loading '%s': %s", file.u8string().c_str(), ex.what()); + throw; + } + } + + // Initialize LUT work flow. + try { + _lut_producer = std::make_shared(); + _lut_consumer = std::make_shared(); + _lut_initialized = true; + } catch (std::exception const& ex) { + D_LOG_WARNING("Failed to initialize LUT rendering, falling back to direct rendering.\n%s", ex.what()); + _lut_initialized = false; + } + + // Allocate render target for rendering. + try { + allocate_rendertarget(GS_RGBA); + } catch (std::exception const& ex) { + D_LOG_ERROR("Failed to acquire render target for rendering: %s", ex.what()); + throw; + } + } + + update(data); +} + +void color_grade_instance::allocate_rendertarget(gs_color_format format) +{ + _cache_rt = std::make_unique(format, GS_ZS_NONE); +} + +float fix_gamma_value(double_t v) +{ + if (v < 0.0) { + return static_cast(-v + 1.0); + } else { + return static_cast(1.0 / (v + 1.0)); + } +} + +void color_grade_instance::load(obs_data_t* data) +{ + update(data); +} + +void color_grade_instance::migrate(obs_data_t* data, uint64_t version) {} + +void color_grade_instance::update(obs_data_t* data) +{ + _lift.x = static_cast(obs_data_get_double(data, ST_KEY_LIFT_(ST_RED)) / 100.0); + _lift.y = static_cast(obs_data_get_double(data, ST_KEY_LIFT_(ST_GREEN)) / 100.0); + _lift.z = static_cast(obs_data_get_double(data, ST_KEY_LIFT_(ST_BLUE)) / 100.0); + _lift.w = static_cast(obs_data_get_double(data, ST_KEY_LIFT_(ST_ALL)) / 100.0); + _gamma.x = fix_gamma_value(obs_data_get_double(data, ST_KEY_GAMMA_(ST_RED)) / 100.0); + _gamma.y = fix_gamma_value(obs_data_get_double(data, ST_KEY_GAMMA_(ST_GREEN)) / 100.0); + _gamma.z = fix_gamma_value(obs_data_get_double(data, ST_KEY_GAMMA_(ST_BLUE)) / 100.0); + _gamma.w = fix_gamma_value(obs_data_get_double(data, ST_KEY_GAMMA_(ST_ALL)) / 100.0); + _gain.x = static_cast(obs_data_get_double(data, ST_KEY_GAIN_(ST_RED)) / 100.0); + _gain.y = static_cast(obs_data_get_double(data, ST_KEY_GAIN_(ST_GREEN)) / 100.0); + _gain.z = static_cast(obs_data_get_double(data, ST_KEY_GAIN_(ST_BLUE)) / 100.0); + _gain.w = static_cast(obs_data_get_double(data, ST_KEY_GAIN_(ST_ALL)) / 100.0); + _offset.x = static_cast(obs_data_get_double(data, ST_KEY_OFFSET_(ST_RED)) / 100.0); + _offset.y = static_cast(obs_data_get_double(data, ST_KEY_OFFSET_(ST_GREEN)) / 100.0); + _offset.z = static_cast(obs_data_get_double(data, ST_KEY_OFFSET_(ST_BLUE)) / 100.0); + _offset.w = static_cast(obs_data_get_double(data, ST_KEY_OFFSET_(ST_ALL)) / 100.0); + _tint_detection = static_cast(obs_data_get_int(data, ST_KEY_TINT_DETECTION)); + _tint_luma = static_cast(obs_data_get_int(data, ST_KEY_TINT_MODE)); + _tint_exponent = static_cast(obs_data_get_double(data, ST_KEY_TINT_EXPONENT)); + _tint_low.x = static_cast(obs_data_get_double(data, ST_KEY_TINT_(ST_TONE_LOW, ST_RED)) / 100.0); + _tint_low.y = static_cast(obs_data_get_double(data, ST_KEY_TINT_(ST_TONE_LOW, ST_GREEN)) / 100.0); + _tint_low.z = static_cast(obs_data_get_double(data, ST_KEY_TINT_(ST_TONE_LOW, ST_BLUE)) / 100.0); + _tint_mid.x = static_cast(obs_data_get_double(data, ST_KEY_TINT_(ST_TONE_MID, ST_RED)) / 100.0); + _tint_mid.y = static_cast(obs_data_get_double(data, ST_KEY_TINT_(ST_TONE_MID, ST_GREEN)) / 100.0); + _tint_mid.z = static_cast(obs_data_get_double(data, ST_KEY_TINT_(ST_TONE_MID, ST_BLUE)) / 100.0); + _tint_hig.x = static_cast(obs_data_get_double(data, ST_KEY_TINT_(ST_TONE_HIGH, ST_RED)) / 100.0); + _tint_hig.y = static_cast(obs_data_get_double(data, ST_KEY_TINT_(ST_TONE_HIGH, ST_GREEN)) / 100.0); + _tint_hig.z = static_cast(obs_data_get_double(data, ST_KEY_TINT_(ST_TONE_HIGH, ST_BLUE)) / 100.0); + _correction.x = static_cast(obs_data_get_double(data, ST_KEY_CORRECTION_(ST_HUE)) / 360.0); + _correction.y = static_cast(obs_data_get_double(data, ST_KEY_CORRECTION_(ST_SATURATION)) / 100.0); + _correction.z = static_cast(obs_data_get_double(data, ST_KEY_CORRECTION_(ST_LIGHTNESS)) / 100.0); + _correction.w = static_cast(obs_data_get_double(data, ST_KEY_CORRECTION_(ST_CONTRAST)) / 100.0); + + { + int64_t v = obs_data_get_int(data, ST_KEY_RENDERMODE); + + // LUT status depends on selected option. + _lut_enabled = v != 0; // 0 (Direct) + + if (v == -1) { + _lut_depth = streamfx::gfx::lut::color_depth::_8; + } else if (v > 0) { + _lut_depth = static_cast(v); + } + } + + if (_lut_enabled && _lut_initialized) + _lut_dirty = true; +} + +void color_grade_instance::prepare_effect() +{ + if (auto p = _effect.get_parameter("pLift"); p) { + p.set_float4(_lift); + } + + if (auto p = _effect.get_parameter("pGamma"); p) { + p.set_float4(_gamma); + } + + if (auto p = _effect.get_parameter("pGain"); p) { + p.set_float4(_gain); + } + + if (auto p = _effect.get_parameter("pOffset"); p) { + p.set_float4(_offset); + } + + if (auto p = _effect.get_parameter("pLift"); p) { + p.set_float4(_lift); + } + + if (auto p = _effect.get_parameter("pTintDetection"); p) { + p.set_int(static_cast(_tint_detection)); + } + + if (auto p = _effect.get_parameter("pTintMode"); p) { + p.set_int(static_cast(_tint_luma)); + } + + if (auto p = _effect.get_parameter("pTintExponent"); p) { + p.set_float(_tint_exponent); + } + + if (auto p = _effect.get_parameter("pTintLow"); p) { + p.set_float3(_tint_low); + } + + if (auto p = _effect.get_parameter("pTintMid"); p) { + p.set_float3(_tint_mid); + } + + if (auto p = _effect.get_parameter("pTintHig"); p) { + p.set_float3(_tint_hig); + } + + if (auto p = _effect.get_parameter("pCorrection"); p) { + p.set_float4(_correction); + } +} + +void color_grade_instance::rebuild_lut() +{ +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + streamfx::obs::gs::debug_marker gdm{streamfx::obs::gs::debug_color_cache, "Rebuild LUT"}; +#endif + + // Generate a fresh LUT texture. + auto lut_texture = _lut_producer->produce(_lut_depth); + + // Modify the LUT with our color grade. + if (lut_texture) { + // Check if we have a render target to work with and if it's the correct format. + if (!_lut_rt || (lut_texture->color_format() != _lut_rt->color_format())) { + // Create a new render target with new format. + _lut_rt = std::make_unique(lut_texture->color_format(), GS_ZS_NONE); + } + + // Prepare our color grade effect. + prepare_effect(); + + // Assign texture. + if (auto p = _effect.get_parameter("image"); p) { + p.set_texture(lut_texture); + } + + { // Begin rendering. + auto op = _lut_rt->render(lut_texture->width(), lut_texture->height()); + + // Set up graphics context. + gs_ortho(0, 1, 0, 1, 0, 1); + gs_blend_state_push(); + gs_enable_blending(false); + gs_enable_color(true, true, true, true); + gs_enable_stencil_test(false); + gs_enable_stencil_write(false); + + while (gs_effect_loop(_effect.get_object(), "Draw")) { + _gfx_util->draw_fullscreen_triangle(); + } + + gs_blend_state_pop(); + } + + _lut_rt->get_texture(_lut_texture); + if (!_lut_texture) { + throw std::runtime_error("Failed to produce modified LUT texture."); + } + } else { + throw std::runtime_error("Failed to produce LUT texture."); + } + + _lut_dirty = false; +} + +void color_grade_instance::video_tick(float) +{ + _ccache_fresh = false; + _cache_fresh = false; +} + +void color_grade_instance::video_render(gs_effect_t* shader) +{ + // Grab initial values. + obs_source_t* parent = obs_filter_get_parent(_self); + obs_source_t* target = obs_filter_get_target(_self); + uint32_t width = obs_source_get_base_width(target); + uint32_t height = obs_source_get_base_height(target); + vec4 blank = vec4{0, 0, 0, 0}; + shader = shader ? shader : obs_get_base_effect(OBS_EFFECT_DEFAULT); + + // Skip filter if anything is wrong. + if (!parent || !target || !width || !height) { + obs_source_skip_video_filter(_self); + return; + } + +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + streamfx::obs::gs::debug_marker gdmp{streamfx::obs::gs::debug_color_source, "Color Grading '%s'", obs_source_get_name(_self)}; +#endif + + // TODO: Optimize this once (https://github.com/obsproject/obs-studio/pull/4199) is merged. + // - We can skip the original capture and reduce the overall impact of this. + + // 1. Capture the filter/source rendered above this. + if (!_ccache_fresh || !_ccache_texture) { +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + streamfx::obs::gs::debug_marker gdmp{streamfx::obs::gs::debug_color_cache, "Cache '%s'", obs_source_get_name(target)}; +#endif + // If the input cache render target doesn't exist, create it. + if (!_ccache_rt) { + _ccache_rt = std::make_shared(GS_RGBA, GS_ZS_NONE); + } + + { + auto op = _ccache_rt->render(width, height); + gs_ortho(0, static_cast(width), 0, static_cast(height), 0, 1); + + // Blank out the input cache. + gs_clear(GS_CLEAR_COLOR | GS_CLEAR_DEPTH, &blank, 0., 0); + + // Begin rendering the actual input source. + obs_source_process_filter_begin(_self, GS_RGBA, OBS_ALLOW_DIRECT_RENDERING); + + // Enable all colors for rendering. + gs_enable_color(true, true, true, true); + + // Prevent blending with existing content, even if it is cleared. + gs_blend_state_push(); + gs_enable_blending(false); + gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO); + + // Disable depth testing. + gs_enable_depth_test(false); + + // Disable stencil testing. + gs_enable_stencil_test(false); + + // Disable culling. + gs_set_cull_mode(GS_NEITHER); + + // End rendering the actual input source. + obs_source_process_filter_end(_self, obs_get_base_effect(OBS_EFFECT_DEFAULT), width, height); + + // Restore original blend mode. + gs_blend_state_pop(); + } + + // Try and retrieve the input cache as a texture for later use. + _ccache_rt->get_texture(_ccache_texture); + if (!_ccache_texture) { + throw std::runtime_error("Failed to cache original source."); + } + + // Mark the input cache as valid. + _ccache_fresh = true; + } + + // 2. Apply one of the two rendering methods (LUT or Direct). + if (_lut_initialized && _lut_enabled) { // Try to apply with the LUT based method. + try { +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + streamfx::obs::gs::debug_marker gdm{streamfx::obs::gs::debug_color_convert, "LUT Rendering"}; +#endif + // If the LUT was changed, rebuild the LUT first. + if (_lut_dirty) { + rebuild_lut(); + + // Mark the cache as invalid, since the LUT has been changed. + _cache_fresh = false; + } + + // Reallocate the rendertarget if necessary. + if (_cache_rt->color_format() != GS_RGBA) { + allocate_rendertarget(GS_RGBA); + } + + if (!_cache_fresh) { + { // Render the source to the cache. + auto op = _cache_rt->render(width, height); + gs_ortho(0, 1., 0, 1., 0, 1); + + // Blank out the input cache. + gs_clear(GS_CLEAR_COLOR | GS_CLEAR_DEPTH, &blank, 0., 0); + + // Enable all colors for rendering. + gs_enable_color(true, true, true, true); + + // Prevent blending with existing content, even if it is cleared. + gs_blend_state_push(); + gs_enable_blending(false); + gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO); + + // Disable depth testing. + gs_enable_depth_test(false); + + // Disable stencil testing. + gs_enable_stencil_test(false); + + // Disable culling. + gs_set_cull_mode(GS_NEITHER); + + auto effect = _lut_consumer->prepare(_lut_depth, _lut_texture); + effect->get_parameter("image").set_texture(_ccache_texture); + while (gs_effect_loop(effect->get_object(), "Draw")) { + _gfx_util->draw_fullscreen_triangle(); + } + + // Restore original blend mode. + gs_blend_state_pop(); + } + + // Try and retrieve the render cache as a texture. + _cache_rt->get_texture(_cache_texture); + + // Mark the render cache as valid. + _cache_fresh = true; + } + } catch (std::exception const& ex) { + // If anything happened, revert to direct rendering. + _lut_rt.reset(); + _lut_texture.reset(); + _lut_enabled = false; + D_LOG_WARNING("Reverting to direct rendering due to error: %s", ex.what()); + } + } + if ((!_lut_initialized || !_lut_enabled) && !_cache_fresh) { +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + streamfx::obs::gs::debug_marker gdm{streamfx::obs::gs::debug_color_convert, "Direct Rendering"}; +#endif + // Reallocate the rendertarget if necessary. + if (_cache_rt->color_format() != GS_RGBA) { + allocate_rendertarget(GS_RGBA); + } + + { // Render the source to the cache. + auto op = _cache_rt->render(width, height); + gs_ortho(0, 1, 0, 1, 0, 1); + + prepare_effect(); + + // Blank out the input cache. + gs_clear(GS_CLEAR_COLOR | GS_CLEAR_DEPTH, &blank, 0., 0); + + // Enable all colors for rendering. + gs_enable_color(true, true, true, true); + + // Prevent blending with existing content, even if it is cleared. + gs_blend_state_push(); + gs_enable_blending(false); + gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO); + + // Disable depth testing. + gs_enable_depth_test(false); + + // Disable stencil testing. + gs_enable_stencil_test(false); + + // Disable culling. + gs_set_cull_mode(GS_NEITHER); + + // Render the effect. + _effect.get_parameter("image").set_texture(_ccache_texture); + while (gs_effect_loop(_effect.get_object(), "Draw")) { + _gfx_util->draw_fullscreen_triangle(); + } + + // Restore original blend mode. + gs_blend_state_pop(); + } + + // Try and retrieve the render cache as a texture. + _cache_rt->get_texture(_cache_texture); + + // Mark the render cache as valid. + _cache_fresh = true; + } + if (!_cache_texture) { + throw std::runtime_error("Failed to cache processed source."); + } + + // 3. Render the output cache. + { +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + streamfx::obs::gs::debug_marker gdm{streamfx::obs::gs::debug_color_cache_render, "Draw Cache"}; +#endif + // Revert GPU status to what OBS Studio expects. + gs_enable_depth_test(false); + gs_enable_color(true, true, true, true); + gs_set_cull_mode(GS_NEITHER); + + // Draw the render cache. + while (gs_effect_loop(shader, "Draw")) { + gs_effect_set_texture(gs_effect_get_param_by_name(shader, "image"), _cache_texture ? *_cache_texture : nullptr); + gs_draw_sprite(nullptr, 0, width, height); + } + } +} + +color_grade_factory::color_grade_factory() +{ + _info.id = S_PREFIX "filter-color-grade"; + _info.type = OBS_SOURCE_TYPE_FILTER; + _info.output_flags = OBS_SOURCE_VIDEO; + + support_size(false); + finish_setup(); + register_proxy("obs-stream-effects-filter-color-grade"); +} + +color_grade_factory::~color_grade_factory() {} + +const char* color_grade_factory::get_name() +{ + return D_TRANSLATE(ST_I18N); +} + +void color_grade_factory::get_defaults2(obs_data_t* data) +{ + obs_data_set_default_double(data, ST_KEY_LIFT_(ST_RED), 0); + obs_data_set_default_double(data, ST_KEY_LIFT_(ST_GREEN), 0); + obs_data_set_default_double(data, ST_KEY_LIFT_(ST_BLUE), 0); + obs_data_set_default_double(data, ST_KEY_LIFT_(ST_ALL), 0); + obs_data_set_default_double(data, ST_KEY_GAMMA_(ST_RED), 0); + obs_data_set_default_double(data, ST_KEY_GAMMA_(ST_GREEN), 0); + obs_data_set_default_double(data, ST_KEY_GAMMA_(ST_BLUE), 0); + obs_data_set_default_double(data, ST_KEY_GAMMA_(ST_ALL), 0); + obs_data_set_default_double(data, ST_KEY_GAIN_(ST_RED), 100.0); + obs_data_set_default_double(data, ST_KEY_GAIN_(ST_GREEN), 100.0); + obs_data_set_default_double(data, ST_KEY_GAIN_(ST_BLUE), 100.0); + obs_data_set_default_double(data, ST_KEY_GAIN_(ST_ALL), 100.0); + obs_data_set_default_double(data, ST_KEY_OFFSET_(ST_RED), 0.0); + obs_data_set_default_double(data, ST_KEY_OFFSET_(ST_GREEN), 0.0); + obs_data_set_default_double(data, ST_KEY_OFFSET_(ST_BLUE), 0.0); + obs_data_set_default_double(data, ST_KEY_OFFSET_(ST_ALL), 0.0); + obs_data_set_default_int(data, ST_KEY_TINT_MODE, static_cast(luma_mode::Linear)); + obs_data_set_default_int(data, ST_KEY_TINT_DETECTION, static_cast(detection_mode::YUV_SDR)); + obs_data_set_default_double(data, ST_KEY_TINT_EXPONENT, 1.5); + obs_data_set_default_double(data, ST_KEY_TINT_(ST_TONE_LOW, ST_RED), 100.0); + obs_data_set_default_double(data, ST_KEY_TINT_(ST_TONE_LOW, ST_GREEN), 100.0); + obs_data_set_default_double(data, ST_KEY_TINT_(ST_TONE_LOW, ST_BLUE), 100.0); + obs_data_set_default_double(data, ST_KEY_TINT_(ST_TONE_MID, ST_RED), 100.0); + obs_data_set_default_double(data, ST_KEY_TINT_(ST_TONE_MID, ST_GREEN), 100.0); + obs_data_set_default_double(data, ST_KEY_TINT_(ST_TONE_MID, ST_BLUE), 100.0); + obs_data_set_default_double(data, ST_KEY_TINT_(ST_TONE_HIGH, ST_RED), 100.0); + obs_data_set_default_double(data, ST_KEY_TINT_(ST_TONE_HIGH, ST_GREEN), 100.0); + obs_data_set_default_double(data, ST_KEY_TINT_(ST_TONE_HIGH, ST_BLUE), 100.0); + obs_data_set_default_double(data, ST_KEY_CORRECTION_(ST_HUE), 0.0); + obs_data_set_default_double(data, ST_KEY_CORRECTION_(ST_SATURATION), 100.0); + obs_data_set_default_double(data, ST_KEY_CORRECTION_(ST_LIGHTNESS), 100.0); + obs_data_set_default_double(data, ST_KEY_CORRECTION_(ST_CONTRAST), 100.0); + + obs_data_set_default_int(data, ST_KEY_RENDERMODE, -1); +} + +obs_properties_t* color_grade_factory::get_properties2(color_grade_instance* data) +{ + obs_properties_t* pr = obs_properties_create(); + + { + obs_properties_add_button2(pr, S_MANUAL_OPEN, D_TRANSLATE(S_MANUAL_OPEN), streamfx::filter::color_grade::color_grade_factory::on_manual_open, nullptr); + } + + { + obs_properties_t* grp = obs_properties_create(); + obs_properties_add_group(pr, ST_KEY_LIFT, D_TRANSLATE(ST_I18N_LIFT), OBS_GROUP_NORMAL, grp); + + { + auto p = obs_properties_add_float_slider(grp, ST_KEY_LIFT_(ST_RED), D_TRANSLATE(ST_I18N_LIFT_(ST_RED)), -1000., 100., .01); + obs_property_float_set_suffix(p, " %"); + } + { + auto p = obs_properties_add_float_slider(grp, ST_KEY_LIFT_(ST_GREEN), D_TRANSLATE(ST_I18N_LIFT_(ST_GREEN)), -1000., 100., .01); + obs_property_float_set_suffix(p, " %"); + } + { + auto p = obs_properties_add_float_slider(grp, ST_KEY_LIFT_(ST_BLUE), D_TRANSLATE(ST_I18N_LIFT_(ST_BLUE)), -1000., 100., .01); + obs_property_float_set_suffix(p, " %"); + } + { + auto p = obs_properties_add_float_slider(grp, ST_KEY_LIFT_(ST_ALL), D_TRANSLATE(ST_I18N_LIFT_(ST_ALL)), -1000., 100., .01); + obs_property_float_set_suffix(p, " %"); + } + } + + { + obs_properties_t* grp = obs_properties_create(); + obs_properties_add_group(pr, ST_KEY_GAMMA, D_TRANSLATE(ST_I18N_GAMMA), OBS_GROUP_NORMAL, grp); + + { + auto p = obs_properties_add_float_slider(grp, ST_KEY_GAMMA_(ST_RED), D_TRANSLATE(ST_I18N_GAMMA_(ST_RED)), -1000., 1000., .01); + obs_property_float_set_suffix(p, " %"); + } + { + auto p = obs_properties_add_float_slider(grp, ST_KEY_GAMMA_(ST_GREEN), D_TRANSLATE(ST_I18N_GAMMA_(ST_GREEN)), -1000., 1000., .01); + obs_property_float_set_suffix(p, " %"); + } + { + auto p = obs_properties_add_float_slider(grp, ST_KEY_GAMMA_(ST_BLUE), D_TRANSLATE(ST_I18N_GAMMA_(ST_BLUE)), -1000., 1000., .01); + obs_property_float_set_suffix(p, " %"); + } + { + auto p = obs_properties_add_float_slider(grp, ST_KEY_GAMMA_(ST_ALL), D_TRANSLATE(ST_I18N_GAMMA_(ST_ALL)), -1000., 1000., .01); + obs_property_float_set_suffix(p, " %"); + } + } + + { + obs_properties_t* grp = obs_properties_create(); + obs_properties_add_group(pr, ST_KEY_GAIN, D_TRANSLATE(ST_I18N_GAIN), OBS_GROUP_NORMAL, grp); + + { + auto p = obs_properties_add_float_slider(grp, ST_KEY_GAIN_(ST_RED), D_TRANSLATE(ST_I18N_GAIN_(ST_RED)), -1000., 1000., .01); + obs_property_float_set_suffix(p, " %"); + } + { + auto p = obs_properties_add_float_slider(grp, ST_KEY_GAIN_(ST_GREEN), D_TRANSLATE(ST_I18N_GAIN_(ST_GREEN)), -1000., 1000., .01); + obs_property_float_set_suffix(p, " %"); + } + { + auto p = obs_properties_add_float_slider(grp, ST_KEY_GAIN_(ST_BLUE), D_TRANSLATE(ST_I18N_GAIN_(ST_BLUE)), -1000., 1000., .01); + obs_property_float_set_suffix(p, " %"); + } + { + auto p = obs_properties_add_float_slider(grp, ST_KEY_GAIN_(ST_ALL), D_TRANSLATE(ST_I18N_GAIN_(ST_ALL)), -1000., 1000., .01); + obs_property_float_set_suffix(p, " %"); + } + } + + { + obs_properties_t* grp = obs_properties_create(); + obs_properties_add_group(pr, ST_KEY_OFFSET, D_TRANSLATE(ST_I18N_OFFSET), OBS_GROUP_NORMAL, grp); + + { + auto p = obs_properties_add_float_slider(grp, ST_KEY_OFFSET_(ST_RED), D_TRANSLATE(ST_I18N_OFFSET_(ST_RED)), -1000., 1000., .01); + obs_property_float_set_suffix(p, " %"); + } + { + auto p = obs_properties_add_float_slider(grp, ST_KEY_OFFSET_(ST_GREEN), D_TRANSLATE(ST_I18N_OFFSET_(ST_GREEN)), -1000., 1000., .01); + obs_property_float_set_suffix(p, " %"); + } + { + auto p = obs_properties_add_float_slider(grp, ST_KEY_OFFSET_(ST_BLUE), D_TRANSLATE(ST_I18N_OFFSET_(ST_BLUE)), -1000., 1000., .01); + obs_property_float_set_suffix(p, " %"); + } + { + auto p = obs_properties_add_float_slider(grp, ST_KEY_OFFSET_(ST_ALL), D_TRANSLATE(ST_I18N_OFFSET_(ST_ALL)), -1000., 1000., .01); + obs_property_float_set_suffix(p, " %"); + } + } + + { + obs_properties_t* grp = obs_properties_create(); + obs_properties_add_group(pr, ST_KEY_TINT, D_TRANSLATE(ST_I18N_TINT), OBS_GROUP_NORMAL, grp); + + { + auto p = obs_properties_add_float_slider(grp, ST_KEY_TINT_(ST_TONE_LOW, ST_RED), D_TRANSLATE(ST_I18N_TINT_(ST_TONE_LOW, ST_RED)), 0, 1000., .01); + obs_property_float_set_suffix(p, " %"); + } + { + auto p = obs_properties_add_float_slider(grp, ST_KEY_TINT_(ST_TONE_LOW, ST_GREEN), D_TRANSLATE(ST_I18N_TINT_(ST_TONE_LOW, ST_GREEN)), 0, 1000., .01); + obs_property_float_set_suffix(p, " %"); + } + { + auto p = obs_properties_add_float_slider(grp, ST_KEY_TINT_(ST_TONE_LOW, ST_BLUE), D_TRANSLATE(ST_I18N_TINT_(ST_TONE_LOW, ST_BLUE)), 0, 1000., .01); + obs_property_float_set_suffix(p, " %"); + } + + { + auto p = obs_properties_add_float_slider(grp, ST_KEY_TINT_(ST_TONE_MID, ST_RED), D_TRANSLATE(ST_I18N_TINT_(ST_TONE_MID, ST_RED)), 0, 1000., 0.01); + obs_property_float_set_suffix(p, " %"); + } + { + auto p = obs_properties_add_float_slider(grp, ST_KEY_TINT_(ST_TONE_MID, ST_GREEN), D_TRANSLATE(ST_I18N_TINT_(ST_TONE_MID, ST_GREEN)), 0, 1000., .01); + obs_property_float_set_suffix(p, " %"); + } + { + auto p = obs_properties_add_float_slider(grp, ST_KEY_TINT_(ST_TONE_MID, ST_BLUE), D_TRANSLATE(ST_I18N_TINT_(ST_TONE_MID, ST_BLUE)), 0, 1000., .01); + obs_property_float_set_suffix(p, " %"); + } + + { + auto p = obs_properties_add_float_slider(grp, ST_KEY_TINT_(ST_TONE_HIGH, ST_RED), D_TRANSLATE(ST_I18N_TINT_(ST_TONE_HIGH, ST_RED)), 0, 1000., .01); + obs_property_float_set_suffix(p, " %"); + } + { + auto p = obs_properties_add_float_slider(grp, ST_KEY_TINT_(ST_TONE_HIGH, ST_GREEN), D_TRANSLATE(ST_I18N_TINT_(ST_TONE_HIGH, ST_GREEN)), 0, 1000., .01); + obs_property_float_set_suffix(p, " %"); + } + { + auto p = obs_properties_add_float_slider(grp, ST_KEY_TINT_(ST_TONE_HIGH, ST_BLUE), D_TRANSLATE(ST_I18N_TINT_(ST_TONE_HIGH, ST_BLUE)), 0, 1000., .01); + obs_property_float_set_suffix(p, " %"); + } + } + + { + obs_properties_t* grp = obs_properties_create(); + obs_properties_add_group(pr, ST_KEY_CORRECTION, D_TRANSLATE(ST_I18N_CORRECTION), OBS_GROUP_NORMAL, grp); + + { + auto p = obs_properties_add_float_slider(grp, ST_KEY_CORRECTION_(ST_HUE), D_TRANSLATE(ST_I18N_CORRECTION_(ST_HUE)), -180., 180., .01); + obs_property_float_set_suffix(p, " °"); + } + { + auto p = obs_properties_add_float_slider(grp, ST_KEY_CORRECTION_(ST_SATURATION), D_TRANSLATE(ST_I18N_CORRECTION_(ST_SATURATION)), 0., 1000., .01); + obs_property_float_set_suffix(p, " %"); + } + { + auto p = obs_properties_add_float_slider(grp, ST_KEY_CORRECTION_(ST_LIGHTNESS), D_TRANSLATE(ST_I18N_CORRECTION_(ST_LIGHTNESS)), 0., 1000., .01); + obs_property_float_set_suffix(p, " %"); + } + { + auto p = obs_properties_add_float_slider(grp, ST_KEY_CORRECTION_(ST_CONTRAST), D_TRANSLATE(ST_I18N_CORRECTION_(ST_CONTRAST)), 0., 1000., .01); + obs_property_float_set_suffix(p, " %"); + } + } + + { + obs_properties_t* grp = obs_properties_create(); + obs_properties_add_group(pr, S_ADVANCED, D_TRANSLATE(S_ADVANCED), OBS_GROUP_NORMAL, grp); + + { + auto p = obs_properties_add_list(grp, ST_KEY_TINT_MODE, D_TRANSLATE(ST_I18N_TINT_MODE), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); + std::pair els[] = {{ST_I18N_TINT_MODE_(ST_MODE_LINEAR), luma_mode::Linear}, {ST_I18N_TINT_MODE_(ST_MODE_EXP), luma_mode::Exp}, {ST_I18N_TINT_MODE_(ST_MODE_EXP2), luma_mode::Exp2}, {ST_I18N_TINT_MODE_(ST_MODE_LOG), luma_mode::Log}, {ST_I18N_TINT_MODE_(ST_MODE_LOG10), luma_mode::Log10}}; + for (auto kv : els) { + obs_property_list_add_int(p, D_TRANSLATE(kv.first), static_cast(kv.second)); + } + } + + { + auto p = obs_properties_add_list(grp, ST_KEY_TINT_DETECTION, D_TRANSLATE(ST_I18N_TINT_DETECTION), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); + std::pair els[] = {{ST_I18N_TINT_DETECTION_(ST_DETECTION_HSV), detection_mode::HSV}, {ST_I18N_TINT_DETECTION_(ST_DETECTION_HSL), detection_mode::HSL}, {ST_I18N_TINT_DETECTION_(ST_DETECTION_YUV_SDR), detection_mode::YUV_SDR}}; + for (auto kv : els) { + obs_property_list_add_int(p, D_TRANSLATE(kv.first), static_cast(kv.second)); + } + } + + obs_properties_add_float_slider(grp, ST_KEY_TINT_EXPONENT, D_TRANSLATE(ST_I18N_TINT_EXPONENT), 0., 10., .01); + + { + auto p = obs_properties_add_list(grp, ST_KEY_RENDERMODE, D_TRANSLATE(ST_I18N_RENDERMODE), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); + std::pair els[] = { + {S_STATE_AUTOMATIC, -1}, {ST_I18N_RENDERMODE_DIRECT, 0}, {ST_I18N_RENDERMODE_LUT_2BIT, static_cast(streamfx::gfx::lut::color_depth::_2)}, {ST_I18N_RENDERMODE_LUT_4BIT, static_cast(streamfx::gfx::lut::color_depth::_4)}, {ST_I18N_RENDERMODE_LUT_6BIT, static_cast(streamfx::gfx::lut::color_depth::_6)}, {ST_I18N_RENDERMODE_LUT_8BIT, static_cast(streamfx::gfx::lut::color_depth::_8)}, + //{ST_RENDERMODE_LUT_10BIT, static_cast(gfx::lut::color_depth::_10)}, + }; + for (auto kv : els) { + obs_property_list_add_int(p, D_TRANSLATE(kv.first), kv.second); + } + } + } + + return pr; +} + +bool color_grade_factory::on_manual_open(obs_properties_t* props, obs_property_t* property, void* data) +{ + try { + streamfx::open_url(HELP_URL); + return false; + } catch (const std::exception& ex) { + D_LOG_ERROR("Failed to open manual due to error: %s", ex.what()); + return false; + } catch (...) { + D_LOG_ERROR("Failed to open manual due to unknown error.", ""); + return false; + } +} + +std::shared_ptr streamfx::filter::color_grade::color_grade_factory::instance() +{ + static std::weak_ptr winst; + static std::mutex mtx; + + std::unique_lock lock(mtx); + auto instance = winst.lock(); + if (!instance) { + try { + instance = std::shared_ptr(new color_grade_factory()); + winst = instance; + } catch (const std::exception& ex) { + D_LOG_ERROR("Failed to initialize due to error: %s", ex.what()); + } catch (...) { + D_LOG_ERROR("Failed to initialize due to unknown error.", ""); + } + } + return instance; +} + +static std::shared_ptr loader_instance; + +static auto loader = streamfx::component( + "color_grade", + []() { // Initializer + loader_instance = color_grade_factory::instance(); + }, + []() { // Finalizer + loader_instance.reset(); + }, + {}); diff --git a/components/color-grade/source/filters/filter-color-grade.hpp b/components/color-grade/source/filters/filter-color-grade.hpp new file mode 100644 index 0000000..57359f6 --- /dev/null +++ b/components/color-grade/source/filters/filter-color-grade.hpp @@ -0,0 +1,106 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "gfx/gfx-mipmapper.hpp" +#include "gfx/lut/gfx-lut-consumer.hpp" +#include "gfx/lut/gfx-lut-producer.hpp" +#include "gfx/lut/gfx-lut.hpp" +#include "obs/gs/gs-texrender.hpp" +#include "obs/gs/gs-texture.hpp" +#include "obs/gs/gs-vertexbuffer.hpp" +#include "obs/obs-source-factory.hpp" +#include "plugin.hpp" + +#include "warning-disable.hpp" +#include +#include "warning-enable.hpp" + +namespace streamfx::filter::color_grade { + enum class detection_mode { + HSV, + HSL, + YUV_SDR, + }; + + enum class luma_mode { + Linear, + Exp, + Exp2, + Log, + Log10, + }; + + class color_grade_instance : public obs::source_instance { + streamfx::obs::gs::effect _effect; + std::shared_ptr _gfx_util; + + // User Configuration + vec4 _lift; + vec4 _gamma; + vec4 _gain; + vec4 _offset; + detection_mode _tint_detection; + luma_mode _tint_luma; + float _tint_exponent; + vec3 _tint_low; + vec3 _tint_mid; + vec3 _tint_hig; + vec4 _correction; + bool _lut_enabled; + streamfx::gfx::lut::color_depth _lut_depth; + + // Capture Cache + std::shared_ptr _ccache_rt; + std::shared_ptr _ccache_texture; + bool _ccache_fresh; + + // LUT work flow + bool _lut_initialized; + bool _lut_dirty; + std::shared_ptr _lut_producer; + std::shared_ptr _lut_consumer; + std::shared_ptr _lut_rt; + std::shared_ptr _lut_texture; + + // Render Cache + std::shared_ptr _cache_rt; + std::shared_ptr _cache_texture; + bool _cache_fresh; + + public: + color_grade_instance(obs_data_t* data, obs_source_t* self); + virtual ~color_grade_instance(); + + void allocate_rendertarget(gs_color_format format); + + virtual void load(obs_data_t* data) override; + virtual void migrate(obs_data_t* data, uint64_t version) override; + virtual void update(obs_data_t* data) override; + + void prepare_effect(); + + void rebuild_lut(); + + virtual void video_tick(float time) override; + virtual void video_render(gs_effect_t* effect) override; + }; + + class color_grade_factory : public obs::source_factory { + public: + color_grade_factory(); + virtual ~color_grade_factory(); + + virtual const char* get_name() override; + + virtual void get_defaults2(obs_data_t* data) override; + + virtual obs_properties_t* get_properties2(color_grade_instance* data) override; + + static bool on_manual_open(obs_properties_t* props, obs_property_t* property, void* data); + + public: // Singleton + static std::shared_ptr instance(); + }; +} // namespace streamfx::filter::color_grade diff --git a/components/color-grade/source/gfx/lut/gfx-lut-consumer.cpp b/components/color-grade/source/gfx/lut/gfx-lut-consumer.cpp new file mode 100644 index 0000000..31b34b9 --- /dev/null +++ b/components/color-grade/source/gfx/lut/gfx-lut-consumer.cpp @@ -0,0 +1,62 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2021-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2022 lainon +// AUTOGENERATED COPYRIGHT HEADER END + +#include "gfx-lut-consumer.hpp" +#include "obs/gs/gs-helper.hpp" + +streamfx::gfx::lut::consumer::consumer() +{ + _data = streamfx::gfx::lut::data::instance(); + if (!_data->consumer_effect()) + throw std::runtime_error("Unable to get LUT consumer effect."); +} + +streamfx::gfx::lut::consumer::~consumer() = default; + +std::shared_ptr streamfx::gfx::lut::consumer::prepare(streamfx::gfx::lut::color_depth depth, std::shared_ptr lut) +{ + auto gctx = streamfx::obs::gs::context(); + + auto effect = _data->consumer_effect(); + + int32_t idepth = static_cast(depth); + int32_t size = static_cast(pow(2l, idepth)); + int32_t grid_size = static_cast(pow(2l, (idepth / 2))); + int32_t container_size = static_cast(pow(2l, (idepth + (idepth / 2)))); + + if (streamfx::obs::gs::effect_parameter efp = effect->get_parameter("lut_params_0"); efp) { + efp.set_int4(size, grid_size, container_size, 0l); + } + + if (streamfx::obs::gs::effect_parameter efp = effect->get_parameter("lut_params_1"); efp) { + float inverse_size = 1.f / static_cast(size); + float inverse_z_size = 1.f / static_cast(grid_size); + float inverse_container_size = 1.f / static_cast(container_size); + float half_texel = inverse_container_size / 2.f; + efp.set_float4(inverse_size, inverse_z_size, inverse_container_size, half_texel); + } + + if (streamfx::obs::gs::effect_parameter efp = effect->get_parameter("lut"); efp) { + efp.set_texture(lut); + } + + return effect; +} + +void streamfx::gfx::lut::consumer::consume(streamfx::gfx::lut::color_depth depth, std::shared_ptr lut, std::shared_ptr texture) +{ + auto gctx = streamfx::obs::gs::context(); + + auto effect = prepare(depth, lut); + + if (streamfx::obs::gs::effect_parameter efp = effect->get_parameter("image"); efp) { + efp.set_texture(texture->get_object()); + } + + // Draw a simple quad. + while (gs_effect_loop(effect->get_object(), "Draw")) { + gs_draw_sprite(nullptr, 0, 1, 1); + } +} diff --git a/components/color-grade/source/gfx/lut/gfx-lut-consumer.hpp b/components/color-grade/source/gfx/lut/gfx-lut-consumer.hpp new file mode 100644 index 0000000..d970c85 --- /dev/null +++ b/components/color-grade/source/gfx/lut/gfx-lut-consumer.hpp @@ -0,0 +1,26 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "gfx-lut.hpp" +#include "obs/gs/gs-effect.hpp" +#include "obs/gs/gs-texture.hpp" + +#include "warning-disable.hpp" +#include +#include "warning-enable.hpp" + +namespace streamfx::gfx::lut { + class consumer { + std::shared_ptr _data; + + public: + consumer(); + ~consumer(); + + std::shared_ptr prepare(streamfx::gfx::lut::color_depth depth, std::shared_ptr lut); + + void consume(streamfx::gfx::lut::color_depth depth, std::shared_ptr lut, std::shared_ptr texture); + }; +} // namespace streamfx::gfx::lut diff --git a/components/color-grade/source/gfx/lut/gfx-lut-producer.cpp b/components/color-grade/source/gfx/lut/gfx-lut-producer.cpp new file mode 100644 index 0000000..acc8cc7 --- /dev/null +++ b/components/color-grade/source/gfx/lut/gfx-lut-producer.cpp @@ -0,0 +1,76 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2021-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2022 lainon +// AUTOGENERATED COPYRIGHT HEADER END + +#include "gfx-lut-producer.hpp" + +#include "obs/gs/gs-helper.hpp" + +gs_color_format format_from_depth(streamfx::gfx::lut::color_depth depth) +{ + switch (depth) { + case streamfx::gfx::lut::color_depth::_2: + case streamfx::gfx::lut::color_depth::_4: + case streamfx::gfx::lut::color_depth::_6: + case streamfx::gfx::lut::color_depth::_8: + return gs_color_format::GS_RGBA; + case streamfx::gfx::lut::color_depth::_10: + return gs_color_format::GS_R10G10B10A2; + case streamfx::gfx::lut::color_depth::_12: + case streamfx::gfx::lut::color_depth::_14: + case streamfx::gfx::lut::color_depth::_16: + return gs_color_format::GS_RGBA16; + default: + return GS_RGBA32F; + } +} + +streamfx::gfx::lut::producer::producer() : _gfx_util(::streamfx::gfx::util::get()) +{ + _data = streamfx::gfx::lut::data::instance(); + if (!_data->producer_effect()) + throw std::runtime_error("Unable to get LUT producer effect."); +} + +streamfx::gfx::lut::producer::~producer() = default; + +std::shared_ptr streamfx::gfx::lut::producer::produce(streamfx::gfx::lut::color_depth depth) +{ + auto gctx = streamfx::obs::gs::context(); + + if (!_rt || (_rt->color_format() != format_from_depth((depth)))) { + _rt = std::make_shared(format_from_depth(depth), GS_ZS_NONE); + } + + auto effect = _data->producer_effect(); + + int32_t idepth = static_cast(depth); + int32_t size = static_cast(pow(2l, idepth)); + int32_t grid_size = static_cast(pow(2l, (idepth / 2))); + int32_t container_size = static_cast(pow(2l, (idepth + (idepth / 2)))); + + { + auto op = _rt->render(static_cast(container_size), static_cast(container_size)); + + gs_blend_state_push(); + gs_enable_color(true, true, true, false); + gs_enable_blending(false); + gs_enable_stencil_test(false); + gs_enable_stencil_write(false); + gs_ortho(0, 1, 0, 1, 0, 1); + + if (streamfx::obs::gs::effect_parameter efp = effect->get_parameter("lut_params_0"); efp) { + efp.set_int4(size, grid_size, container_size, 0l); + } + + while (gs_effect_loop(effect->get_object(), "Draw")) { + _gfx_util->draw_fullscreen_triangle(); + } + + gs_enable_color(true, true, true, true); + gs_blend_state_pop(); + } + + return _rt->get_texture(); +} diff --git a/components/color-grade/source/gfx/lut/gfx-lut-producer.hpp b/components/color-grade/source/gfx/lut/gfx-lut-producer.hpp new file mode 100644 index 0000000..45a3ce8 --- /dev/null +++ b/components/color-grade/source/gfx/lut/gfx-lut-producer.hpp @@ -0,0 +1,26 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "warning-disable.hpp" +#include +#include "gfx-lut.hpp" +#include "gfx/gfx-util.hpp" +#include "obs/gs/gs-effect.hpp" +#include "obs/gs/gs-texrender.hpp" +#include "warning-enable.hpp" + +namespace streamfx::gfx::lut { + class producer { + std::shared_ptr _data; + std::shared_ptr _rt; + std::shared_ptr _gfx_util; + + public: + producer(); + ~producer(); + + std::shared_ptr produce(streamfx::gfx::lut::color_depth depth); + }; +} // namespace streamfx::gfx::lut diff --git a/components/color-grade/source/gfx/lut/gfx-lut.cpp b/components/color-grade/source/gfx/lut/gfx-lut.cpp new file mode 100644 index 0000000..2774b8f --- /dev/null +++ b/components/color-grade/source/gfx/lut/gfx-lut.cpp @@ -0,0 +1,73 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2021-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#include "gfx-lut.hpp" +#include "obs/gs/gs-helper.hpp" +#include "plugin.hpp" +#include "util/util-logging.hpp" + +#include "warning-disable.hpp" +#include +#include "warning-enable.hpp" + +#ifdef _DEBUG +#define ST_PREFIX "<%s> " +#define D_LOG_ERROR(x, ...) P_LOG_ERROR(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_WARNING(x, ...) P_LOG_WARN(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_INFO(x, ...) P_LOG_INFO(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_DEBUG(x, ...) P_LOG_DEBUG(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#else +#define ST_PREFIX " " +#define D_LOG_ERROR(...) P_LOG_ERROR(ST_PREFIX __VA_ARGS__) +#define D_LOG_WARNING(...) P_LOG_WARN(ST_PREFIX __VA_ARGS__) +#define D_LOG_INFO(...) P_LOG_INFO(ST_PREFIX __VA_ARGS__) +#define D_LOG_DEBUG(...) P_LOG_DEBUG(ST_PREFIX __VA_ARGS__) +#endif + +using namespace streamfx; + +std::shared_ptr streamfx::gfx::lut::data::instance() +{ + static std::weak_ptr _instance; + static std::mutex _mutex; + + std::lock_guard lock(_mutex); + + auto reference = _instance.lock(); + if (!reference) { + reference = std::shared_ptr(new streamfx::gfx::lut::data()); + _instance = reference; + } + return reference; +} + +streamfx::gfx::lut::data::data() : _producer_effect(), _consumer_effect() +{ + auto gctx = streamfx::obs::gs::context(); + + std::filesystem::path lut_producer_path = streamfx::data_file_path("effects/lut-producer.effect"); + if (std::filesystem::exists(lut_producer_path)) { + try { + _producer_effect = std::make_shared(lut_producer_path); + } catch (std::exception const& ex) { + D_LOG_ERROR("Loading LUT Producer effect failed: %s", ex.what()); + } + } + + std::filesystem::path lut_consumer_path = streamfx::data_file_path("effects/lut-consumer.effect"); + if (std::filesystem::exists(lut_consumer_path)) { + try { + _consumer_effect = std::make_shared(lut_consumer_path); + } catch (std::exception const& ex) { + D_LOG_ERROR("Loading LUT Consumer effect failed: %s", ex.what()); + } + } +} + +streamfx::gfx::lut::data::~data() +{ + auto gctx = streamfx::obs::gs::context(); + _producer_effect.reset(); + _consumer_effect.reset(); +} diff --git a/components/color-grade/source/gfx/lut/gfx-lut.hpp b/components/color-grade/source/gfx/lut/gfx-lut.hpp new file mode 100644 index 0000000..d9e412c --- /dev/null +++ b/components/color-grade/source/gfx/lut/gfx-lut.hpp @@ -0,0 +1,48 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "obs/gs/gs-effect.hpp" + +#include "warning-disable.hpp" +#include +#include "warning-enable.hpp" + +namespace streamfx::gfx::lut { + class data { + std::shared_ptr _producer_effect; + std::shared_ptr _consumer_effect; + + public: + static std::shared_ptr instance(); + + private: + data(); + + public: + ~data(); + + inline std::shared_ptr producer_effect() + { + return _producer_effect; + }; + + inline std::shared_ptr consumer_effect() + { + return _consumer_effect; + }; + }; + + enum class color_depth { + Invalid = 0, + _2 = 2, + _4 = 4, + _6 = 6, + _8 = 8, + _10 = 10, + _12 = 12, + _14 = 14, + _16 = 16, + }; +} // namespace streamfx::gfx::lut diff --git a/components/denoising/CMakeLists.txt b/components/denoising/CMakeLists.txt new file mode 100644 index 0000000..f3ad636 --- /dev/null +++ b/components/denoising/CMakeLists.txt @@ -0,0 +1,23 @@ +# AUTOGENERATED COPYRIGHT HEADER START +# Copyright (C) 2023 Michael Fabian 'Xaymar' Dirks +# AUTOGENERATED COPYRIGHT HEADER END + +cmake_minimum_required(VERSION 3.26) +project("Denoising") +list(APPEND CMAKE_MESSAGE_INDENT "[${PROJECT_NAME}] ") + +streamfx_add_component("Denoising" + RESOLVER streamfx_denoising_resolver +) +streamfx_add_component_dependency("NVIDIA" OPTIONAL) + +function(streamfx_denoising_resolver) + # Providers + #- NVIDIA + streamfx_enabled_component("NVIDIA" T_CHECK) + if(T_CHECK) + target_compile_definitions(${COMPONENT_TARGET} PRIVATE + ENABLE_NVIDIA + ) + endif() +endfunction() diff --git a/components/denoising/source/filters/filter-denoising.cpp b/components/denoising/source/filters/filter-denoising.cpp new file mode 100644 index 0000000..8ef21f0 --- /dev/null +++ b/components/denoising/source/filters/filter-denoising.cpp @@ -0,0 +1,634 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2021-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#include "filter-denoising.hpp" +#include "obs/gs/gs-helper.hpp" +#include "plugin.hpp" +#include "util/util-logging.hpp" + +#include "warning-disable.hpp" +#include +#include "warning-enable.hpp" + +#ifdef _DEBUG +#define ST_PREFIX "<%s> " +#define D_LOG_ERROR(x, ...) P_LOG_ERROR(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_WARNING(x, ...) P_LOG_WARN(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_INFO(x, ...) P_LOG_INFO(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_DEBUG(x, ...) P_LOG_DEBUG(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#else +#define ST_PREFIX " " +#define D_LOG_ERROR(...) P_LOG_ERROR(ST_PREFIX __VA_ARGS__) +#define D_LOG_WARNING(...) P_LOG_WARN(ST_PREFIX __VA_ARGS__) +#define D_LOG_INFO(...) P_LOG_INFO(ST_PREFIX __VA_ARGS__) +#define D_LOG_DEBUG(...) P_LOG_DEBUG(ST_PREFIX __VA_ARGS__) +#endif + +#define ST_I18N "Filter.Denoising" +#define ST_KEY_PROVIDER "Provider" +#define ST_I18N_PROVIDER ST_I18N "." ST_KEY_PROVIDER +#define ST_I18N_PROVIDER_NVIDIA_DENOISING ST_I18N_PROVIDER ".NVIDIA.Denoising" + +#ifdef ENABLE_NVIDIA +#define ST_KEY_NVIDIA_DENOISING "NVIDIA.Denoising" +#define ST_I18N_NVIDIA_DENOISING ST_I18N "." ST_KEY_NVIDIA_DENOISING +#define ST_KEY_NVIDIA_DENOISING_STRENGTH "NVIDIA.Denoising.Strength" +#define ST_I18N_NVIDIA_DENOISING_STRENGTH ST_I18N "." ST_KEY_NVIDIA_DENOISING_STRENGTH +#define ST_I18N_NVIDIA_DENOISING_STRENGTH_WEAK ST_I18N_NVIDIA_DENOISING_STRENGTH ".Weak" +#define ST_I18N_NVIDIA_DENOISING_STRENGTH_STRONG ST_I18N_NVIDIA_DENOISING_STRENGTH ".Strong" +#endif + +using streamfx::filter::denoising::denoising_factory; +using streamfx::filter::denoising::denoising_instance; +using streamfx::filter::denoising::denoising_provider; + +static constexpr std::string_view HELP_URL = "https://github.com/Xaymar/obs-StreamFX/wiki/Filter-Denoising"; + +static denoising_provider provider_priority[] = { + denoising_provider::NVIDIA_DENOISING, +}; + +const char* streamfx::filter::denoising::cstring(denoising_provider provider) +{ + switch (provider) { + case denoising_provider::INVALID: + return "N/A"; + case denoising_provider::AUTOMATIC: + return D_TRANSLATE(S_STATE_AUTOMATIC); + case denoising_provider::NVIDIA_DENOISING: + return D_TRANSLATE(ST_I18N_PROVIDER_NVIDIA_DENOISING); + default: + throw std::runtime_error("Missing Conversion Entry"); + } +} + +std::string streamfx::filter::denoising::string(denoising_provider provider) +{ + return cstring(provider); +} + +//------------------------------------------------------------------------------ +// Instance +//------------------------------------------------------------------------------ +denoising_instance::denoising_instance(obs_data_t* data, obs_source_t* self) + : obs::source_instance(data, self), + + _size(1, 1), _provider(denoising_provider::INVALID), _provider_ui(denoising_provider::INVALID), _provider_ready(false), _provider_lock(), _provider_task(), _input(), _output() +{ + D_LOG_DEBUG("Initializating... (Addr: 0x%" PRIuPTR ")", this); + + { + ::streamfx::obs::gs::context gctx; + + // Create the render target for the input buffering. + _input = std::make_shared<::streamfx::obs::gs::texrender>(GS_RGBA_UNORM, GS_ZS_NONE); + _input->render(1, 1); // Preallocate the RT on the driver and GPU. + _output = _input->get_texture(); + + // Load the required effect. + _standard_effect = std::make_shared<::streamfx::obs::gs::effect>(::streamfx::data_file_path("effects/standard.effect")); + } + + if (data) { + load(data); + } +} + +denoising_instance::~denoising_instance() +{ + D_LOG_DEBUG("Finalizing... (Addr: 0x%" PRIuPTR ")", this); + + { // Unload the underlying effect ASAP. + std::unique_lock ul(_provider_lock); + + // De-queue the underlying task. + if (_provider_task) { + streamfx::util::threadpool::threadpool::instance()->pop(_provider_task); + _provider_task->await_completion(); + _provider_task.reset(); + } + + // TODO: Make this asynchronous. + switch (_provider) { +#ifdef ENABLE_NVIDIA + case denoising_provider::NVIDIA_DENOISING: + nvvfx_denoising_unload(); + break; +#endif + default: + break; + } + } +} + +void denoising_instance::load(obs_data_t* data) +{ + update(data); +} + +void denoising_instance::migrate(obs_data_t* data, uint64_t version) {} + +void denoising_instance::update(obs_data_t* data) +{ + // Check if the user changed which Denoising provider we use. + denoising_provider provider = static_cast(obs_data_get_int(data, ST_KEY_PROVIDER)); + if (provider == denoising_provider::AUTOMATIC) { + provider = denoising_factory::instance()->find_ideal_provider(); + } + + // Check if the provider was changed, and if so switch. + if (provider != _provider) { + _provider_ui = provider; + switch_provider(provider); + } + + if (_provider_ready) { + std::unique_lock ul(_provider_lock); + + switch (_provider) { +#ifdef ENABLE_NVIDIA + case denoising_provider::NVIDIA_DENOISING: + nvvfx_denoising_update(data); + break; +#endif + default: + break; + } + } +} + +void streamfx::filter::denoising::denoising_instance::properties(obs_properties_t* properties) +{ + switch (_provider_ui) { +#ifdef ENABLE_NVIDIA + case denoising_provider::NVIDIA_DENOISING: + nvvfx_denoising_properties(properties); + break; +#endif + default: + break; + } +} + +uint32_t streamfx::filter::denoising::denoising_instance::get_width() +{ + return std::max(_size.first, 1); +} + +uint32_t streamfx::filter::denoising::denoising_instance::get_height() +{ + return std::max(_size.second, 1); +} + +void denoising_instance::video_tick(float time) +{ + auto parent = obs_filter_get_parent(_self); + auto target = obs_filter_get_target(_self); + auto width = obs_source_get_base_width(target); + auto height = obs_source_get_base_height(target); + + // Verify that the detected size makes sense. + if ((width > 0) && (height > 0)) { + _size = {width, height}; + } + + // Allow the provider to restrict the size. + if (target && _provider_ready) { + std::unique_lock ul(_provider_lock); + + switch (_provider) { +#ifdef ENABLE_NVIDIA + case denoising_provider::NVIDIA_DENOISING: + nvvfx_denoising_size(); + break; +#endif + default: + break; + } + } + + _dirty = true; +} + +void denoising_instance::video_render(gs_effect_t* effect) +{ + auto parent = obs_filter_get_parent(_self); + auto target = obs_filter_get_target(_self); + auto width = obs_source_get_base_width(target); + auto height = obs_source_get_base_height(target); + vec4 blank = vec4{0, 0, 0, 0}; + + // Ensure we have the bare minimum of valid information. + target = target ? target : parent; + effect = effect ? effect : obs_get_base_effect(OBS_EFFECT_DEFAULT); + + // Skip the filter if: + // - The Provider isn't ready yet. + // - We don't have a target. + // - The width/height of the next filter in the chain is empty. + if (!_provider_ready || !target || (width == 0) || (height == 0)) { + obs_source_skip_video_filter(_self); + return; + } + +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + ::streamfx::obs::gs::debug_marker profiler0{::streamfx::obs::gs::debug_color_source, "StreamFX Denoising"}; + ::streamfx::obs::gs::debug_marker profiler0_0{::streamfx::obs::gs::debug_color_gray, "'%s' on '%s'", obs_source_get_name(_self), obs_source_get_name(parent)}; +#endif + + if (_dirty) { // Lock the provider from being changed. + std::unique_lock ul(_provider_lock); + + { // Allow the provider to restrict the size. + switch (_provider) { +#ifdef ENABLE_NVIDIA + case denoising_provider::NVIDIA_DENOISING: + nvvfx_denoising_size(); + break; +#endif + default: + _size = {width, height}; + break; + } + } + + { // Capture the incoming frame. +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + ::streamfx::obs::gs::debug_marker profiler1{::streamfx::obs::gs::debug_color_capture, "Capture"}; +#endif + if (obs_source_process_filter_begin(_self, GS_RGBA, OBS_ALLOW_DIRECT_RENDERING)) { + auto op = _input->render(_size.first, _size.second); + + // Matrix + gs_matrix_push(); + gs_ortho(0., 1., 0., 1., 0., 1.); + + // Clear the buffer + gs_clear(GS_CLEAR_COLOR | GS_CLEAR_DEPTH, &blank, 0, 0); + + // Set GPU state + gs_blend_state_push(); + gs_enable_color(true, true, true, true); + gs_enable_blending(false); + gs_enable_depth_test(false); + gs_enable_stencil_test(false); + gs_set_cull_mode(GS_NEITHER); + + // Render +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + ::streamfx::obs::gs::debug_marker profiler2{::streamfx::obs::gs::debug_color_capture, "Storage"}; +#endif + obs_source_process_filter_end(_self, obs_get_base_effect(OBS_EFFECT_DEFAULT), 1, 1); + + // Reset GPU state + gs_blend_state_pop(); + gs_matrix_pop(); + } else { + obs_source_skip_video_filter(_self); + return; + } + } + + try { // Process the captured input with the provider. +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + ::streamfx::obs::gs::debug_marker profiler1{::streamfx::obs::gs::debug_color_convert, "Process"}; +#endif + switch (_provider) { +#ifdef ENABLE_NVIDIA + case denoising_provider::NVIDIA_DENOISING: + nvvfx_denoising_process(); + break; +#endif + default: + _output.reset(); + break; + } + } catch (...) { + obs_source_skip_video_filter(_self); + return; + } + + if (!_output) { + D_LOG_ERROR("Provider '%s' did not return a result.", cstring(_provider)); + obs_source_skip_video_filter(_self); + return; + } + + // Mark "clean". + _dirty = false; + + // Unlock the provider, as we are no longer doing critical work with it. + } + + { // Draw the result for the next filter to use. +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + ::streamfx::obs::gs::debug_marker profiler1{::streamfx::obs::gs::debug_color_render, "Render"}; +#endif + if (_standard_effect->has_parameter("InputA", ::streamfx::obs::gs::effect_parameter::type::Texture)) { + _standard_effect->get_parameter("InputA").set_texture(_output); + } + if (_standard_effect->has_parameter("InputB", ::streamfx::obs::gs::effect_parameter::type::Texture)) { + _standard_effect->get_parameter("InputB").set_texture(_input->get_texture()); + } + while (gs_effect_loop(_standard_effect->get_object(), "RestoreAlpha")) { + gs_draw_sprite(nullptr, 0, _size.first, _size.second); + } + } +} + +struct switch_provider_data_t { + denoising_provider provider; +}; + +void streamfx::filter::denoising::denoising_instance::switch_provider(denoising_provider provider) +{ + std::unique_lock ul(_provider_lock); + + // Safeguard against calls made from unlocked memory. + if (provider == _provider) { + return; + } + + // This doesn't work correctly. + // - Need to allow multiple switches at once because OBS is weird. + // - Doesn't guarantee that the task is properly killed off. + + // Log information. + D_LOG_INFO("Instance '%s' is switching provider from '%s' to '%s'.", obs_source_get_name(_self), cstring(_provider), cstring(provider)); + + // If there is an existing task, attempt to cancel it. + if (_provider_task) { + // De-queue it. + streamfx::util::threadpool::threadpool::instance()->pop(_provider_task); + + // Await the death of the task itself. + _provider_task->await_completion(); + + // Clear any memory associated with it. + _provider_task.reset(); + } + + // Build data to pass into the task. + auto spd = std::make_shared(); + spd->provider = _provider; + _provider = provider; + + // Then spawn a new task to switch provider. + _provider_task = streamfx::util::threadpool::threadpool::instance()->push(std::bind(&denoising_instance::task_switch_provider, this, std::placeholders::_1), spd); +} + +void streamfx::filter::denoising::denoising_instance::task_switch_provider(util::threadpool::task_data_t data) +{ + std::shared_ptr spd = std::static_pointer_cast(data); + + // 1. Mark the provider as no longer ready. + _provider_ready = false; + + // 2. Lock the provider from being used. + std::unique_lock ul(_provider_lock); + + try { + // 3. Unload the previous provider. + switch (spd->provider) { +#ifdef ENABLE_NVIDIA + case denoising_provider::NVIDIA_DENOISING: + nvvfx_denoising_unload(); + break; +#endif + default: + break; + } + + // 4. Load the new provider. + switch (_provider) { +#ifdef ENABLE_NVIDIA + case denoising_provider::NVIDIA_DENOISING: + nvvfx_denoising_load(); + break; +#endif + default: + break; + } + + // Log information. + D_LOG_INFO("Instance '%s' switched provider from '%s' to '%s'.", obs_source_get_name(_self), cstring(spd->provider), cstring(_provider)); + + _provider_ready = true; + } catch (std::exception const& ex) { + // Log information. + D_LOG_ERROR("Instance '%s' failed switching provider with error: %s", obs_source_get_name(_self), ex.what()); + } +} + +#ifdef ENABLE_NVIDIA +void streamfx::filter::denoising::denoising_instance::nvvfx_denoising_load() +{ + _nvidia_fx = std::make_shared<::streamfx::nvidia::vfx::denoising>(); +} + +void streamfx::filter::denoising::denoising_instance::nvvfx_denoising_unload() +{ + _nvidia_fx.reset(); +} + +void streamfx::filter::denoising::denoising_instance::nvvfx_denoising_size() +{ + if (!_nvidia_fx) { + return; + } + + _nvidia_fx->size(_size); +} + +void streamfx::filter::denoising::denoising_instance::nvvfx_denoising_process() +{ + if (!_nvidia_fx) { + _output = _input->get_texture(); + return; + } + + _output = _nvidia_fx->process(_input->get_texture()); +} + +void streamfx::filter::denoising::denoising_instance::nvvfx_denoising_properties(obs_properties_t* props) +{ + obs_properties_t* grp = obs_properties_create(); + obs_properties_add_group(props, ST_KEY_NVIDIA_DENOISING, D_TRANSLATE(ST_I18N_NVIDIA_DENOISING), OBS_GROUP_NORMAL, grp); + + { + auto p = obs_properties_add_list(grp, ST_KEY_NVIDIA_DENOISING_STRENGTH, D_TRANSLATE(ST_I18N_NVIDIA_DENOISING_STRENGTH), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); + obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_NVIDIA_DENOISING_STRENGTH_WEAK), 0); + obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_NVIDIA_DENOISING_STRENGTH_STRONG), 1); + } +} + +void streamfx::filter::denoising::denoising_instance::nvvfx_denoising_update(obs_data_t* data) +{ + if (!_nvidia_fx) + return; + + _nvidia_fx->set_strength(static_cast(obs_data_get_int(data, ST_KEY_NVIDIA_DENOISING_STRENGTH) == 0 ? 0. : 1.)); +} + +#endif + +//------------------------------------------------------------------------------ +// Factory +//------------------------------------------------------------------------------ +denoising_factory::~denoising_factory() {} + +denoising_factory::denoising_factory() +{ + bool any_available = false; + + // 1. Try and load any configured providers. +#ifdef ENABLE_NVIDIA + try { + // Load CVImage and Video Effects SDK. + _nvcuda = ::streamfx::nvidia::cuda::obs::get(); + _nvcvi = ::streamfx::nvidia::cv::cv::get(); + _nvvfx = ::streamfx::nvidia::vfx::vfx::get(); + _nvidia_available = true; + any_available |= _nvidia_available; + } catch (const std::exception& ex) { + _nvidia_available = false; + _nvvfx.reset(); + _nvcvi.reset(); + _nvcuda.reset(); + D_LOG_WARNING("Failed to make NVIDIA providers available due to error: %s", ex.what()); + } catch (...) { + _nvidia_available = false; + _nvvfx.reset(); + _nvcvi.reset(); + _nvcuda.reset(); + D_LOG_WARNING("Failed to make NVIDIA providers available with unknown error.", nullptr); + } +#endif + + // 2. Check if any of them managed to load at all. + if (!any_available) { + D_LOG_ERROR("All supported providers failed to initialize, disabling effect.", 0); + return; + } + + // 3. In any other case, register the filter! + _info.id = S_PREFIX "filter-denoising"; + _info.type = OBS_SOURCE_TYPE_FILTER; + _info.output_flags = OBS_SOURCE_VIDEO | OBS_SOURCE_CUSTOM_DRAW; + + support_size(true); + finish_setup(); + + // Proxies + register_proxy("streamfx-filter-video-denoising"); +} + +const char* denoising_factory::get_name() +{ + return D_TRANSLATE(ST_I18N); +} + +void denoising_factory::get_defaults2(obs_data_t* data) +{ + obs_data_set_default_int(data, ST_KEY_PROVIDER, static_cast(denoising_provider::AUTOMATIC)); + +#ifdef ENABLE_NVIDIA + obs_data_set_default_double(data, ST_KEY_NVIDIA_DENOISING_STRENGTH, 1.); +#endif +} + +static bool modified_provider(obs_properties_t* props, obs_property_t*, obs_data_t* settings) noexcept +{ + try { + return true; + } catch (const std::exception& ex) { + DLOG_ERROR("Unexpected exception in function '%s': %s.", __FUNCTION_NAME__, ex.what()); + return false; + } catch (...) { + DLOG_ERROR("Unexpected exception in function '%s'.", __FUNCTION_NAME__); + return false; + } +} + +obs_properties_t* denoising_factory::get_properties2(denoising_instance* data) +{ + obs_properties_t* pr = obs_properties_create(); + + { + obs_properties_add_button2(pr, S_MANUAL_OPEN, D_TRANSLATE(S_MANUAL_OPEN), denoising_factory::on_manual_open, nullptr); + } + + if (data) { + data->properties(pr); + } + + { // Advanced Settings + auto grp = obs_properties_create(); + obs_properties_add_group(pr, S_ADVANCED, D_TRANSLATE(S_ADVANCED), OBS_GROUP_NORMAL, grp); + + { + auto p = obs_properties_add_list(grp, ST_KEY_PROVIDER, D_TRANSLATE(ST_I18N_PROVIDER), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); + obs_property_set_modified_callback(p, modified_provider); + obs_property_list_add_int(p, D_TRANSLATE(S_STATE_AUTOMATIC), static_cast(denoising_provider::AUTOMATIC)); + obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_PROVIDER_NVIDIA_DENOISING), static_cast(denoising_provider::NVIDIA_DENOISING)); + } + } + + return pr; +} + +bool denoising_factory::on_manual_open(obs_properties_t* props, obs_property_t* property, void* data) +{ + streamfx::open_url(HELP_URL); + return false; +} + +bool streamfx::filter::denoising::denoising_factory::is_provider_available(denoising_provider provider) +{ + switch (provider) { +#ifdef ENABLE_NVIDIA + case denoising_provider::NVIDIA_DENOISING: + return _nvidia_available; +#endif + default: + return false; + } +} + +denoising_provider streamfx::filter::denoising::denoising_factory::find_ideal_provider() +{ + for (auto v : provider_priority) { + if (is_provider_available(v)) { + return v; + break; + } + } + return denoising_provider::AUTOMATIC; +} + +std::shared_ptr denoising_factory::instance() +{ + static std::weak_ptr winst; + static std::mutex mtx; + + std::unique_lock lock(mtx); + auto instance = winst.lock(); + if (!instance) { + instance = std::shared_ptr(new denoising_factory()); + winst = instance; + } + return instance; +} + +static std::shared_ptr loader_instance; + +static auto loader = streamfx::component( + "denoising", + []() { // Initializer + loader_instance = denoising_factory::instance(); + }, + []() { // Finalizer + loader_instance.reset(); + }, + {"core::threadpool", "core::gs::sampler", "core::gs::texrender", "core::gs::texture"}); diff --git a/components/denoising/source/filters/filter-denoising.hpp b/components/denoising/source/filters/filter-denoising.hpp new file mode 100644 index 0000000..4bbbc3e --- /dev/null +++ b/components/denoising/source/filters/filter-denoising.hpp @@ -0,0 +1,110 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2021-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "obs/gs/gs-effect.hpp" +#include "obs/gs/gs-texrender.hpp" +#include "obs/gs/gs-texture.hpp" +#include "obs/obs-source-factory.hpp" +#include "plugin.hpp" +#include "util/util-threadpool.hpp" + +#include "warning-disable.hpp" +#include +#include +#include +#include "warning-enable.hpp" + +#ifdef ENABLE_NVIDIA +#include "nvidia/vfx/nvidia-vfx-denoising.hpp" +#endif + +namespace streamfx::filter::denoising { + enum denoising_provider { + INVALID = -1, + AUTOMATIC = 0, + NVIDIA_DENOISING = 1, + }; + + const char* cstring(denoising_provider provider); + + std::string string(denoising_provider provider); + + class denoising_instance : public obs::source_instance { + std::pair _size; + + denoising_provider _provider; + denoising_provider _provider_ui; + std::atomic _provider_ready; + std::mutex _provider_lock; + std::shared_ptr _provider_task; + + std::shared_ptr<::streamfx::obs::gs::effect> _standard_effect; + + std::shared_ptr<::streamfx::obs::gs::texrender> _input; + std::shared_ptr<::streamfx::obs::gs::texture> _output; + bool _dirty; + +#ifdef ENABLE_NVIDIA + std::shared_ptr<::streamfx::nvidia::vfx::denoising> _nvidia_fx; +#endif + + public: + denoising_instance(obs_data_t* data, obs_source_t* self); + ~denoising_instance() override; + + void load(obs_data_t* data) override; + void migrate(obs_data_t* data, uint64_t version) override; + void update(obs_data_t* data) override; + void properties(obs_properties_t* properties); + + uint32_t get_width() override; + uint32_t get_height() override; + + void video_tick(float time) override; + void video_render(gs_effect_t* effect) override; + + private: + void switch_provider(denoising_provider provider); + void task_switch_provider(util::threadpool::task_data_t data); + +#ifdef ENABLE_NVIDIA + void nvvfx_denoising_load(); + void nvvfx_denoising_unload(); + void nvvfx_denoising_size(); + void nvvfx_denoising_process(); + void nvvfx_denoising_properties(obs_properties_t* props); + void nvvfx_denoising_update(obs_data_t* data); +#endif + }; + + class denoising_factory : public obs::source_factory<::streamfx::filter::denoising::denoising_factory, ::streamfx::filter::denoising::denoising_instance> { +#ifdef ENABLE_NVIDIA + bool _nvidia_available; + std::shared_ptr<::streamfx::nvidia::cuda::obs> _nvcuda; + std::shared_ptr<::streamfx::nvidia::cv::cv> _nvcvi; + std::shared_ptr<::streamfx::nvidia::vfx::vfx> _nvvfx; +#endif + + public: + virtual ~denoising_factory(); + denoising_factory(); + + virtual const char* get_name() override; + + virtual void get_defaults2(obs_data_t* data) override; + virtual obs_properties_t* get_properties2(denoising_instance* data) override; + + static bool on_manual_open(obs_properties_t* props, obs_property_t* property, void* data); + + bool is_provider_available(denoising_provider); + denoising_provider find_ideal_provider(); + + public: // Singleton + static void initialize(); + static void finalize(); + static std::shared_ptr<::streamfx::filter::denoising::denoising_factory> instance(); + }; + +} // namespace streamfx::filter::denoising diff --git a/components/dynamic-mask/CMakeLists.txt b/components/dynamic-mask/CMakeLists.txt new file mode 100644 index 0000000..9e7c7a1 --- /dev/null +++ b/components/dynamic-mask/CMakeLists.txt @@ -0,0 +1,9 @@ +# AUTOGENERATED COPYRIGHT HEADER START +# Copyright (C) 2023 Michael Fabian 'Xaymar' Dirks +# AUTOGENERATED COPYRIGHT HEADER END + +cmake_minimum_required(VERSION 3.26) +project("DynamicMask") +list(APPEND CMAKE_MESSAGE_INDENT "[${PROJECT_NAME}] ") + +streamfx_add_component("Dynamic Mask") diff --git a/components/dynamic-mask/source/filters/filter-dynamic-mask.cpp b/components/dynamic-mask/source/filters/filter-dynamic-mask.cpp new file mode 100644 index 0000000..8baa99e --- /dev/null +++ b/components/dynamic-mask/source/filters/filter-dynamic-mask.cpp @@ -0,0 +1,845 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2022 lainon +// AUTOGENERATED COPYRIGHT HEADER END + +#include "filter-dynamic-mask.hpp" +#include "strings.hpp" +#include "obs/gs/gs-helper.hpp" +#include "util/util-logging.hpp" + +#include "warning-disable.hpp" +#include +#include +#include +#include +#include "warning-enable.hpp" + +#ifdef _DEBUG +#define ST_PREFIX "<%s> " +#define D_LOG_ERROR(x, ...) P_LOG_ERROR(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_WARNING(x, ...) P_LOG_WARN(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_INFO(x, ...) P_LOG_INFO(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_DEBUG(x, ...) P_LOG_DEBUG(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#else +#define ST_PREFIX " " +#define D_LOG_ERROR(...) P_LOG_ERROR(ST_PREFIX __VA_ARGS__) +#define D_LOG_WARNING(...) P_LOG_WARN(ST_PREFIX __VA_ARGS__) +#define D_LOG_INFO(...) P_LOG_INFO(ST_PREFIX __VA_ARGS__) +#define D_LOG_DEBUG(...) P_LOG_DEBUG(ST_PREFIX __VA_ARGS__) +#endif + +// Filter to allow dynamic masking +// Allow any channel to affect any other channel +// +// Red/Green/Blue/Alpha Mask Input +// - Red Mask Output +// - Blue Mask Output +// - Green Mask Output +// - Alpha Mask Output + +#define ST_I18N "Filter.DynamicMask" + +#define ST_I18N_INPUT "Filter.DynamicMask.Input" +#define ST_KEY_INPUT "Filter.DynamicMask.Input" +#define ST_I18N_CHANNEL "Filter.DynamicMask.Channel" +#define ST_KEY_CHANNEL "Filter.DynamicMask.Channel" +#define ST_I18N_CHANNEL_VALUE "Filter.DynamicMask.Channel.Value" +#define ST_KEY_CHANNEL_VALUE "Filter.DynamicMask.Channel.Value" +#define ST_I18N_CHANNEL_MULTIPLIER "Filter.DynamicMask.Channel.Multiplier" +#define ST_KEY_CHANNEL_MULTIPLIER "Filter.DynamicMask.Channel.Multiplier" +#define ST_I18N_CHANNEL_INPUT "Filter.DynamicMask.Channel.Input" +#define ST_KEY_CHANNEL_INPUT "Filter.DynamicMask.Channel.Input" +#define ST_KEY_DEBUG_TEXTURE "Debug.Texture" +#define ST_I18N_DEBUG_TEXTURE ST_I18N ".Debug.Texture" +#define ST_I18N_DEBUG_TEXTURE_BASE ST_I18N_DEBUG_TEXTURE ".Base" +#define ST_I18N_DEBUG_TEXTURE_INPUT ST_I18N_DEBUG_TEXTURE ".Input" + +using namespace streamfx::filter::dynamic_mask; + +static constexpr std::string_view HELP_URL = "https://github.com/Xaymar/obs-StreamFX/wiki/Filter-Dynamic-Mask"; + +static std::pair channel_translations[] = { + {channel::Red, S_CHANNEL_RED}, + {channel::Green, S_CHANNEL_GREEN}, + {channel::Blue, S_CHANNEL_BLUE}, + {channel::Alpha, S_CHANNEL_ALPHA}, +}; + +data::data() +{ + auto gctx = streamfx::obs::gs::context(); + + _channel_mask_fx = streamfx::obs::gs::effect::create(streamfx::data_file_path("effects/channel-mask.effect")); +} + +data::~data() {} + +streamfx::obs::gs::effect data::channel_mask_fx() +{ + return _channel_mask_fx; +} + +std::shared_ptr data::get() +{ + static std::mutex instance_lock; + static std::weak_ptr weak_instance; + + std::lock_guard lock(instance_lock); + auto instance = weak_instance.lock(); + if (!instance) { + instance = std::shared_ptr{new streamfx::filter::dynamic_mask::data()}; + weak_instance = instance; + } + return instance; +} + +dynamic_mask_instance::dynamic_mask_instance(obs_data_t* settings, obs_source_t* self) + : obs::source_instance(settings, self), // + _data(streamfx::filter::dynamic_mask::data::get()), // + _gfx_util(::streamfx::gfx::util::get()), // + _translation_map(), // + _input(), // + _input_child(), // + _input_vs(), // + _input_ac(), // + _have_base(false), // + _base_rt(), // + _base_tex(), // + _base_color_space(GS_CS_SRGB), // + _base_color_format(GS_RGBA), // + _have_input(false), // + _input_rt(), // + _input_tex(), // + _input_color_space(GS_CS_SRGB), // + _input_color_format(GS_RGBA), // + _have_final(false), // + _final_rt(), // + _final_tex(), // + _channels(), // + _precalc(), // + _debug_texture(-1) // +{ + update(settings); +} + +dynamic_mask_instance::~dynamic_mask_instance() +{ + release(); +} + +void dynamic_mask_instance::load(obs_data_t* settings) +{ + update(settings); +} + +void dynamic_mask_instance::migrate(obs_data_t* data, uint64_t version) {} + +void dynamic_mask_instance::update(obs_data_t* settings) +{ + // Update source. + if (const char* v = obs_data_get_string(settings, ST_KEY_INPUT); (v != nullptr) && (v[0] != '\0')) { + if (!acquire(v)) + DLOG_ERROR("Failed to acquire Input source '%s'.", v); + } else { + release(); + } + + // Update data store + for (auto kv1 : channel_translations) { + auto found = _channels.find(kv1.first); + if (found == _channels.end()) { + _channels.insert({kv1.first, channel_data()}); + found = _channels.find(kv1.first); + if (found == _channels.end()) { + assert(found != _channels.end()); + throw std::runtime_error("Unable to insert element into data _store."); + } + } + + std::string chv_key = std::string(ST_KEY_CHANNEL_VALUE) + "." + kv1.second; + found->second.value = static_cast(obs_data_get_double(settings, chv_key.c_str())); + _precalc.base.ptr[static_cast(kv1.first)] = found->second.value; + + std::string chm_key = std::string(ST_KEY_CHANNEL_MULTIPLIER) + "." + kv1.second; + found->second.scale = static_cast(obs_data_get_double(settings, chm_key.c_str())); + _precalc.scale.ptr[static_cast(kv1.first)] = found->second.scale; + + vec4* ch = &_precalc.matrix.x; + switch (kv1.first) { + case channel::Red: + ch = &_precalc.matrix.x; + break; + case channel::Green: + ch = &_precalc.matrix.y; + break; + case channel::Blue: + ch = &_precalc.matrix.z; + break; + case channel::Alpha: + ch = &_precalc.matrix.t; + break; + default: + break; + } + + for (auto kv2 : channel_translations) { + std::string ab_key = std::string(ST_KEY_CHANNEL_INPUT) + "." + kv1.second + "." + kv2.second; + found->second.values.ptr[static_cast(kv2.first)] = static_cast(obs_data_get_double(settings, ab_key.c_str())); + ch->ptr[static_cast(kv2.first)] = found->second.values.ptr[static_cast(kv2.first)]; + } + } + + _debug_texture = obs_data_get_int(settings, ST_KEY_DEBUG_TEXTURE); +} + +void dynamic_mask_instance::save(obs_data_t* settings) +{ + if (auto source = _input.lock(); source) { + obs_data_set_string(settings, ST_KEY_INPUT, source.name().data()); + } + + for (auto kv1 : channel_translations) { + auto found = _channels.find(kv1.first); + if (found == _channels.end()) { + _channels.insert({kv1.first, channel_data()}); + found = _channels.find(kv1.first); + if (found == _channels.end()) { + assert(found != _channels.end()); + throw std::runtime_error("Unable to insert element into data _store."); + } + } + + std::string chv_key = std::string(ST_KEY_CHANNEL_VALUE) + "." + kv1.second; + obs_data_set_double(settings, chv_key.c_str(), static_cast(found->second.value)); + + std::string chm_key = std::string(ST_KEY_CHANNEL_MULTIPLIER) + "." + kv1.second; + obs_data_set_double(settings, chm_key.c_str(), static_cast(found->second.scale)); + + for (auto kv2 : channel_translations) { + std::string ab_key = std::string(ST_KEY_CHANNEL_INPUT) + "." + kv1.second + "." + kv2.second; + obs_data_set_double(settings, ab_key.c_str(), static_cast(found->second.values.ptr[static_cast(kv2.first)])); + } + } +} + +gs_color_space dynamic_mask_instance::video_get_color_space(size_t count, const gs_color_space* preferred_spaces) +{ + return _base_color_space; +} + +void dynamic_mask_instance::video_tick(float time) +{ + { // Base Information + _have_base = false; + + std::array preferred_formats = {GS_CS_SRGB}; + _base_color_space = obs_source_get_color_space(obs_filter_get_target(_self), preferred_formats.size(), preferred_formats.data()); + switch (_base_color_space) { + case GS_CS_SRGB: + _base_color_format = GS_RGBA; + break; + case GS_CS_SRGB_16F: + case GS_CS_709_EXTENDED: + case GS_CS_709_SCRGB: + _base_color_format = GS_RGBA16F; + break; + default: + _base_color_format = GS_RGBA_UNORM; + } + + if ((obs_source_get_output_flags(obs_filter_get_target(_self)) & OBS_SOURCE_SRGB) == OBS_SOURCE_SRGB) { + _base_srgb = (_base_color_space <= GS_CS_SRGB_16F); + } else { + _base_srgb = false; + } + } + + if (auto input = _input.lock(); input) { // Input Information + _have_input = false; + + std::array preferred_formats = {GS_CS_SRGB}; + _input_color_space = obs_source_get_color_space(input, preferred_formats.size(), preferred_formats.data()); + switch (_input_color_space) { + case GS_CS_SRGB: + _input_color_format = GS_RGBA; + break; + case GS_CS_SRGB_16F: + case GS_CS_709_EXTENDED: + case GS_CS_709_SCRGB: + _input_color_format = GS_RGBA16F; + break; + default: + _input_color_format = GS_RGBA_UNORM; + } + + if ((input.output_flags() & OBS_SOURCE_SRGB) == OBS_SOURCE_SRGB) { + _input_srgb = (_base_color_space <= GS_CS_SRGB_16F); + } else { + _input_srgb = false; + } + } else { + _have_input = false; + } + + _have_final = false; + _final_srgb = _base_srgb; +} + +void dynamic_mask_instance::video_render(gs_effect_t* in_effect) +{ + gs_effect_t* default_effect = obs_get_base_effect(obs_base_effect::OBS_EFFECT_DEFAULT); + auto effect = _data->channel_mask_fx(); + obs_source_t* parent = obs_filter_get_parent(_self); + obs_source_t* target = obs_filter_get_target(_self); + uint32_t width = obs_source_get_base_width(target); + uint32_t height = obs_source_get_base_height(target); + auto input = _input.lock(); + +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + streamfx::obs::gs::debug_marker gdmp{streamfx::obs::gs::debug_color_source, "Dynamic Mask '%s' on '%s'", obs_source_get_name(_self), obs_source_get_name(obs_filter_get_parent(_self))}; +#endif + + // If there's some issue acquiring information, skip rendering entirely. + if (!_self || !parent || !target || !width || !height) { + _self.skip_video_filter(); + return; + } else if (input && (!input.width() || !input.height())) { + _self.skip_video_filter(); + return; + } + + // Capture the base texture for later rendering. + if (!_have_base) { +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + streamfx::obs::gs::debug_marker gdm{streamfx::obs::gs::debug_color_cache, "Base Texture"}; +#endif + // Ensure the Render Target matches the expected format. + if (!_base_rt || (_base_rt->color_format() != _base_color_format)) { + _base_rt = std::make_shared(_base_color_format, GS_ZS_NONE); + } + + bool previous_srgb = gs_framebuffer_srgb_enabled(); + auto previous_lsrgb = gs_get_linear_srgb(); + gs_set_linear_srgb(_base_srgb); + gs_enable_framebuffer_srgb(false); + + // Begin rendering the source with a certain color space. + if (obs_source_process_filter_begin_with_color_space(_self, _base_color_format, _base_color_space, OBS_ALLOW_DIRECT_RENDERING)) { + try { + { + auto op = _base_rt->render(width, height, _base_color_space); + + // Push a new blend state to stack. + gs_blend_state_push(); + gs_reset_blend_state(); + gs_enable_blending(false); + gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO); + try { + // Enable all channels. + gs_enable_color(true, true, true, true); + + // Disable culling. + gs_set_cull_mode(GS_NEITHER); + + // Disable depth testing. + gs_enable_depth_test(false); + gs_depth_function(GS_ALWAYS); + + // Disable stencil testing + gs_enable_stencil_test(false); + gs_enable_stencil_write(false); + gs_stencil_function(GS_STENCIL_BOTH, GS_ALWAYS); + gs_stencil_op(GS_STENCIL_BOTH, GS_KEEP, GS_KEEP, GS_KEEP); + + // Set up rendering matrix. + gs_ortho(0, static_cast(width), 0, static_cast(height), -1., 1.); + + { // Clear to black. + vec4 clr = {0., 0., 0., 0.}; + gs_clear(GS_CLEAR_COLOR, &clr, 0., 0); + } + + // Render the source. + _self.process_filter_end(default_effect, width, height); + + // Pop the old blend state. + gs_blend_state_pop(); + + } catch (...) { + gs_blend_state_pop(); + throw; + } + } + + _have_base = true; + _base_rt->get_texture(_base_tex); + } catch (const std::exception& ex) { + _self.process_filter_end(default_effect, width, height); + DLOG_ERROR("Failed to capture base texture: %s", ex.what()); + } catch (...) { + _self.process_filter_end(default_effect, width, height); + DLOG_ERROR("Failed to capture base texture.", nullptr); + } + } + + gs_set_linear_srgb(previous_lsrgb); + gs_enable_framebuffer_srgb(previous_srgb); + } + + // Capture the input texture for later rendering. + if (!_have_input) { + if (!input) { + // Treat no selection as selecting the target filter. + _have_input = _have_base; + _input_tex = _base_tex; + _input_color_format = _base_color_format; + _input_color_space = _base_color_space; + } else { +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + streamfx::obs::gs::debug_marker gdm{streamfx::obs::gs::debug_color_source, "Input '%s'", input.name().data()}; +#endif + // Ensure the Render Target matches the expected format. + if (!_input_rt || (_input_rt->color_format() != _input_color_format)) { + _input_rt = std::make_shared(_input_color_format, GS_ZS_NONE); + } + + auto previous_lsrgb = gs_get_linear_srgb(); + gs_set_linear_srgb(_input_srgb); + bool previous_srgb = gs_framebuffer_srgb_enabled(); + gs_enable_framebuffer_srgb(false); + + try { + { + auto op = _input_rt->render(input.width(), input.height(), _input_color_space); + + // Push a new blend state to stack. + gs_blend_state_push(); + gs_reset_blend_state(); + gs_enable_blending(false); + gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO); + try { + // Enable all channels. + gs_enable_color(true, true, true, true); + + // Disable culling. + gs_set_cull_mode(GS_NEITHER); + + // Disable depth testing. + gs_enable_depth_test(false); + gs_depth_function(GS_ALWAYS); + + // Disable stencil testing + gs_enable_stencil_test(false); + gs_enable_stencil_write(false); + gs_stencil_function(GS_STENCIL_BOTH, GS_ALWAYS); + gs_stencil_op(GS_STENCIL_BOTH, GS_KEEP, GS_KEEP, GS_KEEP); + + // Set up rendering matrix. + gs_ortho(0, static_cast(input.width()), 0, static_cast(input.height()), -1., 1.); + + { // Clear to black. + vec4 clr = {0., 0., 0., 0.}; + gs_clear(GS_CLEAR_COLOR, &clr, 0., 0); + } + + // Render the source. + obs_source_video_render(input); + + // Pop the old blend state. + gs_blend_state_pop(); + } catch (...) { + gs_blend_state_pop(); + throw; + } + } + + _have_input = true; + _input_rt->get_texture(_input_tex); + } catch (const std::exception& ex) { + DLOG_ERROR("Failed to capture input texture: %s", ex.what()); + } catch (...) { + DLOG_ERROR("Failed to capture input texture.", nullptr); + } + + gs_enable_framebuffer_srgb(previous_srgb); + gs_set_linear_srgb(previous_lsrgb); + } + } + + // Capture the final texture. + if (!_have_final && _have_base) { +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + streamfx::obs::gs::debug_marker gdm{streamfx::obs::gs::debug_color_render, "Final Calculation"}; +#endif + + // Ensure the Render Target matches the expected format. + if (!_final_rt || (_final_rt->color_format() != _base_color_format)) { + _final_rt = std::make_shared(_base_color_format, GS_ZS_NONE); + } + + bool previous_srgb = gs_framebuffer_srgb_enabled(); + auto previous_lsrgb = gs_get_linear_srgb(); + gs_enable_framebuffer_srgb(_final_srgb); + gs_set_linear_srgb(_final_srgb); + + try { + { + auto op = _final_rt->render(width, height); + + // Push a new blend state to stack. + gs_blend_state_push(); + gs_reset_blend_state(); + gs_enable_blending(false); + gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO); + try { + // Enable all channels. + gs_enable_color(true, true, true, true); + + // Disable culling. + gs_set_cull_mode(GS_NEITHER); + + // Disable depth testing. + gs_enable_depth_test(false); + gs_depth_function(GS_ALWAYS); + + // Disable stencil testing + gs_enable_stencil_test(false); + gs_enable_stencil_write(false); + gs_stencil_function(GS_STENCIL_BOTH, GS_ALWAYS); + gs_stencil_op(GS_STENCIL_BOTH, GS_KEEP, GS_KEEP, GS_KEEP); + + // Set up rendering matrix. + gs_ortho(0, 1, 0, 1, -1., 1.); + + { // Clear to black. + vec4 clr = {0., 0., 0., 0.}; + gs_clear(GS_CLEAR_COLOR, &clr, 0., 0); + } + + effect.get_parameter("pMaskInputA").set_texture(_base_tex, _base_srgb); + effect.get_parameter("pMaskInputB").set_texture(_input_tex, _input_srgb); + + effect.get_parameter("pMaskBase").set_float4(_precalc.base); + effect.get_parameter("pMaskMatrix").set_matrix(_precalc.matrix); + effect.get_parameter("pMaskMultiplier").set_float4(_precalc.scale); + + while (gs_effect_loop(effect.get(), "Mask")) { + _gfx_util->draw_fullscreen_triangle(); + } + + // Pop the old blend state. + gs_blend_state_pop(); + } catch (...) { + gs_blend_state_pop(); + throw; + } + } + + _final_tex = _final_rt->get_texture(); + _have_final = true; + } catch (const std::exception& ex) { + DLOG_ERROR("Failed to render final texture: %s", ex.what()); + } catch (...) { + DLOG_ERROR("Failed to render final texture.", nullptr); + } + + gs_set_linear_srgb(previous_lsrgb); + gs_enable_framebuffer_srgb(previous_srgb); + } + + // Enable texture debugging + switch (_debug_texture) { + case 0: + _have_final = _have_base; + _final_tex = _base_tex; + break; + case 1: + _have_final = _have_input; + _final_tex = _input_tex; + break; + } + + // Abort if we don't have a final render. + if (!_have_final || !_final_tex->get_object()) { + obs_source_skip_video_filter(_self); + return; + } + + // Draw source + { +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + streamfx::obs::gs::debug_marker gdm{streamfx::obs::gs::debug_color_render, "Render"}; +#endif + + // It is important that we do not modify the blend state here, as it is set correctly by OBS + gs_set_cull_mode(GS_NEITHER); + gs_enable_color(true, true, true, true); + gs_enable_depth_test(false); + gs_depth_function(GS_ALWAYS); + gs_enable_stencil_test(false); + gs_enable_stencil_write(false); + gs_stencil_function(GS_STENCIL_BOTH, GS_ALWAYS); + gs_stencil_op(GS_STENCIL_BOTH, GS_ZERO, GS_ZERO, GS_ZERO); + + const bool previous_srgb = gs_framebuffer_srgb_enabled(); + gs_enable_framebuffer_srgb(gs_get_linear_srgb()); + + gs_effect_t* final_effect = in_effect ? in_effect : default_effect; + gs_eparam_t* param = gs_effect_get_param_by_name(final_effect, "image"); + if (!param) { + DLOG_ERROR(" Failed to set image param.", obs_source_get_name(_self)); + gs_enable_framebuffer_srgb(previous_srgb); + obs_source_skip_video_filter(_self); + return; + } else { + if (gs_get_linear_srgb()) { + gs_effect_set_texture_srgb(param, *_final_tex); + } else { + gs_effect_set_texture(param, *_final_tex); + } + } + while (gs_effect_loop(final_effect, "Draw")) { + gs_draw_sprite(0, 0, width, height); + } + + gs_enable_framebuffer_srgb(previous_srgb); + } +} + +void dynamic_mask_instance::enum_active_sources(obs_source_enum_proc_t enum_callback, void* param) +{ + if (_input) + enum_callback(_self, _input.lock().get(), param); +} + +void dynamic_mask_instance::enum_all_sources(obs_source_enum_proc_t enum_callback, void* param) +{ + if (_input) + enum_callback(_self, _input.lock().get(), param); +} + +void streamfx::filter::dynamic_mask::dynamic_mask_instance::show() +{ + if (!_input || !_self.showing() || !(_self.get_filter_parent().showing())) + return; + + auto input = _input.lock(); + _input_vs = streamfx::obs::source_showing_reference::add_showing_reference(input); +} + +void streamfx::filter::dynamic_mask::dynamic_mask_instance::hide() +{ + _input_vs.reset(); +} + +void streamfx::filter::dynamic_mask::dynamic_mask_instance::activate() +{ + if (!_input || !_self.active() || !(_self.get_filter_parent().active())) + return; + + auto input = _input.lock(); + _input_ac = streamfx::obs::source_active_reference::add_active_reference(input); +} + +void streamfx::filter::dynamic_mask::dynamic_mask_instance::deactivate() +{ + _input_ac.reset(); +} + +bool dynamic_mask_instance::acquire(std::string_view name) +{ + try { + // Try and acquire the source. + _input = streamfx::obs::weak_source(name); + + // Ensure that this wouldn't cause recursion. + _input_child = std::make_unique(_self, _input.lock()); + + // Handle the active and showing stuff. + activate(); + show(); + + return true; + } catch (...) { + release(); + return false; + } +} + +void dynamic_mask_instance::release() +{ + // Handle the active and showing stuff. + deactivate(); + hide(); + + // Release any references. + _input_child.reset(); + _input.reset(); +} + +dynamic_mask_factory::dynamic_mask_factory() +{ + _info.id = S_PREFIX "filter-dynamic-mask"; + _info.type = OBS_SOURCE_TYPE_FILTER; + _info.output_flags = OBS_SOURCE_VIDEO | OBS_SOURCE_SRGB; + + support_active_child_sources(true); + support_child_sources(true); + support_size(false); + support_activity_tracking(true); + support_visibility_tracking(true); + support_color_space(true); + finish_setup(); + register_proxy("obs-stream-effects-filter-dynamic-mask"); +} + +dynamic_mask_factory::~dynamic_mask_factory() {} + +const char* dynamic_mask_factory::get_name() +{ + return D_TRANSLATE(ST_I18N); +} + +void dynamic_mask_factory::get_defaults2(obs_data_t* data) +{ + obs_data_set_default_int(data, ST_KEY_CHANNEL, static_cast(channel::Red)); + for (auto kv : channel_translations) { + obs_data_set_default_double(data, (std::string(ST_KEY_CHANNEL_VALUE) + "." + kv.second).c_str(), 1.0); + obs_data_set_default_double(data, (std::string(ST_KEY_CHANNEL_MULTIPLIER) + "." + kv.second).c_str(), 1.0); + for (auto kv2 : channel_translations) { + obs_data_set_default_double(data, (std::string(ST_KEY_CHANNEL_INPUT) + "." + kv.second + "." + kv2.second).c_str(), 0.0); + } + } + obs_data_set_default_int(data, ST_KEY_DEBUG_TEXTURE, -1); +} + +obs_properties_t* dynamic_mask_factory::get_properties2(dynamic_mask_instance* data) +{ + obs_properties_t* props = obs_properties_create(); + obs_property_t* p; + + _translation_cache.clear(); + + { + obs_properties_add_button2(props, S_MANUAL_OPEN, D_TRANSLATE(S_MANUAL_OPEN), streamfx::filter::dynamic_mask::dynamic_mask_factory::on_manual_open, nullptr); + } + + { // Input + p = obs_properties_add_list(props, ST_KEY_INPUT, D_TRANSLATE(ST_I18N_INPUT), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + obs_property_list_add_string(p, "", ""); + obs::source_tracker::instance()->enumerate( + [&p](std::string name, ::streamfx::obs::source) { + std::stringstream sstr; + sstr << name << " (" << D_TRANSLATE(S_SOURCETYPE_SOURCE) << ")"; + obs_property_list_add_string(p, sstr.str().c_str(), name.c_str()); + return false; + }, + obs::source_tracker::filter_video_sources); + obs::source_tracker::instance()->enumerate( + [&p](std::string name, ::streamfx::obs::source) { + std::stringstream sstr; + sstr << name << " (" << D_TRANSLATE(S_SOURCETYPE_SCENE) << ")"; + obs_property_list_add_string(p, sstr.str().c_str(), name.c_str()); + return false; + }, + obs::source_tracker::filter_scenes); + } + + const char* pri_chs[] = {S_CHANNEL_RED, S_CHANNEL_GREEN, S_CHANNEL_BLUE, S_CHANNEL_ALPHA}; + for (auto pri_ch : pri_chs) { + auto grp = obs_properties_create(); + + { + _translation_cache.push_back(translate_string(D_TRANSLATE(ST_I18N_CHANNEL_VALUE), D_TRANSLATE(pri_ch))); + std::string buf = std::string(ST_KEY_CHANNEL_VALUE) + "." + pri_ch; + p = obs_properties_add_float_slider(grp, buf.c_str(), _translation_cache.back().c_str(), -100.0, 100.0, 0.01); + obs_property_set_long_description(p, _translation_cache.back().c_str()); + } + + const char* sec_chs[] = {S_CHANNEL_RED, S_CHANNEL_GREEN, S_CHANNEL_BLUE, S_CHANNEL_ALPHA}; + for (auto sec_ch : sec_chs) { + _translation_cache.push_back(translate_string(D_TRANSLATE(ST_I18N_CHANNEL_INPUT), D_TRANSLATE(sec_ch))); + std::string buf = std::string(ST_KEY_CHANNEL_INPUT) + "." + pri_ch + "." + sec_ch; + p = obs_properties_add_float_slider(grp, buf.c_str(), _translation_cache.back().c_str(), -100.0, 100.0, 0.01); + obs_property_set_long_description(p, _translation_cache.back().c_str()); + } + + { + _translation_cache.push_back(translate_string(D_TRANSLATE(ST_I18N_CHANNEL_MULTIPLIER), D_TRANSLATE(pri_ch))); + std::string buf = std::string(ST_KEY_CHANNEL_MULTIPLIER) + "." + pri_ch; + p = obs_properties_add_float_slider(grp, buf.c_str(), _translation_cache.back().c_str(), -100.0, 100.0, 0.01); + obs_property_set_long_description(p, _translation_cache.back().c_str()); + } + + { + _translation_cache.push_back(translate_string(D_TRANSLATE(ST_I18N_CHANNEL), D_TRANSLATE(pri_ch))); + std::string buf = std::string(ST_KEY_CHANNEL) + "." + pri_ch; + obs_properties_add_group(props, buf.c_str(), _translation_cache.back().c_str(), obs_group_type::OBS_GROUP_NORMAL, grp); + } + } + + { + auto grp = obs_properties_create(); + obs_properties_add_group(props, "Debug", D_TRANSLATE(S_ADVANCED), OBS_GROUP_NORMAL, grp); + + { + auto p = obs_properties_add_list(grp, ST_KEY_DEBUG_TEXTURE, D_TRANSLATE(ST_I18N_DEBUG_TEXTURE), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); + obs_property_list_add_int(p, D_TRANSLATE(S_STATE_DISABLED), -1); + obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_DEBUG_TEXTURE_BASE), 0); + obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_DEBUG_TEXTURE_INPUT), 1); + } + } + + return props; +} + +std::string dynamic_mask_factory::translate_string(const char* format, ...) +{ + va_list vargs; + va_start(vargs, format); + std::vector buffer(2048); + std::size_t len = static_cast(vsnprintf(buffer.data(), buffer.size(), format, vargs)); + va_end(vargs); + return std::string(buffer.data(), buffer.data() + len); +} + +bool dynamic_mask_factory::on_manual_open(obs_properties_t* props, obs_property_t* property, void* data) +{ + try { + streamfx::open_url(HELP_URL); + return false; + } catch (const std::exception& ex) { + D_LOG_ERROR("Failed to open manual due to error: %s", ex.what()); + return false; + } catch (...) { + D_LOG_ERROR("Failed to open manual due to unknown error.", ""); + return false; + } +} + +std::shared_ptr dynamic_mask_factory::instance() +{ + static std::weak_ptr winst; + static std::mutex mtx; + + std::unique_lock lock(mtx); + auto instance = winst.lock(); + if (!instance) { + instance = std::shared_ptr(new dynamic_mask_factory()); + winst = instance; + } + return instance; +} + +static std::shared_ptr loader_instance; + +static auto loader = streamfx::component( + "dynamic_mask", + []() { // Initializer + loader_instance = dynamic_mask_factory::instance(); + }, + []() { // Finalizer + loader_instance.reset(); + }, + {"core::source_tracker", "core::gs::texrender", "core::gs::texture", "core::gs::sampler"}); diff --git a/components/dynamic-mask/source/filters/filter-dynamic-mask.hpp b/components/dynamic-mask/source/filters/filter-dynamic-mask.hpp new file mode 100644 index 0000000..aee31d1 --- /dev/null +++ b/components/dynamic-mask/source/filters/filter-dynamic-mask.hpp @@ -0,0 +1,136 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "common.hpp" +#include "gfx/gfx-source-texture.hpp" +#include "gfx/gfx-util.hpp" +#include "obs/gs/gs-effect.hpp" +#include "obs/obs-source-active-child.hpp" +#include "obs/obs-source-active-reference.hpp" +#include "obs/obs-source-factory.hpp" +#include "obs/obs-source-showing-reference.hpp" +#include "obs/obs-source-tracker.hpp" +#include "obs/obs-source.hpp" +#include "obs/obs-tools.hpp" + +#include "warning-disable.hpp" +#include +#include +#include "warning-enable.hpp" + +namespace streamfx::filter::dynamic_mask { + enum class channel : int8_t { Invalid = -1, Red, Green, Blue, Alpha }; + + class data { + streamfx::obs::gs::effect _channel_mask_fx; + + private: + data(); + + public: + ~data(); + + streamfx::obs::gs::effect channel_mask_fx(); + + public: + static std::shared_ptr get(); + }; + + class dynamic_mask_instance : public obs::source_instance { + std::shared_ptr _data; + std::shared_ptr _gfx_util; + + std::map, std::string> _translation_map; + + streamfx::obs::weak_source _input; + std::unique_ptr _input_child; + std::shared_ptr _input_vs; + std::shared_ptr _input_ac; + + // Base texture for filtering + bool _have_base; + std::shared_ptr _base_rt; + std::shared_ptr _base_tex; + gs_color_space _base_color_space; + gs_color_format _base_color_format; + bool _base_srgb; + + bool _have_input; + std::shared_ptr _input_rt; + std::shared_ptr _input_tex; + gs_color_space _input_color_space; + gs_color_format _input_color_format; + bool _input_srgb; + + bool _have_final; + std::shared_ptr _final_rt; + std::shared_ptr _final_tex; + bool _final_srgb; + + int64_t _debug_texture; + + struct channel_data { + float value = 0.0; + float scale = 1.0; + vec4 values = {0, 0, 0, 0}; + }; + std::map _channels; + + struct _precalc { + vec4 base; + vec4 scale; + matrix4 matrix; + } _precalc; + + public: + dynamic_mask_instance(obs_data_t* data, obs_source_t* self); + virtual ~dynamic_mask_instance(); + + virtual void load(obs_data_t* settings) override; + virtual void migrate(obs_data_t* data, uint64_t version) override; + virtual void update(obs_data_t* settings) override; + virtual void save(obs_data_t* settings) override; + + virtual gs_color_space video_get_color_space(size_t count, const gs_color_space* preferred_spaces) override; + virtual void video_tick(float time) override; + virtual void video_render(gs_effect_t* effect) override; + + void enum_active_sources(obs_source_enum_proc_t enum_callback, void* param) override; + void enum_all_sources(obs_source_enum_proc_t enum_callback, void* param) override; + + void show() override; + void hide() override; + void activate() override; + void deactivate() override; + + bool acquire(std::string_view name); + void release(); + }; + + class dynamic_mask_factory : public obs::source_factory { + std::list _translation_cache; + + public: + dynamic_mask_factory(); + virtual ~dynamic_mask_factory() override; + + virtual const char* get_name() override; + + virtual void get_defaults2(obs_data_t* data) override; + + virtual obs_properties_t* get_properties2(filter::dynamic_mask::dynamic_mask_instance* data) override; + + std::string translate_string(const char* format, ...); + + static bool on_manual_open(obs_properties_t* props, obs_property_t* property, void* data); + + public: // Singleton + static void initialize(); + + static void finalize(); + + static std::shared_ptr instance(); + }; +} // namespace streamfx::filter::dynamic_mask diff --git a/components/ffmpeg/CMakeLists.txt b/components/ffmpeg/CMakeLists.txt new file mode 100644 index 0000000..58c1716 --- /dev/null +++ b/components/ffmpeg/CMakeLists.txt @@ -0,0 +1,24 @@ +# AUTOGENERATED COPYRIGHT HEADER START +# Copyright (C) 2023 Michael Fabian 'Xaymar' Dirks +# AUTOGENERATED COPYRIGHT HEADER END + +cmake_minimum_required(VERSION 3.26) +project("FFmpeg") +list(APPEND CMAKE_MESSAGE_INDENT "[${PROJECT_NAME}] ") + +streamfx_add_component(${PROJECT_NAME}) + +find_package("FFmpeg" + COMPONENTS "avutil" "avcodec" "swscale" +) +if(NOT FFmpeg_FOUND) + streamfx_disable_component(${COMPONENT_TARGET} "FFmpeg is not available.") + return() +else() + target_link_libraries(${COMPONENT_TARGET} + PUBLIC + FFmpeg::avutil + FFmpeg::avcodec + FFmpeg::swscale + ) +endif() diff --git a/components/ffmpeg/source/encoders/codecs/av1.cpp b/components/ffmpeg/source/encoders/codecs/av1.cpp new file mode 100644 index 0000000..e95cb9e --- /dev/null +++ b/components/ffmpeg/source/encoders/codecs/av1.cpp @@ -0,0 +1,19 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#include "av1.hpp" + +const char* streamfx::encoder::codec::av1::profile_to_string(profile p) +{ + switch (p) { + case profile::MAIN: + return D_TRANSLATE(S_CODEC_AV1_PROFILE ".Main"); + case profile::HIGH: + return D_TRANSLATE(S_CODEC_AV1_PROFILE ".High"); + case profile::PROFESSIONAL: + return D_TRANSLATE(S_CODEC_AV1_PROFILE ".Professional"); + default: + return "Unknown"; + } +} diff --git a/components/ffmpeg/source/encoders/codecs/av1.hpp b/components/ffmpeg/source/encoders/codecs/av1.hpp new file mode 100644 index 0000000..725e3f3 --- /dev/null +++ b/components/ffmpeg/source/encoders/codecs/av1.hpp @@ -0,0 +1,21 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "common.hpp" + +#define S_CODEC_AV1 "Codec.AV1" +#define S_CODEC_AV1_PROFILE "Codec.AV1.Profile" +#define S_CODEC_AV1_LEVEL "Codec.AV1.Level" + +namespace streamfx::encoder::codec::av1 { + enum class profile { + MAIN = 0, + HIGH = 1, + PROFESSIONAL = 2, + UNKNOWN = -1, + }; + + const char* profile_to_string(profile p); +} // namespace streamfx::encoder::codec::av1 diff --git a/components/ffmpeg/source/encoders/codecs/dnxhr.cpp b/components/ffmpeg/source/encoders/codecs/dnxhr.cpp new file mode 100644 index 0000000..13e809b --- /dev/null +++ b/components/ffmpeg/source/encoders/codecs/dnxhr.cpp @@ -0,0 +1,6 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2022 Carsten Braun +// Copyright (C) 2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#include "dnxhr.hpp" diff --git a/components/ffmpeg/source/encoders/codecs/dnxhr.hpp b/components/ffmpeg/source/encoders/codecs/dnxhr.hpp new file mode 100644 index 0000000..457c682 --- /dev/null +++ b/components/ffmpeg/source/encoders/codecs/dnxhr.hpp @@ -0,0 +1,13 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2022 Carsten Braun +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "common.hpp" + +// Codec: DNxHR +#define S_CODEC_DNXHR "Codec.DNxHR" +#define S_CODEC_DNXHR_PROFILE "Codec.DNxHR.Profile" + +namespace streamfx::encoder::codec::dnxhr {} // namespace streamfx::encoder::codec::dnxhr diff --git a/components/ffmpeg/source/encoders/codecs/h264.cpp b/components/ffmpeg/source/encoders/codecs/h264.cpp new file mode 100644 index 0000000..49e61dd --- /dev/null +++ b/components/ffmpeg/source/encoders/codecs/h264.cpp @@ -0,0 +1,70 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#include "h264.hpp" + +uint8_t* is_nal_start(uint8_t* ptr, uint8_t* end_ptr, size_t& size) +{ + // Ensure that the remaining space actually can contain a prefix and NAL header. + if ((ptr + (3 + 1)) >= end_ptr) + return nullptr; + + if (*ptr != 0x0) + return nullptr; + if (*(ptr + 1) != 0x0) + return nullptr; + + // 3-Byte NAL prefix. + if (*(ptr + 2) == 0x1) { + size = 3; + return ptr + 3; + } + + // 4-Byte NAL Prefix + if ((ptr + (4 + 1)) >= end_ptr) + return nullptr; + if (*(ptr + 2) != 0x0) + return nullptr; + if (*(ptr + 3) != 0x01) + return nullptr; + + size = 4; + return ptr + 4; +} + +uint8_t* streamfx::encoder::codec::h264::find_closest_nal(uint8_t* ptr, uint8_t* end_ptr, size_t& size) +{ + for (uint8_t* seek_ptr = ptr; seek_ptr < end_ptr; seek_ptr++) { + if (auto nal_ptr = is_nal_start(seek_ptr, end_ptr, size); nal_ptr != nullptr) + return nal_ptr; + } + return nullptr; +} + +uint32_t streamfx::encoder::codec::h264::get_packet_reference_count(uint8_t* ptr, uint8_t* end_ptr) +{ + size_t nal_ptr_prefix = 0; + uint8_t* nal_ptr = find_closest_nal(ptr, end_ptr, nal_ptr_prefix); + while ((nal_ptr != nullptr) && (nal_ptr < end_ptr)) { + // Try and figure out the actual size of the NAL. + size_t nal_end_ptr_prefix = 0; + uint8_t* nal_end_ptr = find_closest_nal(nal_ptr, end_ptr, nal_end_ptr_prefix); + size_t nal_size = (nal_end_ptr ? nal_end_ptr : end_ptr) - nal_ptr - nal_end_ptr_prefix; + + // Try and figure out the ideal priority. + switch (static_cast((*nal_ptr) & 0x5)) { + case nal_unit_type::CODED_SLICE_NONIDR: + return static_cast((*nal_ptr >> 5) & 0x2); + case nal_unit_type::CODED_SLICE_IDR: + return static_cast((*nal_ptr >> 5) & 0x2); + default: + break; + } + + // Update our NAL pointer. + nal_ptr = nal_end_ptr; + } + + return std::numeric_limits::max(); +} diff --git a/components/ffmpeg/source/encoders/codecs/h264.hpp b/components/ffmpeg/source/encoders/codecs/h264.hpp new file mode 100644 index 0000000..6de7b2c --- /dev/null +++ b/components/ffmpeg/source/encoders/codecs/h264.hpp @@ -0,0 +1,82 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "common.hpp" + +// Codec: H264 +#define S_CODEC_H264 "Codec.H264" +#define S_CODEC_H264_PROFILE "Codec.H264.Profile" +#define S_CODEC_H264_LEVEL "Codec.H264.Level" + +namespace streamfx::encoder::codec::h264 { + enum class profile { + CONSTRAINED_BASELINE, + BASELINE, + MAIN, + HIGH, + HIGH444_PREDICTIVE, + UNKNOWN = -1, + }; + + enum class level { + L1_0 = 10, + L1_0b, + L1_1, + L1_2, + L1_3, + L2_0 = 20, + L2_1, + L2_2, + L3_0 = 30, + L3_1, + L3_2, + L4_0 = 40, + L4_1, + L4_2, + L5_0 = 50, + L5_1, + L5_2, + L6_0 = 60, + L6_1, + L6_2, + UNKNOWN = -1, + }; + + // See ITU-T H.264 + enum class nal_unit_type : uint8_t { + UNSPECIFIED = 0, + CODED_SLICE_NONIDR = 1, + CODED_SLICE_DATA_PARTITION_A = 2, + CODED_SLICE_DATA_PARTITION_B = 3, + CODED_SLICE_DATA_PARTITION_C = 4, + CODED_SLICE_IDR = 5, + SUPPLEMENTAL_ENHANCEMENT_INFORMATION = 6, + SEQUENCE_PARAMETER_SET = 7, + PICTURE_PARAMETER_SET = 8, + ACCESS_UNIT_DELIMITER = 9, + END_OF_SEQUENCE = 10, + END_OF_STREAM = 11, + FILLER_DATA = 12, + SEQUENCE_PARAMETER_SET_EXTENSION = 13, + PREFIX_NAL_UNIT = 14, + SUBSET_SEQUENCE_PARAMETER_SET = 15, + DEPTH_PARAMETER_SET = 16, + CODED_SLICE_AUXILIARY_PICTURE = 19, + CODED_SLICE_EXTENSION = 20, + CODED_SLICE_EXTENSION_DEPTH_VIEW = 21, + }; + + /** Search for the closest NAL unit. + * + * \param ptr Beginning of the search range. + * \param endptr End of the search range (exclusive). + * + * \return A valid pointer if a NAL was found, otherwise \ref nullptr. + */ + uint8_t* find_closest_nal(uint8_t* ptr, uint8_t* endptr, size_t& size); + + uint32_t get_packet_reference_count(uint8_t* ptr, uint8_t* endptr); + +} // namespace streamfx::encoder::codec::h264 diff --git a/components/ffmpeg/source/encoders/codecs/hevc.cpp b/components/ffmpeg/source/encoders/codecs/hevc.cpp new file mode 100644 index 0000000..c5aadf7 --- /dev/null +++ b/components/ffmpeg/source/encoders/codecs/hevc.cpp @@ -0,0 +1,221 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#include "hevc.hpp" + +using namespace streamfx::encoder::codec; + +enum class nal_unit_type : uint8_t { // 6 bits + TRAIL_N = 0, + TRAIL_R = 1, + TSA_N = 2, + TSA_R = 3, + STSA_N = 4, + STSA_R = 5, + RADL_N = 6, + RADL_R = 7, + RASL_N = 8, + RASL_R = 9, + RSV_VCL_N10 = 10, + RSV_VCL_R11 = 11, + RSV_VCL_N12 = 12, + RSV_VCL_R13 = 13, + RSV_VCL_N14 = 14, + RSV_VCL_R15 = 15, + BLA_W_LP = 16, + BLA_W_RADL = 17, + BLA_N_LP = 18, + IDR_W_RADL = 19, + IDR_N_LP = 20, + CRA = 21, + RSV_IRAP_VCL22 = 22, + RSV_IRAP_VCL23 = 23, + RSV_VCL24 = 24, + RSV_VCL25 = 25, + RSV_VCL26 = 26, + RSV_VCL27 = 27, + RSV_VCL28 = 28, + RSV_VCL29 = 29, + RSV_VCL30 = 30, + RSV_VCL31 = 31, + VPS = 32, + SPS = 33, + PPS = 34, + AUD = 35, + EOS = 36, + EOB = 37, + FD = 38, + PREFIX_SEI = 39, + SUFFIX_SEI = 40, + RSV_NVCL41 = 41, + RSV_NVCL42 = 42, + RSV_NVCL43 = 43, + RSV_NVCL44 = 44, + RSV_NVCL45 = 45, + RSV_NVCL46 = 46, + RSV_NVCL47 = 47, + UNSPEC48 = 48, + UNSPEC49 = 49, + UNSPEC50 = 50, + UNSPEC51 = 51, + UNSPEC52 = 52, + UNSPEC53 = 53, + UNSPEC54 = 54, + UNSPEC55 = 55, + UNSPEC56 = 56, + UNSPEC57 = 57, + UNSPEC58 = 58, + UNSPEC59 = 59, + UNSPEC60 = 60, + UNSPEC61 = 61, + UNSPEC62 = 62, + UNSPEC63 = 63, +}; + +struct hevc_nal_unit_header { + bool zero_bit : 1; + nal_unit_type nut : 6; + uint8_t layer_id : 6; + uint8_t temporal_id_plus1 : 3; +}; + +struct hevc_nal { + hevc_nal_unit_header* header; + std::size_t size = 0; + uint8_t* data = nullptr; +}; + +bool is_nal(uint8_t* data, uint8_t* end) +{ + std::size_t s = static_cast(end - data); + if (s < 4) + return false; + + if (*data != 0x0) + return false; + if (*(data + 1) != 0x0) + return false; + if (*(data + 2) != 0x0) + return false; + if (*(data + 3) != 0x1) + return false; + + return true; +} + +bool seek_to_nal(uint8_t*& data, uint8_t* end) +{ + if (data > end) + return false; + + for (; data <= end; data++) { + if (is_nal(data, end)) { + return true; + } + } + + return false; +} + +std::size_t get_nal_size(uint8_t* data, uint8_t* end) +{ + uint8_t* ptr = data + 4; + if (!seek_to_nal(ptr, end)) { + return static_cast(end - data); + } + return static_cast(ptr - data); +} + +bool is_discard_marker(uint8_t* data, uint8_t* end) +{ + std::size_t s = static_cast(end - data); + if (s < 4) + return false; + + if (*data != 0x0) + return false; + if (*(data + 1) != 0x0) + return false; + + if (*(data + 2) == 0x3) { + // Discard marker only if the next byte is not 0x0, 0x1, 0x2 or 0x3. + if (*(data + 3) != 0x0) + return false; + if (*(data + 3) != 0x1) + return false; + if (*(data + 3) != 0x2) + return false; + if (*(data + 3) != 0x3) + return false; + + return true; + } else { + if (*(data + 2) == 0x0) + return true; + if (*(data + 2) == 0x1) + return true; + if (*(data + 2) == 0x2) + return true; + + return false; + } +} + +bool should_discard_nal(uint8_t* data, uint8_t* end) +{ + if (data > end) + return true; + + for (; data <= end; data++) { + if (is_discard_marker(data, end)) + return true; + } + + return false; +} + +void progress_parse(uint8_t*& ptr, uint8_t* end, size_t& sz) +{ + ptr += sz; + sz = get_nal_size(ptr, end); +} + +void hevc::extract_header_sei(uint8_t* data, std::size_t sz_data, std::vector& header, std::vector& sei) +{ + uint8_t* ptr = data; + uint8_t* end = data + sz_data; + + // Reserve enough memory to store the entire packet data if necessary. + header.reserve(sz_data); + sei.reserve(sz_data); + + if (!seek_to_nal(ptr, end)) { + return; + } + + for (std::size_t nal_sz = get_nal_size(ptr, end); nal_sz > 0; progress_parse(ptr, end, nal_sz)) { + if (should_discard_nal(ptr + 4, ptr + nal_sz)) { + continue; + } + + hevc_nal nal; + nal.header = reinterpret_cast(ptr + 4); + nal.size = nal_sz - 4 - 2; + nal.data = ptr + 4 + 2; + + switch (nal.header->nut) { + case nal_unit_type::VPS: + case nal_unit_type::SPS: + case nal_unit_type::PPS: + header.insert(header.end(), ptr, ptr + nal_sz); + break; + case nal_unit_type::PREFIX_SEI: + case nal_unit_type::SUFFIX_SEI: + sei.insert(sei.end(), ptr, ptr + nal_sz); + break; + default: + break; + } + } +} diff --git a/components/ffmpeg/source/encoders/codecs/hevc.hpp b/components/ffmpeg/source/encoders/codecs/hevc.hpp new file mode 100644 index 0000000..c20943b --- /dev/null +++ b/components/ffmpeg/source/encoders/codecs/hevc.hpp @@ -0,0 +1,46 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "common.hpp" + +// Codec: HEVC +#define S_CODEC_HEVC "Codec.HEVC" +#define S_CODEC_HEVC_PROFILE "Codec.HEVC.Profile" +#define S_CODEC_HEVC_TIER "Codec.HEVC.Tier" +#define S_CODEC_HEVC_LEVEL "Codec.HEVC.Level" + +namespace streamfx::encoder::codec::hevc { + enum class profile { + MAIN, + MAIN10, + RANGE_EXTENDED, + UNKNOWN = -1, + }; + + enum class tier { + MAIN, + HIGH, + UNKNOWN = -1, + }; + + enum class level { + L1_0 = 30, + L2_0 = 60, + L2_1 = 63, + L3_0 = 90, + L3_1 = 93, + L4_0 = 120, + L4_1 = 123, + L5_0 = 150, + L5_1 = 153, + L5_2 = 156, + L6_0 = 180, + L6_1 = 183, + L6_2 = 186, + UNKNOWN = -1, + }; + + void extract_header_sei(uint8_t* data, std::size_t sz_data, std::vector& header, std::vector& sei); +} // namespace streamfx::encoder::codec::hevc diff --git a/components/ffmpeg/source/encoders/codecs/prores.cpp b/components/ffmpeg/source/encoders/codecs/prores.cpp new file mode 100644 index 0000000..677554a --- /dev/null +++ b/components/ffmpeg/source/encoders/codecs/prores.cpp @@ -0,0 +1,5 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#include "prores.hpp" diff --git a/components/ffmpeg/source/encoders/codecs/prores.hpp b/components/ffmpeg/source/encoders/codecs/prores.hpp new file mode 100644 index 0000000..039697e --- /dev/null +++ b/components/ffmpeg/source/encoders/codecs/prores.hpp @@ -0,0 +1,34 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "common.hpp" + +// Codec: ProRes +#define S_CODEC_PRORES "Codec.ProRes" +#define S_CODEC_PRORES_PROFILE "Codec.ProRes.Profile" +#define S_CODEC_PRORES_PROFILE_APCS "Codec.ProRes.Profile.APCS" +#define S_CODEC_PRORES_PROFILE_APCO "Codec.ProRes.Profile.APCO" +#define S_CODEC_PRORES_PROFILE_APCN "Codec.ProRes.Profile.APCN" +#define S_CODEC_PRORES_PROFILE_APCH "Codec.ProRes.Profile.APCH" +#define S_CODEC_PRORES_PROFILE_AP4H "Codec.ProRes.Profile.AP4H" +#define S_CODEC_PRORES_PROFILE_AP4X "Codec.ProRes.Profile.AP4X" + +namespace streamfx::encoder::codec::prores { + enum class profile : int32_t { + APCO = 0, + Y422_PROXY = APCO, + APCS = 1, + Y422_LT = APCS, + APCN = 2, + Y422 = APCN, + APCH = 3, + Y422_HQ = APCH, + AP4H = 4, + Y4444 = AP4H, + AP4X = 5, + Y4444_XQ = AP4X, + _COUNT, + }; +} diff --git a/components/ffmpeg/source/encoders/encoder-ffmpeg.cpp b/components/ffmpeg/source/encoders/encoder-ffmpeg.cpp new file mode 100644 index 0000000..971a571 --- /dev/null +++ b/components/ffmpeg/source/encoders/encoder-ffmpeg.cpp @@ -0,0 +1,1275 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2020 Daniel Molkentin +// Copyright (C) 2022 Chris Pence <6cpence@gmail.com> +// Copyright (C) 2022 Carsten Braun +// Copyright (C) 2022 lainon +// AUTOGENERATED COPYRIGHT HEADER END + +#include "encoder-ffmpeg.hpp" +#include "strings.hpp" +#include "codecs/hevc.hpp" +#include "ffmpeg/tools.hpp" +#include "obs/gs/gs-helper.hpp" +#include "plugin.hpp" + +#include "warning-disable.hpp" +#include +#include "warning-enable.hpp" + +extern "C" { +#include "warning-disable.hpp" +#include +#include "warning-enable.hpp" + +#include "warning-disable.hpp" +#include +#include +#include +#include +#include +#include +#include "warning-enable.hpp" +} + +#ifdef WIN32 +#include "ffmpeg/hwapi/d3d11.hpp" +#endif + +// FFmpeg +#define ST_I18N_FFMPEG "Encoder.FFmpeg" +#define ST_I18N_FFMPEG_SUFFIX ST_I18N_FFMPEG ".Suffix" +#define ST_I18N_FFMPEG_CUSTOMSETTINGS ST_I18N_FFMPEG ".CustomSettings" +#define ST_KEY_FFMPEG_CUSTOMSETTINGS "FFmpeg.CustomSettings" +#define ST_I18N_FFMPEG_THREADS ST_I18N_FFMPEG ".Threads" +#define ST_KEY_FFMPEG_THREADS "FFmpeg.Threads" +#define ST_I18N_FFMPEG_FRAMERATE ST_I18N_FFMPEG ".Framerate" +#define ST_KEY_FFMPEG_FRAMERATE "FFmpeg.Framerate" +#define ST_I18N_FFMPEG_GPU ST_I18N_FFMPEG ".GPU" +#define ST_KEY_FFMPEG_GPU "FFmpeg.GPU" + +#define ST_I18N_KEYFRAMES ST_I18N_FFMPEG ".KeyFrames" +#define ST_I18N_KEYFRAMES_INTERVALTYPE ST_I18N_KEYFRAMES ".IntervalType" +#define ST_I18N_KEYFRAMES_INTERVALTYPE_(x) ST_I18N_KEYFRAMES_INTERVALTYPE "." x +#define ST_KEY_KEYFRAMES_INTERVALTYPE "KeyFrames.IntervalType" +#define ST_I18N_KEYFRAMES_INTERVAL ST_I18N_KEYFRAMES ".Interval" +#define ST_KEY_KEYFRAMES_INTERVAL_SECONDS "KeyFrames.Interval.Seconds" +#define ST_KEY_KEYFRAMES_INTERVAL_FRAMES "KeyFrames.Interval.Frames" + +using namespace streamfx::encoder::ffmpeg; +using namespace streamfx::encoder::codec; + +enum class keyframe_type { SECONDS, FRAMES }; + +ffmpeg_instance::ffmpeg_instance(obs_data_t* settings, obs_encoder_t* self, bool is_hw) + : encoder_instance(settings, self, is_hw), + + _factory(reinterpret_cast(obs_encoder_get_type_data(self))), + + _codec(_factory->get_avcodec()), _context(nullptr), _handler(ffmpeg_manager::instance()->get_handler(_codec->name)), + + _scaler(), _packet(), + + _hwapi(), _hwinst(), + + _lag_in_frames(0), _sent_frames(0), _have_first_frame(false), _extra_data(), _sei_data(), + + _free_frames(), _used_frames(), _free_frames_last_used() +{ + // Initialize GPU Stuff + if (is_hw) { + // Abort if user specified manual override. + if ((obs_data_get_int(settings, ST_KEY_FFMPEG_GPU) != -1) || (obs_encoder_scaling_enabled(_self)) || (video_output_get_info(obs_encoder_video(_self))->format != VIDEO_FORMAT_NV12)) { + throw std::runtime_error("Selected settings prevent the use of hardware encoding, falling back to software."); + } + +#ifdef WIN32 + auto gctx = streamfx::obs::gs::context(); + if (gs_get_device_type() == GS_DEVICE_DIRECT3D_11) { + _hwapi = std::make_shared<::streamfx::ffmpeg::hwapi::d3d11>(); + } +#endif + if (!_hwapi) { + throw std::runtime_error("Failed to create acceleration context."); + } + + _hwinst = _hwapi->create_from_obs(); + } + + // Initialize context. + _context = avcodec_alloc_context3(_codec); + if (!_context) { + DLOG_ERROR("Failed to create context for encoder '%s'.", _codec->name); + throw std::runtime_error("Failed to create encoder context."); + } + + // Allocate a small packet for later use. + _packet = {av_packet_alloc(), [](AVPacket* ptr) { av_packet_free(&ptr); }}; + av_new_packet(_packet.get(), 8 * 1024 * 1024); // 8 MiB is usually enough for compressed data. + + // Initialize + if (is_hw) { + initialize_hw(settings); + } else { + initialize_sw(settings); + } + + { // Set up framerate division. + _framerate_divisor = obs_data_get_int(settings, ST_KEY_FFMPEG_FRAMERATE); + + _context->ticks_per_frame = 1; + _context->time_base.num *= _framerate_divisor; + _context->framerate.den *= _framerate_divisor; + } + + // Update settings + update(settings); + + // Initialize Encoder + auto gctx = streamfx::obs::gs::context(); + int res = avcodec_open2(_context, _codec, NULL); + if (res < 0) { + throw std::runtime_error(::streamfx::ffmpeg::tools::get_error_description(res)); + } + + log(); +} + +ffmpeg_instance::~ffmpeg_instance() +{ + auto gctx = streamfx::obs::gs::context(); + if (_context) { + // Flush encoders that require it. + if ((_codec->capabilities & AV_CODEC_CAP_DELAY) != 0) { + avcodec_send_frame(_context, nullptr); + while (avcodec_receive_packet(_context, _packet.get()) >= 0) { + avcodec_send_frame(_context, nullptr); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + } + + // Close and free context. + avcodec_free_context(&_context); + } + + av_packet_unref(_packet.get()); + + _scaler.finalize(); +} + +void ffmpeg_instance::get_properties(obs_properties_t* props) +{ + if (_handler) + _handler->properties(this->_factory, this, props); + + obs_property_set_enabled(obs_properties_get(props, ST_KEY_KEYFRAMES_INTERVALTYPE), false); + obs_property_set_enabled(obs_properties_get(props, ST_KEY_KEYFRAMES_INTERVAL_SECONDS), false); + obs_property_set_enabled(obs_properties_get(props, ST_KEY_KEYFRAMES_INTERVAL_FRAMES), false); + + obs_property_set_enabled(obs_properties_get(props, ST_KEY_FFMPEG_THREADS), false); + obs_property_set_enabled(obs_properties_get(props, ST_KEY_FFMPEG_GPU), false); +} + +void ffmpeg_instance::migrate(obs_data_t* settings, uint64_t version) +{ + if (_handler) + _handler->migrate(this->_factory, this, settings, version); +} + +bool ffmpeg_instance::update(obs_data_t* settings) +{ + bool support_reconfig = false; + bool support_reconfig_threads = false; + bool support_reconfig_gpu = false; + bool support_reconfig_keyframes = false; + if (_handler) { + support_reconfig = _handler->is_reconfigurable(_factory, support_reconfig_threads, support_reconfig_gpu, support_reconfig_keyframes); + } + + if (!_context->internal) { + // FFmpeg Options + _context->debug = 0; + _context->strict_std_compliance = FF_COMPLIANCE_NORMAL; + } + + if (!_context->internal || (support_reconfig && support_reconfig_threads)) { + /// Threading + if (!_hwinst) { + _context->thread_type = 0; + if (_codec->capabilities & AV_CODEC_CAP_FRAME_THREADS) { + _context->thread_type |= FF_THREAD_FRAME; + } + if (_codec->capabilities & AV_CODEC_CAP_SLICE_THREADS) { + _context->thread_type |= FF_THREAD_SLICE; + } + if (_context->thread_type != 0) { + int64_t threads = obs_data_get_int(settings, ST_I18N_FFMPEG_THREADS); + if (threads > 0) { + _context->thread_count = static_cast(threads); + } else { + _context->thread_count = static_cast(std::thread::hardware_concurrency()); + } + } else { + _context->thread_count = 1; + } + + // Frame Delay (Lag In Frames) + _context->delay = _context->thread_count; + } else { + _context->delay = 0; + } + } + + if (!_context->internal || (support_reconfig && support_reconfig_gpu)) { + // Apply GPU Selection + if (!_hwinst && ::streamfx::ffmpeg::tools::can_hardware_encode(_codec)) { + av_opt_set_int(_context, "gpu", (int)obs_data_get_int(settings, ST_KEY_FFMPEG_GPU), AV_OPT_SEARCH_CHILDREN); + } + } + + if (!_context->internal || (support_reconfig && support_reconfig_keyframes)) { + // Keyframes + if (_handler && _handler->has_keyframes(_factory)) { + // Key-Frame Options + obs_video_info ovi; + if (!obs_get_video_info(&ovi)) { + throw std::runtime_error("obs_get_video_info failed, restart OBS Studio to fix it (hopefully)."); + } + + int64_t kf_type = obs_data_get_int(settings, ST_KEY_KEYFRAMES_INTERVALTYPE); + bool is_seconds = (kf_type == 0); + + if (is_seconds) { + double framerate = static_cast(ovi.fps_num) / (static_cast(ovi.fps_den) * _framerate_divisor); + _context->gop_size = static_cast(obs_data_get_double(settings, ST_KEY_KEYFRAMES_INTERVAL_SECONDS) * framerate); + } else { + _context->gop_size = static_cast(obs_data_get_int(settings, ST_KEY_KEYFRAMES_INTERVAL_FRAMES)); + } + _context->keyint_min = _context->gop_size; + } + } + + if (!_context->internal || support_reconfig) { + // Handler Options + if (_handler) + _handler->update(this->_factory, this, settings); + + { // FFmpeg Custom Options + const char* opts = obs_data_get_string(settings, ST_KEY_FFMPEG_CUSTOMSETTINGS); + std::size_t opts_len = strnlen(opts, 65535); + + parse_ffmpeg_commandline(std::string{opts, opts + opts_len}); + } + + // Handler Overrides + if (_handler) + _handler->override_update(this->_factory, this, settings); + } + + // Generate a command line? + if (_context->internal && support_reconfig) { + log(); + } + + return true; +} + +static inline void copy_data(encoder_frame* frame, AVFrame* vframe) +{ + int h_chroma_shift, v_chroma_shift; + av_pix_fmt_get_chroma_sub_sample(static_cast(vframe->format), &h_chroma_shift, &v_chroma_shift); + + for (std::size_t idx = 0; idx < MAX_AV_PLANES; idx++) { + if (!frame->data[idx] || !vframe->data[idx]) + continue; + + std::size_t plane_height = static_cast(vframe->height) >> (idx ? v_chroma_shift : 0); + + if (static_cast(vframe->linesize[idx]) == frame->linesize[idx]) { + std::memcpy(vframe->data[idx], frame->data[idx], frame->linesize[idx] * plane_height); + } else { + std::size_t ls_in = static_cast(frame->linesize[idx]); + std::size_t ls_out = static_cast(vframe->linesize[idx]); + std::size_t bytes = ls_in < ls_out ? ls_in : ls_out; + + uint8_t* to = vframe->data[idx]; + uint8_t* from = frame->data[idx]; + + for (std::size_t y = 0; y < plane_height; y++) { + std::memcpy(to, from, bytes); + to += ls_out; + from += ls_in; + } + } + } +} + +bool ffmpeg_instance::encode_audio(struct encoder_frame* frame, struct encoder_packet* packet, bool* received_packet) +{ + throw std::logic_error("The method or operation is not implemented."); +} + +bool ffmpeg_instance::encode_video(struct encoder_frame* frame, struct encoder_packet* packet, bool* received_packet) +{ + if ((_framerate_divisor > 1) && (frame->pts % _framerate_divisor != 0)) { + return true; + } + + std::shared_ptr vframe = pop_free_frame(); // Retrieve an empty frame. + + // Convert frame. + { + vframe->height = _context->height; + vframe->format = _context->pix_fmt; + vframe->color_range = _context->color_range; + vframe->colorspace = _context->colorspace; + vframe->color_primaries = _context->color_primaries; + vframe->color_trc = _context->color_trc; + vframe->pts = frame->pts; + + if ((_scaler.is_source_full_range() == _scaler.is_target_full_range()) && (_scaler.get_source_colorspace() == _scaler.get_target_colorspace()) && (_scaler.get_source_format() == _scaler.get_target_format())) { + copy_data(frame, vframe.get()); + } else { + int res = _scaler.convert(reinterpret_cast(frame->data), reinterpret_cast(frame->linesize), 0, _context->height, vframe->data, vframe->linesize); + if (res <= 0) { + DLOG_ERROR("Failed to convert frame: %s (%" PRId32 ").", ::streamfx::ffmpeg::tools::get_error_description(res), res); + return false; + } + } + } + + if (!encode_avframe(vframe, packet, received_packet)) + return false; + + return true; +} + +bool ffmpeg_instance::encode_video(uint32_t handle, int64_t pts, uint64_t lock_key, uint64_t* next_key, struct encoder_packet* packet, bool* received_packet) +{ + if ((_framerate_divisor > 1) && (pts % _framerate_divisor != 0)) { + *next_key = lock_key; + return true; + } + +#ifdef D_PLATFORM_WINDOWS + if (handle == GS_INVALID_HANDLE) { + DLOG_ERROR("Received invalid handle."); + *next_key = lock_key; + return false; + } + + std::shared_ptr vframe = pop_free_frame(); + _hwinst->copy_from_obs(_context->hw_frames_ctx, handle, lock_key, next_key, vframe); + + vframe->color_range = _context->color_range; + vframe->colorspace = _context->colorspace; + vframe->color_primaries = _context->color_primaries; + vframe->color_trc = _context->color_trc; + vframe->pts = pts; + + if (!encode_avframe(vframe, packet, received_packet)) + return false; + + *next_key = lock_key; + + return true; +#else + return false; +#endif +} + +void ffmpeg_instance::initialize_sw(obs_data_t* settings) +{ + // Initialize Video Encoding + auto voi = video_output_get_info(obs_encoder_video(_self)); + + // Figure out a suitable pixel format to convert to if necessary. + AVPixelFormat pix_fmt_source = ::streamfx::ffmpeg::tools::obs_videoformat_to_avpixelformat(voi->format); + AVPixelFormat pix_fmt_target = AV_PIX_FMT_NONE; + { + if (_codec->pix_fmts) { + pix_fmt_target = ::streamfx::ffmpeg::tools::get_least_lossy_format(_codec->pix_fmts, pix_fmt_source); + } else { // If there are no supported formats, just pass in the current one. + pix_fmt_target = pix_fmt_source; + } + + if (_handler) // Allow Handler to override the automatic color format for sanity reasons. + _handler->override_colorformat(this->_factory, this, settings, pix_fmt_target); + } + + // Setup from OBS information. + ::streamfx::ffmpeg::tools::context_setup_from_obs(voi, _context); + + // Override with other information. + _context->width = static_cast(obs_encoder_get_width(_self)); + _context->height = static_cast(obs_encoder_get_height(_self)); + _context->pix_fmt = pix_fmt_target; + + _scaler.set_source_size(static_cast(_context->width), static_cast(_context->height)); + _scaler.set_source_color(_context->color_range == AVCOL_RANGE_JPEG, _context->colorspace); + _scaler.set_source_format(pix_fmt_source); + + _scaler.set_target_size(static_cast(_context->width), static_cast(_context->height)); + _scaler.set_target_color(_context->color_range == AVCOL_RANGE_JPEG, _context->colorspace); + _scaler.set_target_format(pix_fmt_target); + + // Create Scaler + if (!_scaler.initialize(SWS_SINC | SWS_FULL_CHR_H_INT | SWS_FULL_CHR_H_INP | SWS_ACCURATE_RND | SWS_BITEXACT)) { + std::stringstream sstr; + sstr << "Initializing scaler failed for conversion from '" << ::streamfx::ffmpeg::tools::get_pixel_format_name(_scaler.get_source_format()) << "' to '" << ::streamfx::ffmpeg::tools::get_pixel_format_name(_scaler.get_target_format()) << "' with color space '" << ::streamfx::ffmpeg::tools::get_color_space_name(_scaler.get_source_colorspace()) << "' and " << (_scaler.is_source_full_range() ? "full" : "partial") << " range."; + throw std::runtime_error(sstr.str()); + } +} + +void ffmpeg_instance::initialize_hw(obs_data_t*) +{ +#ifndef D_PLATFORM_WINDOWS + throw std::runtime_error("OBS Studio currently does not support zero copy encoding for this platform."); +#else + // Initialize Video Encoding + const video_output_info* voi = video_output_get_info(obs_encoder_video(_self)); + + // Apply pixel format settings. + ::streamfx::ffmpeg::tools::context_setup_from_obs(voi, _context); + _context->sw_pix_fmt = _context->pix_fmt; + _context->pix_fmt = AV_PIX_FMT_D3D11; + + // Try to create a hardware context. + _context->hw_device_ctx = _hwinst->create_device_context(); + _context->hw_frames_ctx = av_hwframe_ctx_alloc(_context->hw_device_ctx); + if (!_context->hw_frames_ctx) { + throw std::runtime_error("Creating hardware context failed."); + } + + // Initialize Hardware Context + AVHWFramesContext* ctx = reinterpret_cast(_context->hw_frames_ctx->data); + ctx->width = _context->width; + ctx->height = _context->height; + ctx->format = _context->pix_fmt; + ctx->sw_format = _context->sw_pix_fmt; + if (int32_t res = av_hwframe_ctx_init(_context->hw_frames_ctx); res < 0) { + std::array buffer; + + int len = snprintf(buffer.data(), buffer.size(), "Failed initialize hardware context: %s (%" PRIu32 ")", ::streamfx::ffmpeg::tools::get_error_description(res), res); + throw std::runtime_error(std::string(buffer.data(), buffer.data() + len)); + } +#endif +} + +void ffmpeg_instance::push_free_frame(std::shared_ptr frame) +{ + auto now = std::chrono::high_resolution_clock::now(); + if (_free_frames.size() > 0) { + if ((now - _free_frames_last_used) < std::chrono::seconds(1)) { + _free_frames.push(frame); + } + } else { + _free_frames.push(frame); + _free_frames_last_used = std::chrono::high_resolution_clock::now(); + } +} + +std::shared_ptr ffmpeg_instance::pop_free_frame() +{ + std::shared_ptr frame; + if (_free_frames.size() > 0) { + // Re-use existing frames first. + frame = _free_frames.top(); + _free_frames.pop(); + } else { + if (_hwinst) { + frame = _hwinst->allocate_frame(_context->hw_frames_ctx); + } else { + frame = std::shared_ptr(av_frame_alloc(), [](AVFrame* frame) { + av_frame_unref(frame); + av_frame_free(&frame); + }); + + frame->width = _context->width; + frame->height = _context->height; + frame->format = _context->pix_fmt; + + int res = av_frame_get_buffer(frame.get(), 32); + if (res < 0) { + throw std::runtime_error(::streamfx::ffmpeg::tools::get_error_description(res)); + } + } + } + + return frame; +} + +void ffmpeg_instance::push_used_frame(std::shared_ptr frame) +{ + _used_frames.push(frame); +} + +std::shared_ptr ffmpeg_instance::pop_used_frame() +{ + auto frame = _used_frames.front(); + _used_frames.pop(); + return frame; +} + +bool ffmpeg_instance::get_extra_data(uint8_t** data, size_t* size) +{ + if (!_have_first_frame) + return false; + + *data = _extra_data.data(); + *size = _extra_data.size(); + return true; +} + +bool ffmpeg_instance::get_sei_data(uint8_t** data, size_t* size) +{ + if (!_have_first_frame) + return false; + + *data = _sei_data.data(); + *size = _sei_data.size(); + return true; +} + +void ffmpeg_instance::get_video_info(struct video_scale_info* info) +{ + if (!is_hardware_encode()) { + // Override input with supported format if software encode. + info->format = ::streamfx::ffmpeg::tools::avpixelformat_to_obs_videoformat(_scaler.get_source_format()); + } +} + +int ffmpeg_instance::receive_packet(bool* received_packet, struct encoder_packet* packet) +{ + int res = 0; + + av_packet_unref(_packet.get()); + + { + auto gctx = streamfx::obs::gs::context(); + res = avcodec_receive_packet(_context, _packet.get()); + } + if (res != 0) { + return res; + } + + if (!_have_first_frame) { + if (_codec->id == AV_CODEC_ID_H264) { + uint8_t* tmp_packet; + uint8_t* tmp_header; + uint8_t* tmp_sei; + std::size_t sz_packet, sz_header, sz_sei; + + obs_extract_avc_headers(_packet->data, static_cast(_packet->size), &tmp_packet, &sz_packet, &tmp_header, &sz_header, &tmp_sei, &sz_sei); + + if (sz_header) { + _extra_data.resize(sz_header); + std::memcpy(_extra_data.data(), tmp_header, sz_header); + } + + if (sz_sei) { + _sei_data.resize(sz_sei); + std::memcpy(_sei_data.data(), tmp_sei, sz_sei); + } + + // Not required, we only need the Extra Data and SEI Data anyway. + //std::memcpy(_current_packet.data, tmp_packet, sz_packet); + //_current_packet.size = static_cast(sz_packet); + + bfree(tmp_packet); + bfree(tmp_header); + bfree(tmp_sei); + } else if (_codec->id == AV_CODEC_ID_HEVC) { + hevc::extract_header_sei(_packet->data, static_cast(_packet->size), _extra_data, _sei_data); + } else if (_context->extradata != nullptr) { + _extra_data.resize(static_cast(_context->extradata_size)); + std::memcpy(_extra_data.data(), _context->extradata, static_cast(_context->extradata_size)); + } + _have_first_frame = true; + } + + // Allow Handler Post-Processing + //FIXME! Is this still necessary? + //if (_handler) + // _handler->process_avpacket(_packet, _codec, _context); + + // Build packet for use in OBS. + packet->type = OBS_ENCODER_VIDEO; + packet->pts = _packet->pts; + packet->dts = _packet->dts; + packet->data = _packet->data; + packet->size = static_cast(_packet->size); + packet->keyframe = !!(_packet->flags & AV_PKT_FLAG_KEY); + *received_packet = true; + + // Figure out priority and drop_priority. + // In theory, this is done by OBS, but its not doing a great job. + packet->priority = packet->keyframe ? 3 : 2; + packet->drop_priority = 3; + for (size_t idx = 0, edx = static_cast(_packet->side_data_elems); idx < edx; idx++) { + auto& side_data = _packet->side_data[idx]; + if (side_data.type == AV_PKT_DATA_NEW_EXTRADATA) { + _extra_data.resize(side_data.size); + std::memcpy(_extra_data.data(), side_data.data, side_data.size); + } else if (side_data.type == AV_PKT_DATA_QUALITY_STATS) { + // Decisions based on picture type, if present. + switch (side_data.data[sizeof(uint32_t)]) { + case AV_PICTURE_TYPE_I: // I-Frame + case AV_PICTURE_TYPE_SI: // Switching I-Frame + if (_packet->flags & AV_PKT_FLAG_KEY) { + // Recovery only via IDR-Frame. + packet->priority = 3; // OBS_NAL_PRIORITY_HIGHEST + packet->drop_priority = 2; // OBS_NAL_PRIORITY_HIGH + } else { + // Recovery via I- or IDR-Frame. + packet->priority = 2; // OBS_NAL_PRIORITY_HIGH + packet->drop_priority = 2; // OBS_NAL_PRIORITY_HIGH + } + break; + case AV_PICTURE_TYPE_P: // P-Frame + case AV_PICTURE_TYPE_SP: // Switching P-Frame + // Recovery via I- or IDR-Frame. + packet->priority = 1; // OBS_NAL_PRIORITY_LOW + packet->drop_priority = 2; // OBS_NAL_PRIORITY_HIGH + break; + case AV_PICTURE_TYPE_B: // B-Frame + // Recovery via I- or IDR-Frame. + packet->priority = 0; // OBS_NAL_PRIORITY_DISPOSABLE + packet->drop_priority = 2; // OBS_NAL_PRIORITY_HIGH + break; + case AV_PICTURE_TYPE_BI: // BI-Frame, theoretically identical to I-Frame. + // Recovery via I- or IDR-Frame. + packet->priority = 2; // OBS_NAL_PRIORITY_HIGH + packet->drop_priority = 2; // OBS_NAL_PRIORITY_HIGH + break; + default: // Unknown picture type. + // Recovery only via IDR-Frame + packet->priority = 2; // OBS_NAL_PRIORITY_HIGH + packet->drop_priority = 3; // OBS_NAL_PRIORITY_HIGHEST + break; + } + } + } + + // Push free frame back into pool. + push_free_frame(pop_used_frame()); + + return res; +} + +int ffmpeg_instance::send_frame(std::shared_ptr const frame) +{ + int res = 0; + { + auto gctx = streamfx::obs::gs::context(); + res = avcodec_send_frame(_context, frame.get()); + } + if (res == 0) { + push_used_frame(frame); + } + + return res; +} + +bool ffmpeg_instance::encode_avframe(std::shared_ptr frame, encoder_packet* packet, bool* received_packet) +{ + bool sent_frame = false; + bool recv_packet = false; + bool should_lag = (_sent_frames >= _lag_in_frames); + + auto loop_begin = std::chrono::high_resolution_clock::now(); + auto loop_end = loop_begin + std::chrono::milliseconds(50); + + while ((!sent_frame || (should_lag && !recv_packet)) && !(std::chrono::high_resolution_clock::now() > loop_end)) { + bool eagain_is_stupid = false; + + if (!sent_frame) { + int res = send_frame(frame); + switch (res) { + case 0: + sent_frame = true; + frame = nullptr; + break; + case AVERROR(EAGAIN): + // This means we should call receive_packet again, but what do we do with that data? + // Why can't we queue on both? Do I really have to implement threading for this stuff? + if (*received_packet == true) { + DLOG_WARNING("Skipped frame due to EAGAIN when a packet was already returned."); + sent_frame = true; + } + eagain_is_stupid = true; + break; + case AVERROR(EOF): + DLOG_ERROR("Skipped frame due to end of stream."); + sent_frame = true; + break; + default: + DLOG_ERROR("Failed to encode frame: %s (%" PRId32 ").", ::streamfx::ffmpeg::tools::get_error_description(res), res); + return false; + } + } + + if (!recv_packet) { + int res = receive_packet(received_packet, packet); + switch (res) { + case 0: + recv_packet = true; + break; + case AVERROR(EOF): + DLOG_ERROR("Received end of file."); + recv_packet = true; + break; + case AVERROR(EAGAIN): + if (sent_frame) { + recv_packet = true; + } + if (eagain_is_stupid) { + DLOG_ERROR("Both send and recieve returned EAGAIN, encoder is broken."); + return false; + } + break; + default: + DLOG_ERROR("Failed to receive packet: %s (%" PRId32 ").", ::streamfx::ffmpeg::tools::get_error_description(res), res); + return false; + } + } + + if (!sent_frame || !recv_packet) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + } + + if (!sent_frame) + push_free_frame(frame); + + return true; +} + +bool ffmpeg_instance::is_hardware_encode() +{ + return _hwinst != nullptr; +} + +const AVCodec* ffmpeg_instance::get_avcodec() +{ + return _codec; +} + +AVCodecContext* ffmpeg_instance::get_avcodeccontext() +{ + return _context; +} + +void ffmpeg_instance::generate_ffmpeg_commandline(std::unordered_map& buffer, const AVClass* cls, void* data) +{ + std::string_view ignore_opts[] = { + "debug", // Debug logging. + "cmp", + "mbcmp", + "subcmp", + "ildctcmp", + "precmp", // Some sort of motion thing that can't be set on command line. + "max_pixels", + "max_samples", // Maximum Limits of some kind. + "tcplx_mask", + "scplx_mask", // Weird masks for temporal and spatial stuff. + "last_pred", // Not actually an encoder option, but listed as such. + "sub_charenc", + "sub_charenc_mode", // Subtitle encoding, not useful for video encoding. + "dump_separator", // Not used for encoding. + "aspect", // Automatically set by demuxer. + }; + + for (const AVOption* opt = nullptr; (opt = av_opt_next(&cls, opt)) != nullptr;) { + std::string name = opt->name; + std::string value; + + // Ignore constants, since they are not modifiable. + if (opt->type == AV_OPT_TYPE_CONST) + continue; + + // Ignore any option that is not for videos. + if (!(opt->flags & AV_OPT_FLAG_VIDEO_PARAM)) + continue; + + // Ignore any option that is not for encoding. + if (!(opt->flags & AV_OPT_FLAG_ENCODING_PARAM)) + continue; + + // Ignore some preselected options. + bool ignored = false; + for (auto& ignore : ignore_opts) { + if (ignore == opt->name) { + ignored = true; + break; + } + } + if (ignored) + continue; + + // Figure out the real value for unit options. + if (opt->unit) { + int64_t opt_value; + if (av_opt_get_int(data, opt->name, AV_OPT_SEARCH_CHILDREN | AV_OPT_ALLOW_NULL, &opt_value) >= 0) { + auto real_value = streamfx::ffmpeg::tools::avoption_name_from_unit_value(cls, opt->unit, opt_value); + if (real_value) { + value = std::string(real_value); + } + } + } + + // If this is not a unit option, or the unit option does not have a name, convert it to string. + if (value.empty()) { + // Print non-unit options directly. + uint8_t* opt_value; + if (av_opt_get(data, opt->name, AV_OPT_SEARCH_CHILDREN | AV_OPT_ALLOW_NULL, &opt_value) >= 0) { + if (opt_value) { + value = std::string((char*)opt_value); + av_freep(&opt_value); + } + } + } + + // Insert/Assign into the map. + if (!name.empty() && !value.empty()) { + buffer.insert_or_assign(name, value); + } + } +} + +void ffmpeg_instance::parse_ffmpeg_commandline(std::string_view text) +{ + // Split by quotes and spaces first. + std::list opts; + std::string_view::value_type quote = 0; + std::stringstream opt; + for (size_t i = 0; i <= text.size(); ++i) { + std::string_view::value_type chr = (i < text.size()) ? text.at(i) : 0; + + // We only need to track how many quotes are still remaining on the stack. + if (chr == '\\') { + ++i; // Advance cursor by one. + if (i >= text.size()) { + DLOG_ERROR("Malformed escape sequence in command line: %s", text.data()); + opts.push_back(opt.str()); + opt.clear(); + break; + } else { + chr = text.at(i); + if (isdigit(chr)) { // Octal + // !FIXME! Ignore for now. + i = i + 2; + } else if (chr == 'u') { // Unicode + // !FIXME! Ignore for now. + i = i + 4; + } else if (chr == 'x') { // Hexidecimal + // !FIXME! Ignore for now. + i = i + 2; + } else if (chr == 'a') { + opt << '\a'; + } else if (chr == 'b') { + opt << '\b'; + } else if (chr == 'f') { + opt << '\f'; + } else if (chr == 'n') { + opt << '\n'; + } else if (chr == 'r') { + opt << '\r'; + } else if (chr == 't') { + opt << '\t'; + } else if (chr == 'v') { + opt << '\v'; + } else if (chr == '\\') { + opt << '\\'; + } else if (chr == '\'') { + opt << '\''; + } else if (chr == '"') { + opt << '"'; + } else if (chr == '?') { + opt << '\?'; + } else { + opt << chr; + } + } + } else if ((chr == '\"') || (chr == '\'')) { // Quotes + if (quote == chr) { + quote = 0; + opts.push_back(opt.str()); + std::stringstream().swap(opt); + } else if (quote == 0) { + quote = chr; + } else { + opt << chr; + } + } else if (chr == ' ') { // Space + if (quote != 0) { + opt << chr; + } else if (opt.seekg(0, std::ios::end); opt.tellg() > 0) { + opts.push_back(opt.str()); + std::stringstream().swap(opt); + } + } else if (chr == 0) { // EOL + if (quote != 0) { + DLOG_WARNING("Unterminated quote in command line: %s", text.data()); + } + if (opt.seekg(0, std::ios::end); opt.tellg() > 0) { + opts.push_back(opt.str()); + std::stringstream().swap(opt); + } + } else { + opt << chr; + } + } + + // Apply options. + for (auto iter = opts.begin(); iter != opts.cend(); ++iter) { + std::string opt_str = *iter; + + // Skip options without the necessary '-' in front of them. + if (opt_str[0] != '-') { + DLOG_ERROR("Invalid option '%s', skipping...", iter->c_str()); + continue; + } else { + opt_str = opt_str.substr(1); + } + + // Check if the option has an equal sign in it. + if (auto eq_at = strchr(opt_str.c_str(), '='); eq_at != nullptr) { + // Yes, parse the old way. + try { + std::string key = opt_str.substr(0, static_cast((eq_at - opt_str.c_str()))); + std::string value = opt_str.substr(static_cast((eq_at - opt_str.c_str()) + 1)); + DLOG_WARNING("Found old-style option '%s', parsed to '%s' and '%s'. Please update your custom FFmpeg settings.", opt_str.c_str(), key.c_str(), value.c_str()); + + int res = av_opt_set(_context, key.c_str(), value.c_str(), AV_OPT_SEARCH_CHILDREN); + if (res < 0) { + DLOG_ERROR("Failed to set option '%s' to '%s': %s' (code: %" PRId32 ")", opt_str.c_str(), key.c_str(), value.c_str(), ::streamfx::ffmpeg::tools::get_error_description(res), res); + continue; + } + } catch (const std::exception& ex) { + DLOG_ERROR("Failed to set option '%s' due to exception: %s", opt_str.c_str(), ex.what()); + continue; + } + } else { + // No, parse the normal way. + + // Advance and ensure we're not out of bounds yet. + auto viter = iter; + if (++viter == opts.cend()) { + DLOG_ERROR("Missing value for option '%s', skipping...", iter->c_str()); + continue; + } else { + iter = viter; + } + + // If everything went well, try to set things. + std::string value_str = *iter; + int res = av_opt_set(_context, opt_str.c_str(), value_str.c_str(), AV_OPT_SEARCH_CHILDREN); + if (res < 0) { + DLOG_ERROR("Failed to set option '%s' to '%s': %s (code: %" PRId32 ")", opt_str.c_str(), value_str.c_str(), ::streamfx::ffmpeg::tools::get_error_description(res), res); + continue; + } + } + } +} + +void ffmpeg_instance::log() +{ + std::unordered_map values; + std::stringstream buffer; + generate_ffmpeg_commandline(values, *(AVClass**)_context->priv_data, _context); + generate_ffmpeg_commandline(values, *(AVClass**)_context, _context); + for (auto& kv : values) { + buffer << "-" << kv.first << " " << kv.second << " "; + } + DLOG_INFO("[%s] Full Command Line: -c:v %s %s", _codec->name, _codec->name, buffer.str().c_str()); +} + +ffmpeg_factory::ffmpeg_factory(ffmpeg_manager* manager, const AVCodec* codec) : _avcodec(codec) +{ + // Generate default identifier. + { + std::stringstream str; + str << S_PREFIX << _avcodec->name; + _id = str.str(); + } + + { // Generate default name. + std::stringstream str; + if (_avcodec->long_name) { + str << _avcodec->long_name; + str << " (" << _avcodec->name << ")"; + } else { + str << _avcodec->name; + } + str << D_TRANSLATE(ST_I18N_FFMPEG_SUFFIX); + _name = str.str(); + } + + // Try and find a codec name that libOBS understands. + if (auto* desc = avcodec_descriptor_get(_avcodec->id); desc) { + _codec = desc->name; + } else { + // If FFmpeg doesn't know better, fall back to the name. + _codec = _avcodec->name; + } + + // Find any available handlers for this codec. + if (_handler = manager->get_handler(_avcodec->name); _handler) { + // Override any found info with the one specified by the handler. + _handler->adjust_info(this, _id, _name, _codec); + + // Add texture capability for hardware encoders. + if (_handler->is_hardware(this)) { + _info.caps |= OBS_ENCODER_CAP_PASS_TEXTURE; + } + } else { + // If there are no handlers, default to mark it deprecated. + _info.caps |= OBS_ENCODER_CAP_DEPRECATED; + } + + { // Build Info structure. + _info.id = _id.c_str(); + _info.codec = _codec.c_str(); + if (_avcodec->type == AVMediaType::AVMEDIA_TYPE_VIDEO) { + _info.type = obs_encoder_type::OBS_ENCODER_VIDEO; + } else if (_avcodec->type == AVMediaType::AVMEDIA_TYPE_AUDIO) { + _info.type = obs_encoder_type::OBS_ENCODER_AUDIO; + } + } + + // Register encoder and proxies. + finish_setup(); + const std::string proxies[] = { + std::string("streamfx--") + _avcodec->name, + std::string("StreamFX-") + _avcodec->name, + std::string("obs-ffmpeg-encoder_") + _avcodec->name, + }; + for (auto proxy_id : proxies) { + register_proxy(proxy_id); + if (_info.caps & OBS_ENCODER_CAP_PASS_TEXTURE) { + std::string proxy_fallback_id = proxy_id + "_sw"; + register_proxy(proxy_fallback_id); + } + } +} + +ffmpeg_factory::~ffmpeg_factory() {} + +const char* ffmpeg_factory::get_name() +{ + return _name.c_str(); +} + +void ffmpeg_factory::get_defaults2(obs_data_t* settings) +{ + if (_handler) { + _handler->defaults(this, settings); + + if (_handler->has_keyframes(this)) { + obs_data_set_default_int(settings, ST_KEY_KEYFRAMES_INTERVALTYPE, 0); + obs_data_set_default_double(settings, ST_KEY_KEYFRAMES_INTERVAL_SECONDS, 2.0); + obs_data_set_default_int(settings, ST_KEY_KEYFRAMES_INTERVAL_FRAMES, 300); + } + } + + { // Integrated Options + // FFmpeg + obs_data_set_default_string(settings, ST_KEY_FFMPEG_CUSTOMSETTINGS, ""); + obs_data_set_default_int(settings, ST_KEY_FFMPEG_THREADS, 0); + obs_data_set_default_int(settings, ST_KEY_FFMPEG_GPU, -1); + } +} + +void ffmpeg_factory::migrate(obs_data_t* data, uint64_t version) +{ + if (_handler) + _handler->migrate(this, nullptr, data, version); +} + +static bool modified_keyframes(obs_properties_t* props, obs_property_t*, obs_data_t* settings) noexcept +{ + try { + bool is_seconds = obs_data_get_int(settings, ST_KEY_KEYFRAMES_INTERVALTYPE) == 0; + obs_property_set_visible(obs_properties_get(props, ST_KEY_KEYFRAMES_INTERVAL_FRAMES), !is_seconds); + obs_property_set_visible(obs_properties_get(props, ST_KEY_KEYFRAMES_INTERVAL_SECONDS), is_seconds); + return true; + } catch (const std::exception& ex) { + DLOG_ERROR("Unexpected exception in function '%s': %s.", __FUNCTION_NAME__, ex.what()); + return false; + } catch (...) { + DLOG_ERROR("Unexpected exception in function '%s'.", __FUNCTION_NAME__); + return false; + } +} + +obs_properties_t* ffmpeg_factory::get_properties2(instance_t* data) +{ + obs_properties_t* props = obs_properties_create(); + + { + obs_properties_add_button2(props, S_MANUAL_OPEN, D_TRANSLATE(S_MANUAL_OPEN), streamfx::encoder::ffmpeg::ffmpeg_factory::on_manual_open, this); + } + + if (data) { + data->get_properties(props); + } + + if (_handler) + _handler->properties(this, data, props); + + if (_handler && _handler->has_keyframes(this)) { + // Key-Frame Options + obs_properties_t* grp = props; + if (!streamfx::util::are_property_groups_broken()) { + grp = obs_properties_create(); + obs_properties_add_group(props, ST_I18N_KEYFRAMES, D_TRANSLATE(ST_I18N_KEYFRAMES), OBS_GROUP_NORMAL, grp); + } + + { // Key-Frame Interval Type + auto p = obs_properties_add_list(grp, ST_KEY_KEYFRAMES_INTERVALTYPE, D_TRANSLATE(ST_I18N_KEYFRAMES_INTERVALTYPE), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); + obs_property_set_modified_callback(p, modified_keyframes); + obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_KEYFRAMES_INTERVALTYPE_("Seconds")), 0); + obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_KEYFRAMES_INTERVALTYPE_("Frames")), 1); + } + { // Key-Frame Interval Seconds + auto p = obs_properties_add_float(grp, ST_KEY_KEYFRAMES_INTERVAL_SECONDS, D_TRANSLATE(ST_I18N_KEYFRAMES_INTERVAL), 0.00, std::numeric_limits::max(), 0.01); + obs_property_float_set_suffix(p, " seconds"); + } + { // Key-Frame Interval Frames + auto p = obs_properties_add_int(grp, ST_KEY_KEYFRAMES_INTERVAL_FRAMES, D_TRANSLATE(ST_I18N_KEYFRAMES_INTERVAL), 0, std::numeric_limits::max(), 1); + obs_property_int_set_suffix(p, " frames"); + } + } + + { + obs_properties_t* grp = props; + if (!streamfx::util::are_property_groups_broken()) { + auto prs = obs_properties_create(); + obs_properties_add_group(props, ST_I18N_FFMPEG, D_TRANSLATE(ST_I18N_FFMPEG), OBS_GROUP_NORMAL, prs); + grp = prs; + } + + { // Custom Settings + auto p = obs_properties_add_text(grp, ST_KEY_FFMPEG_CUSTOMSETTINGS, D_TRANSLATE(ST_I18N_FFMPEG_CUSTOMSETTINGS), obs_text_type::OBS_TEXT_DEFAULT); + } + + if (_handler && _handler->is_hardware(this)) { + auto p = obs_properties_add_int(grp, ST_KEY_FFMPEG_GPU, D_TRANSLATE(ST_I18N_FFMPEG_GPU), -1, std::numeric_limits::max(), 1); + } + + if (_handler && _handler->has_threading(this)) { + auto p = obs_properties_add_int_slider(grp, ST_KEY_FFMPEG_THREADS, D_TRANSLATE(ST_I18N_FFMPEG_THREADS), 0, static_cast(std::thread::hardware_concurrency()) * 2, 1); + } + + { // Frame Skipping + obs_video_info ovi; + if (!obs_get_video_info(&ovi)) { + throw std::runtime_error("obs_get_video_info failed unexpectedly."); + } + + auto p = obs_properties_add_list(grp, ST_KEY_FFMPEG_FRAMERATE, D_TRANSLATE(ST_I18N_FFMPEG_FRAMERATE), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); + // For now, an arbitrary limit of 1/10th the Framerate should be fine. + std::vector buf{size_t{256}, 0, std::allocator()}; + for (uint32_t divisor = 1; divisor <= 10; divisor++) { + double fps_num = static_cast(ovi.fps_num) / static_cast(divisor); + double fps = fps_num / static_cast(ovi.fps_den); + snprintf(buf.data(), buf.size(), "%8.2f (%" PRIu32 "/%" PRIu32 ")", fps, ovi.fps_num, ovi.fps_den * divisor); + obs_property_list_add_int(p, buf.data(), divisor); + } + } + }; + + return props; +} + +bool ffmpeg_factory::on_manual_open(obs_properties_t* props, obs_property_t* property, void* data) +{ + ffmpeg_factory* ptr = static_cast(data); + streamfx::open_url(ptr->_handler->help(ptr)); + return false; +} + +const AVCodec* ffmpeg_factory::get_avcodec() +{ + return _avcodec; +} + +obs_encoder_info* streamfx::encoder::ffmpeg::ffmpeg_factory::get_info() +{ + return &_info; +} + +ffmpeg_manager::ffmpeg_manager() : _factories() +{ + // Encoders + void* iterator = nullptr; + for (const AVCodec* codec = av_codec_iterate(&iterator); codec != nullptr; codec = av_codec_iterate(&iterator)) { + // Only register encoders. + if (!av_codec_is_encoder(codec)) + continue; + + if (codec->type == AVMediaType::AVMEDIA_TYPE_VIDEO) { + try { + _factories.emplace(codec, std::make_shared(this, codec)); + } catch (const std::exception& ex) { + DLOG_ERROR("Failed to register encoder '%s': %s", codec->name, ex.what()); + } + } + } +} + +ffmpeg_manager::~ffmpeg_manager() +{ + _factories.clear(); +} + +std::shared_ptr ffmpeg_manager::instance() +{ + static std::weak_ptr winst; + static std::mutex mtx; + + std::unique_lock lock(mtx); + auto instance = winst.lock(); + if (!instance) { + instance = std::shared_ptr(new ffmpeg_manager()); + winst = instance; + } + return instance; +} + +streamfx::encoder::ffmpeg::handler* ffmpeg_manager::find_handler(std::string_view codec) +{ + auto handlers = streamfx::encoder::ffmpeg::handler::handlers(); + if (auto kv = handlers.find(std::string{codec}); kv != handlers.end()) { + return kv->second; + } +#ifdef _DEBUG + if (auto kv = handlers.find(""); kv != handlers.end()) { + return kv->second; + } +#endif + return nullptr; +} + +streamfx::encoder::ffmpeg::handler* ffmpeg_manager::get_handler(std::string_view codec) +{ + return find_handler(codec); +} + +bool ffmpeg_manager::has_handler(std::string_view codec) +{ + return find_handler(codec) != nullptr; +} + +static std::shared_ptr loader_instance; + +static auto loader = streamfx::component( + "ffmpeg::encoder", + []() { // Initializer + loader_instance = ffmpeg_manager::instance(); + }, + []() { // Finalizer + loader_instance.reset(); + }, + {"core::threadpool", "core::configuration"}); diff --git a/components/ffmpeg/source/encoders/encoder-ffmpeg.hpp b/components/ffmpeg/source/encoders/encoder-ffmpeg.hpp new file mode 100644 index 0000000..1b66eb3 --- /dev/null +++ b/components/ffmpeg/source/encoders/encoder-ffmpeg.hpp @@ -0,0 +1,161 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2022 lainon +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "common.hpp" +#include "encoders/ffmpeg/handler.hpp" +#include "ffmpeg/avframe-queue.hpp" +#include "ffmpeg/hwapi/base.hpp" +#include "ffmpeg/swscale.hpp" +#include "obs/obs-encoder-factory.hpp" + +#include "warning-disable.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +extern "C" { +#include +#include +} +#include "warning-enable.hpp" + +namespace streamfx::encoder::ffmpeg { + class ffmpeg_instance; + class ffmpeg_factory; + class ffmpeg_manager; + + class ffmpeg_instance : public obs::encoder_instance { + ffmpeg_factory* _factory; + const AVCodec* _codec; + AVCodecContext* _context; + + streamfx::encoder::ffmpeg::handler* _handler; + + ::streamfx::ffmpeg::swscale _scaler; + std::shared_ptr _packet; + + std::shared_ptr<::streamfx::ffmpeg::hwapi::base> _hwapi; + std::shared_ptr<::streamfx::ffmpeg::hwapi::instance> _hwinst; + + std::size_t _lag_in_frames; + std::size_t _sent_frames; + std::size_t _framerate_divisor; + + // Extra Data + bool _have_first_frame; + std::vector _extra_data; + std::vector _sei_data; + + // Frame Stack and Queue + std::stack> _free_frames; + std::queue> _used_frames; + std::chrono::high_resolution_clock::time_point _free_frames_last_used; + + public: + ffmpeg_instance(obs_data_t* settings, obs_encoder_t* self, bool is_hw); + virtual ~ffmpeg_instance(); + + public: + void get_properties(obs_properties_t* props); + + void migrate(obs_data_t* settings, uint64_t version) override; + + bool update(obs_data_t* settings) override; + + bool encode_audio(struct encoder_frame* frame, struct encoder_packet* packet, bool* received_packet) override; + + bool encode_video(struct encoder_frame* frame, struct encoder_packet* packet, bool* received_packet) override; + + bool encode_video(uint32_t handle, int64_t pts, uint64_t lock_key, uint64_t* next_key, struct encoder_packet* packet, bool* received_packet) override; + + bool get_extra_data(uint8_t** extra_data, size_t* size) override; + + bool get_sei_data(uint8_t** sei_data, size_t* size) override; + + void get_video_info(struct video_scale_info* info) override; + + public: + void initialize_sw(obs_data_t* settings); + void initialize_hw(obs_data_t* settings); + + void push_free_frame(std::shared_ptr frame); + std::shared_ptr pop_free_frame(); + + void push_used_frame(std::shared_ptr frame); + std::shared_ptr pop_used_frame(); + + int receive_packet(bool* received_packet, struct encoder_packet* packet); + + int send_frame(std::shared_ptr frame); + + bool encode_avframe(std::shared_ptr frame, struct encoder_packet* packet, bool* received_packet); + + public: // Handler API + bool is_hardware_encode(); + + const AVCodec* get_avcodec(); + + AVCodecContext* get_avcodeccontext(); + + void log(); + + void generate_ffmpeg_commandline(std::unordered_map& buffer, const AVClass* obj, void* data); + + void parse_ffmpeg_commandline(std::string_view text); + }; + + class ffmpeg_factory : public obs::encoder_factory { + std::string _id; + std::string _codec; + std::string _name; + + const AVCodec* _avcodec; + + streamfx::encoder::ffmpeg::handler* _handler; + + public: + ffmpeg_factory(ffmpeg_manager* manager, const AVCodec* codec); + virtual ~ffmpeg_factory(); + + const char* get_name() override; + + void get_defaults2(obs_data_t* data) override; + + void migrate(obs_data_t* data, uint64_t version) override; + + obs_properties_t* get_properties2(instance_t* data) override; + + static bool on_manual_open(obs_properties_t* props, obs_property_t* property, void* data); + + public: + const AVCodec* get_avcodec(); + + obs_encoder_info* get_info(); + }; + + class ffmpeg_manager { + std::map> _factories; + + public: + ffmpeg_manager(); + ~ffmpeg_manager(); + + streamfx::encoder::ffmpeg::handler* find_handler(std::string_view codec); + + streamfx::encoder::ffmpeg::handler* get_handler(std::string_view codec); + + bool has_handler(std::string_view codec); + + public: // Singleton + static std::shared_ptr instance(); + }; +} // namespace streamfx::encoder::ffmpeg diff --git a/components/ffmpeg/source/encoders/ffmpeg/amf.cpp b/components/ffmpeg/source/encoders/ffmpeg/amf.cpp new file mode 100644 index 0000000..f0c39bd --- /dev/null +++ b/components/ffmpeg/source/encoders/ffmpeg/amf.cpp @@ -0,0 +1,1099 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#include "amf.hpp" +#include "common.hpp" +#include "strings.hpp" +#include "encoders/codecs/av1.hpp" +#include "encoders/codecs/h264.hpp" +#include "encoders/codecs/hevc.hpp" +#include "encoders/encoder-ffmpeg.hpp" +#include "ffmpeg/tools.hpp" +#include "plugin.hpp" + +#include "warning-disable.hpp" +extern "C" { +#include +} +#include "warning-enable.hpp" + +#define ST_I18N "Encoder.FFmpeg.AMF" +#define ST_KEY_USAGE "Usage" +#define ST_I18N_USAGE ST_I18N ".Usage" +#define ST_I18N_USAGE_(x) ST_I18N_USAGE "." D_VSTR(x) +#define ST_KEY_QUALITY "Quality" +#define ST_I18N_QUALITY ST_I18N ".Quality" +#define ST_I18N_QUALITY_(x) ST_I18N_QUALITY "." D_VSTR(x) +#define ST_I18N_RATECONTROL ST_I18N ".RateControl" +#define ST_KEY_RATECONTROL "RateControl" +#define ST_I18N_RATECONTROL_MODE ST_I18N_RATECONTROL ".Mode" +#define ST_I18N_RATECONTROL_MODE_(x) ST_I18N_RATECONTROL_MODE "." D_VSTR(x) +#define ST_KEY_RATECONTROL_MODE ST_KEY_RATECONTROL ".Mode" +#define ST_I18N_RATECONTROL_PREENCODE ST_I18N_RATECONTROL ".PreEncode" +#define ST_KEY_RATECONTROL_PREENCODE ST_KEY_RATECONTROL ".PreEncode " +#define ST_I18N_RATECONTROL_LIMITS ST_I18N_RATECONTROL ".Limits" +#define ST_KEY_RATECONTROL_LIMITS ST_KEY_RATECONTROL ".Limits" +#define ST_I18N_RATECONTROL_LIMITS_BUFFERSIZE ST_I18N_RATECONTROL_LIMITS ".BufferSize" +#define ST_KEY_RATECONTROL_LIMITS_BUFFERSIZE ST_KEY_RATECONTROL_LIMITS ".BufferSize" +#define ST_I18N_RATECONTROL_LIMITS_QUALITY ST_I18N_RATECONTROL_LIMITS ".Quality" +#define ST_KEY_RATECONTROL_LIMITS_QUALITY ST_KEY_RATECONTROL_LIMITS ".Quality" +#define ST_I18N_RATECONTROL_LIMITS_BITRATE ST_I18N_RATECONTROL_LIMITS ".Bitrate" +#define ST_KEY_RATECONTROL_LIMITS_BITRATE ST_KEY_RATECONTROL_LIMITS ".Bitrate" +#define ST_I18N_RATECONTROL_LIMITS_BITRATE_TARGET ST_I18N_RATECONTROL_LIMITS_BITRATE ".Target" +#define ST_KEY_RATECONTROL_LIMITS_BITRATE_TARGET ST_KEY_RATECONTROL_LIMITS_BITRATE ".Target" +#define ST_I18N_RATECONTROL_LIMITS_BITRATE_MAXIMUM ST_I18N_RATECONTROL_LIMITS_BITRATE ".Maximum" +#define ST_KEY_RATECONTROL_LIMITS_BITRATE_MAXIMUM ST_KEY_RATECONTROL_LIMITS_BITRATE ".Maximum" +#define ST_I18N_RATECONTROL_QP ST_I18N_RATECONTROL ".QP" +#define ST_KEY_RATECONTROL_QP ST_KEY_RATECONTROL ".QP" +#define ST_I18N_RATECONTROL_QP_I ST_I18N_RATECONTROL_QP ".I" +#define ST_KEY_RATECONTROL_QP_I ST_KEY_RATECONTROL_QP ".I" +#define ST_I18N_RATECONTROL_QP_P ST_I18N_RATECONTROL_QP ".P" +#define ST_KEY_RATECONTROL_QP_P ST_KEY_RATECONTROL_QP ".P" +#define ST_I18N_RATECONTROL_QP_B ST_I18N_RATECONTROL_QP ".B" +#define ST_KEY_RATECONTROL_QP_B ST_KEY_RATECONTROL_QP ".B" + +#define ST_I18N_PA ST_I18N ".PreAnalysis" +#define ST_KEY_PA "PreAnalysis" +#define ST_I18N_PA_LOOKAHEAD ST_I18N_PA ".LookAhead" +#define ST_KEY_PA_LOOKAHEAD ST_KEY_PA ".LookAhead" +#define ST_I18N_PA_SCENECHANGEDETECTION ST_I18N_PA ".SceneChangeDetection" +#define ST_KEY_PA_SCENECHANGEDETECTION ST_KEY_PA ".SceneChangeDetection" +#define ST_I18N_PA_STATICSCENEDETECTION ST_I18N_PA ".StaticSceneDetection" +#define ST_KEY_PA_STATICSCENEDETECTION ST_KEY_PA ".StaticSceneDetection" +#define ST_I18N_PA_ACTIVITYTYPE ST_I18N_PA ".ActivityType" +#define ST_KEY_PA_ACTIVITYTYPE ST_KEY_PA ".ActivityType" +#define ST_I18N_PA_AQ ST_I18N_PA ".AQ" +#define ST_KEY_PA_AQ ST_KEY_PA ".AQ" +#define ST_I18N_PA_AQ_PERCEPTUAL ST_I18N_PA_AQ ".Perceptual" +#define ST_KEY_PA_AQ_PERCEPTUAL ST_KEY_PA_AQ ".Perceptual" +#define ST_I18N_PA_AQ_CONTENT ST_I18N_PA_AQ ".Content" +#define ST_KEY_PA_AQ_CONTENT ST_KEY_PA_AQ ".Content" +#define ST_I18N_PA_AQ_TEMPORAL ST_I18N_PA_AQ ".Temporal" +#define ST_KEY_PA_AQ_TEMPORAL ST_KEY_PA_AQ ".Temporal" +#define ST_I18N_PA_HIGHMOTIONQUALITYBOOST ST_I18N_PA ".HighMotionQualityBoost" +#define ST_KEY_PA_HIGHMOTIONQUALITYBOOST ST_KEY_PA ".HighMotionQualityBoost" +#define ST_I18N_PA_ADAPTIVEMINIGOP ST_I18N_PA ".AdaptiveMiniGOP" +#define ST_KEY_PA_ADAPTIVEMINIGOP ST_KEY_PA ".AdaptiveMiniGOP" +#define ST_I18N_PA_FRAME_SAD ST_I18N_PA ".FrameSAD" +#define ST_KEY_PA_FRAME_SAD ST_KEY_PA ".FrameSAD" +#define ST_I18N_PA_LTR ST_I18N_PA ".LTR" +#define ST_KEY_PA_LTR ST_KEY_PA ".LTR" + +#define ST_I18N_OTHER ST_I18N ".Other" +#define ST_KEY_OTHER "Other" +#define ST_I18N_OTHER_BFRAMES ST_I18N_OTHER ".BFrames" +#define ST_KEY_OTHER_BFRAMES ST_KEY_OTHER ".BFrames" +#define ST_I18N_OTHER_BFRAMEREFERENCES ST_I18N_OTHER ".BFrameReferences" +#define ST_KEY_OTHER_BFRAMEREFERENCES ST_KEY_OTHER ".BFrameReferences" +#define ST_I18N_OTHER_VBAQ ST_I18N_OTHER ".VBAQ" +#define ST_KEY_OTHER_VBAQ ST_KEY_OTHER ".VBAQ" +#define ST_I18N_OTHER_HIGHMOTIONQUALITYBOOST ST_I18N_OTHER ".HighMotionQualityBoost" +#define ST_KEY_OTHER_HIGHMOTIONQUALITYBOOST ST_KEY_OTHER ".HighMotionQualityBoost" +#define ST_I18N_OTHER_REFERENCEFRAMES ST_I18N_OTHER ".ReferenceFrames" +#define ST_KEY_OTHER_REFERENCEFRAMES ST_KEY_OTHER ".ReferenceFrames" + +#define ST_KEY_H264_PROFILE "H264.Profile" +#define ST_KEY_H264_LEVEL "H264.Level" + +#define ST_KEY_H265_PROFILE "H265.Profile" +#define ST_KEY_H265_TIER "H265.Tier" +#define ST_KEY_H265_LEVEL "H265.Level" + +#define ST_KEY_AV1_PROFILE "AV1.Profile" +#define ST_KEY_AV1_LEVEL "AV1.Level" + +#define TEST_AVERROR(x) \ + if (int ret = x; ret != 0) { \ + DLOG_ERROR("%s: %s (%" PRId32 ")", D_VSTR(x), get_error_description(ret), ret); \ + } + +using namespace streamfx::encoder::ffmpeg; +using namespace streamfx::encoder::codec; +using namespace streamfx::ffmpeg::tools; + +bool amf::is_available() +{ +#if defined(D_PLATFORM_WINDOWS) +#if defined(D_PLATFORM_64BIT) + std::filesystem::path lib_name = "amfrt64.dll"; +#else + std::filesystem::path lib_name = "amfrt32.dll"; +#endif +#else +#if defined(D_PLATFORM_64BIT) + std::filesystem::path lib_name = "libamfrt64.so.1"; +#else + std::filesystem::path lib_name = "libamfrt32.so.1"; +#endif +#endif + try { + streamfx::util::library::load(lib_name); + return true; + } catch (...) { + return false; + } +} + +void amf::defaults(ffmpeg_factory* factory, obs_data_t* settings) +{ + obs_data_set_default_string(settings, ST_KEY_QUALITY, "balanced"); + obs_data_set_default_string(settings, ST_KEY_USAGE, "transcoding"); + + obs_data_set_default_string(settings, ST_KEY_RATECONTROL_MODE, "cbr"); + obs_data_set_default_int(settings, ST_KEY_RATECONTROL_PREENCODE, -1); + obs_data_set_default_int(settings, ST_KEY_RATECONTROL_LIMITS_BITRATE_TARGET, 6000); + obs_data_set_default_int(settings, ST_KEY_RATECONTROL_LIMITS_BITRATE_MAXIMUM, 0); + obs_data_set_default_int(settings, ST_KEY_RATECONTROL_LIMITS_BUFFERSIZE, 0); + obs_data_set_default_int(settings, ST_KEY_RATECONTROL_LIMITS_QUALITY, -1); + obs_data_set_default_int(settings, ST_KEY_RATECONTROL_QP_I, -1); + obs_data_set_default_int(settings, ST_KEY_RATECONTROL_QP_P, -1); + obs_data_set_default_int(settings, ST_KEY_RATECONTROL_QP_B, -1); + + obs_data_set_default_int(settings, ST_KEY_PA, -1); + obs_data_set_default_int(settings, ST_KEY_PA_LOOKAHEAD, -1); + obs_data_set_default_string(settings, ST_KEY_PA_SCENECHANGEDETECTION, "default"); + obs_data_set_default_string(settings, ST_KEY_PA_STATICSCENEDETECTION, "default"); + obs_data_set_default_string(settings, ST_KEY_PA_ACTIVITYTYPE, "default"); + obs_data_set_default_string(settings, ST_KEY_PA_AQ_PERCEPTUAL, "default"); + obs_data_set_default_string(settings, ST_KEY_PA_AQ_CONTENT, "default"); + obs_data_set_default_string(settings, ST_KEY_PA_AQ_TEMPORAL, "default"); + obs_data_set_default_string(settings, ST_KEY_PA_HIGHMOTIONQUALITYBOOST, "default"); + obs_data_set_default_int(settings, ST_KEY_PA_ADAPTIVEMINIGOP, -1); + obs_data_set_default_int(settings, ST_KEY_PA_FRAME_SAD, -1); + obs_data_set_default_int(settings, ST_KEY_PA_LTR, -1); + + obs_data_set_default_int(settings, ST_KEY_OTHER_BFRAMES, -1); + obs_data_set_default_int(settings, ST_KEY_OTHER_BFRAMEREFERENCES, -1); + obs_data_set_default_int(settings, ST_KEY_OTHER_VBAQ, -1); + obs_data_set_default_int(settings, ST_KEY_OTHER_HIGHMOTIONQUALITYBOOST, -1); + obs_data_set_default_int(settings, ST_KEY_OTHER_REFERENCEFRAMES, -1); + + // Dynamic Bitrate and Replay Buffer + obs_data_set_default_int(settings, "bitrate", -1); + obs_data_unset_user_value(settings, "bitrate"); +} + +static bool is_cbr(const char* value) +{ + return (strcmp("cbr", value) == 0) || (strcmp("hqcbr", value) == 0); +} + +static bool is_vbr(const char* value) +{ + return (strcmp("vbr", value) == 0) || (strcmp("hqvbr", value) == 0) || (strcmp("vbr_peak", value) == 0) || (strcmp("vbr_latency", value) == 0); +} + +static bool is_cq(const char* value) +{ + return (strcmp("qvbr", value) == 0); +} + +static bool is_quality(const char* value) +{ + return (strcmp("hqvbr", value) == 0) || (strcmp("hqcbr", value) == 0) || (strcmp("qvbr", value) == 0); +} + +static bool is_cqp(const char* value) +{ + return (strcmp("cqp", value) == 0); +} + +static void have_flags(const char* value, bool& bitrate, bool& bitrate_range, bool& quality, bool& qp_limits, bool& qp) +{ + bitrate = false; + bitrate_range = false; + quality = false; + qp_limits = false; + qp = false; + if (is_cbr(value)) { + bitrate = true; + } else if (is_vbr(value)) { + bitrate = true; + bitrate_range = true; + qp_limits = true; + qp = true; + } else if (is_cq(value)) { + bitrate = true; + bitrate_range = true; + quality = true; + qp_limits = true; + qp = true; + } else if (is_cqp(value)) { + qp = true; + } else { + bitrate = true; + bitrate_range = true; + quality = true; + qp_limits = true; + qp = true; + } +} + +static bool modified_pa(obs_properties_t* props, obs_property_t*, obs_data_t* settings) noexcept +{ + const char* rc_mode = obs_data_get_string(settings, ST_KEY_RATECONTROL_MODE); + bool is_pa_on = (is_quality(rc_mode) || (obs_data_get_int(settings, ST_KEY_PA) != 0)) && !is_cqp(rc_mode); + + // Doesn't have VBAQ, hide the entire block if needed. + obs_property_set_visible(obs_properties_get(props, ST_I18N_PA_AQ), is_pa_on); + + return true; +} + +static bool modified_ratecontrol(obs_properties_t* props, obs_property_t* prop, obs_data_t* settings) noexcept +{ + // Decode the name into useful flags. + const char* value = obs_data_get_string(settings, ST_KEY_RATECONTROL_MODE); + bool have_bitrate = false; + bool have_bitrate_range = false; + bool have_quality = false; + bool _unused = false; + bool have_qp = false; + have_flags(value, have_bitrate, have_bitrate_range, have_quality, _unused, have_qp); + + obs_property_set_visible(obs_properties_get(props, ST_I18N_RATECONTROL_LIMITS), have_bitrate || have_quality); + obs_property_set_visible(obs_properties_get(props, ST_KEY_RATECONTROL_LIMITS_BUFFERSIZE), have_bitrate); + obs_property_set_visible(obs_properties_get(props, ST_KEY_RATECONTROL_LIMITS_QUALITY), have_quality); + obs_property_set_visible(obs_properties_get(props, ST_KEY_RATECONTROL_LIMITS_BITRATE_TARGET), have_bitrate); + obs_property_set_visible(obs_properties_get(props, ST_KEY_RATECONTROL_LIMITS_BITRATE_MAXIMUM), have_bitrate_range); + + obs_property_set_visible(obs_properties_get(props, ST_I18N_RATECONTROL_QP), have_qp); + obs_property_set_visible(obs_properties_get(props, ST_KEY_RATECONTROL_QP_I), have_qp); + obs_property_set_visible(obs_properties_get(props, ST_KEY_RATECONTROL_QP_P), have_qp); + obs_property_set_visible(obs_properties_get(props, ST_KEY_RATECONTROL_QP_B), have_qp); + + // Update Pre-Analysis + obs_property_set_visible(obs_properties_get(props, ST_I18N_PA), !is_cqp(value)); + modified_pa(props, prop, settings); + + // Update Other Options + obs_property_set_visible(obs_properties_get(props, ST_KEY_OTHER_VBAQ), !is_cqp(value)); + obs_property_set_visible(obs_properties_get(props, ST_KEY_OTHER_HIGHMOTIONQUALITYBOOST), !is_cqp(value)); + + return true; +} + +static bool modified_pa_aq_perceptual(obs_properties_t* props, obs_property_t*, obs_data_t* settings) noexcept +{ + const char* paq_mode = obs_data_get_string(settings, ST_KEY_PA_AQ_PERCEPTUAL); + obs_property_set_visible(obs_properties_get(props, ST_KEY_PA_AQ_CONTENT), std::string_view{"caq"} == paq_mode); + + return true; +} + +void amf::properties_before(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_properties_t* props, AVCodecContext* context) +{ + auto codec = factory->get_avcodec(); + + { // Quality, but not the one from FFmpeg. + // We ignore the FFmpeg one here, since AMD is - as usual - inconsistent and not backwards compatible. + auto p = obs_properties_add_list(props, ST_KEY_QUALITY, D_TRANSLATE(ST_I18N_QUALITY), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + /* streamfx::ffmpeg::tools::avoption_list_add_entries(context->priv_data, "quality", [&p](const AVOption* opt) { + char buffer[1024]; + snprintf(buffer, sizeof(buffer), "%s.%s", ST_I18N_QUALITY, opt->name); + obs_property_list_add_string(p, D_TRANSLATE_DEFAULT(buffer, opt->name), opt->name); + });*/ + for (auto key : {"speed", "balanced", "quality", "high_quality"}) { + char buffer[1024]; + snprintf(buffer, sizeof(buffer), "%s.%s", ST_I18N_QUALITY, key); + obs_property_list_add_string(p, D_TRANSLATE_DEFAULT(buffer, key), key); + } + } + + { // Usage, but again not from FFmpeg. + // We ignore the FFmpeg one here, since AMD is - as usual - inconsistent and not backwards compatible. + auto p = obs_properties_add_list(props, ST_KEY_USAGE, D_TRANSLATE(ST_I18N_USAGE), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + /* streamfx::ffmpeg::tools::avoption_list_add_entries(context->priv_data, "usage", [&p](const AVOption* opt) { + char buffer[1024]; + snprintf(buffer, sizeof(buffer), "%s.%s", ST_I18N_USAGE, opt->name); + obs_property_list_add_string(p, D_TRANSLATE_DEFAULT(buffer, opt->name), opt->name); + });*/ + for (auto key : {"transcoding", "lowlatency"}) { + char buffer[1024]; + snprintf(buffer, sizeof(buffer), "%s.%s", ST_I18N_USAGE, key); + obs_property_list_add_string(p, D_TRANSLATE_DEFAULT(buffer, key), key); + } + } +} + +void amf::properties_after(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_properties_t* props, AVCodecContext* context) +{ + auto codec = factory->get_avcodec(); + + { // Rate Control + obs_properties_t* grp = obs_properties_create(); + obs_properties_add_group(props, ST_I18N_RATECONTROL, D_TRANSLATE(ST_I18N_RATECONTROL), OBS_GROUP_NORMAL, grp); + + { // Mode + auto p = obs_properties_add_list(grp, ST_KEY_RATECONTROL_MODE, D_TRANSLATE(ST_I18N_RATECONTROL_MODE), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + obs_property_set_modified_callback(p, modified_ratecontrol); + obs_property_list_add_string(p, D_TRANSLATE(S_STATE_DEFAULT), ""); + streamfx::ffmpeg::tools::avoption_list_add_entries(context->priv_data, "rc", [&p](const AVOption* opt) { + char buffer[1024]; + snprintf(buffer, sizeof(buffer), "%s.%s", ST_I18N_RATECONTROL_MODE, opt->name); + obs_property_list_add_string(p, D_TRANSLATE_DEFAULT(buffer, opt->help ? opt->help : opt->name), opt->name); + }); + } + + if (avoption_exists(context->priv_data, "preencode")) { // Pre-Encode + auto p = streamfx::util::obs_properties_add_tristate(grp, ST_KEY_RATECONTROL_PREENCODE, D_TRANSLATE(ST_I18N_RATECONTROL_PREENCODE)); + } + + { // Limits + obs_properties_t* grp2 = obs_properties_create(); + obs_properties_add_group(grp, ST_I18N_RATECONTROL_LIMITS, D_TRANSLATE(ST_I18N_RATECONTROL_LIMITS), OBS_GROUP_NORMAL, grp2); + + if (avoption_exists(context->priv_data, "qvbr_quality_level")) { // Quality + auto p = obs_properties_add_int_slider(grp2, ST_KEY_RATECONTROL_LIMITS_QUALITY, D_TRANSLATE(ST_I18N_RATECONTROL_LIMITS_QUALITY), -1, 51, 1); + } + + { // Bitrate Target + auto p = obs_properties_add_int(grp2, ST_KEY_RATECONTROL_LIMITS_BITRATE_TARGET, D_TRANSLATE(ST_I18N_RATECONTROL_LIMITS_BITRATE_TARGET), -1, std::numeric_limits::max(), 1); + obs_property_int_set_suffix(p, " kbit/s"); + } + + { // Bitrate Maximum + auto p = obs_properties_add_int(grp2, ST_KEY_RATECONTROL_LIMITS_BITRATE_MAXIMUM, D_TRANSLATE(ST_I18N_RATECONTROL_LIMITS_BITRATE_MAXIMUM), -1, std::numeric_limits::max(), 1); + obs_property_int_set_suffix(p, " kbit/s"); + } + + { // Buffer Size + auto p = obs_properties_add_int(grp2, ST_KEY_RATECONTROL_LIMITS_BUFFERSIZE, D_TRANSLATE(ST_I18N_RATECONTROL_LIMITS_BUFFERSIZE), 0, std::numeric_limits::max(), 1); + obs_property_int_set_suffix(p, " kbit"); + } + } + + { // QP + obs_properties_t* grp2 = obs_properties_create(); + obs_properties_add_group(grp, ST_I18N_RATECONTROL_QP, D_TRANSLATE(ST_I18N_RATECONTROL_QP), OBS_GROUP_NORMAL, grp2); + + if (avoption_exists(context->priv_data, "qp_i")) { + auto p = obs_properties_add_int_slider(grp2, ST_KEY_RATECONTROL_QP_I, D_TRANSLATE(ST_I18N_RATECONTROL_QP_I), -1, 51, 1); + } + if (avoption_exists(context->priv_data, "qp_p")) { + auto p = obs_properties_add_int_slider(grp2, ST_KEY_RATECONTROL_QP_P, D_TRANSLATE(ST_I18N_RATECONTROL_QP_P), -1, 51, 1); + } + if (avoption_exists(context->priv_data, "qp_b")) { + auto p = obs_properties_add_int_slider(grp2, ST_KEY_RATECONTROL_QP_B, D_TRANSLATE(ST_I18N_RATECONTROL_QP_B), -1, 51, 1); + } + } + } + + if (avoption_exists(context->priv_data, "preanalysis")) { // Pre-Analysis + obs_properties_t* grp = obs_properties_create(); + obs_properties_add_group(props, ST_I18N_PA, D_TRANSLATE(ST_I18N_PA), OBS_GROUP_NORMAL, grp); + + { // Pre-Analysis + auto p = streamfx::util::obs_properties_add_tristate(grp, ST_KEY_PA, D_TRANSLATE(ST_I18N_PA)); + obs_property_set_modified_callback(p, modified_pa); + } + + if (avoption_exists(context->priv_data, "pa_lookahead_buffer_depth")) { // Look-Ahead + auto p = obs_properties_add_int_slider(grp, ST_KEY_PA_LOOKAHEAD, D_TRANSLATE(ST_I18N_PA_LOOKAHEAD), -1, 41, 1); + obs_property_int_set_suffix(p, " frames"); + } + + if (avoption_exists(context->priv_data, "pa_scene_change_detection_enable")) { // Scene Change Detection + auto p = obs_properties_add_list(grp, ST_KEY_PA_SCENECHANGEDETECTION, D_TRANSLATE(ST_I18N_PA_SCENECHANGEDETECTION), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + obs_property_list_add_string(p, D_TRANSLATE(S_STATE_DEFAULT), "default"); + obs_property_list_add_string(p, D_TRANSLATE(S_STATE_DISABLED), "disabled"); + streamfx::ffmpeg::tools::avoption_list_add_entries(context->priv_data, "pa_scene_change_detection_sensitivity", [&p](const AVOption* opt) { + char buffer[1024]; + snprintf(buffer, sizeof(buffer), "%s.%s", ST_I18N_PA_SCENECHANGEDETECTION, opt->name); + obs_property_list_add_string(p, D_TRANSLATE_DEFAULT(buffer, opt->help ? opt->help : opt->name), opt->name); + }); + } + + if (avoption_exists(context->priv_data, "pa_static_scene_detection_enable")) { // Static Scene Detection + auto p = obs_properties_add_list(grp, ST_KEY_PA_STATICSCENEDETECTION, D_TRANSLATE(ST_I18N_PA_STATICSCENEDETECTION), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + obs_property_list_add_string(p, D_TRANSLATE(S_STATE_DEFAULT), "default"); + obs_property_list_add_string(p, D_TRANSLATE(S_STATE_DISABLED), "disabled"); + streamfx::ffmpeg::tools::avoption_list_add_entries(context->priv_data, "pa_static_scene_detection_sensitivity", [&p](const AVOption* opt) { + char buffer[1024]; + snprintf(buffer, sizeof(buffer), "%s.%s", ST_I18N_PA_STATICSCENEDETECTION, opt->name); + obs_property_list_add_string(p, D_TRANSLATE_DEFAULT(buffer, opt->help ? opt->help : opt->name), opt->name); + }); + } + + if (avoption_exists(context->priv_data, "pa_paq_mode") || avoption_exists(context->priv_data, "pa_taq_mode")) { // Adaptive Quantization + obs_properties_t* grp2 = obs_properties_create(); + obs_properties_add_group(grp, ST_I18N_PA_AQ, D_TRANSLATE(ST_I18N_PA_AQ), OBS_GROUP_NORMAL, grp2); + + if (avoption_exists(context->priv_data, "pa_paq_mode")) { // Perceptual Adaptive Quantization + auto p = obs_properties_add_list(grp2, ST_KEY_PA_AQ_PERCEPTUAL, D_TRANSLATE(ST_I18N_PA_AQ_PERCEPTUAL), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + obs_property_set_modified_callback(p, modified_pa_aq_perceptual); + obs_property_list_add_string(p, D_TRANSLATE(S_STATE_DEFAULT), "default"); + streamfx::ffmpeg::tools::avoption_list_add_entries(context->priv_data, "pa_paq_mode", [&p](const AVOption* opt) { + char buffer[1024]; + snprintf(buffer, sizeof(buffer), "%s.%s", ST_I18N_PA_AQ_PERCEPTUAL, opt->name); + obs_property_list_add_string(p, D_TRANSLATE_DEFAULT(buffer, opt->help ? opt->help : opt->name), opt->name); + }); + + if (avoption_exists(context->priv_data, "pa_caq_strength")) { // Content Adaptive Quantization Strength + auto p = obs_properties_add_list(grp2, ST_KEY_PA_AQ_CONTENT, D_TRANSLATE(ST_I18N_PA_AQ_CONTENT), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + obs_property_list_add_string(p, D_TRANSLATE(S_STATE_DEFAULT), "default"); + streamfx::ffmpeg::tools::avoption_list_add_entries(context->priv_data, "pa_caq_strength", [&p](const AVOption* opt) { + char buffer[1024]; + snprintf(buffer, sizeof(buffer), "%s.%s", ST_I18N_PA_SCENECHANGEDETECTION, opt->name); + obs_property_list_add_string(p, D_TRANSLATE_DEFAULT(buffer, opt->help ? opt->help : opt->name), opt->name); + }); + } + } + + if (avoption_exists(context->priv_data, "pa_taq_mode")) { // Temporal Adaptive Quantization + auto p = obs_properties_add_list(grp2, ST_KEY_PA_AQ_TEMPORAL, D_TRANSLATE(ST_I18N_PA_AQ_TEMPORAL), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + obs_property_list_add_string(p, D_TRANSLATE(S_STATE_DEFAULT), "default"); + streamfx::ffmpeg::tools::avoption_list_add_entries(context->priv_data, "pa_taq_mode", [&p](const AVOption* opt) { + char buffer[1024]; + snprintf(buffer, sizeof(buffer), "%s.%s", ST_I18N_PA_AQ_TEMPORAL, opt->name); + obs_property_list_add_string(p, D_TRANSLATE_DEFAULT(buffer, opt->help ? opt->help : opt->name), opt->name); + }); + } + } + + if (avoption_exists(context->priv_data, "pa_high_motion_quality_boost_mode")) { // High Motion Quality Boost + auto p = obs_properties_add_list(grp, ST_KEY_PA_HIGHMOTIONQUALITYBOOST, D_TRANSLATE(ST_I18N_PA_HIGHMOTIONQUALITYBOOST), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + obs_property_list_add_string(p, D_TRANSLATE(S_STATE_DEFAULT), "default"); + streamfx::ffmpeg::tools::avoption_list_add_entries(context->priv_data, "pa_high_motion_quality_boost_mode", [&p](const AVOption* opt) { + char buffer[1024]; + snprintf(buffer, sizeof(buffer), "%s.%s", ST_I18N_PA_HIGHMOTIONQUALITYBOOST, opt->name); + obs_property_list_add_string(p, D_TRANSLATE_DEFAULT(buffer, opt->help ? opt->help : opt->name), opt->name); + }); + } + } + + { // Other Options + obs_properties_t* grp = obs_properties_create(); + obs_properties_add_group(props, ST_I18N_OTHER, D_TRANSLATE(ST_I18N_OTHER), OBS_GROUP_NORMAL, grp); + + // B-Frames + if (std::string_view{"h264_amf"} == codec->name) { // Only H264 + { + auto p = obs_properties_add_int_slider(grp, ST_KEY_OTHER_BFRAMES, D_TRANSLATE(ST_I18N_OTHER_BFRAMES), -1, 3, 1); + } + + { + auto p = streamfx::util::obs_properties_add_tristate(grp, ST_KEY_OTHER_BFRAMEREFERENCES, D_TRANSLATE(ST_I18N_OTHER_BFRAMEREFERENCES)); + } + } + + // Variance Based Adaptive Quantization + if (avoption_exists(context->priv_data, "vbaq")) { // Not AV1 + { + auto p = streamfx::util::obs_properties_add_tristate(grp, ST_KEY_OTHER_VBAQ, D_TRANSLATE(ST_I18N_OTHER_VBAQ)); + } + } + + // High Motion Quality Boost + if (avoption_exists(context->priv_data, "high_motion_quality_boost_enable")) { + auto p = streamfx::util::obs_properties_add_tristate(grp, ST_KEY_OTHER_HIGHMOTIONQUALITYBOOST, D_TRANSLATE(ST_I18N_OTHER_HIGHMOTIONQUALITYBOOST)); + } + + // Maximum Reference Frames (16 for non-AV1, 8 for AV1) + if (std::string_view{"av1_amf"} != codec->name) { // Not AV1 + auto p = obs_properties_add_int_slider(grp, ST_KEY_OTHER_REFERENCEFRAMES, D_TRANSLATE(ST_I18N_OTHER_REFERENCEFRAMES), -1, 16, 1); + } else { + auto p = obs_properties_add_int_slider(grp, ST_KEY_OTHER_REFERENCEFRAMES, D_TRANSLATE(ST_I18N_OTHER_REFERENCEFRAMES), -1, 8, 1); + } + } +} + +void amf::migrate(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings, uint64_t version) {} + +void amf::update(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings) +{ + auto codec = factory->get_avcodec(); + auto context = instance->get_avcodeccontext(); + + auto quality = obs_data_get_string(settings, ST_KEY_QUALITY); + auto usage = obs_data_get_string(settings, ST_KEY_USAGE); + auto rc = obs_data_get_string(settings, ST_KEY_RATECONTROL_MODE); + + if (!context->internal) { // Pre-initialization only + // Usage, Quality + // Need to jump through some hoops to get something useful out of AMD's AMF. + if ((strcmp("high_quality", quality) == 0) && (strcmp("av1_amf", codec->name) != 0)) { + if (strcmp("transcoding", usage) == 0) { + TEST_AVERROR(av_opt_set(context, "usage", "high_quality", AV_OPT_SEARCH_CHILDREN)); + TEST_AVERROR(av_opt_set(context, "quality", "quality", AV_OPT_SEARCH_CHILDREN)); + } else if (strcmp("lowlatency", usage) == 0) { + TEST_AVERROR(av_opt_set(context, "usage", "lowlatency_high_quality", AV_OPT_SEARCH_CHILDREN)); + TEST_AVERROR(av_opt_set(context, "quality", "quality", AV_OPT_SEARCH_CHILDREN)); + } else { + TEST_AVERROR(av_opt_set(context, "usage", usage, AV_OPT_SEARCH_CHILDREN)); + TEST_AVERROR(av_opt_set(context, "quality", quality, AV_OPT_SEARCH_CHILDREN)); + } + } else { + TEST_AVERROR(av_opt_set(context, "usage", usage, AV_OPT_SEARCH_CHILDREN)); + TEST_AVERROR(av_opt_set(context, "quality", quality, AV_OPT_SEARCH_CHILDREN)); + } + } + + { // Rate Control + if (context->internal) { + // ToDo: Retrieve current rate control setting from FFmpeg. + } + + bool have_bitrate = false; + bool have_bitrate_range = false; + bool have_quality = false; + bool _unused = false; + bool have_qp = false; + have_flags(rc, have_bitrate, have_bitrate_range, have_quality, _unused, have_qp); + + if (!context->internal) { // Pre-initialization only, though AMD does support switching this on the go. + // Rate Control Mode + TEST_AVERROR(av_opt_set(context, "rc", rc, AV_OPT_SEARCH_CHILDREN)); + + // Filler Data setting, required for CBR only. + TEST_AVERROR(av_opt_set_int(context, "filler_data", is_cbr(rc) ? 1 : 0, AV_OPT_SEARCH_CHILDREN)); + + // Pre-Encode + if (auto v = obs_data_get_int(settings, ST_KEY_RATECONTROL_PREENCODE); v != -1) + TEST_AVERROR(av_opt_set_int(context, "preencode", v, AV_OPT_SEARCH_CHILDREN)); + } + + if (have_bitrate) { + // Target Bitrate + int64_t v = obs_data_get_int(settings, ST_KEY_RATECONTROL_LIMITS_BITRATE_TARGET); + + // Allow OBS to specify a maximum allowed bitrate, as well as a dynamically adjusted bitrate. + if (auto v2 = obs_data_get_int(settings, "bitrate"); (v2 != -1) && (v2 != v)) { + v = std::clamp(v, -1, v2); + } + + if (v > -1) { + context->bit_rate = static_cast(v * 1000); + } + } + + if (have_bitrate_range) { + // Maximum Bitrate + if (int64_t max = obs_data_get_int(settings, ST_KEY_RATECONTROL_LIMITS_BITRATE_MAXIMUM); max > -1) { + context->rc_max_rate = static_cast(max * 1000); + } else { + context->rc_max_rate = context->bit_rate; + } + + // Allow OBS to specify a maximum allowed bitrate, as well as a dynamically adjusted bitrate. + if (auto v2 = obs_data_get_int(settings, "bitrate"); (v2 != -1) && (v2 != context->rc_max_rate)) { + context->rc_max_rate = std::clamp(context->rc_max_rate, -1, v2); + } + + context->rc_min_rate = context->bit_rate; + } else { + context->rc_min_rate = context->bit_rate; + context->rc_max_rate = context->bit_rate; + } + + // Buffer Size + if (have_bitrate || have_bitrate_range) { + if (int64_t v = obs_data_get_int(settings, ST_KEY_RATECONTROL_LIMITS_BUFFERSIZE); v > 0) { + context->rc_buffer_size = static_cast(v * 1000); + } else { + context->rc_buffer_size = context->bit_rate * 2; + } + } else { + context->rc_buffer_size = 0; + } + + if (!context->internal) { // Pre-initialization only + // Quality Limits + context->qmin = -1; + context->qmax = -1; + + // Quality Target + if (have_quality && (avoption_exists(context->priv_data, "qvbr_quality_level"))) { + TEST_AVERROR(av_opt_set_int(context->priv_data, "qvbr_quality_level", obs_data_get_int(settings, ST_KEY_RATECONTROL_LIMITS_QUALITY), AV_OPT_SEARCH_CHILDREN)); + } + + // QP Settings + if (have_qp) { + if (int64_t qp = obs_data_get_int(settings, ST_KEY_RATECONTROL_QP_I); avoption_exists(context->priv_data, "qp_i") && qp > -1) + av_opt_set_int(context->priv_data, "qp_i", static_cast(qp), AV_OPT_SEARCH_CHILDREN); + if (int64_t qp = obs_data_get_int(settings, ST_KEY_RATECONTROL_QP_P); avoption_exists(context->priv_data, "qp_p") && qp > -1) + av_opt_set_int(context->priv_data, "qp_p", static_cast(qp), AV_OPT_SEARCH_CHILDREN); + if (int64_t qp = obs_data_get_int(settings, ST_KEY_RATECONTROL_QP_B); avoption_exists(context->priv_data, "qp_b") && qp > -1) + av_opt_set_int(context->priv_data, "qp_b", static_cast(qp), AV_OPT_SEARCH_CHILDREN); + } + } + + if (!context->internal) { // Pre-initialization only + // "Enforce HRD", which is required for bitrate and buffer control. + TEST_AVERROR(av_opt_set_int(context, "enforce_hrd", (have_bitrate || have_bitrate_range) ? 1 : 0, AV_OPT_SEARCH_CHILDREN)); + + // Replay Buffer and Dynamic Bitrate + if (have_bitrate) { + obs_data_set_int(settings, "bitrate", context->rc_max_rate); + } + } + } + + if (!context->internal) { // Pre-initialization only + if (avoption_exists(context->priv_data, "preanalysis")) { // Pre-Analysis + // - Required by qvbr, hqvbr and hqcbr. + auto pav = obs_data_get_int(settings, ST_KEY_PA); + if (is_quality(rc)) { + pav = 1; + } else if (is_cqp(rc)) { + pav = 0; + } + + if (pav != -1) { + TEST_AVERROR(av_opt_set_int(context, "preanalysis", pav, AV_OPT_SEARCH_CHILDREN)); + } + + if (pav != 0) { + // Look-Ahead + if (auto v = obs_data_get_int(settings, ST_KEY_PA_LOOKAHEAD); v != -1) + TEST_AVERROR(av_opt_set_int(context, "pa_lookahead_buffer_depth", v, AV_OPT_SEARCH_CHILDREN)); + + // Activity Type + if (auto v = obs_data_get_string(settings, ST_KEY_PA_ACTIVITYTYPE); true) + TEST_AVERROR(av_opt_set(context, "pa_activity_type", v, AV_OPT_SEARCH_CHILDREN)); + + // Scene Change Detection + if (auto v = obs_data_get_string(settings, ST_KEY_PA_SCENECHANGEDETECTION); std::string_view{"default"} != v) { + if (std::string_view{"disabled"} == v) { + TEST_AVERROR(av_opt_set(context, "pa_scene_change_detection_enable", "false", AV_OPT_SEARCH_CHILDREN)); + } else { + TEST_AVERROR(av_opt_set(context, "pa_scene_change_detection_enable", "true", AV_OPT_SEARCH_CHILDREN)); + TEST_AVERROR(av_opt_set(context, "pa_scene_change_detection_sensitivity", v, AV_OPT_SEARCH_CHILDREN)); + } + } + + // Static Scene Detection + if (auto v = obs_data_get_string(settings, ST_KEY_PA_STATICSCENEDETECTION); std::string_view{"default"} != v) { + if (std::string_view{"disabled"} == v) { + TEST_AVERROR(av_opt_set(context, "pa_static_scene_detection_enable", "false", AV_OPT_SEARCH_CHILDREN)); + } else { + TEST_AVERROR(av_opt_set(context, "pa_static_scene_detection_enable", "true", AV_OPT_SEARCH_CHILDREN)); + TEST_AVERROR(av_opt_set(context, "pa_static_scene_detection_sensitivity", v, AV_OPT_SEARCH_CHILDREN)); + } + } + + // Adaptive Quantization + { + // Perceptual Adaptive Quantization + if (auto v = obs_data_get_string(settings, ST_KEY_PA_AQ_PERCEPTUAL); std::string_view{"default"} != v) { + TEST_AVERROR(av_opt_set(context, "pa_paq_mode", v, AV_OPT_SEARCH_CHILDREN)); + if (std::string_view{"caq"} == v) { + // Content Adaptive Quantization Strength + if (auto v2 = obs_data_get_string(settings, ST_KEY_PA_AQ_CONTENT); std::string_view{"default"} != v2) { + TEST_AVERROR(av_opt_set(context, "pa_caq_strength", v2, AV_OPT_SEARCH_CHILDREN)); + } + } + } + + // Temporal Adaptive Quantization + if (auto v = obs_data_get_string(settings, ST_KEY_PA_AQ_TEMPORAL); std::string_view{"default"} != v) { + TEST_AVERROR(av_opt_set(context, "pa_taq_mode", v, AV_OPT_SEARCH_CHILDREN)); + } + } + + // High Motion Quality Boost + if (auto v = obs_data_get_string(settings, ST_KEY_PA_HIGHMOTIONQUALITYBOOST); v) { + TEST_AVERROR(av_opt_set(context, "pa_high_motion_quality_boost_mode", v, AV_OPT_SEARCH_CHILDREN)); + } + + // Adaptive Mini-GOP (H264 only) + if (std::string_view{"h264_amf"} == codec->name) { + if (auto v = obs_data_get_int(settings, ST_KEY_PA_ADAPTIVEMINIGOP); v != -1) { + TEST_AVERROR(av_opt_set_int(context, "pa_adaptive_mini_gop", v, AV_OPT_SEARCH_CHILDREN)); + } else { + // AMD suggests turning this on if B-Frames are > 0 + if (obs_data_get_int(settings, ST_KEY_OTHER_BFRAMES) != 0) { + TEST_AVERROR(av_opt_set_int(context, "pa_adaptive_mini_gop", 1, AV_OPT_SEARCH_CHILDREN)); + } + } + } + + // Frame SAD + if (auto v = obs_data_get_int(settings, ST_KEY_PA_FRAME_SAD); v != -1) { + TEST_AVERROR(av_opt_set_int(context, "pa_frame_sad_enable", v, AV_OPT_SEARCH_CHILDREN)); + } + + // Long-Term Reference Frame Management + if (auto v = obs_data_get_int(settings, ST_KEY_PA_LTR); v != -1) { + TEST_AVERROR(av_opt_set_int(context, "pa_ltr_enable", v, AV_OPT_SEARCH_CHILDREN)); + } + } + } + + // Other Options + { + // B-Frames (H264 only) + if (std::string_view{"h264_amf"} == codec->name) { + if (auto v = obs_data_get_int(settings, ST_KEY_OTHER_BFRAMES); v != -1) { + TEST_AVERROR(av_opt_set_int(context, "bf", v, AV_OPT_SEARCH_CHILDREN)); + TEST_AVERROR(av_opt_set_int(context, "max_b_frames", v, AV_OPT_SEARCH_CHILDREN)); + } + } + + // The following do nothing in CQP, so we skip them entirely. + if (!is_cqp(rc)) { + // Variance Based Adaptive Quantization (non-AV1 only) + if (auto v = obs_data_get_int(settings, ST_KEY_OTHER_VBAQ); avoption_exists(context->priv_data, "vbaq") && v != -1) { + TEST_AVERROR(av_opt_set_int(context, "vbaq", v, AV_OPT_SEARCH_CHILDREN)); + } + + // High Quality Motion Boost + if (auto v = obs_data_get_int(settings, ST_KEY_OTHER_HIGHMOTIONQUALITYBOOST); avoption_exists(context->priv_data, "high_quality_motion_boost_enable") && v != -1) { + TEST_AVERROR(av_opt_set_int(context, "vbaq", v, AV_OPT_SEARCH_CHILDREN)); + } + } + + if (auto v = obs_data_get_int(settings, ST_KEY_OTHER_REFERENCEFRAMES); v != -1) { + context->refs = v; + //TEST_AVERROR(av_opt_set_int(context, "refs", v, AV_OPT_SEARCH_CHILDREN)); + } + } + + // Header Mode + if (avoption_exists(context->priv_data, "header_spacing")) { + TEST_AVERROR(av_opt_set_int(context, "header_spacing", context->keyint_min, AV_OPT_SEARCH_CHILDREN)); + } else if (avoption_exists(context->priv_data, "header_insertion_mode")) { + TEST_AVERROR(av_opt_set(context, "header_insertion_mode", "gop", AV_OPT_SEARCH_CHILDREN)); + } + + // Access Unit Delimiter (H264, H265, H266) + if (avoption_exists(context->priv_data, "aud")) { + TEST_AVERROR(av_opt_set_int(context, "aud", 1, AV_OPT_SEARCH_CHILDREN)); + } + } +} + +void amf::override_update(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings) {} + +// H264/AVC Handler +//------------------- + +static streamfx::encoder::ffmpeg::amf::avc inst_avc{}; + +amf::avc::avc() : handler("h264_amf") {} + +amf::avc::~avc() {} + +bool amf::avc::has_keyframes(ffmpeg_factory* factory) +{ + return true; +} + +bool amf::avc::has_threading(ffmpeg_factory* factory) +{ + return false; +} + +bool amf::avc::is_hardware(ffmpeg_factory* factory) +{ + return true; +} + +bool amf::avc::is_reconfigurable(ffmpeg_factory* factory, bool& threads, bool& gpu, bool& keyframes) +{ + threads = false; + gpu = false; + keyframes = false; + return true; +} + +void amf::avc::adjust_info(ffmpeg_factory* factory, std::string& id, std::string& name, std::string& codec) +{ + name = "AMD AMF H264/AVC (via FFmpeg)"; + factory->get_info()->caps |= OBS_ENCODER_CAP_DYN_BITRATE; +#ifndef _DEBUG + if ((obs_get_encoder_caps("h264_texture_amf") == 0) || (!amf::is_available())) { + // If AMF is not present, or the default encoder is not present, don't list ours either. + factory->get_info()->caps |= OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + } +#endif // _DEBUG +} + +void amf::avc::defaults(ffmpeg_factory* factory, obs_data_t* settings) +{ + amf::defaults(factory, settings); +} + +void amf::avc::properties(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_properties_t* props) +{ + std::shared_ptr context{avcodec_alloc_context3(factory->get_avcodec()), [](AVCodecContext* ptr) { avcodec_free_context(&ptr); }}; + amf::properties_before(factory, instance, props, context.get()); + + { + obs_properties_t* grp = props; + if (!streamfx::util::are_property_groups_broken()) { + grp = obs_properties_create(); + obs_properties_add_group(props, S_CODEC_H264, D_TRANSLATE(S_CODEC_H264), OBS_GROUP_NORMAL, grp); + } + + { + auto p = obs_properties_add_list(grp, ST_KEY_H264_PROFILE, D_TRANSLATE(S_CODEC_H264_PROFILE), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + obs_property_list_add_string(p, D_TRANSLATE(S_STATE_DEFAULT), ""); + streamfx::ffmpeg::tools::avoption_list_add_entries(context->priv_data, "profile", [&p](const AVOption* opt) { + char buffer[1024]; + snprintf(buffer, sizeof(buffer), "%s.%s", S_CODEC_H264_PROFILE, opt->name); + obs_property_list_add_string(p, D_TRANSLATE(buffer), opt->name); + }); + } + { + auto p = obs_properties_add_list(grp, ST_KEY_H264_LEVEL, D_TRANSLATE(S_CODEC_H264_LEVEL), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + streamfx::ffmpeg::tools::avoption_list_add_entries(context->priv_data, "level", [&p](const AVOption* opt) { + if (std::string_view{"auto"} == opt->name) { + obs_property_list_add_string(p, D_TRANSLATE(S_STATE_AUTOMATIC), opt->name); + } else { + obs_property_list_add_string(p, opt->name, opt->name); + } + }); + } + } + + amf::properties_after(factory, instance, props, context.get()); +} + +void amf::avc::migrate(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings, uint64_t version) +{ + amf::migrate(factory, instance, settings, version); +} + +void amf::avc::update(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings) +{ + auto codec = instance->get_avcodec(); + auto context = instance->get_avcodeccontext(); + + TEST_AVERROR(av_opt_set(context->priv_data, "profile", obs_data_get_string(settings, ST_KEY_H264_PROFILE), AV_OPT_SEARCH_CHILDREN)); + TEST_AVERROR(av_opt_set(context->priv_data, "level", obs_data_get_string(settings, ST_KEY_H264_LEVEL), AV_OPT_SEARCH_CHILDREN)); + + amf::update(factory, instance, settings); +} + +void amf::avc::override_update(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings) +{ + amf::override_update(factory, instance, settings); +} + +void amf::avc::override_colorformat(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings, AVPixelFormat& target_format) {} + +// H265/HEVC Handler +//------------------- +static streamfx::encoder::ffmpeg::amf::hevc inst_hevc{}; + +amf::hevc::hevc() : handler("hevc_amf") {} + +amf::hevc::~hevc() {} + +bool amf::hevc::has_keyframes(ffmpeg_factory* factory) +{ + return true; +} + +bool amf::hevc::has_threading(ffmpeg_factory* factory) +{ + return false; +} + +bool amf::hevc::is_hardware(ffmpeg_factory* factory) +{ + return true; +} + +bool amf::hevc::is_reconfigurable(ffmpeg_factory* factory, bool& threads, bool& gpu, bool& keyframes) +{ + threads = false; + gpu = false; + keyframes = false; + return true; +} + +void amf::hevc::adjust_info(ffmpeg_factory* factory, std::string& id, std::string& name, std::string& codec) +{ + name = "AMD AMF H265/HEVC (via FFmpeg)"; + factory->get_info()->caps |= OBS_ENCODER_CAP_DYN_BITRATE; +#ifndef _DEBUG + if ((obs_get_encoder_caps("h265_texture_amf") == 0) || (!amf::is_available())) { + // If AMF is not present, or the default encoder is not present, don't list ours either. + factory->get_info()->caps |= OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + } +#endif // _DEBUG +} + +void amf::hevc::defaults(ffmpeg_factory* factory, obs_data_t* settings) +{ + amf::defaults(factory, settings); +} + +void amf::hevc::properties(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_properties_t* props) +{ + std::shared_ptr context{avcodec_alloc_context3(factory->get_avcodec()), [](AVCodecContext* ptr) { avcodec_free_context(&ptr); }}; + amf::properties_before(factory, instance, props, context.get()); + + { + obs_properties_t* grp = props; + if (!streamfx::util::are_property_groups_broken()) { + grp = obs_properties_create(); + obs_properties_add_group(props, S_CODEC_HEVC, D_TRANSLATE(S_CODEC_HEVC), OBS_GROUP_NORMAL, grp); + } + + { + auto p = obs_properties_add_list(grp, ST_KEY_H265_PROFILE, D_TRANSLATE(S_CODEC_HEVC_PROFILE), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + obs_property_list_add_int(p, D_TRANSLATE(S_STATE_DEFAULT), -1); + streamfx::ffmpeg::tools::avoption_list_add_entries(context->priv_data, "profile", [&p](const AVOption* opt) { + char buffer[1024]; + snprintf(buffer, sizeof(buffer), "%s.%s", S_CODEC_HEVC_PROFILE, opt->name); + obs_property_list_add_string(p, D_TRANSLATE(buffer), opt->name); + }); + } + { + auto p = obs_properties_add_list(grp, ST_KEY_H265_TIER, D_TRANSLATE(S_CODEC_HEVC_TIER), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + obs_property_list_add_string(p, D_TRANSLATE(S_STATE_DEFAULT), "default"); + streamfx::ffmpeg::tools::avoption_list_add_entries(context->priv_data, "profile_tier", [&p](const AVOption* opt) { + char buffer[1024]; + snprintf(buffer, sizeof(buffer), "%s.%s", S_CODEC_HEVC_TIER, opt->name); + obs_property_list_add_string(p, D_TRANSLATE(buffer), opt->name); + }); + } + { + auto p = obs_properties_add_list(grp, ST_KEY_H265_LEVEL, D_TRANSLATE(S_CODEC_HEVC_LEVEL), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + streamfx::ffmpeg::tools::avoption_list_add_entries(context->priv_data, "level", [&p](const AVOption* opt) { + if (std::string_view{"auto"} == opt->name) { + obs_property_list_add_string(p, D_TRANSLATE(S_STATE_AUTOMATIC), opt->name); + } else { + obs_property_list_add_string(p, opt->name, opt->name); + } + }); + } + } + + amf::properties_after(factory, instance, props, context.get()); +} + +void amf::hevc::migrate(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings, uint64_t version) +{ + amf::migrate(factory, instance, settings, version); +} + +void amf::hevc::update(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings) +{ + auto codec = instance->get_avcodec(); + auto context = instance->get_avcodeccontext(); + + TEST_AVERROR(av_opt_set(context->priv_data, "profile", obs_data_get_string(settings, ST_KEY_H265_PROFILE), AV_OPT_SEARCH_CHILDREN)); + TEST_AVERROR(av_opt_set(context->priv_data, "profile_tier", obs_data_get_string(settings, ST_KEY_H265_TIER), AV_OPT_SEARCH_CHILDREN)); + TEST_AVERROR(av_opt_set(context->priv_data, "level", obs_data_get_string(settings, ST_KEY_H265_LEVEL), AV_OPT_SEARCH_CHILDREN)); + + amf::update(factory, instance, settings); +} + +void amf::hevc::override_update(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings) +{ + amf::override_update(factory, instance, settings); +} + +void amf::hevc::override_colorformat(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings, AVPixelFormat& target_format) {} + +// AV1 Handler +//------------------- +static streamfx::encoder::ffmpeg::amf::av1 inst_av1{}; + +amf::av1::av1() : handler("av1_amf") {} + +amf::av1::~av1() {} + +bool amf::av1::has_keyframes(ffmpeg_factory* factory) +{ + return true; +} + +bool amf::av1::has_threading(ffmpeg_factory* factory) +{ + return false; +} + +bool amf::av1::is_hardware(ffmpeg_factory* factory) +{ + return true; +} + +bool amf::av1::is_reconfigurable(ffmpeg_factory* factory, bool& threads, bool& gpu, bool& keyframes) +{ + threads = false; + gpu = false; + keyframes = false; + return true; +} + +void amf::av1::adjust_info(ffmpeg_factory* factory, std::string& id, std::string& name, std::string& codec) +{ + name = "AMD AMF AV1 (via FFmpeg)"; + factory->get_info()->caps |= OBS_ENCODER_CAP_DYN_BITRATE; +#ifndef _DEBUG + if ((obs_get_encoder_caps("av1_texture_amf") == 0) || (!amf::is_available())) { + // If AMF is not present, or the default encoder is not present, don't list ours either. + factory->get_info()->caps |= OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + } +#endif // _DEBUG +} + +void amf::av1::defaults(ffmpeg_factory* factory, obs_data_t* settings) +{ + amf::defaults(factory, settings); +} + +void amf::av1::properties(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_properties_t* props) +{ + std::shared_ptr context{avcodec_alloc_context3(factory->get_avcodec()), [](AVCodecContext* ptr) { avcodec_free_context(&ptr); }}; + amf::properties_before(factory, instance, props, context.get()); + + { + obs_properties_t* grp = props; + if (!streamfx::util::are_property_groups_broken()) { + grp = obs_properties_create(); + obs_properties_add_group(props, S_CODEC_AV1, D_TRANSLATE(S_CODEC_AV1), OBS_GROUP_NORMAL, grp); + } + + { + auto p = obs_properties_add_list(grp, ST_KEY_AV1_PROFILE, D_TRANSLATE(S_CODEC_AV1_PROFILE), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + obs_property_list_add_string(p, D_TRANSLATE(S_STATE_DEFAULT), "default"); + streamfx::ffmpeg::tools::avoption_list_add_entries(context->priv_data, "profile", [&p](const AVOption* opt) { + char buffer[1024]; + snprintf(buffer, sizeof(buffer), "%s.%s", S_CODEC_AV1_PROFILE, opt->name); + obs_property_list_add_string(p, D_TRANSLATE(buffer), opt->name); + }); + } + { + auto p = obs_properties_add_list(grp, ST_KEY_AV1_LEVEL, D_TRANSLATE(S_CODEC_AV1_LEVEL), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + streamfx::ffmpeg::tools::avoption_list_add_entries(context->priv_data, "level", [&p](const AVOption* opt) { + if (std::string_view{"auto"} == opt->name) { + obs_property_list_add_string(p, D_TRANSLATE(S_STATE_AUTOMATIC), opt->name); + } else { + obs_property_list_add_string(p, opt->name, opt->name); + } + }); + } + } + + amf::properties_after(factory, instance, props, context.get()); +} + +void amf::av1::migrate(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings, uint64_t version) +{ + amf::migrate(factory, instance, settings, version); +} + +void amf::av1::update(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings) +{ + auto codec = instance->get_avcodec(); + auto context = instance->get_avcodeccontext(); + + TEST_AVERROR(av_opt_set(context->priv_data, "profile", obs_data_get_string(settings, ST_KEY_AV1_PROFILE), AV_OPT_SEARCH_CHILDREN)); + TEST_AVERROR(av_opt_set(context->priv_data, "level", obs_data_get_string(settings, ST_KEY_AV1_LEVEL), AV_OPT_SEARCH_CHILDREN)); + + amf::update(factory, instance, settings); +} + +void amf::av1::override_update(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings) +{ + amf::override_update(factory, instance, settings); +} + +void amf::av1::override_colorformat(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings, AVPixelFormat& target_format) {} diff --git a/components/ffmpeg/source/encoders/ffmpeg/amf.hpp b/components/ffmpeg/source/encoders/ffmpeg/amf.hpp new file mode 100644 index 0000000..c7a6de5 --- /dev/null +++ b/components/ffmpeg/source/encoders/ffmpeg/amf.hpp @@ -0,0 +1,221 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END +// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks + +#pragma once +#include "encoders/encoder-ffmpeg.hpp" +#include "encoders/ffmpeg/handler.hpp" + +#include "warning-disable.hpp" +#include +extern "C" { +#include +} +#include "warning-enable.hpp" + +/* Parameters by their codec specific name. + * '#' denotes a parameter specified via the context itself. + +H.264 H.265 Options Done? +usage usage transcoding -- +preset preset speed,balanced,quality Defines +profile profile Defines +level level Defines + tier main,high +rc rc cqp,cbr,vbr_peak,vbr_latency Defines +preanalysis preanalysis false,true Defines +vbaq vbaq false,true Defines +enforce_hrd enforce_hrd false,true Defines +filler_data filler_data false,true -- +frame_skipping skip_frame false,true Defines +qp_i qp_i range(-1 - 51) Defines +qp_p qp_p range(-1 - 51) Defines +qp_b range(-1 - 51) Defines +#max_b_frames Defines +bf_delta_qp range(-10 - 10) -- +bf_ref false,true Defines +bf_ref_delta_qp range(-10 - 10) -- +me_half_pel me_half_pel false,true -- +me_quarter_pel me_quarter_pel false,true -- +aud aud false,true Defines +max_au_size max_au_size range(0 - Inf) -- +#refs range(0 - 16?) Defines +#color_range AVCOL_RANGE_JPEG FFmpeg +#bit_rate Defines +#rc_max_rate Defines +#rc_buffer_size Defines +#rc_initial_buffer_occupancy -- +#flags AV_CODEC_FLAG_LOOP_FILTER -- +#gop_size FFmpeg +*/ + +// AMF H.264 +// intra_refresh_mb: 0 - Inf +// header_spacing: -1 - 1000 +// coder: auto, cavlc, cabac +// qmin, qmax (HEVC uses its own settings) + +// AMF H.265 +// header_insertion_mode: none, gop, idr +// gops_per_idr: 0 - Inf +// min_qp_i: -1 - 51 +// max_qp_i: -1 - 51 +// min_qp_p: -1 - 51 +// max_qp_p: -1 - 51 + +namespace streamfx::encoder::ffmpeg { + namespace amf { + bool is_available(); + + void defaults(ffmpeg_factory* factory, obs_data_t* settings); + void properties_before(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_properties_t* props, AVCodecContext* context); + void properties_after(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_properties_t* props, AVCodecContext* context); + void migrate(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings, uint64_t version); + void update(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings); + void override_update(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings); + + class avc : public handler { + public: + avc(); + + public: + virtual ~avc(); + + public: + virtual std::string help(ffmpeg_factory* factory) override + { + return "https://github.com/Xaymar/obs-StreamFX/wiki/Encoder-FFmpeg-AMF"; + }; + + public: + bool has_keyframes(ffmpeg_factory* factory) override; + + public: + bool has_threading(ffmpeg_factory* factory) override; + + public: + bool is_hardware(ffmpeg_factory* factory) override; + + public: + bool is_reconfigurable(ffmpeg_factory* factory, bool& threads, bool& gpu, bool& keyframes) override; + + public: + void adjust_info(ffmpeg_factory* factory, std::string& id, std::string& name, std::string& codec) override; + + public: + void defaults(ffmpeg_factory* factory, obs_data_t* settings) override; + + public: + void properties(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_properties_t* props) override; + + public: + void migrate(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings, uint64_t version) override; + + public: + void update(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings) override; + + public: + void override_update(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings) override; + + public: + void override_colorformat(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings, AVPixelFormat& target_format) override; + }; + + class hevc : public handler { + public: + hevc(); + + public: + virtual ~hevc(); + + public: + virtual std::string help(ffmpeg_factory* factory) override + { + return "https://github.com/Xaymar/obs-StreamFX/wiki/Encoder-FFmpeg-AMF"; + }; + + public: + bool has_keyframes(ffmpeg_factory* factory) override; + + public: + bool has_threading(ffmpeg_factory* factory) override; + + public: + bool is_hardware(ffmpeg_factory* factory) override; + + public: + bool is_reconfigurable(ffmpeg_factory* factory, bool& threads, bool& gpu, bool& keyframes) override; + + public: + void adjust_info(ffmpeg_factory* factory, std::string& id, std::string& name, std::string& codec) override; + + public: + void defaults(ffmpeg_factory* factory, obs_data_t* settings) override; + + public: + void properties(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_properties_t* props) override; + + public: + void migrate(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings, uint64_t version) override; + + public: + void update(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings) override; + + public: + void override_update(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings) override; + + public: + void override_colorformat(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings, AVPixelFormat& target_format) override; + }; + + class av1 : public handler { + public: + av1(); + + public: + virtual ~av1(); + + public: + virtual std::string help(ffmpeg_factory* factory) override + { + return "https://github.com/Xaymar/obs-StreamFX/wiki/Encoder-FFmpeg-AMF"; + }; + + public: + bool has_keyframes(ffmpeg_factory* factory) override; + + public: + bool has_threading(ffmpeg_factory* factory) override; + + public: + bool is_hardware(ffmpeg_factory* factory) override; + + public: + bool is_reconfigurable(ffmpeg_factory* factory, bool& threads, bool& gpu, bool& keyframes) override; + + public: + void adjust_info(ffmpeg_factory* factory, std::string& id, std::string& name, std::string& codec) override; + + public: + void defaults(ffmpeg_factory* factory, obs_data_t* settings) override; + + public: + void properties(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_properties_t* props) override; + + public: + void migrate(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings, uint64_t version) override; + + public: + void update(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings) override; + + public: + void override_update(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings) override; + + public: + void override_colorformat(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings, AVPixelFormat& target_format) override; + }; + + } // namespace amf + +} // namespace streamfx::encoder::ffmpeg diff --git a/components/ffmpeg/source/encoders/ffmpeg/cfhd.cpp b/components/ffmpeg/source/encoders/ffmpeg/cfhd.cpp new file mode 100644 index 0000000..05d7ecb --- /dev/null +++ b/components/ffmpeg/source/encoders/ffmpeg/cfhd.cpp @@ -0,0 +1,89 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END +// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2020 Daniel Molkentin + +#include "cfhd.hpp" +#include "common.hpp" +#include "encoders/encoder-ffmpeg.hpp" +#include "ffmpeg/tools.hpp" +#include "handler.hpp" +#include "plugin.hpp" + +#include "warning-disable.hpp" +#include +#include +#include +#include +extern "C" { +#include +} +#include "warning-enable.hpp" + +using namespace streamfx::encoder::ffmpeg; + +struct strings { + struct quality { + static constexpr const char* ffmpeg = "quality"; + static constexpr const char* obs = "Quality"; + static constexpr const char* i18n = "Encoder.FFmpeg.CineForm.Quality"; + }; +}; + +cfhd::cfhd() : handler("cfhd") {} + +bool cfhd::has_keyframes(ffmpeg_factory* factory) +{ + return false; +} + +void cfhd::properties(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_properties_t* props) +{ + // Try and acquire a valid context. + std::shared_ptr ctx; + if (instance) { + ctx = std::shared_ptr(instance->get_avcodeccontext(), [](AVCodecContext*) {}); + } else { // If we don't have a context, create a temporary one that is automatically freed. + ctx = std::shared_ptr(avcodec_alloc_context3(factory->get_avcodec()), [](AVCodecContext* v) { avcodec_free_context(&v); }); + if (!ctx->priv_data) { + return; + } + } + + { // Quality parameter + auto to_string = [](const char* v) { + char buffer[1024]; + snprintf(buffer, sizeof(buffer), "%s.%s", strings::quality::i18n, v); + return D_TRANSLATE(buffer); + }; + + auto p = obs_properties_add_list(props, strings::quality::obs, D_TRANSLATE(strings::quality::i18n), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + streamfx::ffmpeg::tools::avoption_list_add_entries(ctx->priv_data, strings::quality::ffmpeg, [&p, &to_string](const AVOption* opt) { + // FFmpeg returns this list in the wrong order. We want to start at the lowest, and go to the highest. + // So simply always insert at the top, and this will reverse the list. + obs_property_list_insert_string(p, 0, to_string(opt->name), opt->name); + }); + } +} + +std::string cfhd::help(ffmpeg_factory* factory) +{ + return "https://github.com/Xaymar/obs-StreamFX/wiki/Encoder-FFmpeg-GoPro-CineForm"; +} + +void cfhd::defaults(ffmpeg_factory* factory, obs_data_t* settings) +{ + obs_data_set_string(settings, strings::quality::obs, "film3+"); +} + +void cfhd::migrate(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings, uint64_t version) {} + +void cfhd::update(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings) +{ + if (const char* v = obs_data_get_string(settings, strings::quality::obs); v && (v[0] != '\0')) { + av_opt_set(instance->get_avcodeccontext()->priv_data, strings::quality::ffmpeg, v, AV_OPT_SEARCH_CHILDREN); + } +} + +static cfhd handler = cfhd(); diff --git a/components/ffmpeg/source/encoders/ffmpeg/cfhd.hpp b/components/ffmpeg/source/encoders/ffmpeg/cfhd.hpp new file mode 100644 index 0000000..067b32d --- /dev/null +++ b/components/ffmpeg/source/encoders/ffmpeg/cfhd.hpp @@ -0,0 +1,26 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "handler.hpp" + +namespace streamfx::encoder::ffmpeg { + class cfhd : public handler { + public: + cfhd(); + virtual ~cfhd(){}; + + bool has_keyframes(ffmpeg_factory* factory) override; + + std::string help(ffmpeg_factory* factory) override; + + void defaults(ffmpeg_factory* factory, obs_data_t* settings) override; + + void properties(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_properties_t* props) override; + + void migrate(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings, uint64_t version) override; + + void update(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings) override; + }; +} // namespace streamfx::encoder::ffmpeg diff --git a/components/ffmpeg/source/encoders/ffmpeg/debug.cpp b/components/ffmpeg/source/encoders/ffmpeg/debug.cpp new file mode 100644 index 0000000..d32c1c2 --- /dev/null +++ b/components/ffmpeg/source/encoders/ffmpeg/debug.cpp @@ -0,0 +1,164 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2020 Daniel Molkentin +// AUTOGENERATED COPYRIGHT HEADER END + +#include "debug.hpp" +#include "common.hpp" +#include "../encoder-ffmpeg.hpp" +#include "handler.hpp" +#include "plugin.hpp" + +#include "warning-disable.hpp" +#include +#include +#include +#include +extern "C" { +#include +} +#include "warning-enable.hpp" + +template +std::string to_string(T value) +{ + return std::string("Error: to_string not implemented for this type!"); +}; + +template<> +std::string to_string(int64_t value) +{ + std::vector buf(32); + snprintf(buf.data(), buf.size(), "%" PRId64, value); + return std::string(buf.data(), buf.data() + buf.size()); +} + +template<> +std::string to_string(uint64_t value) +{ + std::vector buf(32); + snprintf(buf.data(), buf.size(), "%" PRIu64, value); + return std::string(buf.data(), buf.data() + buf.size()); +} + +template<> +std::string to_string(double_t value) +{ + std::vector buf(32); + snprintf(buf.data(), buf.size(), "%f", value); + return std::string(buf.data(), buf.data() + buf.size()); +} + +using namespace streamfx::encoder::ffmpeg; + +debug::debug() : handler("") {} + +void debug::properties(ffmpeg_instance* instance, obs_properties_t* props) +{ + const AVCodec* codec = instance->get_avcodec(); + + if (instance->get_avcodeccontext()) + return; + + AVCodecContext* ctx = avcodec_alloc_context3(codec); + if (!ctx->priv_data) { + avcodec_free_context(&ctx); + return; + } + + DLOG_INFO("Options for '%s':", codec->name); + + std::pair opt_type_name[] = { + {AV_OPT_TYPE_FLAGS, "Flags"}, {AV_OPT_TYPE_INT, "Int"}, {AV_OPT_TYPE_INT64, "Int64"}, {AV_OPT_TYPE_DOUBLE, "Double"}, {AV_OPT_TYPE_FLOAT, "Float"}, {AV_OPT_TYPE_STRING, "String"}, {AV_OPT_TYPE_RATIONAL, "Rational"}, {AV_OPT_TYPE_BINARY, "Binary"}, {AV_OPT_TYPE_DICT, "Dictionary"}, {AV_OPT_TYPE_UINT64, "Unsigned Int64"}, {AV_OPT_TYPE_CONST, "Constant"}, {AV_OPT_TYPE_IMAGE_SIZE, "Image Size"}, {AV_OPT_TYPE_PIXEL_FMT, "Pixel Format"}, {AV_OPT_TYPE_SAMPLE_FMT, "Sample Format"}, {AV_OPT_TYPE_VIDEO_RATE, "Video Rate"}, {AV_OPT_TYPE_DURATION, "Duration"}, {AV_OPT_TYPE_COLOR, "Color"}, {AV_OPT_TYPE_CHANNEL_LAYOUT, "Layout"}, {AV_OPT_TYPE_BOOL, "Bool"}, + }; + std::map unit_types; + + const AVOption* opt = nullptr; + while ((opt = av_opt_next(ctx->priv_data, opt)) != nullptr) { + std::string type_name = ""; + for (auto kv : opt_type_name) { + if (opt->type == kv.first) { + type_name = kv.second; + break; + } + } + + if (opt->type == AV_OPT_TYPE_CONST) { + if (opt->unit == nullptr) { + DLOG_INFO(" Constant '%s' and help text '%s' with unknown settings.", opt->name, opt->help); + } else { + auto unit_type = unit_types.find(opt->unit); + if (unit_type == unit_types.end()) { + DLOG_INFO(" [%s] Flag '%s' and help text '%s' with value '%" PRId64 "'.", opt->unit, opt->name, opt->help, opt->default_val.i64); + } else { + std::string out; + switch (unit_type->second) { + case AV_OPT_TYPE_BOOL: + out = opt->default_val.i64 ? "true" : "false"; + break; + case AV_OPT_TYPE_INT: + out = to_string(opt->default_val.i64); + break; + case AV_OPT_TYPE_UINT64: + out = to_string(static_cast(opt->default_val.i64)); + break; + case AV_OPT_TYPE_FLAGS: + out = to_string(static_cast(opt->default_val.i64)); + break; + case AV_OPT_TYPE_FLOAT: + case AV_OPT_TYPE_DOUBLE: + out = to_string(opt->default_val.dbl); + break; + case AV_OPT_TYPE_STRING: + out = opt->default_val.str; + break; + default: + break; + } + + DLOG_INFO(" [%s] Constant '%s' and help text '%s' with value '%s'.", opt->unit, opt->name, opt->help, out.c_str()); + } + } + } else { + if (opt->unit != nullptr) { + unit_types.emplace(opt->name, opt->type); + } + + std::string minimum = "", maximum = "", out; + minimum = to_string(opt->min); + maximum = to_string(opt->max); + { + switch (opt->type) { + case AV_OPT_TYPE_BOOL: + out = opt->default_val.i64 ? "true" : "false"; + break; + case AV_OPT_TYPE_INT: + out = to_string(opt->default_val.i64); + break; + case AV_OPT_TYPE_UINT64: + out = to_string(static_cast(opt->default_val.i64)); + break; + case AV_OPT_TYPE_FLAGS: + out = to_string(static_cast(opt->default_val.i64)); + break; + case AV_OPT_TYPE_FLOAT: + case AV_OPT_TYPE_DOUBLE: + out = to_string(opt->default_val.dbl); + break; + case AV_OPT_TYPE_STRING: + out = opt->default_val.str ? opt->default_val.str : ""; + break; + default: + break; + } + } + + DLOG_INFO( + " Option '%s'%s%s%s with help '%s' of type '%s' with default value '%s', minimum '%s' and maximum " + "'%s'.", + opt->name, opt->unit ? " with unit (" : "", opt->unit ? opt->unit : "", opt->unit ? ")" : "", opt->help, type_name.c_str(), out.c_str(), minimum.c_str(), maximum.c_str()); + } + } +} + +static debug handler(); diff --git a/components/ffmpeg/source/encoders/ffmpeg/debug.hpp b/components/ffmpeg/source/encoders/ffmpeg/debug.hpp new file mode 100644 index 0000000..2aeba13 --- /dev/null +++ b/components/ffmpeg/source/encoders/ffmpeg/debug.hpp @@ -0,0 +1,16 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "handler.hpp" + +namespace streamfx::encoder::ffmpeg { + class debug : public handler { + public: + debug(); + virtual ~debug(){}; + + virtual void properties(ffmpeg_instance* instance, obs_properties_t* props); + }; +} // namespace streamfx::encoder::ffmpeg diff --git a/components/ffmpeg/source/encoders/ffmpeg/dnxhd.cpp b/components/ffmpeg/source/encoders/ffmpeg/dnxhd.cpp new file mode 100644 index 0000000..ca72a81 --- /dev/null +++ b/components/ffmpeg/source/encoders/ffmpeg/dnxhd.cpp @@ -0,0 +1,99 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2022 Carsten Braun +// Copyright (C) 2022 lainon +// Copyright (C) 2022-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#include "dnxhd.hpp" +#include "common.hpp" +#include "../codecs/dnxhr.hpp" +#include "ffmpeg/tools.hpp" +#include "plugin.hpp" + +#include "warning-disable.hpp" +#include +#include "warning-enable.hpp" + +using namespace streamfx::encoder::ffmpeg; +using namespace streamfx::encoder::codec::dnxhr; + +inline const char* dnx_profile_to_display_name(const char* profile) +{ + char buffer[1024]; + snprintf(buffer, sizeof(buffer), "%s.%s", S_CODEC_DNXHR_PROFILE, profile); + return D_TRANSLATE(buffer); +} + +dnxhd::dnxhd() : handler("dnxhd") {} + +dnxhd::~dnxhd() {} + +void dnxhd::adjust_info(ffmpeg_factory* factory, std::string& id, std::string& name, std::string& codec) +{ + //Most people don't know what VC3 is and only know it as DNx. + //Change name to make it easier to find. + name = "Avid DNxHR (via FFmpeg)"; +} + +bool dnxhd::has_keyframes(ffmpeg_factory* instance) +{ + return false; +} + +void dnxhd::defaults(ffmpeg_factory* factory, obs_data_t* settings) +{ + obs_data_set_default_string(settings, S_CODEC_DNXHR_PROFILE, "dnxhr_sq"); +} + +void dnxhd::properties(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_properties_t* props) +{ + // Try and acquire a valid context. + std::shared_ptr ctx; + if (instance) { + ctx = std::shared_ptr(instance->get_avcodeccontext(), [](AVCodecContext*) {}); + } else { // If we don't have a context, create a temporary one that is automatically freed. + ctx = std::shared_ptr(avcodec_alloc_context3(factory->get_avcodec()), [](AVCodecContext* v) { avcodec_free_context(&v); }); + if (!ctx->priv_data) { + return; + } + } + + auto p = obs_properties_add_list(props, S_CODEC_DNXHR_PROFILE, D_TRANSLATE(S_CODEC_DNXHR_PROFILE), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + + streamfx::ffmpeg::tools::avoption_list_add_entries(ctx->priv_data, "profile", [&p](const AVOption* opt) { + if (strcmp(opt->name, "dnxhd") == 0) { + //Do not show DNxHD profile as it is outdated and should not be used. + //It's also very picky about framerate and framesize combos, which makes it even less useful + return; + } + + //ffmpeg returns the profiles for DNxHR from highest to lowest. + //Lowest to highest is what people usually expect. + //Therefore, new entries will always be inserted at the top, effectively reversing the list + obs_property_list_insert_string(p, 0, dnx_profile_to_display_name(opt->name), opt->name); + }); +} + +void dnxhd::update(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings) +{ + const char* profile = obs_data_get_string(settings, S_CODEC_DNXHR_PROFILE); + av_opt_set(instance->get_avcodeccontext(), "profile", profile, AV_OPT_SEARCH_CHILDREN); +} + +void dnxhd::override_colorformat(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings, AVPixelFormat& target_format) +{ + static const std::array, static_cast(5)> profile_to_format_map{std::pair{"dnxhr_lb", AV_PIX_FMT_YUV422P}, std::pair{"dnxhr_sq", AV_PIX_FMT_YUV422P}, std::pair{"dnxhr_hq", AV_PIX_FMT_YUV422P}, std::pair{"dnxhr_hqx", AV_PIX_FMT_YUV422P10}, std::pair{"dnxhr_444", AV_PIX_FMT_YUV444P10}}; + + const char* selected_profile = obs_data_get_string(settings, S_CODEC_DNXHR_PROFILE); + for (const auto& kv : profile_to_format_map) { + if (strcmp(kv.first, selected_profile) == 0) { + target_format = kv.second; + return; + } + } + + // Fallback for (yet) unknown formats + target_format = AV_PIX_FMT_YUV422P; +} + +static auto inst = dnxhd(); diff --git a/components/ffmpeg/source/encoders/ffmpeg/dnxhd.hpp b/components/ffmpeg/source/encoders/ffmpeg/dnxhd.hpp new file mode 100644 index 0000000..cf22f4f --- /dev/null +++ b/components/ffmpeg/source/encoders/ffmpeg/dnxhd.hpp @@ -0,0 +1,36 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END +// Copyright (C) 2022 Carsten Braun + +#pragma once +#include "encoders/encoder-ffmpeg.hpp" +#include "encoders/ffmpeg/handler.hpp" + +#include "warning-disable.hpp" +extern "C" { +#include +} +#include "warning-enable.hpp" + +namespace streamfx::encoder::ffmpeg { + class dnxhd : public handler { + public: + dnxhd(); + virtual ~dnxhd(); + + virtual bool has_keyframes(ffmpeg_factory* factory); + + virtual void adjust_info(ffmpeg_factory* factory, std::string& id, std::string& name, std::string& codec); + + virtual std::string help(ffmpeg_factory* factory) { + return "https://github.com/Xaymar/obs-StreamFX/wiki/Encoder-FFmpeg-Avid-DNxHR"; + } + + virtual void defaults(ffmpeg_factory* factory, obs_data_t* settings); + virtual void properties(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_properties_t* props); + virtual void update(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings); + + virtual void override_colorformat(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings, AVPixelFormat& target_format); + }; +} // namespace streamfx::encoder::ffmpeg diff --git a/components/ffmpeg/source/encoders/ffmpeg/handler.cpp b/components/ffmpeg/source/encoders/ffmpeg/handler.cpp new file mode 100644 index 0000000..e2bd3a2 --- /dev/null +++ b/components/ffmpeg/source/encoders/ffmpeg/handler.cpp @@ -0,0 +1,79 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#include "handler.hpp" +#include "../encoder-ffmpeg.hpp" + +streamfx::encoder::ffmpeg::handler::handler_map_t& streamfx::encoder::ffmpeg::handler::handlers() +{ + static handler_map_t handlers; + return handlers; +} + +streamfx::encoder::ffmpeg::handler::handler(std::string codec) +{ + handlers().emplace(codec, this); +} + +bool streamfx::encoder::ffmpeg::handler::has_keyframes(ffmpeg_factory* factory) +{ +#if defined(AV_CODEC_PROP_INTRA_ONLY) // TODO: Determine if we need to check for an exact version. + if (auto* desc = avcodec_descriptor_get(factory->get_avcodec()->id); desc) { + return (desc->props & AV_CODEC_PROP_INTRA_ONLY) == 0; + } +#endif + +#ifdef AV_CODEC_CAP_INTRA_ONLY + return (factory->get_avcodec()->capabilities & AV_CODEC_CAP_INTRA_ONLY) == 0; +#else + return false; +#endif +} + +bool streamfx::encoder::ffmpeg::handler::has_threading(ffmpeg_factory* factory) +{ + return (factory->get_avcodec()->capabilities + & (AV_CODEC_CAP_FRAME_THREADS | AV_CODEC_CAP_SLICE_THREADS +#if defined(AV_CODEC_CAP_OTHER_THREADS) // TODO: Determine if we need to check for an exact version. + | AV_CODEC_CAP_OTHER_THREADS +#else + | AV_CODEC_CAP_AUTO_THREADS +#endif + )); +} + +bool streamfx::encoder::ffmpeg::handler::is_hardware(ffmpeg_factory* factory) +{ + if (factory->get_avcodec()->capabilities & AV_CODEC_CAP_HARDWARE) { + return true; + } + return false; +} + +bool streamfx::encoder::ffmpeg::handler::is_reconfigurable(ffmpeg_factory* factory, bool& threads, bool& gpu, bool& keyframes) +{ + if (factory->get_avcodec()->capabilities & AV_CODEC_CAP_PARAM_CHANGE) { + return true; + } + return false; +} + +void streamfx::encoder::ffmpeg::handler::adjust_info(ffmpeg_factory* factory, std::string& id, std::string& name, std::string& codec) {} + +std::string streamfx::encoder::ffmpeg::handler::help(ffmpeg_factory* factory) +{ + return "about:blank"; +} + +void streamfx::encoder::ffmpeg::handler::defaults(ffmpeg_factory* factory, obs_data_t* settings) {} + +void streamfx::encoder::ffmpeg::handler::properties(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_properties_t* props) {} + +void streamfx::encoder::ffmpeg::handler::migrate(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings, uint64_t version) {} + +void streamfx::encoder::ffmpeg::handler::update(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings) {} + +void streamfx::encoder::ffmpeg::handler::override_update(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings) {} + +void streamfx::encoder::ffmpeg::handler::override_colorformat(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings, AVPixelFormat& target_format) {} diff --git a/components/ffmpeg/source/encoders/ffmpeg/handler.hpp b/components/ffmpeg/source/encoders/ffmpeg/handler.hpp new file mode 100644 index 0000000..011bc2c --- /dev/null +++ b/components/ffmpeg/source/encoders/ffmpeg/handler.hpp @@ -0,0 +1,46 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "warning-disable.hpp" +#include +#include +#include +extern "C" { +#include +#include +} +#include "warning-enable.hpp" + +namespace streamfx::encoder::ffmpeg { + class ffmpeg_factory; + class ffmpeg_instance; + + struct handler { + handler(std::string codec); + virtual ~handler(){}; + + virtual bool has_keyframes(ffmpeg_factory* factory); + virtual bool has_threading(ffmpeg_factory* factory); + virtual bool is_hardware(ffmpeg_factory* factory); + virtual bool is_reconfigurable(ffmpeg_factory* factory, bool& threads, bool& gpu, bool& keyframes); + + virtual void adjust_info(ffmpeg_factory* factory, std::string& id, std::string& name, std::string& codec); + + virtual std::string help(ffmpeg_factory* factory); + + virtual void defaults(ffmpeg_factory* factory, obs_data_t* settings); + virtual void properties(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_properties_t* props); + virtual void migrate(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings, uint64_t version); + virtual void update(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings); + virtual void override_update(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings); + + virtual void override_colorformat(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings, AVPixelFormat& target_format); + + public: + typedef std::map handler_map_t; + + static handler_map_t& handlers(); + }; +} // namespace streamfx::encoder::ffmpeg diff --git a/components/ffmpeg/source/encoders/ffmpeg/nvenc.cpp b/components/ffmpeg/source/encoders/ffmpeg/nvenc.cpp new file mode 100644 index 0000000..f3792e8 --- /dev/null +++ b/components/ffmpeg/source/encoders/ffmpeg/nvenc.cpp @@ -0,0 +1,1214 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2022 lainon +// AUTOGENERATED COPYRIGHT HEADER END + +#include "nvenc.hpp" +#include "common.hpp" +#include "strings.hpp" +#include "encoders/codecs/av1.hpp" +#include "encoders/codecs/h264.hpp" +#include "encoders/codecs/hevc.hpp" +#include "encoders/encoder-ffmpeg.hpp" +#include "ffmpeg/tools.hpp" +#include "plugin.hpp" + +#include "warning-disable.hpp" +extern "C" { +#include +} +#include "warning-enable.hpp" + +#define ST_I18N_PRESET "Encoder.FFmpeg.NVENC.Preset" +#define ST_I18N_PRESET_(x) ST_I18N_PRESET "." D_VSTR(x) +#define ST_KEY_PRESET "Preset" +#define ST_I18N_TUNE "Encoder.FFmpeg.NVENC.Tune" +#define ST_I18N_TUNE_(x) ST_I18N_TUNE "." D_VSTR(x) +#define ST_KEY_TUNE "Tune" +#define ST_I18N_RATECONTROL "Encoder.FFmpeg.NVENC.RateControl" +#define ST_I18N_RATECONTROL_MODE ST_I18N_RATECONTROL ".Mode" +#define ST_I18N_RATECONTROL_MODE_(x) ST_I18N_RATECONTROL_MODE "." D_VSTR(x) +#define ST_KEY_RATECONTROL_MODE "RateControl.Mode" +#define ST_I18N_RATECONTROL_TWOPASS ST_I18N_RATECONTROL ".TwoPass" +#define ST_KEY_RATECONTROL_TWOPASS "RateControl.TwoPass" +#define ST_I18N_RATECONTROL_MULTIPASS ST_I18N_RATECONTROL ".MultiPass" +#define ST_KEY_RATECONTROL_MULTIPASS "RateControl.MultiPass" +#define ST_I18N_RATECONTROL_LOOKAHEAD ST_I18N_RATECONTROL ".LookAhead" +#define ST_KEY_RATECONTROL_LOOKAHEAD "RateControl.LookAhead" +#define ST_I18N_RATECONTROL_ADAPTIVEI ST_I18N_RATECONTROL ".AdaptiveI" +#define ST_KEY_RATECONTROL_ADAPTIVEI "RateControl.AdaptiveI" +#define ST_I18N_RATECONTROL_ADAPTIVEB ST_I18N_RATECONTROL ".AdaptiveB" +#define ST_KEY_RATECONTROL_ADAPTIVEB "RateControl.AdaptiveB" +#define ST_I18N_RATECONTROL_LIMITS ST_I18N_RATECONTROL ".Limits" +#define ST_I18N_RATECONTROL_LIMITS_BUFFERSIZE ST_I18N_RATECONTROL_LIMITS ".BufferSize" +#define ST_KEY_RATECONTROL_LIMITS_BUFFERSIZE "RateControl.Limits.BufferSize" +#define ST_I18N_RATECONTROL_LIMITS_QUALITY ST_I18N_RATECONTROL_LIMITS ".Quality" +#define ST_KEY_RATECONTROL_LIMITS_QUALITY "RateControl.Limits.Quality" +#define ST_I18N_RATECONTROL_LIMITS_BITRATE ST_I18N_RATECONTROL_LIMITS ".Bitrate" +#define ST_I18N_RATECONTROL_LIMITS_BITRATE_TARGET ST_I18N_RATECONTROL_LIMITS_BITRATE ".Target" +#define ST_KEY_RATECONTROL_LIMITS_BITRATE_TARGET "RateControl.Limits.Bitrate.Target" +#define ST_I18N_RATECONTROL_LIMITS_BITRATE_MAXIMUM ST_I18N_RATECONTROL_LIMITS_BITRATE ".Maximum" +#define ST_KEY_RATECONTROL_LIMITS_BITRATE_MAXIMUM "RateControl.Limits.Bitrate.Maximum" +#define ST_I18N_RATECONTROL_QP ST_I18N_RATECONTROL ".QP" +#define ST_I18N_RATECONTROL_QP_I ST_I18N_RATECONTROL_QP ".I" +#define ST_KEY_RATECONTROL_QP_I "RateControl.QP.I" +#define ST_I18N_RATECONTROL_QP_P ST_I18N_RATECONTROL_QP ".P" +#define ST_KEY_RATECONTROL_QP_P "RateControl.QP.P" +#define ST_I18N_RATECONTROL_QP_B ST_I18N_RATECONTROL_QP ".B" +#define ST_KEY_RATECONTROL_QP_B "RateControl.QP.B" +#define ST_I18N_AQ "Encoder.FFmpeg.NVENC.AQ" +#define ST_I18N_AQ_SPATIAL ST_I18N_AQ ".Spatial" +#define ST_KEY_AQ_SPATIAL "AQ.Spatial" +#define ST_I18N_AQ_TEMPORAL ST_I18N_AQ ".Temporal" +#define ST_KEY_AQ_TEMPORAL "AQ.Temporal" +#define ST_I18N_AQ_STRENGTH ST_I18N_AQ ".Strength" +#define ST_KEY_AQ_STRENGTH "AQ.Strength" +#define ST_I18N_OTHER "Encoder.FFmpeg.NVENC.Other" +#define ST_I18N_OTHER_BFRAMES ST_I18N_OTHER ".BFrames" +#define ST_KEY_OTHER_BFRAMES "Other.BFrames" +#define ST_I18N_OTHER_BFRAMEREFERENCEMODE ST_I18N_OTHER ".BFrameReferenceMode" +#define ST_KEY_OTHER_BFRAMEREFERENCEMODE "Other.BFrameReferenceMode" +#define ST_I18N_OTHER_ZEROLATENCY ST_I18N_OTHER ".ZeroLatency" +#define ST_KEY_OTHER_ZEROLATENCY "Other.ZeroLatency" +#define ST_I18N_OTHER_WEIGHTEDPREDICTION ST_I18N_OTHER ".WeightedPrediction" +#define ST_KEY_OTHER_WEIGHTEDPREDICTION "Other.WeightedPrediction" +#define ST_I18N_OTHER_NONREFERENCEPFRAMES ST_I18N_OTHER ".NonReferencePFrames" +#define ST_KEY_OTHER_NONREFERENCEPFRAMES "Other.NonReferencePFrames" +#define ST_I18N_OTHER_REFERENCEFRAMES ST_I18N_OTHER ".ReferenceFrames" +#define ST_KEY_OTHER_REFERENCEFRAMES "Other.ReferenceFrames" +#define ST_I18N_OTHER_LOWDELAYKEYFRAMESCALE ST_I18N_OTHER ".LowDelayKeyFrameScale" +#define ST_KEY_OTHER_LOWDELAYKEYFRAMESCALE "Other.LowDelayKeyFrameScale" + +#define ST_KEY_H264_PROFILE "H264.Profile" +#define ST_KEY_H264_LEVEL "H264.Level" + +#define ST_KEY_H265_PROFILE "H265.Profile" +#define ST_KEY_H265_TIER "H265.Tier" +#define ST_KEY_H265_LEVEL "H265.Level" + +#define ST_KEY_AV1_PROFILE "AV1.Profile" +#define ST_KEY_AV1_LEVEL "AV1.Level" + +using namespace streamfx::encoder::ffmpeg; +using namespace streamfx::encoder::codec; + +inline bool is_cqp(std::string_view rc) +{ + return std::string_view("constqp") == rc; +} + +inline bool is_cbr(std::string_view rc) +{ + return std::string_view("cbr") == rc; +} + +inline bool is_vbr(std::string_view rc) +{ + return std::string_view("vbr") == rc; +} + +bool nvenc::is_available() +{ +#if defined(D_PLATFORM_WINDOWS) +#if defined(D_PLATFORM_64BIT) + std::filesystem::path lib_name = "nvEncodeAPI64.dll"; +#else + std::filesystem::path lib_name = "nvEncodeAPI.dll"; +#endif +#else + std::filesystem::path lib_name = "libnvidia-encode.so.1"; +#endif + try { + streamfx::util::library::load(lib_name); + return true; + } catch (...) { + return false; + } +} + +void nvenc::defaults(ffmpeg_factory* factory, obs_data_t* settings) +{ + obs_data_set_default_string(settings, ST_KEY_PRESET, "default"); + obs_data_set_default_string(settings, ST_I18N_TUNE, "hq"); + + obs_data_set_default_string(settings, ST_KEY_RATECONTROL_MODE, "cbr"); + obs_data_set_default_int(settings, ST_KEY_RATECONTROL_TWOPASS, -1); + obs_data_set_default_int(settings, ST_KEY_RATECONTROL_MULTIPASS, -1); + obs_data_set_default_int(settings, ST_KEY_RATECONTROL_LOOKAHEAD, -1); + obs_data_set_default_int(settings, ST_KEY_RATECONTROL_ADAPTIVEI, -1); + obs_data_set_default_int(settings, ST_KEY_RATECONTROL_ADAPTIVEB, -1); + + obs_data_set_default_int(settings, ST_KEY_RATECONTROL_LIMITS_BITRATE_TARGET, 6000); + obs_data_set_default_int(settings, ST_KEY_RATECONTROL_LIMITS_BITRATE_MAXIMUM, 0); + obs_data_set_default_int(settings, ST_KEY_RATECONTROL_LIMITS_BUFFERSIZE, 0); + obs_data_set_default_double(settings, ST_KEY_RATECONTROL_LIMITS_QUALITY, 0); + + obs_data_set_default_int(settings, ST_KEY_RATECONTROL_QP_I, -1); + obs_data_set_default_int(settings, ST_KEY_RATECONTROL_QP_P, -1); + obs_data_set_default_int(settings, ST_KEY_RATECONTROL_QP_B, -1); + + obs_data_set_default_int(settings, ST_KEY_AQ_SPATIAL, -1); + obs_data_set_default_int(settings, ST_KEY_AQ_STRENGTH, -1); + obs_data_set_default_int(settings, ST_KEY_AQ_TEMPORAL, -1); + + obs_data_set_default_int(settings, ST_KEY_OTHER_BFRAMES, -1); + obs_data_set_default_int(settings, ST_KEY_OTHER_BFRAMEREFERENCEMODE, -1); + obs_data_set_default_int(settings, ST_KEY_OTHER_ZEROLATENCY, -1); + obs_data_set_default_int(settings, ST_KEY_OTHER_WEIGHTEDPREDICTION, -1); + obs_data_set_default_int(settings, ST_KEY_OTHER_NONREFERENCEPFRAMES, -1); + obs_data_set_default_int(settings, ST_KEY_OTHER_REFERENCEFRAMES, -1); + obs_data_set_default_int(settings, ST_KEY_OTHER_LOWDELAYKEYFRAMESCALE, -1); + + // Replay Buffer + obs_data_set_default_int(settings, "bitrate", 0); +} + +static bool modified_ratecontrol(obs_properties_t* props, obs_property_t*, obs_data_t* settings) noexcept +{ + // Decode the name into useful flags. + const char* value = obs_data_get_string(settings, ST_KEY_RATECONTROL_MODE); + bool have_bitrate = false; + bool have_bitrate_range = false; + bool have_quality = false; + bool have_qp = false; + if (value == std::string_view("cbr")) { + have_bitrate = true; + } else if (value == std::string_view("vbr")) { + have_bitrate = true; + have_bitrate_range = true; + have_quality = true; + have_qp = true; + } else if (value == std::string_view("constqp")) { + have_qp = true; + } else { + have_bitrate = true; + have_bitrate_range = true; + have_quality = true; + have_qp = true; + } + + obs_property_set_visible(obs_properties_get(props, ST_I18N_RATECONTROL_LIMITS), have_bitrate || have_quality); + obs_property_set_visible(obs_properties_get(props, ST_KEY_RATECONTROL_LIMITS_BUFFERSIZE), have_bitrate); + obs_property_set_visible(obs_properties_get(props, ST_KEY_RATECONTROL_LIMITS_QUALITY), have_quality); + obs_property_set_visible(obs_properties_get(props, ST_KEY_RATECONTROL_LIMITS_BITRATE_TARGET), have_bitrate); + obs_property_set_visible(obs_properties_get(props, ST_KEY_RATECONTROL_LIMITS_BITRATE_MAXIMUM), have_bitrate_range); + + obs_property_set_visible(obs_properties_get(props, ST_I18N_RATECONTROL_QP), have_qp); + obs_property_set_visible(obs_properties_get(props, ST_KEY_RATECONTROL_QP_I), have_qp); + obs_property_set_visible(obs_properties_get(props, ST_KEY_RATECONTROL_QP_P), have_qp); + obs_property_set_visible(obs_properties_get(props, ST_KEY_RATECONTROL_QP_B), have_qp); + + return true; +} + +static bool modified_aq(obs_properties_t* props, obs_property_t*, obs_data_t* settings) noexcept +{ + bool spatial_aq = streamfx::util::is_tristate_enabled(obs_data_get_int(settings, ST_KEY_AQ_SPATIAL)); + obs_property_set_visible(obs_properties_get(props, ST_KEY_AQ_STRENGTH), spatial_aq); + return true; +} + +void nvenc::properties_before(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_properties_t* props, AVCodecContext* context) +{ + auto codec = factory->get_avcodec(); + + { + auto p = obs_properties_add_list(props, ST_KEY_PRESET, D_TRANSLATE(ST_I18N_PRESET), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + streamfx::ffmpeg::tools::avoption_list_add_entries(context->priv_data, "preset", [&p](const AVOption* opt) { + char buffer[1024]; + snprintf(buffer, sizeof(buffer), "%s.%s", ST_I18N_PRESET, opt->name); + obs_property_list_add_string(p, D_TRANSLATE(buffer), opt->name); + }); + } + + if (streamfx::ffmpeg::tools::avoption_exists(context->priv_data, "tune")) { + auto p = obs_properties_add_list(props, ST_KEY_TUNE, D_TRANSLATE(ST_I18N_TUNE), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + streamfx::ffmpeg::tools::avoption_list_add_entries(context->priv_data, "tune", [&p](const AVOption* opt) { + char buffer[1024]; + snprintf(buffer, sizeof(buffer), "%s.%s", ST_I18N_TUNE, opt->name); + obs_property_list_add_string(p, D_TRANSLATE(buffer), opt->name); + }); + } +} + +void nvenc::properies_after(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_properties_t* props, AVCodecContext* context) +{ + auto codec = factory->get_avcodec(); + + { // Rate Control + obs_properties_t* grp = props; + if (!streamfx::util::are_property_groups_broken()) { + grp = obs_properties_create(); + obs_properties_add_group(props, ST_I18N_RATECONTROL, D_TRANSLATE(ST_I18N_RATECONTROL), OBS_GROUP_NORMAL, grp); + } + + { + auto p = obs_properties_add_list(grp, ST_KEY_RATECONTROL_MODE, D_TRANSLATE(ST_I18N_RATECONTROL_MODE), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + obs_property_set_modified_callback(p, modified_ratecontrol); + obs_property_list_add_string(p, D_TRANSLATE(S_STATE_DEFAULT), ""); + streamfx::ffmpeg::tools::avoption_list_add_entries(context->priv_data, "rc", [&p](const AVOption* opt) { + // Ignore options that are "deprecated" but not flagged as such. + if (opt->default_val.i64 & (1 << 23)) + return; + + char buffer[1024]; + snprintf(buffer, sizeof(buffer), "%s.%s", ST_I18N_RATECONTROL_MODE, opt->name); + obs_property_list_add_string(p, D_TRANSLATE(buffer), opt->name); + }); + } + + if (streamfx::ffmpeg::tools::avoption_exists(context->priv_data, "multipass")) { + auto p = obs_properties_add_list(grp, ST_KEY_RATECONTROL_MULTIPASS, D_TRANSLATE(ST_I18N_RATECONTROL_MULTIPASS), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + obs_property_list_add_string(p, D_TRANSLATE(S_STATE_DEFAULT), ""); + streamfx::ffmpeg::tools::avoption_list_add_entries(context->priv_data, "multipass", [&p](const AVOption* opt) { + char buffer[1024]; + snprintf(buffer, sizeof(buffer), "%s.%s", ST_I18N_RATECONTROL_MULTIPASS, opt->name); + obs_property_list_add_string(p, D_TRANSLATE(buffer), opt->name); + }); + } else { + auto p = streamfx::util::obs_properties_add_tristate(grp, ST_KEY_RATECONTROL_TWOPASS, D_TRANSLATE(ST_I18N_RATECONTROL_TWOPASS)); + } + + { + auto p = obs_properties_add_int_slider(grp, ST_KEY_RATECONTROL_LOOKAHEAD, D_TRANSLATE(ST_I18N_RATECONTROL_LOOKAHEAD), -1, 32, 1); + obs_property_int_set_suffix(p, " frames"); + //obs_property_set_modified_callback(p, modified_lookahead); + } + + { + auto p = streamfx::util::obs_properties_add_tristate(grp, ST_KEY_RATECONTROL_ADAPTIVEI, D_TRANSLATE(ST_I18N_RATECONTROL_ADAPTIVEI)); + } + + // Unavailable with hevc_nvenc + if (std::string_view{"hevc_nvenc"} != codec->name) { + auto p = streamfx::util::obs_properties_add_tristate(grp, ST_KEY_RATECONTROL_ADAPTIVEB, D_TRANSLATE(ST_I18N_RATECONTROL_ADAPTIVEB)); + } + } + + { + obs_properties_t* grp = props; + if (!streamfx::util::are_property_groups_broken()) { + grp = obs_properties_create(); + obs_properties_add_group(props, ST_I18N_RATECONTROL_LIMITS, D_TRANSLATE(ST_I18N_RATECONTROL_LIMITS), OBS_GROUP_NORMAL, grp); + } + + { + auto p = obs_properties_add_float_slider(grp, ST_KEY_RATECONTROL_LIMITS_QUALITY, D_TRANSLATE(ST_I18N_RATECONTROL_LIMITS_QUALITY), 0, 51, 0.01); + } + + { + auto p = obs_properties_add_int(grp, ST_KEY_RATECONTROL_LIMITS_BITRATE_TARGET, D_TRANSLATE(ST_I18N_RATECONTROL_LIMITS_BITRATE_TARGET), -1, std::numeric_limits::max(), 1); + obs_property_int_set_suffix(p, " kbit/s"); + } + + { + auto p = obs_properties_add_int(grp, ST_KEY_RATECONTROL_LIMITS_BITRATE_MAXIMUM, D_TRANSLATE(ST_I18N_RATECONTROL_LIMITS_BITRATE_MAXIMUM), -1, std::numeric_limits::max(), 1); + obs_property_int_set_suffix(p, " kbit/s"); + } + + { + auto p = obs_properties_add_int(grp, ST_KEY_RATECONTROL_LIMITS_BUFFERSIZE, D_TRANSLATE(ST_I18N_RATECONTROL_LIMITS_BUFFERSIZE), 0, std::numeric_limits::max(), 1); + obs_property_int_set_suffix(p, " kbit"); + } + } + + { + obs_properties_t* grp = props; + if (!streamfx::util::are_property_groups_broken()) { + grp = obs_properties_create(); + obs_properties_add_group(props, ST_I18N_RATECONTROL_QP, D_TRANSLATE(ST_I18N_RATECONTROL_QP), OBS_GROUP_NORMAL, grp); + } + + { + auto p = obs_properties_add_int_slider(grp, ST_KEY_RATECONTROL_QP_I, D_TRANSLATE(ST_I18N_RATECONTROL_QP_I), -1, 51, 1); + } + { + auto p = obs_properties_add_int_slider(grp, ST_KEY_RATECONTROL_QP_P, D_TRANSLATE(ST_I18N_RATECONTROL_QP_P), -1, 51, 1); + } + { + auto p = obs_properties_add_int_slider(grp, ST_KEY_RATECONTROL_QP_B, D_TRANSLATE(ST_I18N_RATECONTROL_QP_B), -1, 51, 1); + } + } + + { + obs_properties_t* grp = props; + if (!streamfx::util::are_property_groups_broken()) { + grp = obs_properties_create(); + obs_properties_add_group(props, ST_I18N_AQ, D_TRANSLATE(ST_I18N_AQ), OBS_GROUP_NORMAL, grp); + } + + { + auto p = streamfx::util::obs_properties_add_tristate(grp, ST_KEY_AQ_SPATIAL, D_TRANSLATE(ST_I18N_AQ_SPATIAL)); + obs_property_set_modified_callback(p, modified_aq); + } + { + auto p = obs_properties_add_int_slider(grp, ST_KEY_AQ_STRENGTH, D_TRANSLATE(ST_I18N_AQ_STRENGTH), -1, 15, 1); + } + { + auto p = streamfx::util::obs_properties_add_tristate(grp, ST_KEY_AQ_TEMPORAL, D_TRANSLATE(ST_I18N_AQ_TEMPORAL)); + } + } + + { + obs_properties_t* grp = props; + if (!streamfx::util::are_property_groups_broken()) { + grp = obs_properties_create(); + obs_properties_add_group(props, ST_I18N_OTHER, D_TRANSLATE(ST_I18N_OTHER), OBS_GROUP_NORMAL, grp); + } + + { + auto p = obs_properties_add_int_slider(grp, ST_KEY_OTHER_BFRAMES, D_TRANSLATE(ST_I18N_OTHER_BFRAMES), -1, 4, 1); + obs_property_int_set_suffix(p, " frames"); + } + + { + auto p = obs_properties_add_list(grp, ST_KEY_OTHER_BFRAMEREFERENCEMODE, D_TRANSLATE(ST_I18N_OTHER_BFRAMEREFERENCEMODE), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + obs_property_list_add_string(p, D_TRANSLATE(S_STATE_DEFAULT), ""); + streamfx::ffmpeg::tools::avoption_list_add_entries(context->priv_data, "b_ref_mode", [&p](const AVOption* opt) { + char buffer[1024]; + snprintf(buffer, sizeof(buffer), "%s.%s", ST_I18N_OTHER_BFRAMEREFERENCEMODE, opt->name); + obs_property_list_add_string(p, D_TRANSLATE(buffer), opt->name); + }); + } + + { + auto p = streamfx::util::obs_properties_add_tristate(grp, ST_KEY_OTHER_ZEROLATENCY, D_TRANSLATE(ST_I18N_OTHER_ZEROLATENCY)); + } + + { + auto p = streamfx::util::obs_properties_add_tristate(grp, ST_KEY_OTHER_WEIGHTEDPREDICTION, D_TRANSLATE(ST_I18N_OTHER_WEIGHTEDPREDICTION)); + } + + { + auto p = streamfx::util::obs_properties_add_tristate(grp, ST_KEY_OTHER_NONREFERENCEPFRAMES, D_TRANSLATE(ST_I18N_OTHER_NONREFERENCEPFRAMES)); + } + + { + auto p = obs_properties_add_int_slider(grp, ST_KEY_OTHER_REFERENCEFRAMES, D_TRANSLATE(ST_I18N_OTHER_REFERENCEFRAMES), -1, (strcmp(codec->name, "h264_nvenc") == 0) ? 16 : 4, 1); + obs_property_int_set_suffix(p, " frames"); + } + + if (streamfx::ffmpeg::tools::avoption_exists(context->priv_data, "ldkfs")) { + auto p = obs_properties_add_int_slider(grp, ST_KEY_OTHER_LOWDELAYKEYFRAMESCALE, D_TRANSLATE(ST_I18N_OTHER_LOWDELAYKEYFRAMESCALE), -1, 255, 1); + } + } +} + +void nvenc::properties_runtime(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_properties_t* props) +{ + obs_property_set_enabled(obs_properties_get(props, ST_KEY_PRESET), false); + obs_property_set_enabled(obs_properties_get(props, ST_KEY_TUNE), false); + obs_property_set_enabled(obs_properties_get(props, ST_I18N_RATECONTROL), false); + obs_property_set_enabled(obs_properties_get(props, ST_KEY_RATECONTROL_MODE), false); + obs_property_set_enabled(obs_properties_get(props, ST_KEY_RATECONTROL_TWOPASS), false); + obs_property_set_enabled(obs_properties_get(props, ST_KEY_RATECONTROL_MULTIPASS), false); + obs_property_set_enabled(obs_properties_get(props, ST_KEY_RATECONTROL_LOOKAHEAD), false); + obs_property_set_enabled(obs_properties_get(props, ST_KEY_RATECONTROL_ADAPTIVEI), false); + obs_property_set_enabled(obs_properties_get(props, ST_KEY_RATECONTROL_ADAPTIVEB), false); + obs_property_set_enabled(obs_properties_get(props, ST_I18N_RATECONTROL_LIMITS), true); + obs_property_set_enabled(obs_properties_get(props, ST_KEY_RATECONTROL_LIMITS_BUFFERSIZE), true); + obs_property_set_enabled(obs_properties_get(props, ST_KEY_RATECONTROL_LIMITS_BITRATE_TARGET), true); + obs_property_set_enabled(obs_properties_get(props, ST_KEY_RATECONTROL_LIMITS_BITRATE_MAXIMUM), true); + obs_property_set_enabled(obs_properties_get(props, ST_I18N_RATECONTROL_LIMITS_QUALITY), false); + obs_property_set_enabled(obs_properties_get(props, ST_I18N_RATECONTROL_QP), false); + obs_property_set_enabled(obs_properties_get(props, ST_KEY_RATECONTROL_QP_I), false); + obs_property_set_enabled(obs_properties_get(props, ST_KEY_RATECONTROL_QP_P), false); + obs_property_set_enabled(obs_properties_get(props, ST_KEY_RATECONTROL_QP_B), false); + obs_property_set_enabled(obs_properties_get(props, ST_I18N_AQ), false); + obs_property_set_enabled(obs_properties_get(props, ST_KEY_AQ_SPATIAL), false); + obs_property_set_enabled(obs_properties_get(props, ST_KEY_AQ_STRENGTH), false); + obs_property_set_enabled(obs_properties_get(props, ST_KEY_AQ_TEMPORAL), false); + obs_property_set_enabled(obs_properties_get(props, ST_I18N_OTHER), false); + obs_property_set_enabled(obs_properties_get(props, ST_KEY_OTHER_BFRAMES), false); + obs_property_set_enabled(obs_properties_get(props, ST_KEY_OTHER_BFRAMEREFERENCEMODE), false); + obs_property_set_enabled(obs_properties_get(props, ST_KEY_OTHER_ZEROLATENCY), false); + obs_property_set_enabled(obs_properties_get(props, ST_KEY_OTHER_WEIGHTEDPREDICTION), false); + obs_property_set_enabled(obs_properties_get(props, ST_KEY_OTHER_NONREFERENCEPFRAMES), false); + obs_property_set_enabled(obs_properties_get(props, ST_KEY_OTHER_REFERENCEFRAMES), false); + obs_property_set_enabled(obs_properties_get(props, ST_KEY_OTHER_LOWDELAYKEYFRAMESCALE), false); +} + +void nvenc::migrate(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings, uint64_t version) +{ + // Only test for A.B.C in A.B.C.D + version = version & STREAMFX_MASK_UPDATE; + +#define COPY_UNSET(TYPE, FROM, TO) \ + if (obs_data_has_user_value(settings, FROM)) { \ + obs_data_set_##TYPE(settings, TO, obs_data_get_##TYPE(settings, FROM)); \ + obs_data_unset_user_value(settings, FROM); \ + } + + if (version <= STREAMFX_MAKE_VERSION(0, 8, 0, 0)) { + COPY_UNSET(int, "RateControl.Bitrate.Target", ST_KEY_RATECONTROL_LIMITS_BITRATE_TARGET); + COPY_UNSET(int, "RateControl.Bitrate.Maximum", ST_KEY_RATECONTROL_LIMITS_BITRATE_TARGET); + COPY_UNSET(int, "RateControl.BufferSize", ST_KEY_RATECONTROL_LIMITS_BUFFERSIZE); + COPY_UNSET(int, "RateControl.Quality.Minimum", "RateControl.QP.Minimum"); + COPY_UNSET(int, "RateControl.Quality.Maximum", "RateControl.QP.Maximum"); + COPY_UNSET(double, "RateControl.Quality.Target", ST_KEY_RATECONTROL_LIMITS_QUALITY); + } + + if (version < STREAMFX_MAKE_VERSION(0, 11, 0, 0)) { + obs_data_unset_user_value(settings, "Other.AccessUnitDelimiter"); + obs_data_unset_user_value(settings, "Other.DecodedPictureBufferSize"); + } + + if (version < STREAMFX_MAKE_VERSION(0, 11, 1, 0)) { + // Preset + if (auto v = obs_data_get_int(settings, ST_KEY_PRESET); v != -1) { + std::map preset{ + {0, "default"}, {1, "slow"}, {2, "medium"}, {3, "fast"}, {4, "hp"}, {5, "hq"}, {6, "bd"}, {7, "ll"}, {8, "llhq"}, {9, "llhp"}, {10, "lossless"}, {11, "losslesshp"}, + }; + if (auto k = preset.find(v); k != preset.end()) { + obs_data_set_string(settings, ST_KEY_PRESET, k->second.data()); + } + } + + // Rate Control Mode + if (auto v = obs_data_get_int(settings, ST_KEY_RATECONTROL_MODE); v != -1) { + if (!obs_data_has_user_value(settings, ST_KEY_RATECONTROL_MODE)) + v = 4; + + switch (v) { + case 0: // CQP + obs_data_set_string(settings, ST_KEY_RATECONTROL_MODE, "constqp"); + break; + case 2: // VBR_HQ + obs_data_set_int(settings, ST_KEY_RATECONTROL_TWOPASS, 1); + obs_data_set_string(settings, ST_KEY_RATECONTROL_MULTIPASS, "qres"); + case 1: // VBR + obs_data_set_string(settings, ST_KEY_RATECONTROL_MODE, "vbr"); + break; + case 5: // CBR_LD_HQ + obs_data_set_int(settings, ST_KEY_OTHER_LOWDELAYKEYFRAMESCALE, 1); + case 4: // CBR_HQ + obs_data_set_int(settings, ST_KEY_RATECONTROL_TWOPASS, 1); + obs_data_set_string(settings, ST_KEY_RATECONTROL_MULTIPASS, "qres"); + case 3: // CBR + obs_data_set_string(settings, ST_KEY_RATECONTROL_MODE, "cbr"); + break; + } + } + + // Target Quality + if (auto v = obs_data_get_double(settings, ST_KEY_RATECONTROL_LIMITS_QUALITY); v > 0) { + obs_data_set_double(settings, ST_KEY_RATECONTROL_LIMITS_QUALITY, (v / 100.) * 51.); + } + + // B-Frame Reference Modes + if (auto v = obs_data_get_int(settings, ST_KEY_OTHER_BFRAMEREFERENCEMODE); v != -1) { + std::map preset{ + {0, "default"}, + {1, "each"}, + {2, "middle"}, + }; + if (auto k = preset.find(v); k != preset.end()) { + obs_data_set_string(settings, ST_KEY_OTHER_BFRAMEREFERENCEMODE, k->second.data()); + } + } + } + +#undef COPY_UNSET +} + +void nvenc::update(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings) +{ + auto codec = factory->get_avcodec(); + auto context = instance->get_avcodeccontext(); + + if (const char* v = obs_data_get_string(settings, ST_KEY_PRESET); !context->internal && (v != nullptr) && (v[0] != '\0')) { + av_opt_set(context->priv_data, "preset", v, AV_OPT_SEARCH_CHILDREN); + } + + { // Rate Control + const char* v = obs_data_get_string(settings, ST_KEY_RATECONTROL_MODE); + if (!context->internal && (v != nullptr) && (v[0] != '\0')) { + av_opt_set(context->priv_data, "rc", v, AV_OPT_SEARCH_CHILDREN); + } + + // Decode the name into useful flags. + bool have_bitrate = false; + bool have_bitrate_range = false; + bool have_quality = false; + bool _unused = false; + bool have_qp = false; + if (v && is_cbr(v)) { + have_bitrate = true; + + if (!context->internal) + av_opt_set_int(context->priv_data, "cbr", 1, AV_OPT_SEARCH_CHILDREN); + + // Support for OBS Studio + obs_data_set_string(settings, "rate_control", "CBR"); + } else if (v && is_vbr(v)) { + have_bitrate = true; + have_bitrate_range = true; + have_quality = true; + _unused = true; + have_qp = true; + + if (!context->internal) + av_opt_set_int(context->priv_data, "cbr", 0, AV_OPT_SEARCH_CHILDREN); + + // Support for OBS Studio + obs_data_set_string(settings, "rate_control", "VBR"); + } else if (v && is_cqp(v)) { + have_qp = true; + + if (!context->internal) + av_opt_set_int(context->priv_data, "cbr", 0, AV_OPT_SEARCH_CHILDREN); + + // Support for OBS Studio + obs_data_set_string(settings, "rate_control", "CQP"); + } else { + have_bitrate = true; + have_bitrate_range = true; + have_quality = true; + _unused = true; + have_qp = true; + + if (!context->internal) + av_opt_set_int(context->priv_data, "cbr", 0, AV_OPT_SEARCH_CHILDREN); + } + + if (!context->internal) { + if (streamfx::ffmpeg::tools::avoption_exists(context->priv_data, "multipass")) { + // Multi-Pass + if (const char* v2 = obs_data_get_string(settings, ST_KEY_RATECONTROL_MULTIPASS); (v2 != nullptr) && (v2[0] != '\0')) { + av_opt_set(context->priv_data, "multipass", v2, AV_OPT_SEARCH_CHILDREN); + av_opt_set_int(context->priv_data, "2pass", 0, AV_OPT_SEARCH_CHILDREN); + } + } else { + // Two-Pass + if (int tp = static_cast(obs_data_get_int(settings, ST_KEY_RATECONTROL_TWOPASS)); tp > -1) { + av_opt_set_int(context->priv_data, "2pass", tp ? 1 : 0, AV_OPT_SEARCH_CHILDREN); + } + } + + // Look Ahead # of Frames + int la = static_cast(obs_data_get_int(settings, ST_KEY_RATECONTROL_LOOKAHEAD)); + if (!streamfx::util::is_tristate_default(la)) { + av_opt_set_int(context->priv_data, "rc-lookahead", la, AV_OPT_SEARCH_CHILDREN); + } + + // Adaptive I-Frames + if (int64_t adapt_i = obs_data_get_int(settings, ST_KEY_RATECONTROL_ADAPTIVEI); !streamfx::util::is_tristate_default(adapt_i) && (la != 0)) { + // no-scenecut is inverted compared to our UI. + av_opt_set_int(context->priv_data, "no-scenecut", 1 - adapt_i, AV_OPT_SEARCH_CHILDREN); + } + + // Adaptive B-Frames + if (std::string_view("h264_nvenc") == codec->name) { + if (int64_t adapt_b = obs_data_get_int(settings, ST_KEY_RATECONTROL_ADAPTIVEB); !streamfx::util::is_tristate_default(adapt_b) && (la != 0)) { + av_opt_set_int(context->priv_data, "b_adapt", adapt_b, AV_OPT_SEARCH_CHILDREN); + } + } + } + + if (have_bitrate) { + int64_t v = obs_data_get_int(settings, ST_KEY_RATECONTROL_LIMITS_BITRATE_TARGET); + + // Allow OBS to specify a maximum allowed bitrate. + if (obs_data_get_int(settings, "bitrate") != obs_data_get_default_int(settings, "bitrate")) { + // obs_data_has_user_value(X, Y) is also true if obs_data_set_Z(X, Y, obs_data_get_Z(X, Y)) + v = std::clamp(v, -1, obs_data_get_int(settings, "bitrate")); + } + + if (v > -1) { + context->bit_rate = static_cast(v * 1000); + } + } else { + context->bit_rate = 0; + } + if (have_bitrate_range) { + if (int64_t max = obs_data_get_int(settings, ST_KEY_RATECONTROL_LIMITS_BITRATE_MAXIMUM); max > -1) { + context->rc_max_rate = static_cast(max * 1000); + } else { + context->rc_max_rate = context->bit_rate; + } + context->rc_min_rate = context->bit_rate; + } else { + context->rc_min_rate = context->bit_rate; + context->rc_max_rate = context->bit_rate; + } + { // Support for OBS Studio + obs_data_set_int(settings, "bitrate", context->rc_max_rate); + } + + // Buffer Size + if (have_bitrate || have_bitrate_range) { + if (int64_t v = obs_data_get_int(settings, ST_KEY_RATECONTROL_LIMITS_BUFFERSIZE); v > -1) + context->rc_buffer_size = static_cast(v * 1000); + } else { + context->rc_buffer_size = 0; + } + + if (!context->internal) { + // Quality Limits + context->qmin = -1; + context->qmax = -1; + + // Quality Target + if (have_quality) { + if (double_t v = obs_data_get_double(settings, ST_KEY_RATECONTROL_LIMITS_QUALITY); v > 0) { + av_opt_set_double(context->priv_data, "cq", v, AV_OPT_SEARCH_CHILDREN); + } + } else { + av_opt_set_double(context->priv_data, "cq", 0, AV_OPT_SEARCH_CHILDREN); + } + + // QP Settings + if (have_qp) { + if (int64_t qp = obs_data_get_int(settings, ST_KEY_RATECONTROL_QP_I); qp > -1) + av_opt_set_int(context->priv_data, "init_qpI", static_cast(qp), AV_OPT_SEARCH_CHILDREN); + if (int64_t qp = obs_data_get_int(settings, ST_KEY_RATECONTROL_QP_P); qp > -1) + av_opt_set_int(context->priv_data, "init_qpP", static_cast(qp), AV_OPT_SEARCH_CHILDREN); + if (int64_t qp = obs_data_get_int(settings, ST_KEY_RATECONTROL_QP_B); qp > -1) + av_opt_set_int(context->priv_data, "init_qpB", static_cast(qp), AV_OPT_SEARCH_CHILDREN); + } + } + } + + if (!context->internal) { // AQ + int64_t saq = obs_data_get_int(settings, ST_KEY_AQ_SPATIAL); + int64_t taq = obs_data_get_int(settings, ST_KEY_AQ_TEMPORAL); + + if (strcmp(codec->name, "h264_nvenc") == 0) { + if (!streamfx::util::is_tristate_default(saq)) + av_opt_set_int(context->priv_data, "spatial-aq", saq, AV_OPT_SEARCH_CHILDREN); + if (!streamfx::util::is_tristate_default(taq)) + av_opt_set_int(context->priv_data, "temporal-aq", taq, AV_OPT_SEARCH_CHILDREN); + } else { + if (!streamfx::util::is_tristate_default(saq)) + av_opt_set_int(context->priv_data, "spatial_aq", saq, AV_OPT_SEARCH_CHILDREN); + if (!streamfx::util::is_tristate_default(taq)) + av_opt_set_int(context->priv_data, "temporal_aq", taq, AV_OPT_SEARCH_CHILDREN); + } + if (streamfx::util::is_tristate_enabled(saq)) + if (int64_t aqs = obs_data_get_int(settings, ST_KEY_AQ_STRENGTH); aqs > -1) + av_opt_set_int(context->priv_data, "aq-strength", static_cast(aqs), AV_OPT_SEARCH_CHILDREN); + } + + if (!context->internal) { // Other + if (int64_t bf = obs_data_get_int(settings, ST_KEY_OTHER_BFRAMES); bf > -1) + av_opt_set_int(context, "bf", bf, AV_OPT_SEARCH_CHILDREN); + + if (int64_t zl = obs_data_get_int(settings, ST_KEY_OTHER_ZEROLATENCY); !streamfx::util::is_tristate_default(zl)) + av_opt_set_int(context->priv_data, "zerolatency", zl, AV_OPT_SEARCH_CHILDREN); + if (int64_t nrp = obs_data_get_int(settings, ST_KEY_OTHER_NONREFERENCEPFRAMES); !streamfx::util::is_tristate_default(nrp)) + av_opt_set_int(context->priv_data, "nonref_p", nrp, AV_OPT_SEARCH_CHILDREN); + if (int64_t v = obs_data_get_int(settings, ST_KEY_OTHER_REFERENCEFRAMES); v > -1) + av_opt_set_int(context, "refs", v, AV_OPT_SEARCH_CHILDREN); + + int64_t wp = obs_data_get_int(settings, ST_KEY_OTHER_WEIGHTEDPREDICTION); + if ((context->max_b_frames > 0) && streamfx::util::is_tristate_enabled(wp)) { + DLOG_WARNING("[%s] Weighted Prediction disabled because of B-Frames being used.", codec->name); + av_opt_set_int(context->priv_data, "weighted_pred", 0, AV_OPT_SEARCH_CHILDREN); + } else if (!streamfx::util::is_tristate_default(wp)) { + av_opt_set_int(context->priv_data, "weighted_pred", wp, AV_OPT_SEARCH_CHILDREN); + } + + if (const char* v = obs_data_get_string(settings, ST_KEY_OTHER_BFRAMEREFERENCEMODE); (v != nullptr) && (v[0] != '\0')) { + av_opt_set(context->priv_data, "b_ref_mode", v, AV_OPT_SEARCH_CHILDREN); + } + + if (auto v = obs_data_get_int(settings, ST_KEY_OTHER_LOWDELAYKEYFRAMESCALE); v > -1) { + av_opt_set_int(context->priv_data, "ldkfs", v, AV_OPT_SEARCH_CHILDREN); + } + } +} + +void nvenc::override_update(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings) +{ + AVCodecContext* context = const_cast(instance->get_avcodeccontext()); + + int64_t rclookahead = 0; + int64_t surfaces = 0; + int64_t async_depth = 0; + + av_opt_get_int(context, "rc-lookahead", AV_OPT_SEARCH_CHILDREN, &rclookahead); + av_opt_get_int(context, "surfaces", AV_OPT_SEARCH_CHILDREN, &surfaces); + av_opt_get_int(context, "async_depth", AV_OPT_SEARCH_CHILDREN, &async_depth); + + // Calculate and set the number of surfaces to allocate (if not user overridden). + if (surfaces == 0) { + surfaces = std::max(4ll, (context->max_b_frames + 1ll) * 4ll); + if (rclookahead > 0) { + surfaces = std::max(1ll, std::max(surfaces, rclookahead + (context->max_b_frames + 5ll))); + } else if (context->max_b_frames > 0) { + surfaces = std::max(4ll, (context->max_b_frames + 1ll) * 4ll); + } else { + surfaces = 4; + } + + av_opt_set_int(context, "surfaces", surfaces, AV_OPT_SEARCH_CHILDREN); + } + + // Set delay + context->delay = std::min(std::max(static_cast(async_depth), 3), static_cast(surfaces - 1)); +} + +// H264/AVC Handler +//------------------- + +nvenc_h264::nvenc_h264() : handler("h264_nvenc"){}; + +nvenc_h264::~nvenc_h264(){}; + +bool nvenc_h264::has_keyframes(ffmpeg_factory*) +{ + return true; +} + +bool nvenc_h264::has_threading(ffmpeg_factory*) +{ + return false; +} + +bool nvenc_h264::is_hardware(ffmpeg_factory*) +{ + return true; +} + +bool nvenc_h264::is_reconfigurable(ffmpeg_factory* instance, bool& threads, bool& gpu, bool& keyframes) +{ + threads = false; + gpu = false; + keyframes = false; + return true; +} + +void nvenc_h264::adjust_info(ffmpeg_factory* factory, std::string& id, std::string& name, std::string& codec) +{ + name = "NVIDIA NVENC H.264/AVC (via FFmpeg)"; + factory->get_info()->caps |= OBS_ENCODER_CAP_DYN_BITRATE; +#ifndef _DEBUG + if ((obs_get_encoder_caps("jim_nvenc") == 0) || (!nvenc::is_available())) { + // If NVENC is not present, or the default encoder is not present, don't list ours either. + factory->get_info()->caps |= OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + } +#endif // _DEBUG +} + +void nvenc_h264::defaults(ffmpeg_factory* factory, obs_data_t* settings) +{ + nvenc::defaults(factory, settings); + + obs_data_set_default_string(settings, ST_KEY_H264_PROFILE, ""); + obs_data_set_default_string(settings, ST_KEY_H264_LEVEL, "auto"); +} + +void nvenc_h264::properties(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_properties_t* props) +{ + if (!instance) { + this->properties_encoder(factory, instance, props); + } else { + this->properties_runtime(factory, instance, props); + } +} + +void nvenc_h264::migrate(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings, uint64_t version) +{ + nvenc::migrate(factory, instance, settings, version); + + if (version < STREAMFX_MAKE_VERSION(0, 11, 1, 0)) { + // Profile + if (auto v = obs_data_get_int(settings, ST_KEY_H264_PROFILE); v != -1) { + if (!obs_data_has_user_value(settings, ST_KEY_H264_PROFILE)) + v = 3; + + std::map preset{ + {0, "baseline"}, {1, "baseline"}, {2, "main"}, {3, "high"}, {4, "high444p"}, + }; + if (auto k = preset.find(v); k != preset.end()) { + obs_data_set_string(settings, ST_KEY_H264_PROFILE, k->second.data()); + } + } + + // Level + obs_data_set_string(settings, ST_KEY_H264_LEVEL, "auto"); + } +} + +void nvenc_h264::update(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings) +{ + auto codec = factory->get_avcodec(); + auto context = instance->get_avcodeccontext(); + + nvenc::update(factory, instance, settings); + + if (!context->internal) { + if (const char* v = obs_data_get_string(settings, ST_KEY_H264_PROFILE); v && (v[0] != '\0')) { + av_opt_set(context->priv_data, "profile", v, AV_OPT_SEARCH_CHILDREN); + } + if (const char* v = obs_data_get_string(settings, ST_KEY_H264_LEVEL); v && (v[0] != '\0')) { + av_opt_set(context->priv_data, "level", v, AV_OPT_SEARCH_CHILDREN); + } + } +} + +void nvenc_h264::override_update(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings) +{ + nvenc::override_update(factory, instance, settings); +} + +void nvenc_h264::properties_encoder(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_properties_t* props) +{ + std::shared_ptr context{avcodec_alloc_context3(factory->get_avcodec()), [](AVCodecContext* ptr) { avcodec_free_context(&ptr); }}; + nvenc::properties_before(factory, instance, props, context.get()); + + { + obs_properties_t* grp = props; + if (!streamfx::util::are_property_groups_broken()) { + grp = obs_properties_create(); + obs_properties_add_group(props, S_CODEC_H264, D_TRANSLATE(S_CODEC_H264), OBS_GROUP_NORMAL, grp); + } + + { + auto p = obs_properties_add_list(grp, ST_KEY_H264_PROFILE, D_TRANSLATE(S_CODEC_H264_PROFILE), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + obs_property_list_add_string(p, D_TRANSLATE(S_STATE_DEFAULT), ""); + streamfx::ffmpeg::tools::avoption_list_add_entries(context->priv_data, "profile", [&p](const AVOption* opt) { + char buffer[1024]; + snprintf(buffer, sizeof(buffer), "%s.%s", S_CODEC_H264_PROFILE, opt->name); + obs_property_list_add_string(p, D_TRANSLATE(buffer), opt->name); + }); + } + { + auto p = obs_properties_add_list(grp, ST_KEY_H264_LEVEL, D_TRANSLATE(S_CODEC_H264_LEVEL), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + + streamfx::ffmpeg::tools::avoption_list_add_entries(context->priv_data, "level", [&p](const AVOption* opt) { + if (opt->default_val.i64 == 0) { + obs_property_list_add_string(p, D_TRANSLATE(S_STATE_AUTOMATIC), "auto"); + } else { + obs_property_list_add_string(p, opt->name, opt->name); + } + }); + } + } + + nvenc::properies_after(factory, instance, props, context.get()); +} + +void nvenc_h264::properties_runtime(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_properties_t* props) +{ + nvenc::properties_runtime(factory, instance, props); +} + +static auto inst_h264 = nvenc_h264(); + +// H265/HEVC Handler +//------------------- + +nvenc_hevc::nvenc_hevc() : handler("hevc_nvenc"){}; + +nvenc_hevc::~nvenc_hevc(){}; + +bool nvenc_hevc::has_keyframes(ffmpeg_factory*) +{ + return true; +} + +bool nvenc_hevc::has_threading(ffmpeg_factory* instance) +{ + return false; +} + +bool nvenc_hevc::is_hardware(ffmpeg_factory* instance) +{ + return true; +} + +bool nvenc_hevc::is_reconfigurable(ffmpeg_factory* instance, bool& threads, bool& gpu, bool& keyframes) +{ + threads = false; + gpu = false; + keyframes = false; + return true; +} + +void nvenc_hevc::adjust_info(ffmpeg_factory* factory, std::string& id, std::string& name, std::string& codec) +{ + name = "NVIDIA NVENC H.265/HEVC (via FFmpeg)"; + factory->get_info()->caps |= OBS_ENCODER_CAP_DYN_BITRATE; +#ifndef _DEBUG + if ((obs_get_encoder_caps("jim_hevc_nvenc") == 0) || (!nvenc::is_available())) { + // If NVENC is not present, or the default encoder is not present, don't list ours either. + factory->get_info()->caps |= OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + } +#endif // _DEBUG +} + +void nvenc_hevc::defaults(ffmpeg_factory* factory, obs_data_t* settings) +{ + nvenc::defaults(factory, settings); + + obs_data_set_default_string(settings, ST_KEY_H265_PROFILE, ""); + obs_data_set_default_string(settings, ST_KEY_H265_TIER, ""); + obs_data_set_default_string(settings, ST_KEY_H265_LEVEL, "auto"); +} + +void nvenc_hevc::properties(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_properties_t* props) +{ + if (!instance) { + this->properties_encoder(factory, instance, props); + } else { + this->properties_runtime(factory, instance, props); + } +} + +void nvenc_hevc::migrate(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings, uint64_t version) +{ + nvenc::migrate(factory, instance, settings, version); + + // Migrate: + // - all versions below 0.12. + // - all versions that are larger than 0.12, but smaller than 0.12.0.315. + // - no other version. + if ((version < STREAMFX_MAKE_VERSION(0, 12, 0, 0)) || ((version > STREAMFX_MAKE_VERSION(0, 12, 0, 0)) && (version < STREAMFX_MAKE_VERSION(0, 12, 0, 315)))) { + // Accidentally had this stored int he wrong place. Oops. + obs_data_set_string(settings, ST_KEY_H265_LEVEL, obs_data_get_string(settings, ST_KEY_H264_LEVEL)); + obs_data_unset_user_value(settings, ST_KEY_H264_LEVEL); + } + + if (version < STREAMFX_MAKE_VERSION(0, 11, 1, 0)) { + // Profile + if (auto v = obs_data_get_int(settings, ST_KEY_H265_PROFILE); v != -1) { + if (!obs_data_has_user_value(settings, ST_KEY_H265_PROFILE)) + v = 0; + + std::map preset{ + {0, "main"}, + {1, "main10"}, + {2, "rext"}, + }; + if (auto k = preset.find(v); k != preset.end()) { + obs_data_set_string(settings, ST_KEY_H265_PROFILE, k->second.data()); + } + } + + // Tier + if (auto v = obs_data_get_int(settings, ST_KEY_H265_TIER); v != -1) { + if (!obs_data_has_user_value(settings, ST_KEY_H265_TIER)) + v = 0; + + std::map preset{ + {0, "main"}, + {1, "high"}, + }; + if (auto k = preset.find(v); k != preset.end()) { + obs_data_set_string(settings, ST_KEY_H265_TIER, k->second.data()); + } + } + + // Level + obs_data_set_string(settings, ST_KEY_H265_LEVEL, "auto"); + } +} + +void nvenc_hevc::update(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings) +{ + auto codec = factory->get_avcodec(); + auto context = instance->get_avcodeccontext(); + + nvenc::update(factory, instance, settings); + + if (!context->internal) { + if (const char* v = obs_data_get_string(settings, ST_KEY_H265_PROFILE); v && (v[0] != '\0')) { + av_opt_set(context->priv_data, "profile", v, AV_OPT_SEARCH_CHILDREN); + } + if (const char* v = obs_data_get_string(settings, ST_KEY_H265_TIER); v && (v[0] != '\0')) { + av_opt_set(context->priv_data, "tier", v, AV_OPT_SEARCH_CHILDREN); + } + if (const char* v = obs_data_get_string(settings, ST_KEY_H265_LEVEL); v && (v[0] != '\0')) { + av_opt_set(context->priv_data, "level", v, AV_OPT_SEARCH_CHILDREN); + } + } +} + +void nvenc_hevc::override_update(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings) +{ + nvenc::override_update(factory, instance, settings); +} + +void nvenc_hevc::properties_encoder(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_properties_t* props) +{ + std::shared_ptr context{avcodec_alloc_context3(factory->get_avcodec()), [](AVCodecContext* ptr) { avcodec_free_context(&ptr); }}; + nvenc::properties_before(factory, instance, props, context.get()); + + { + obs_properties_t* grp = props; + if (!streamfx::util::are_property_groups_broken()) { + grp = obs_properties_create(); + obs_properties_add_group(props, S_CODEC_HEVC, D_TRANSLATE(S_CODEC_HEVC), OBS_GROUP_NORMAL, grp); + } + + { + auto p = obs_properties_add_list(grp, ST_KEY_H265_PROFILE, D_TRANSLATE(S_CODEC_HEVC_PROFILE), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + obs_property_list_add_int(p, D_TRANSLATE(S_STATE_DEFAULT), -1); + streamfx::ffmpeg::tools::avoption_list_add_entries(context->priv_data, "profile", [&p](const AVOption* opt) { + char buffer[1024]; + snprintf(buffer, sizeof(buffer), "%s.%s", S_CODEC_HEVC_PROFILE, opt->name); + obs_property_list_add_string(p, D_TRANSLATE(buffer), opt->name); + }); + } + { + auto p = obs_properties_add_list(grp, ST_KEY_H265_TIER, D_TRANSLATE(S_CODEC_HEVC_TIER), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + obs_property_list_add_int(p, D_TRANSLATE(S_STATE_DEFAULT), -1); + streamfx::ffmpeg::tools::avoption_list_add_entries(context->priv_data, "tier", [&p](const AVOption* opt) { + char buffer[1024]; + snprintf(buffer, sizeof(buffer), "%s.%s", S_CODEC_HEVC_TIER, opt->name); + obs_property_list_add_string(p, D_TRANSLATE(buffer), opt->name); + }); + } + { + auto p = obs_properties_add_list(grp, ST_KEY_H265_LEVEL, D_TRANSLATE(S_CODEC_HEVC_LEVEL), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + + streamfx::ffmpeg::tools::avoption_list_add_entries(context->priv_data, "level", [&p](const AVOption* opt) { + if (opt->default_val.i64 == 0) { + obs_property_list_add_string(p, D_TRANSLATE(S_STATE_AUTOMATIC), "auto"); + } else { + obs_property_list_add_string(p, opt->name, opt->name); + } + }); + } + } + + nvenc::properies_after(factory, instance, props, context.get()); +} + +void nvenc_hevc::properties_runtime(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_properties_t* props) +{ + nvenc::properties_runtime(factory, instance, props); +} + +static auto inst_hevc = nvenc_hevc(); + +// AV1 Handler +//------------------- + +nvenc_av1::nvenc_av1() : handler("av1_nvenc"){}; + +nvenc_av1::~nvenc_av1(){}; + +bool nvenc_av1::has_keyframes(ffmpeg_factory*) +{ + return true; +} + +bool nvenc_av1::has_threading(ffmpeg_factory* instance) +{ + return false; +} + +bool nvenc_av1::is_hardware(ffmpeg_factory* instance) +{ + return true; +} + +bool nvenc_av1::is_reconfigurable(ffmpeg_factory* instance, bool& threads, bool& gpu, bool& keyframes) +{ + threads = false; + gpu = false; + keyframes = false; + return true; +} + +void nvenc_av1::adjust_info(ffmpeg_factory* factory, std::string& id, std::string& name, std::string& codec) +{ + name = "NVIDIA NVENC AV1 (via FFmpeg)"; + factory->get_info()->caps |= OBS_ENCODER_CAP_DYN_BITRATE; +#ifndef _DEBUG + if ((obs_get_encoder_caps("jim_av1_nvenc") == 0) || (!nvenc::is_available())) { + // If NVENC is not present, or the default encoder is not present, don't list ours either. + factory->get_info()->caps |= OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + } +#endif // _DEBUG +} + +void nvenc_av1::defaults(ffmpeg_factory* factory, obs_data_t* settings) +{ + nvenc::defaults(factory, settings); + + obs_data_set_default_string(settings, ST_KEY_AV1_PROFILE, ""); + obs_data_set_default_string(settings, ST_KEY_AV1_LEVEL, "auto"); +} + +void nvenc_av1::properties(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_properties_t* props) +{ + if (!instance) { + this->properties_encoder(factory, instance, props); + } else { + this->properties_runtime(factory, instance, props); + } +} + +void nvenc_av1::migrate(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings, uint64_t version) +{ + nvenc::migrate(factory, instance, settings, version); +} + +void nvenc_av1::update(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings) +{ + auto codec = factory->get_avcodec(); + auto context = instance->get_avcodeccontext(); + + nvenc::update(factory, instance, settings); + + if (!context->internal) { + if (const char* v = obs_data_get_string(settings, ST_KEY_AV1_PROFILE); v && (v[0] != '\0')) { + av_opt_set(context->priv_data, "tier", v, AV_OPT_SEARCH_CHILDREN); + } + if (const char* v = obs_data_get_string(settings, ST_KEY_AV1_LEVEL); v && (v[0] != '\0')) { + av_opt_set(context->priv_data, "level", v, AV_OPT_SEARCH_CHILDREN); + } + } +} + +void nvenc_av1::override_update(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings) +{ + nvenc::override_update(factory, instance, settings); +} + +void nvenc_av1::properties_encoder(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_properties_t* props) +{ + std::shared_ptr context{avcodec_alloc_context3(factory->get_avcodec()), [](AVCodecContext* ptr) { avcodec_free_context(&ptr); }}; + nvenc::properties_before(factory, instance, props, context.get()); + + { + obs_properties_t* grp = props; + if (!streamfx::util::are_property_groups_broken()) { + grp = obs_properties_create(); + obs_properties_add_group(props, S_CODEC_AV1, D_TRANSLATE(S_CODEC_AV1), OBS_GROUP_NORMAL, grp); + } + + { + auto p = obs_properties_add_list(grp, ST_KEY_AV1_PROFILE, D_TRANSLATE(S_CODEC_AV1_PROFILE), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + obs_property_list_add_int(p, D_TRANSLATE(S_STATE_DEFAULT), -1); + streamfx::ffmpeg::tools::avoption_list_add_entries(context->priv_data, "tier", [&p](const AVOption* opt) { + char buffer[1024]; + snprintf(buffer, sizeof(buffer), "%s.%s", S_CODEC_AV1_PROFILE, opt->name); + obs_property_list_add_string(p, D_TRANSLATE(buffer), opt->name); + }); + } + { + auto p = obs_properties_add_list(grp, ST_KEY_AV1_LEVEL, D_TRANSLATE(S_CODEC_AV1_LEVEL), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + + streamfx::ffmpeg::tools::avoption_list_add_entries(context->priv_data, "level", [&p](const AVOption* opt) { + if (opt->default_val.i64 == 0) { + obs_property_list_add_string(p, D_TRANSLATE(S_STATE_AUTOMATIC), "auto"); + } else { + obs_property_list_add_string(p, opt->name, opt->name); + } + }); + } + } + + nvenc::properies_after(factory, instance, props, context.get()); +} + +void nvenc_av1::properties_runtime(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_properties_t* props) +{ + nvenc::properties_runtime(factory, instance, props); +} + +static auto inst_av1 = nvenc_av1(); diff --git a/components/ffmpeg/source/encoders/ffmpeg/nvenc.hpp b/components/ffmpeg/source/encoders/ffmpeg/nvenc.hpp new file mode 100644 index 0000000..3360bcf --- /dev/null +++ b/components/ffmpeg/source/encoders/ffmpeg/nvenc.hpp @@ -0,0 +1,121 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END +// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks + +#pragma once +#include "encoders/encoder-ffmpeg.hpp" +#include "encoders/ffmpeg/handler.hpp" + +#include "warning-disable.hpp" +#include +#include +extern "C" { +#include +} +#include "warning-enable.hpp" + +/* NVENC has multiple compression modes: +- CBR: Constant Bitrate (rc=cbr) +- VBR: Variable Bitrate (rc=vbr) +- CQP: Constant QP (rc=cqp) +- CQ: Constant Quality (rc=vbr b=0 maxrate=0 qmin=0 qmax=51 cq=qp), this is basically CRF in X264. +*/ + +namespace streamfx::encoder::ffmpeg { + namespace nvenc { + bool is_available(); + + void defaults(ffmpeg_factory* factory, obs_data_t* settings); + void properties_before(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_properties_t* props, AVCodecContext* context); + void properies_after(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_properties_t* props, AVCodecContext* context); + void properties_runtime(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_properties_t* props); + void migrate(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings, uint64_t version); + void update(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings); + void override_update(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings); + } // namespace nvenc + + class nvenc_h264 : public handler { + public: + nvenc_h264(); + virtual ~nvenc_h264(); + + bool has_keyframes(ffmpeg_factory* factory) override; + bool has_threading(ffmpeg_factory* factory) override; + bool is_hardware(ffmpeg_factory* factory) override; + bool is_reconfigurable(ffmpeg_factory* factory, bool& threads, bool& gpu, bool& keyframes) override; + + void adjust_info(ffmpeg_factory* factory, std::string& id, std::string& name, std::string& codec) override; + + std::string help(ffmpeg_factory* factory) override + { + return "https://github.com/Xaymar/obs-StreamFX/wiki/Encoder-FFmpeg-NVENC"; + }; + + void defaults(ffmpeg_factory* factory, obs_data_t* settings) override; + void properties(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_properties_t* props) override; + void migrate(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings, uint64_t version) override; + void update(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings) override; + void override_update(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings) override; + + private: + void properties_encoder(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_properties_t* props); + void properties_runtime(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_properties_t* props); + }; + + class nvenc_hevc : public handler { + public: + nvenc_hevc(); + virtual ~nvenc_hevc(); + + bool has_keyframes(ffmpeg_factory* factory) override; + bool has_threading(ffmpeg_factory* factory) override; + bool is_hardware(ffmpeg_factory* factory) override; + bool is_reconfigurable(ffmpeg_factory* factory, bool& threads, bool& gpu, bool& keyframes) override; + + void adjust_info(ffmpeg_factory* factory, std::string& id, std::string& name, std::string& codec) override; + + std::string help(ffmpeg_factory* factory) override + { + return "https://github.com/Xaymar/obs-StreamFX/wiki/Encoder-FFmpeg-NVENC"; + }; + + void defaults(ffmpeg_factory* factory, obs_data_t* settings) override; + void properties(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_properties_t* props) override; + void migrate(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings, uint64_t version) override; + void update(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings) override; + void override_update(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings) override; + + private: + void properties_encoder(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_properties_t* props); + void properties_runtime(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_properties_t* props); + }; + + class nvenc_av1 : public handler { + public: + nvenc_av1(); + virtual ~nvenc_av1(); + + bool has_keyframes(ffmpeg_factory* factory) override; + bool has_threading(ffmpeg_factory* factory) override; + bool is_hardware(ffmpeg_factory* factory) override; + bool is_reconfigurable(ffmpeg_factory* factory, bool& threads, bool& gpu, bool& keyframes) override; + + void adjust_info(ffmpeg_factory* factory, std::string& id, std::string& name, std::string& codec) override; + + std::string help(ffmpeg_factory* factory) override + { + return "https://github.com/Xaymar/obs-StreamFX/wiki/Encoder-FFmpeg-NVENC"; + }; + + void defaults(ffmpeg_factory* factory, obs_data_t* settings) override; + void properties(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_properties_t* props) override; + void migrate(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings, uint64_t version) override; + void update(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings) override; + void override_update(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings) override; + + private: + void properties_encoder(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_properties_t* props); + void properties_runtime(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_properties_t* props); + }; +} // namespace streamfx::encoder::ffmpeg diff --git a/components/ffmpeg/source/encoders/ffmpeg/prores_aw.cpp b/components/ffmpeg/source/encoders/ffmpeg/prores_aw.cpp new file mode 100644 index 0000000..5b981f4 --- /dev/null +++ b/components/ffmpeg/source/encoders/ffmpeg/prores_aw.cpp @@ -0,0 +1,86 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#include "prores_aw.hpp" +#include "common.hpp" +#include "../codecs/prores.hpp" +#include "ffmpeg/tools.hpp" +#include "plugin.hpp" + +#include "warning-disable.hpp" +#include +#include "warning-enable.hpp" + +using namespace streamfx::encoder::ffmpeg; +using namespace streamfx::encoder::codec::prores; + +inline const char* profile_to_name(const AVProfile* ptr) +{ + switch (static_cast(ptr->profile)) { + case profile::APCO: + return D_TRANSLATE(S_CODEC_PRORES_PROFILE_APCO); + case profile::APCS: + return D_TRANSLATE(S_CODEC_PRORES_PROFILE_APCS); + case profile::APCN: + return D_TRANSLATE(S_CODEC_PRORES_PROFILE_APCN); + case profile::APCH: + return D_TRANSLATE(S_CODEC_PRORES_PROFILE_APCH); + case profile::AP4H: + return D_TRANSLATE(S_CODEC_PRORES_PROFILE_AP4H); + case profile::AP4X: + return D_TRANSLATE(S_CODEC_PRORES_PROFILE_AP4X); + default: + return ptr->name; + } +} + +prores_aw::prores_aw() : handler("prores_aw") {} + +prores_aw::~prores_aw() {} + +bool prores_aw::has_keyframes(ffmpeg_factory* instance) +{ + return false; +} + +void prores_aw::defaults(ffmpeg_factory* factory, obs_data_t* settings) +{ + obs_data_set_default_int(settings, S_CODEC_PRORES_PROFILE, 0); +} + +void prores_aw::properties(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_properties_t* props) +{ + if (!instance) { + auto p = obs_properties_add_list(props, S_CODEC_PRORES_PROFILE, D_TRANSLATE(S_CODEC_PRORES_PROFILE), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); + for (auto ptr = factory->get_avcodec()->profiles; ptr->profile != FF_PROFILE_UNKNOWN; ptr++) { + obs_property_list_add_int(p, profile_to_name(ptr), static_cast(ptr->profile)); + } + } else { + obs_property_set_enabled(obs_properties_get(props, S_CODEC_PRORES_PROFILE), false); + } +} + +void prores_aw::update(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings) +{ + if (instance) { + instance->get_avcodeccontext()->profile = static_cast(obs_data_get_int(settings, S_CODEC_PRORES_PROFILE)); + } +} + +void prores_aw::override_colorformat(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings, AVPixelFormat& target_format) +{ + static const std::array, static_cast(profile::_COUNT)> profile_to_format_map{ + std::pair{profile::APCO, AV_PIX_FMT_YUV422P10}, std::pair{profile::APCS, AV_PIX_FMT_YUV422P10}, std::pair{profile::APCN, AV_PIX_FMT_YUV422P10}, std::pair{profile::APCH, AV_PIX_FMT_YUV422P10}, std::pair{profile::AP4H, AV_PIX_FMT_YUV444P10}, std::pair{profile::AP4X, AV_PIX_FMT_YUV444P10}, + }; + + const int64_t profile_id = obs_data_get_int(settings, S_CODEC_PRORES_PROFILE); + for (auto kv : profile_to_format_map) { + if (kv.first == static_cast(profile_id)) { + target_format = kv.second; + break; + } + } +} + +static auto inst = prores_aw(); diff --git a/components/ffmpeg/source/encoders/ffmpeg/prores_aw.hpp b/components/ffmpeg/source/encoders/ffmpeg/prores_aw.hpp new file mode 100644 index 0000000..04a0b22 --- /dev/null +++ b/components/ffmpeg/source/encoders/ffmpeg/prores_aw.hpp @@ -0,0 +1,33 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "encoders/encoder-ffmpeg.hpp" +#include "encoders/ffmpeg/handler.hpp" + +#include "warning-disable.hpp" +extern "C" { +#include +} +#include "warning-enable.hpp" + +namespace streamfx::encoder::ffmpeg { + class prores_aw : public handler { + public: + prores_aw(); + virtual ~prores_aw(); + + virtual bool has_keyframes(ffmpeg_factory* factory); + + virtual std::string help(ffmpeg_factory* factory) { + return "https://github.com/Xaymar/obs-StreamFX/wiki/Encoder-FFmpeg-Apple-ProRes"; + } + + virtual void defaults(ffmpeg_factory* factory, obs_data_t* settings); + virtual void properties(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_properties_t* props); + virtual void update(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings); + + virtual void override_colorformat(ffmpeg_factory* factory, ffmpeg_instance* instance, obs_data_t* settings, AVPixelFormat& target_format); + }; +} // namespace streamfx::encoder::ffmpeg diff --git a/components/ffmpeg/source/ffmpeg/avframe-queue.cpp b/components/ffmpeg/source/ffmpeg/avframe-queue.cpp new file mode 100644 index 0000000..33e6f17 --- /dev/null +++ b/components/ffmpeg/source/ffmpeg/avframe-queue.cpp @@ -0,0 +1,131 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2022 lainon +// AUTOGENERATED COPYRIGHT HEADER END + +#include "avframe-queue.hpp" +#include "tools.hpp" + +using namespace streamfx::ffmpeg; + +std::shared_ptr avframe_queue::create_frame() +{ + std::shared_ptr frame = std::shared_ptr(av_frame_alloc(), [](AVFrame* frame) { + av_frame_unref(frame); + av_frame_free(&frame); + }); + frame->width = this->_resolution.first; + frame->height = this->_resolution.second; + frame->format = this->_format; + + int res = av_frame_get_buffer(frame.get(), 32); + if (res < 0) { + throw std::runtime_error(tools::get_error_description(res)); + } + + return frame; +} + +avframe_queue::avframe_queue() = default; + +avframe_queue::~avframe_queue() +{ + clear(); +} + +void avframe_queue::set_resolution(int32_t const width, int32_t const height) +{ + this->_resolution.first = width; + this->_resolution.second = height; +} + +void avframe_queue::get_resolution(int32_t& width, int32_t& height) +{ + width = this->_resolution.first; + height = this->_resolution.second; +} + +int32_t avframe_queue::get_width() +{ + return this->_resolution.first; +} + +int32_t avframe_queue::get_height() +{ + return this->_resolution.second; +} + +void avframe_queue::set_pixel_format(AVPixelFormat const format) +{ + this->_format = format; +} + +AVPixelFormat avframe_queue::get_pixel_format() +{ + return this->_format; +} + +void avframe_queue::precache(std::size_t count) +{ + for (std::size_t n = 0; n < count; n++) { + push(create_frame()); + } +} + +void avframe_queue::clear() +{ + std::unique_lock ulock(this->_lock); + _frames.clear(); +} + +void avframe_queue::push(std::shared_ptr const frame) +{ + std::unique_lock ulock(this->_lock); + _frames.push_back(frame); +} + +std::shared_ptr avframe_queue::pop() +{ + std::unique_lock ulock(this->_lock); + std::shared_ptr ret; + while (ret == nullptr) { + if (_frames.size() == 0) { + ret = create_frame(); + } else { + ret = _frames.front(); + if (ret == nullptr) { + ret = create_frame(); + } else { + _frames.pop_front(); + if ((static_cast(ret->width) != this->_resolution.first) || (static_cast(ret->height) != this->_resolution.second) || (ret->format != this->_format)) { + ret = nullptr; + } + } + } + } + return ret; +} + +std::shared_ptr avframe_queue::pop_only() +{ + std::unique_lock ulock(this->_lock); + if (_frames.size() == 0) { + return nullptr; + } + std::shared_ptr ret = _frames.front(); + if (ret == nullptr) { + return nullptr; + } + _frames.pop_front(); + return ret; +} + +bool avframe_queue::empty() +{ + return _frames.empty(); +} + +std::size_t avframe_queue::size() +{ + return _frames.size(); +} diff --git a/components/ffmpeg/source/ffmpeg/avframe-queue.hpp b/components/ffmpeg/source/ffmpeg/avframe-queue.hpp new file mode 100644 index 0000000..3b82879 --- /dev/null +++ b/components/ffmpeg/source/ffmpeg/avframe-queue.hpp @@ -0,0 +1,55 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "common.hpp" + +#include "warning-disable.hpp" +#include +#include +#include "warning-enable.hpp" + +extern "C" { +#include "warning-disable.hpp" +#include +#include "warning-enable.hpp" +} + +namespace streamfx::ffmpeg { + class avframe_queue { + std::deque> _frames; + std::mutex _lock; + + std::pair _resolution; + AVPixelFormat _format = AV_PIX_FMT_NONE; + + std::shared_ptr create_frame(); + + public: + avframe_queue(); + ~avframe_queue(); + + void set_resolution(int32_t width, int32_t height); + void get_resolution(int32_t& width, int32_t& height); + int32_t get_width(); + int32_t get_height(); + + void set_pixel_format(AVPixelFormat format); + AVPixelFormat get_pixel_format(); + + void precache(std::size_t count); + + void clear(); + + void push(std::shared_ptr frame); + + std::shared_ptr pop(); + + std::shared_ptr pop_only(); + + bool empty(); + + std::size_t size(); + }; +} // namespace streamfx::ffmpeg diff --git a/components/ffmpeg/source/ffmpeg/hwapi/base.cpp b/components/ffmpeg/source/ffmpeg/hwapi/base.cpp new file mode 100644 index 0000000..e3289d6 --- /dev/null +++ b/components/ffmpeg/source/ffmpeg/hwapi/base.cpp @@ -0,0 +1,5 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#include "base.hpp" diff --git a/components/ffmpeg/source/ffmpeg/hwapi/base.hpp b/components/ffmpeg/source/ffmpeg/hwapi/base.hpp new file mode 100644 index 0000000..45c08e6 --- /dev/null +++ b/components/ffmpeg/source/ffmpeg/hwapi/base.hpp @@ -0,0 +1,50 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2022 lainon +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "common.hpp" + +#include "warning-disable.hpp" +#include +#include +#include "warning-enable.hpp" + +extern "C" { +#include "warning-disable.hpp" +#include +#include +#include "warning-enable.hpp" +} + +namespace streamfx::ffmpeg::hwapi { + struct device { + std::pair id; + std::string name; + }; + + class instance { + public: + virtual ~instance(){}; + + virtual AVBufferRef* create_device_context() = 0; + + virtual std::shared_ptr allocate_frame(AVBufferRef* frames) = 0; + + virtual void copy_from_obs(AVBufferRef* frames, uint32_t handle, uint64_t lock_key, uint64_t* next_lock_key, std::shared_ptr frame) = 0; + + virtual std::shared_ptr avframe_from_obs(AVBufferRef* frames, uint32_t handle, uint64_t lock_key, uint64_t* next_lock_key) = 0; + }; + + class base { + public: + virtual ~base(){}; + + virtual std::list enumerate_adapters() = 0; + + virtual std::shared_ptr create(const hwapi::device& target) = 0; + + virtual std::shared_ptr create_from_obs() = 0; + }; +} // namespace streamfx::ffmpeg::hwapi diff --git a/components/ffmpeg/source/ffmpeg/hwapi/d3d11.cpp b/components/ffmpeg/source/ffmpeg/hwapi/d3d11.cpp new file mode 100644 index 0000000..900bf31 --- /dev/null +++ b/components/ffmpeg/source/ffmpeg/hwapi/d3d11.cpp @@ -0,0 +1,235 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2022 lainon +// AUTOGENERATED COPYRIGHT HEADER END + +#ifdef WIN32 + +#include "d3d11.hpp" +#include "obs/gs/gs-helper.hpp" + +#include "warning-disable.hpp" +#include +#include +#include "warning-enable.hpp" + +extern "C" { +#include "warning-disable.hpp" +#include +#include "warning-enable.hpp" +} + +using namespace streamfx::ffmpeg::hwapi; + +d3d11::d3d11() : _dxgi_module(0), _d3d11_module(0) +{ + _dxgi_module = LoadLibraryW(L"dxgi.dll"); + if (!_dxgi_module) + throw std::runtime_error("Unable to load DXGI"); + + _d3d11_module = LoadLibraryW(L"d3d11.dll"); + if (!_d3d11_module) + throw std::runtime_error("Unable to load D3D11"); + +#pragma warning(push) +#pragma warning(disable : 4191) + _CreateDXGIFactory = reinterpret_cast(GetProcAddress(_dxgi_module, "CreateDXGIFactory")); + _CreateDXGIFactory1 = reinterpret_cast(GetProcAddress(_dxgi_module, "CreateDXGIFactory1")); + _D3D11CreateDevice = reinterpret_cast(GetProcAddress(_d3d11_module, "D3D11CreateDevice")); +#pragma warning(pop) + + if (!_CreateDXGIFactory && !_CreateDXGIFactory1) + throw std::runtime_error("DXGI not supported"); + + if (!_D3D11CreateDevice) + throw std::runtime_error("D3D11 not supported"); + + HRESULT hr = _CreateDXGIFactory1(__uuidof(IDXGIFactory1), (void**)&_dxgifactory); + if (FAILED(hr)) { + std::stringstream sstr; + sstr << "Failed to create DXGI Factory (" << hr << ")"; + throw std::runtime_error(sstr.str()); + } +} + +d3d11::~d3d11() +{ + FreeLibrary(_dxgi_module); + FreeLibrary(_d3d11_module); +} + +std::list d3d11::enumerate_adapters() +{ + std::list adapters; + + // Enumerate Adapters + IDXGIAdapter1* dxgi_adapter = nullptr; + for (UINT idx = 0; !FAILED(_dxgifactory->EnumAdapters1(idx, &dxgi_adapter)); idx++) { + DXGI_ADAPTER_DESC1 desc = DXGI_ADAPTER_DESC1(); + dxgi_adapter->GetDesc1(&desc); + + std::vector buf(1024); + std::size_t len = static_cast(snprintf(buf.data(), buf.size(), "%ls (VEN_%04x/DEV_%04x/SUB_%04x/REV_%04x)", desc.Description, desc.VendorId, desc.DeviceId, desc.SubSysId, desc.Revision)); + + device dev; + dev.name = std::string(buf.data(), buf.data() + len); + dev.id.first = desc.AdapterLuid.HighPart; + dev.id.second = desc.AdapterLuid.LowPart; + + adapters.push_back(dev); + } + + return adapters; +} + +std::shared_ptr d3d11::create(const device& target) +{ + std::shared_ptr inst; + ATL::CComPtr device; + IDXGIAdapter1* adapter = nullptr; + + // Find the correct "Adapter" (device). + IDXGIAdapter1* dxgi_adapter = nullptr; + for (UINT idx = 0; !FAILED(_dxgifactory->EnumAdapters1(idx, &dxgi_adapter)); idx++) { + DXGI_ADAPTER_DESC1 desc = DXGI_ADAPTER_DESC1(); + dxgi_adapter->GetDesc1(&desc); + + if ((desc.AdapterLuid.LowPart == target.id.second) && (desc.AdapterLuid.HighPart == target.id.first)) { + adapter = dxgi_adapter; + break; + } + } + + // Create a D3D11 Device + UINT device_flags = D3D11_CREATE_DEVICE_VIDEO_SUPPORT; + std::vector feature_levels = {D3D_FEATURE_LEVEL_12_1, D3D_FEATURE_LEVEL_12_0, D3D_FEATURE_LEVEL_11_1}; + + if (FAILED(_D3D11CreateDevice(adapter, D3D_DRIVER_TYPE_HARDWARE, NULL, device_flags, feature_levels.data(), static_cast(feature_levels.size()), D3D11_SDK_VERSION, &device, NULL, NULL))) { + throw std::runtime_error("Failed to create D3D11 device for target."); + } + + return std::make_shared(device); +} + +std::shared_ptr d3d11::create_from_obs() +{ + auto gctx = streamfx::obs::gs::context(); + + if (GS_DEVICE_DIRECT3D_11 != gs_get_device_type()) { + throw std::runtime_error("OBS Device is not a D3D11 Device."); + } + + ATL::CComPtr device = ATL::CComPtr(reinterpret_cast(gs_get_device_obj())); + + return std::make_shared(device); +} + +struct D3D11AVFrame { + ATL::CComPtr handle; +}; + +d3d11_instance::d3d11_instance(ATL::CComPtr device) : _device(device) +{ + // Acquire immediate rendering context. + device->GetImmediateContext(&_context); +} + +d3d11_instance::~d3d11_instance() +{ + //_context.Release(); // Automatically performed by ATL::CComPtr. +} + +AVBufferRef* d3d11_instance::create_device_context() +{ + AVBufferRef* dctx_ref = av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_D3D11VA); + if (!dctx_ref) + throw std::runtime_error("Failed to allocate AVHWDeviceContext."); + + AVHWDeviceContext* hwdev = reinterpret_cast(dctx_ref->data); + AVD3D11VADeviceContext* device_hwctx = reinterpret_cast(hwdev->hwctx); + + // Provide the base device information only. + device_hwctx->device = _device; + device_hwctx->device->AddRef(); + + // And a way to lock/unlock the device. + device_hwctx->lock = [](void*) { obs_enter_graphics(); }; + device_hwctx->unlock = [](void*) { obs_leave_graphics(); }; + + // Then let FFmpeg do the rest for us. + int ret = av_hwdevice_ctx_init(dctx_ref); + if (ret < 0) + throw std::runtime_error("Failed to initialize AVHWDeviceContext."); + + return dctx_ref; +} + +std::shared_ptr d3d11_instance::allocate_frame(AVBufferRef* frames) +{ + auto gctx = streamfx::obs::gs::context(); + + // Allocate a frame. + auto frame = std::shared_ptr(av_frame_alloc(), [](AVFrame* frame) { av_frame_free(&frame); }); + + // Create the necessary buffers. + if (av_hwframe_get_buffer(frames, frame.get(), 0) < 0) { + throw std::runtime_error("Failed to create AVFrame."); + } + + // Try to prevent this resource from ever leaving the GPU unless absolutely necessary. + reinterpret_cast(frame->data[0])->SetEvictionPriority(DXGI_RESOURCE_PRIORITY_MAXIMUM); + + return frame; +} + +void d3d11_instance::copy_from_obs(AVBufferRef*, uint32_t handle, uint64_t lock_key, uint64_t* next_lock_key, std::shared_ptr frame) +{ + auto gctx = streamfx::obs::gs::context(); + + // Attempt to acquire shared texture. + ATL::CComPtr input; + if (FAILED(_device->OpenSharedResource(reinterpret_cast(static_cast(handle)), __uuidof(ID3D11Texture2D), reinterpret_cast(&input)))) { + throw std::runtime_error("Failed to open shared texture resource."); + } + + // Attempt to acquire texture mutex. + ATL::CComPtr mutex; + if (FAILED(input->QueryInterface(__uuidof(IDXGIKeyedMutex), reinterpret_cast(&mutex)))) { + throw std::runtime_error("Failed to retrieve mutex for texture resource."); + } + + // Attempt to acquire texture lock. + if (FAILED(mutex->AcquireSync(lock_key, 1000))) { + throw std::runtime_error("Failed to acquire lock on input texture."); + } + + // Set some parameters on the input texture, and get its description. + UINT evict = input->GetEvictionPriority(); + input->SetEvictionPriority(DXGI_RESOURCE_PRIORITY_MAXIMUM); + + // Clone the content of the input texture. + _context->CopyResource(reinterpret_cast(frame->data[0]), input); + + // Restore original parameters on input. + input->SetEvictionPriority(evict); + + // Release the acquired lock. + if (FAILED(mutex->ReleaseSync(lock_key))) { + throw std::runtime_error("Failed to release lock on input texture."); + } + + // Release the lock on the next texture. + // TODO: Determine if this is necessary. + mutex->ReleaseSync(*next_lock_key); +} + +std::shared_ptr d3d11_instance::avframe_from_obs(AVBufferRef* frames, uint32_t handle, uint64_t lock_key, uint64_t* next_lock_key) +{ + auto gctx = streamfx::obs::gs::context(); + + auto frame = this->allocate_frame(frames); + this->copy_from_obs(frames, handle, lock_key, next_lock_key, frame); + return frame; +} + +#endif diff --git a/components/ffmpeg/source/ffmpeg/hwapi/d3d11.hpp b/components/ffmpeg/source/ffmpeg/hwapi/d3d11.hpp new file mode 100644 index 0000000..ef16a56 --- /dev/null +++ b/components/ffmpeg/source/ffmpeg/hwapi/d3d11.hpp @@ -0,0 +1,58 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2022 lainon +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "base.hpp" + +#include "warning-disable.hpp" +#include +#include +#include +#include +#include "warning-enable.hpp" + +namespace streamfx::ffmpeg::hwapi { + class d3d11 : public streamfx::ffmpeg::hwapi::base { + typedef HRESULT(__stdcall* CreateDXGIFactory_t)(REFIID, void**); + typedef HRESULT(__stdcall* CreateDXGIFactory1_t)(REFIID, void**); + typedef HRESULT(__stdcall* D3D11CreateDevice_t)(IDXGIAdapter*, D3D_DRIVER_TYPE, HMODULE, UINT, CONST D3D_FEATURE_LEVEL*, UINT, UINT, ID3D11Device**, D3D_FEATURE_LEVEL*, ID3D11DeviceContext**); + + HMODULE _dxgi_module; + CreateDXGIFactory_t _CreateDXGIFactory; + CreateDXGIFactory1_t _CreateDXGIFactory1; + + HMODULE _d3d11_module; + D3D11CreateDevice_t _D3D11CreateDevice; + + ATL::CComPtr _dxgifactory; + + public: + d3d11(); + virtual ~d3d11(); + + virtual std::list enumerate_adapters() override; + + virtual std::shared_ptr create(const hwapi::device& target) override; + + virtual std::shared_ptr create_from_obs() override; + }; + + class d3d11_instance : public streamfx::ffmpeg::hwapi::instance { + ATL::CComPtr _device; + ATL::CComPtr _context; + + public: + d3d11_instance(ATL::CComPtr device); + virtual ~d3d11_instance(); + + virtual AVBufferRef* create_device_context() override; + + virtual std::shared_ptr allocate_frame(AVBufferRef* frames) override; + + virtual void copy_from_obs(AVBufferRef* frames, uint32_t handle, uint64_t lock_key, uint64_t* next_lock_key, std::shared_ptr frame) override; + + virtual std::shared_ptr avframe_from_obs(AVBufferRef* frames, uint32_t handle, uint64_t lock_key, uint64_t* next_lock_key) override; + }; +} // namespace streamfx::ffmpeg::hwapi diff --git a/components/ffmpeg/source/ffmpeg/swscale.cpp b/components/ffmpeg/source/ffmpeg/swscale.cpp new file mode 100644 index 0000000..0fcae53 --- /dev/null +++ b/components/ffmpeg/source/ffmpeg/swscale.cpp @@ -0,0 +1,186 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2022 lainon +// AUTOGENERATED COPYRIGHT HEADER END + +#include "swscale.hpp" + +#include "warning-disable.hpp" +#include +#include "warning-enable.hpp" + +using namespace streamfx::ffmpeg; + +swscale::swscale() = default; + +swscale::~swscale() +{ + finalize(); +} + +void swscale::set_source_size(uint32_t width, uint32_t height) +{ + source_size.first = width; + source_size.second = height; +} + +void swscale::get_source_size(uint32_t& width, uint32_t& height) +{ + width = this->source_size.first; + height = this->source_size.second; +} + +std::pair swscale::get_source_size() +{ + return this->source_size; +} + +uint32_t swscale::get_source_width() +{ + return this->source_size.first; +} + +uint32_t swscale::get_source_height() +{ + return this->source_size.second; +} + +void swscale::set_source_format(AVPixelFormat format) +{ + source_format = format; +} + +AVPixelFormat swscale::get_source_format() +{ + return this->source_format; +} + +void swscale::set_source_color(bool full_range, AVColorSpace space) +{ + source_full_range = full_range; + source_colorspace = space; +} + +void swscale::set_source_colorspace(AVColorSpace space) +{ + this->source_colorspace = space; +} + +AVColorSpace swscale::get_source_colorspace() +{ + return this->source_colorspace; +} + +void swscale::set_source_full_range(bool full_range) +{ + this->source_full_range = full_range; +} + +bool swscale::is_source_full_range() +{ + return this->source_full_range; +} + +void swscale::set_target_size(uint32_t width, uint32_t height) +{ + target_size.first = width; + target_size.second = height; +} + +void swscale::get_target_size(uint32_t& width, uint32_t& height) +{ + width = target_size.first; + height = target_size.second; +} + +std::pair swscale::get_target_size() +{ + return this->target_size; +} + +uint32_t swscale::get_target_width() +{ + return this->target_size.first; +} + +uint32_t swscale::get_target_height() +{ + return this->target_size.second; +} + +void swscale::set_target_format(AVPixelFormat format) +{ + target_format = format; +} + +AVPixelFormat swscale::get_target_format() +{ + return this->target_format; +} + +void swscale::set_target_color(bool full_range, AVColorSpace space) +{ + target_full_range = full_range; + target_colorspace = space; +} + +void swscale::set_target_colorspace(AVColorSpace space) +{ + this->target_colorspace = space; +} + +AVColorSpace swscale::get_target_colorspace() +{ + return this->target_colorspace; +} + +void swscale::set_target_full_range(bool full_range) +{ + this->target_full_range = full_range; +} + +bool swscale::is_target_full_range() +{ + return this->target_full_range; +} + +bool swscale::initialize(int flags) +{ + if (this->context) { + return false; + } + if (source_size.first == 0 || source_size.second == 0 || source_format == AV_PIX_FMT_NONE || source_colorspace == AVCOL_SPC_UNSPECIFIED) { + throw std::invalid_argument("not all source parameters were set"); + } + if (target_size.first == 0 || target_size.second == 0 || target_format == AV_PIX_FMT_NONE || target_colorspace == AVCOL_SPC_UNSPECIFIED) { + throw std::invalid_argument("not all target parameters were set"); + } + + this->context = sws_getContext(static_cast(source_size.first), static_cast(source_size.second), source_format, static_cast(target_size.first), static_cast(target_size.second), target_format, flags, nullptr, nullptr, nullptr); + if (!this->context) { + return false; + } + + sws_setColorspaceDetails(this->context, sws_getCoefficients(source_colorspace), source_full_range ? 1 : 0, sws_getCoefficients(target_colorspace), target_full_range ? 1 : 0, 1L << 16 | 0L, 1L << 16 | 0L, 1L << 16 | 0L); + + return true; +} + +bool swscale::finalize() +{ + if (this->context) { + sws_freeContext(this->context); + this->context = nullptr; + return true; + } + return false; +} + +int32_t swscale::convert(const uint8_t* const source_data[], const int source_stride[], int32_t source_row, int32_t source_rows, uint8_t* const target_data[], const int target_stride[]) +{ + if (!this->context) { + return 0; + } + int height = sws_scale(this->context, source_data, source_stride, source_row, source_rows, target_data, target_stride); + return height; +} diff --git a/components/ffmpeg/source/ffmpeg/swscale.hpp b/components/ffmpeg/source/ffmpeg/swscale.hpp new file mode 100644 index 0000000..dfbbc81 --- /dev/null +++ b/components/ffmpeg/source/ffmpeg/swscale.hpp @@ -0,0 +1,68 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "common.hpp" + +#include "warning-disable.hpp" +#include +#include "warning-enable.hpp" + +extern "C" { +#include "warning-disable.hpp" +#include +#include +#include "warning-enable.hpp" +} + +namespace streamfx::ffmpeg { + class swscale { + std::pair source_size; + AVPixelFormat source_format = AV_PIX_FMT_NONE; + bool source_full_range = false; + AVColorSpace source_colorspace = AVCOL_SPC_UNSPECIFIED; + + std::pair target_size; + AVPixelFormat target_format = AV_PIX_FMT_NONE; + bool target_full_range = false; + AVColorSpace target_colorspace = AVCOL_SPC_UNSPECIFIED; + + SwsContext* context = nullptr; + + public: + swscale(); + ~swscale(); + + void set_source_size(uint32_t width, uint32_t height); + void get_source_size(uint32_t& width, uint32_t& height); + std::pair get_source_size(); + uint32_t get_source_width(); + uint32_t get_source_height(); + void set_source_format(AVPixelFormat format); + AVPixelFormat get_source_format(); + void set_source_color(bool full_range, AVColorSpace space); + void set_source_colorspace(AVColorSpace space); + AVColorSpace get_source_colorspace(); + void set_source_full_range(bool full_range); + bool is_source_full_range(); + + void set_target_size(uint32_t width, uint32_t height); + void get_target_size(uint32_t& width, uint32_t& height); + std::pair get_target_size(); + uint32_t get_target_width(); + uint32_t get_target_height(); + void set_target_format(AVPixelFormat format); + AVPixelFormat get_target_format(); + void set_target_color(bool full_range, AVColorSpace space); + void set_target_colorspace(AVColorSpace space); + AVColorSpace get_target_colorspace(); + void set_target_full_range(bool full_range); + bool is_target_full_range(); + + bool initialize(int flags); + bool finalize(); + + int32_t convert(const uint8_t* const source_data[], const int source_stride[], int32_t source_row, int32_t source_rows, uint8_t* const target_data[], const int target_stride[]); + }; +} // namespace streamfx::ffmpeg diff --git a/components/ffmpeg/source/ffmpeg/tools.cpp b/components/ffmpeg/source/ffmpeg/tools.cpp new file mode 100644 index 0000000..40fc49d --- /dev/null +++ b/components/ffmpeg/source/ffmpeg/tools.cpp @@ -0,0 +1,463 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2022 lainon +// AUTOGENERATED COPYRIGHT HEADER END + +#include "tools.hpp" +#include "plugin.hpp" + +#include "warning-disable.hpp" +#include +#include +#include "warning-enable.hpp" + +extern "C" { +#include "warning-disable.hpp" +#include +#include +#include +#include +#include "warning-enable.hpp" +} + +using namespace streamfx::ffmpeg; + +const char* tools::get_pixel_format_name(AVPixelFormat v) +{ + return av_get_pix_fmt_name(v); +} + +const char* tools::get_color_space_name(AVColorSpace v) +{ + switch (v) { + case AVCOL_SPC_RGB: + return "RGB"; + case AVCOL_SPC_BT709: + return "BT.709"; + case AVCOL_SPC_FCC: + return "FCC Title 47 CoFR 73.682 (a)(20)"; + case AVCOL_SPC_BT470BG: + return "BT.601 625"; + case AVCOL_SPC_SMPTE170M: + case AVCOL_SPC_SMPTE240M: + return "BT.601 525"; + case AVCOL_SPC_YCGCO: + return "ITU-T SG16"; + case AVCOL_SPC_BT2020_NCL: + return "BT.2020 NCL"; + case AVCOL_SPC_BT2020_CL: + return "BT.2020 CL"; + case AVCOL_SPC_SMPTE2085: + return "SMPTE 2085"; + case AVCOL_SPC_CHROMA_DERIVED_NCL: + return "Chroma NCL"; + case AVCOL_SPC_CHROMA_DERIVED_CL: + return "Chroma CL"; + case AVCOL_SPC_ICTCP: + return "BT.2100"; + case AVCOL_SPC_NB: + return "Not Part of ABI"; + default: + return "Unknown"; + } +} + +const char* tools::get_error_description(int error) +{ + thread_local char error_buf[AV_ERROR_MAX_STRING_SIZE + 1]; + if (av_strerror(error, error_buf, AV_ERROR_MAX_STRING_SIZE) < 0) { + snprintf(error_buf, AV_ERROR_MAX_STRING_SIZE, "Unknown Error (%i)", error); + } + return error_buf; +} + +static std::map const obs_to_av_format_map = { + {VIDEO_FORMAT_I420, AV_PIX_FMT_YUV420P}, // 4:2:0 YUV, 8bit, Planar + {VIDEO_FORMAT_NV12, AV_PIX_FMT_NV12}, // 4:2:0 YUV, 8bit, Packed (Y+UV) + {VIDEO_FORMAT_YVYU, AV_PIX_FMT_YVYU422}, // 4:2:0 YUV, 8bit, Packed (Y+UV) + {VIDEO_FORMAT_YUY2, AV_PIX_FMT_YUYV422}, // 4:2:2 YUV, 8bit, Packed (Y+UV) + {VIDEO_FORMAT_UYVY, AV_PIX_FMT_UYVY422}, // 4:2:2 YUV, 8bit, Packed (Y+UV) + {VIDEO_FORMAT_RGBA, AV_PIX_FMT_RGBA}, // 4:4:4:4 RGBA, 8bit, Planar + {VIDEO_FORMAT_BGRA, AV_PIX_FMT_BGRA}, // 4:4:4:4 BGRA, 8bit, Planar + {VIDEO_FORMAT_BGRX, AV_PIX_FMT_BGR0}, // 4:4:4 BGR, 8bit, Planar + {VIDEO_FORMAT_Y800, AV_PIX_FMT_GRAY8}, // 4:0:0 Y, 8bit, Planar + {VIDEO_FORMAT_I444, AV_PIX_FMT_YUV444P}, // 4:4:4 YUV, 8bit, Planar + {VIDEO_FORMAT_BGR3, AV_PIX_FMT_BGR24}, // 4:4:4 BGR, 8bit, Planar + {VIDEO_FORMAT_I422, AV_PIX_FMT_YUV422P}, // 4:2:2 YUV, 8bit, Planar + {VIDEO_FORMAT_I40A, AV_PIX_FMT_YUVA420P}, // 4:2:0:4 YUVA, 8bit, Planar + {VIDEO_FORMAT_I42A, AV_PIX_FMT_YUVA422P}, // 4:2:2:4 YUVA, 8bit, Planar + {VIDEO_FORMAT_YUVA, AV_PIX_FMT_YUVA444P}, // 4:4:4:4 YUVA, 8bit, Planar + {VIDEO_FORMAT_AYUV, AV_PIX_FMT_NONE}, // No compatible format known + {VIDEO_FORMAT_I010, AV_PIX_FMT_YUV420P10}, // 4:2:0, 10bit, Planar + {VIDEO_FORMAT_P010, AV_PIX_FMT_P010}, // 4:2:0, 10bit, Packed (Y+UV) + {VIDEO_FORMAT_I210, AV_PIX_FMT_YUV422P10}, // 4:2:2 YUV, 10bit, Planar + {VIDEO_FORMAT_I412, AV_PIX_FMT_YUV444P12}, // 4:4:4 YUV, 12bit, Planar + {VIDEO_FORMAT_YA2L, AV_PIX_FMT_YUVA444P12}, // 4:4:4:4 YUVA, 12bit, Planar + +}; + +AVPixelFormat tools::obs_videoformat_to_avpixelformat(video_format v) +{ + auto found = obs_to_av_format_map.find(v); + if (found != obs_to_av_format_map.end()) { + return found->second; + } + return AV_PIX_FMT_NONE; +} + +video_format tools::avpixelformat_to_obs_videoformat(AVPixelFormat v) +{ + for (const auto& kv : obs_to_av_format_map) { + if (kv.second == v) + return kv.first; + } + return VIDEO_FORMAT_NONE; +} + +AVPixelFormat tools::get_least_lossy_format(const AVPixelFormat* haystack, AVPixelFormat needle) +{ + int data_loss = 0; + return avcodec_find_best_pix_fmt_of_list(haystack, needle, 0, &data_loss); +} + +AVColorRange tools::obs_to_av_color_range(video_range_type v) +{ + switch (v) { + case VIDEO_RANGE_DEFAULT: + case VIDEO_RANGE_PARTIAL: + return AVCOL_RANGE_MPEG; + case VIDEO_RANGE_FULL: + return AVCOL_RANGE_JPEG; + } + throw std::invalid_argument("Unknown Color Range"); +} + +AVColorSpace tools::obs_to_av_color_space(video_colorspace v) +{ + switch (v) { + case VIDEO_CS_601: // BT.601 + return AVCOL_SPC_SMPTE170M; + case VIDEO_CS_DEFAULT: + case VIDEO_CS_709: // BT.709 + case VIDEO_CS_SRGB: // sRGB + return AVCOL_SPC_BT709; + case VIDEO_CS_2100_PQ: + case VIDEO_CS_2100_HLG: + return AVCOL_SPC_ICTCP; + default: + throw std::invalid_argument("Unknown Color Space"); + } +} + +AVColorPrimaries streamfx::ffmpeg::tools::obs_to_av_color_primary(video_colorspace v) +{ + switch (v) { + case VIDEO_CS_601: // BT.601 + return AVCOL_PRI_SMPTE170M; + case VIDEO_CS_DEFAULT: + case VIDEO_CS_709: // BT.709 + case VIDEO_CS_SRGB: // sRGB + return AVCOL_PRI_BT709; + case VIDEO_CS_2100_PQ: + case VIDEO_CS_2100_HLG: + return AVCOL_PRI_BT2020; + default: + throw std::invalid_argument("Unknown Color Primaries"); + } +} + +AVColorTransferCharacteristic streamfx::ffmpeg::tools::obs_to_av_color_transfer_characteristics(video_colorspace v) +{ + switch (v) { + case VIDEO_CS_601: // BT.601 + return AVCOL_TRC_SMPTE170M; + case VIDEO_CS_DEFAULT: + case VIDEO_CS_709: // BT.709 + return AVCOL_TRC_BT709; + case VIDEO_CS_SRGB: // sRGB with IEC 61966-2-1 + return AVCOL_TRC_IEC61966_2_1; + case VIDEO_CS_2100_PQ: + return AVCOL_TRC_SMPTE2084; + case VIDEO_CS_2100_HLG: + return AVCOL_TRC_ARIB_STD_B67; + default: + throw std::invalid_argument("Unknown Color Transfer Characteristics"); + } +} + +const char* tools::avoption_name_from_unit_value(const AVClass* cls, std::string_view unit, int64_t value) +{ + for (const AVOption* opt = nullptr; (opt = av_opt_next(&cls, opt)) != nullptr;) { + // Skip all irrelevant options. + if (!opt->unit) + continue; + if (opt->unit != unit) + continue; + if (opt->name == unit) + continue; + + if (opt->default_val.i64 == value) + return opt->name; + } + return nullptr; +} + +bool tools::avoption_exists(const void* obj, std::string_view name) +{ + for (const AVOption* opt = nullptr; (opt = av_opt_next(obj, opt)) != nullptr;) { + if (name == opt->name) + return true; + } + return false; +} + +void tools::avoption_list_add_entries(const void* obj, std::string_view unit, std::function inserter) +{ + std::string_view parent_name = unit; + std::string_view parent_unit = unit; + + // Figure out the real unit if this is actually an option name. + const AVOption* parent = av_opt_find(const_cast(obj), unit.data(), nullptr, 0, AV_OPT_SEARCH_CHILDREN); + if (parent != nullptr) { + parent_name = parent->name; + if (parent->unit != nullptr) { + parent_unit = parent->unit; + } + } + + for (const AVOption* opt = nullptr; (opt = av_opt_next(obj, opt)) != nullptr;) { + // Skip all irrelevant options. + if (!opt->unit) + continue; + if (opt->unit != parent_unit) + continue; + if (opt->name == parent_name) + continue; + + // Skip any deprecated options. + if (opt->flags & AV_OPT_FLAG_DEPRECATED) + continue; + + if (inserter) { + inserter(opt); + } else { + break; + } + } +} + +bool tools::can_hardware_encode(const AVCodec* codec) +{ + AVPixelFormat hardware_formats[] = {AV_PIX_FMT_D3D11}; + + for (const AVPixelFormat* fmt = codec->pix_fmts; (fmt != nullptr) && (*fmt != AV_PIX_FMT_NONE); fmt++) { + for (auto cmp : hardware_formats) { + if (*fmt == cmp) { + return true; + } + } + } + return false; +} + +std::vector tools::get_software_formats(const AVPixelFormat* list) +{ + constexpr AVPixelFormat hardware_formats[] = { +#if FF_API_VAAPI + AV_PIX_FMT_VAAPI_MOCO, + AV_PIX_FMT_VAAPI_IDCT, +#endif + AV_PIX_FMT_VAAPI, + AV_PIX_FMT_DXVA2_VLD, + AV_PIX_FMT_VDPAU, + AV_PIX_FMT_QSV, + AV_PIX_FMT_MMAL, + AV_PIX_FMT_D3D11VA_VLD, + AV_PIX_FMT_CUDA, + AV_PIX_FMT_XVMC, + AV_PIX_FMT_VIDEOTOOLBOX, + AV_PIX_FMT_MEDIACODEC, + AV_PIX_FMT_D3D11, + }; + + std::vector fmts; + for (auto fmt = list; fmt && (*fmt != AV_PIX_FMT_NONE); fmt++) { + bool is_blacklisted = false; + for (auto blacklisted : hardware_formats) { + if (*fmt == blacklisted) + is_blacklisted = true; + } + if (!is_blacklisted) + fmts.push_back(*fmt); + } + + fmts.push_back(AV_PIX_FMT_NONE); + + return fmts; +} + +void tools::context_setup_from_obs(const video_output_info* voi, AVCodecContext* context) +{ + // Resolution + context->width = static_cast(voi->width); + context->height = static_cast(voi->height); + + // Framerate + context->ticks_per_frame = 1; + context->framerate.num = context->time_base.den = static_cast(voi->fps_num); + context->framerate.den = context->time_base.num = static_cast(voi->fps_den); + + // Aspect Ratio, Progressive + context->sample_aspect_ratio.num = 1; + context->sample_aspect_ratio.den = 1; + context->field_order = AV_FIELD_PROGRESSIVE; + + // Decipher Pixel information + context->pix_fmt = obs_videoformat_to_avpixelformat(voi->format); + context->color_range = obs_to_av_color_range(voi->range); + context->colorspace = obs_to_av_color_space(voi->colorspace); + context->color_primaries = obs_to_av_color_primary(voi->colorspace); + context->color_trc = obs_to_av_color_transfer_characteristics(voi->colorspace); + + // Chroma Location + switch (context->pix_fmt) { + case AV_PIX_FMT_NV12: + case AV_PIX_FMT_YUV420P: + case AV_PIX_FMT_YUVA420P: + case AV_PIX_FMT_YUV422P: + case AV_PIX_FMT_YUVA422P: + case AV_PIX_FMT_YVYU422: + case AV_PIX_FMT_YUYV422: + case AV_PIX_FMT_UYVY422: + // libOBS merges Chroma at "Top", see H.264 specification. + context->chroma_sample_location = AVCHROMA_LOC_TOP; + break; + default: + // All other cases are unspecified. + context->chroma_sample_location = AVCHROMA_LOC_UNSPECIFIED; + break; + } +} + +const char* tools::get_std_compliance_name(int compliance) +{ + switch (compliance) { + case FF_COMPLIANCE_VERY_STRICT: + return "Very Strict"; + case FF_COMPLIANCE_STRICT: + return "Strict"; + case FF_COMPLIANCE_NORMAL: + return "Normal"; + case FF_COMPLIANCE_UNOFFICIAL: + return "Unofficial"; + case FF_COMPLIANCE_EXPERIMENTAL: + return "Experimental"; + } + return "Invalid"; +} + +const char* tools::get_thread_type_name(int thread_type) +{ + switch (thread_type) { + case FF_THREAD_FRAME | FF_THREAD_SLICE: + return "Slice & Frame"; + case FF_THREAD_FRAME: + return "Frame"; + case FF_THREAD_SLICE: + return "Slice"; + default: + return "None"; + } +} + +void tools::print_av_option_bool(AVCodecContext* ctx_codec, const char* option, std::string_view text, bool inverse) +{ + print_av_option_bool(ctx_codec, ctx_codec, option, text, inverse); +} + +void tools::print_av_option_bool(AVCodecContext* ctx_codec, void* ctx_option, const char* option, std::string_view text, bool inverse) +{ + int64_t v = 0; + if (int err = av_opt_get_int(ctx_option, option, AV_OPT_SEARCH_CHILDREN, &v); err != 0) { + DLOG_INFO("[%s] %s: ", ctx_codec->codec->name, text.data(), streamfx::ffmpeg::tools::get_error_description(err)); + } else { + DLOG_INFO("[%s] %s: %s%s", ctx_codec->codec->name, text.data(), (inverse ? v != 0 : v == 0) ? "Disabled" : "Enabled", av_opt_is_set_to_default_by_name(ctx_option, option, AV_OPT_SEARCH_CHILDREN) > 0 ? " " : ""); + } +} + +void tools::print_av_option_int(AVCodecContext* ctx_codec, const char* option, std::string_view text, std::string_view suffix) +{ + print_av_option_int(ctx_codec, ctx_codec, option, text, suffix); +} + +void tools::print_av_option_int(AVCodecContext* ctx_codec, void* ctx_option, const char* option, std::string_view text, std::string_view suffix) +{ + int64_t v = 0; + bool is_default = av_opt_is_set_to_default_by_name(ctx_option, option, AV_OPT_SEARCH_CHILDREN) > 0; + if (int err = av_opt_get_int(ctx_option, option, AV_OPT_SEARCH_CHILDREN, &v); err != 0) { + if (is_default) { + DLOG_INFO("[%s] %s: ", ctx_codec->codec->name, text.data()); + } else { + DLOG_INFO("[%s] %s: ", ctx_codec->codec->name, text.data(), streamfx::ffmpeg::tools::get_error_description(err)); + } + } else { + DLOG_INFO("[%s] %s: %" PRId64 " %s%s", ctx_codec->codec->name, text.data(), v, suffix.data(), is_default ? " " : ""); + } +} + +void tools::print_av_option_string(AVCodecContext* ctx_codec, const char* option, std::string_view text, std::function decoder) +{ + print_av_option_string(ctx_codec, ctx_codec, option, text, decoder); +} + +void tools::print_av_option_string(AVCodecContext* ctx_codec, void* ctx_option, const char* option, std::string_view text, std::function decoder) +{ + int64_t v = 0; + if (int err = av_opt_get_int(ctx_option, option, AV_OPT_SEARCH_CHILDREN, &v); err != 0) { + DLOG_INFO("[%s] %s: ", ctx_codec->codec->name, text.data(), streamfx::ffmpeg::tools::get_error_description(err)); + } else { + std::string name = ""; + if (decoder) + name = decoder(v); + DLOG_INFO("[%s] %s: %s%s", ctx_codec->codec->name, text.data(), name.c_str(), av_opt_is_set_to_default_by_name(ctx_option, option, AV_OPT_SEARCH_CHILDREN) > 0 ? " " : ""); + } +} + +void tools::print_av_option_string2(AVCodecContext* ctx_codec, std::string_view option, std::string_view text, std::function decoder) +{ + print_av_option_string2(ctx_codec, ctx_codec, option, text, decoder); +} + +void tools::print_av_option_string2(AVCodecContext* ctx_codec, void* ctx_option, std::string_view option, std::string_view text, std::function decoder) +{ + int64_t v = 0; + if (int err = av_opt_get_int(ctx_option, option.data(), AV_OPT_SEARCH_CHILDREN, &v); err != 0) { + DLOG_INFO("[%s] %s: ", ctx_codec->codec->name, text.data(), tools::get_error_description(err)); + } else { + std::string name = ""; + + // Find the unit for the option. + auto* opt = av_opt_find(ctx_option, option.data(), nullptr, 0, AV_OPT_SEARCH_CHILDREN); + if (opt && opt->unit) { + for (auto* opt_test = opt; (opt_test = av_opt_next(ctx_option, opt_test)) != nullptr;) { + // Skip this entry if the unit doesn't match. + if ((opt_test->unit == nullptr) || (strcmp(opt_test->unit, opt->unit) != 0)) { + continue; + } + + // Assign correct name if we found one. + if (opt_test->default_val.i64 == v) { + name = opt_test->name; + break; + } + } + + if (decoder) { + name = decoder(v, name); + } + DLOG_INFO("[%s] %s: %s%s", ctx_codec->codec->name, text.data(), name.c_str(), av_opt_is_set_to_default_by_name(ctx_option, option.data(), AV_OPT_SEARCH_CHILDREN) > 0 ? " " : ""); + } else { + DLOG_INFO("[%s] %s: %" PRId64 "%s", ctx_codec->codec->name, text.data(), v, av_opt_is_set_to_default_by_name(ctx_option, option.data(), AV_OPT_SEARCH_CHILDREN) > 0 ? " " : ""); + } + } +} diff --git a/components/ffmpeg/source/ffmpeg/tools.hpp b/components/ffmpeg/source/ffmpeg/tools.hpp new file mode 100644 index 0000000..0fc2951 --- /dev/null +++ b/components/ffmpeg/source/ffmpeg/tools.hpp @@ -0,0 +1,66 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2022 lainon +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "common.hpp" + +#include "warning-disable.hpp" +#include +#include "warning-enable.hpp" + +extern "C" { +#include "warning-disable.hpp" +#include +#include +#include +#include "warning-enable.hpp" +} + +namespace streamfx::ffmpeg::tools { + const char* get_pixel_format_name(AVPixelFormat v); + + const char* get_color_space_name(AVColorSpace v); + + const char* get_error_description(int error); + + AVPixelFormat obs_videoformat_to_avpixelformat(video_format v); + video_format avpixelformat_to_obs_videoformat(AVPixelFormat v); + + AVPixelFormat get_least_lossy_format(const AVPixelFormat* haystack, AVPixelFormat needle); + + AVColorRange obs_to_av_color_range(video_range_type v); + AVColorSpace obs_to_av_color_space(video_colorspace v); + AVColorPrimaries obs_to_av_color_primary(video_colorspace v); + AVColorTransferCharacteristic obs_to_av_color_transfer_characteristics(video_colorspace v); + + bool can_hardware_encode(const AVCodec* codec); + + std::vector get_software_formats(const AVPixelFormat* list); + + void context_setup_from_obs(const video_output_info* voi, AVCodecContext* context); + + const char* get_std_compliance_name(int compliance); + + const char* get_thread_type_name(int thread_type); + + void print_av_option_bool(AVCodecContext* context, const char* option, std::string_view text, bool inverse = false); + void print_av_option_bool(AVCodecContext* ctx_codec, void* ctx_option, const char* option, std::string_view text, bool inverse = false); + + void print_av_option_int(AVCodecContext* context, const char* option, std::string_view text, std::string_view suffix); + void print_av_option_int(AVCodecContext* ctx_codec, void* ctx_option, const char* option, std::string_view text, std::string_view suffix); + + void print_av_option_string(AVCodecContext* context, const char* option, std::string_view text, std::function decoder); + void print_av_option_string(AVCodecContext* ctx_codec, void* ctx_option, const char* option, std::string_view text, std::function decoder); + + void print_av_option_string2(AVCodecContext* context, std::string_view option, std::string_view text, std::function decoder); + void print_av_option_string2(AVCodecContext* ctx_codec, void* ctx_option, std::string_view option, std::string_view text, std::function decoder); + + bool avoption_exists(const void* obj, std::string_view name); + + const char* avoption_name_from_unit_value(const AVClass* cls, std::string_view unit, int64_t value); + + void avoption_list_add_entries(const void* obj, std::string_view unit, std::function inserter = nullptr); + +} // namespace streamfx::ffmpeg::tools diff --git a/components/mirror/CMakeLists.txt b/components/mirror/CMakeLists.txt new file mode 100644 index 0000000..4897d3e --- /dev/null +++ b/components/mirror/CMakeLists.txt @@ -0,0 +1,9 @@ +# AUTOGENERATED COPYRIGHT HEADER START +# Copyright (C) 2023 Michael Fabian 'Xaymar' Dirks +# AUTOGENERATED COPYRIGHT HEADER END + +cmake_minimum_required(VERSION 3.26) +project("Mirror") +list(APPEND CMAKE_MESSAGE_INDENT "[${PROJECT_NAME}] ") + +streamfx_add_component("Mirror") diff --git a/components/mirror/source/sources/source-mirror.cpp b/components/mirror/source/sources/source-mirror.cpp new file mode 100644 index 0000000..08ddced --- /dev/null +++ b/components/mirror/source/sources/source-mirror.cpp @@ -0,0 +1,404 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2022 lainon +// AUTOGENERATED COPYRIGHT HEADER END + +#include "source-mirror.hpp" +#include "strings.hpp" +#include +#include +#include +#include +#include +#include +#include +#include "obs/gs/gs-helper.hpp" +#include "obs/obs-source-tracker.hpp" +#include "obs/obs-tools.hpp" +#include "util/util-logging.hpp" + +#ifdef _DEBUG +#define ST_PREFIX "<%s> " +#define D_LOG_ERROR(x, ...) P_LOG_ERROR(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_WARNING(x, ...) P_LOG_WARN(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_INFO(x, ...) P_LOG_INFO(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_DEBUG(x, ...) P_LOG_DEBUG(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#else +#define ST_PREFIX " " +#define D_LOG_ERROR(...) P_LOG_ERROR(ST_PREFIX __VA_ARGS__) +#define D_LOG_WARNING(...) P_LOG_WARN(ST_PREFIX __VA_ARGS__) +#define D_LOG_INFO(...) P_LOG_INFO(ST_PREFIX __VA_ARGS__) +#define D_LOG_DEBUG(...) P_LOG_DEBUG(ST_PREFIX __VA_ARGS__) +#endif + +// OBS +#ifdef _MSC_VER +#pragma warning(push) +#pragma warning(disable : 4464) +#pragma warning(disable : 4820) +#pragma warning(disable : 5220) +#else +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wall" +#pragma GCC diagnostic ignored "-Wextra" +#endif +#include +#ifdef _MSC_VER +#pragma warning(pop) +#else +#pragma GCC diagnostic pop +#endif + +#define ST_I18N "Source.Mirror" +#define ST_I18N_SOURCE ST_I18N ".Source" +#define ST_KEY_SOURCE "Source.Mirror.Source" +#define ST_I18N_SOURCE_AUDIO ST_I18N_SOURCE ".Audio" +#define ST_KEY_SOURCE_AUDIO "Source.Mirror.Audio" +#define ST_I18N_SOURCE_AUDIO_LAYOUT ST_I18N_SOURCE_AUDIO ".Layout" +#define ST_KEY_SOURCE_AUDIO_LAYOUT "Source.Mirror.Audio.Layout" +#define ST_I18N_SOURCE_AUDIO_LAYOUT_(x) ST_I18N_SOURCE_AUDIO_LAYOUT "." D_VSTR(x) + +using namespace streamfx::source::mirror; + +static constexpr std::string_view HELP_URL = "https://github.com/Xaymar/obs-StreamFX/wiki/Source-Mirror"; + +mirror_audio_data::mirror_audio_data(const audio_data* audio, speaker_layout layout) +{ + // Build a clone of a packet. + audio_t* oad = obs_get_audio(); + const audio_output_info* aoi = audio_output_get_info(oad); + osa.frames = audio->frames; + osa.timestamp = audio->timestamp; + osa.speakers = layout; + osa.format = aoi->format; + osa.samples_per_sec = aoi->samples_per_sec; + data.resize(MAX_AV_PLANES); + for (std::size_t idx = 0; idx < MAX_AV_PLANES; idx++) { + if (!audio->data[idx]) { + osa.data[idx] = nullptr; + continue; + } + + data[idx].resize(audio->frames * get_audio_bytes_per_channel(osa.format)); + memcpy(data[idx].data(), audio->data[idx], data[idx].size()); + osa.data[idx] = data[idx].data(); + } +} + +mirror_instance::mirror_instance(obs_data_t* settings, obs_source_t* self) : obs::source_instance(settings, self), _source(), _source_child(), _signal_rename(), _audio_enabled(false), _audio_layout(SPEAKERS_UNKNOWN) +{ + update(settings); +} + +mirror_instance::~mirror_instance() +{ + release(); +} + +uint32_t mirror_instance::get_width() +{ + return _source_size.first ? _source_size.first : 1; +} + +uint32_t mirror_instance::get_height() +{ + return _source_size.second ? _source_size.second : 1; +} + +void mirror_instance::load(obs_data_t* data) +{ + update(data); +} + +void mirror_instance::migrate(obs_data_t* data, uint64_t version) +{ + switch (version) { + case 0: + obs_data_set_int(data, ST_KEY_SOURCE_AUDIO_LAYOUT, obs_data_get_int(data, "Source.Mirror.Audio.Layout")); + obs_data_unset_user_value(data, "Source.Mirror.Audio.Layout"); + case STREAMFX_VERSION: + break; + } +} + +void mirror_instance::update(obs_data_t* data) +{ + // Audio + _audio_enabled = obs_data_get_bool(data, ST_KEY_SOURCE_AUDIO); + _audio_layout = static_cast(obs_data_get_int(data, ST_KEY_SOURCE_AUDIO_LAYOUT)); + + // Acquire new source. + acquire(obs_data_get_string(data, ST_KEY_SOURCE)); +} + +void mirror_instance::save(obs_data_t* data) +{ + if (_source) { + obs_data_set_string(data, ST_KEY_SOURCE, obs_source_get_name(_source.get())); + } else { + obs_data_unset_user_value(data, ST_KEY_SOURCE); + } +} + +void mirror_instance::video_tick(float time) {} + +void mirror_instance::video_render(gs_effect_t* effect) +{ + if (!_source) + return; + if ((obs_source_get_output_flags(_source.get()) & OBS_SOURCE_VIDEO) == 0) + return; + +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + streamfx::obs::gs::debug_marker gdmp{streamfx::obs::gs::debug_color_source, "Source Mirror '%s' for '%s'", obs_source_get_name(_self), obs_source_get_name(_source.get())}; +#endif + + _source_size.first = obs_source_get_width(_source.get()); + _source_size.second = obs_source_get_height(_source.get()); + + obs_source_video_render(_source.get()); +} + +void mirror_instance::enum_active_sources(obs_source_enum_proc_t cb, void* ptr) +{ + if (!_source) + return; + cb(_self, _source.get(), ptr); +} + +void mirror_instance::enum_all_sources(obs_source_enum_proc_t cb, void* ptr) +{ + if (!_source) + return; + + cb(_self, _source.get(), ptr); +} + +void mirror_instance::acquire(std::string source_name) +{ + try { + release(); + + // Find source by name if possible. + decltype(_source) source{source_name}; + if ((!source) || (source == _self)) { // If we failed, just exit early. + return; + } + + // Everything went well, store. + _source_child = std::make_shared<::streamfx::obs::source_active_child>(_self, source); + _source = std::move(source); + _source_size.first = obs_source_get_width(_source); + _source_size.second = obs_source_get_height(_source); + + // Listen to any audio the source spews out. + if (_audio_enabled) { + _signal_audio = std::make_shared(_source); + _signal_audio->event.add(std::bind(&mirror_instance::on_audio, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); + } + } catch (...) { + release(); + } +} + +void mirror_instance::release() +{ + _signal_audio.reset(); + _signal_rename.reset(); + _source_child.reset(); + _source.release(); +} + +void mirror_instance::on_audio(::streamfx::obs::source, const audio_data* audio, bool) +{ + // Immediately quit if there isn't any actual audio to send out. + if (!_audio_enabled) { + return; + } + + // Detect Audio Layout from underlying audio. + speaker_layout detected_layout; + if (_audio_layout != SPEAKERS_UNKNOWN) { + detected_layout = _audio_layout; + } else { + std::bitset layout_detection; + for (std::size_t idx = 0; idx < MAX_AV_PLANES; idx++) { + layout_detection.set(idx, audio->data[idx] != nullptr); + } + switch (layout_detection.to_ulong()) { + case 0b00000001: + detected_layout = SPEAKERS_MONO; + break; + case 0b00000011: + detected_layout = SPEAKERS_STEREO; + break; + case 0b00000111: + detected_layout = SPEAKERS_2POINT1; + break; + case 0b00001111: + detected_layout = SPEAKERS_4POINT0; + break; + case 0b00011111: + detected_layout = SPEAKERS_4POINT1; + break; + case 0b00111111: + detected_layout = SPEAKERS_5POINT1; + break; + case 0b11111111: + detected_layout = SPEAKERS_7POINT1; + break; + default: + detected_layout = SPEAKERS_UNKNOWN; + break; + } + } + + { + std::unique_lock ul(_audio_queue_lock); + _audio_queue.emplace(audio, detected_layout); + } + + // Create a clone of the audio data and push it to the thread pool. + streamfx::util::threadpool::threadpool::instance()->push(std::bind(&mirror_instance::audio_output, this, std::placeholders::_1), nullptr); +} + +void mirror_instance::audio_output(std::shared_ptr data) +{ + std::unique_lock ul(_audio_queue_lock); + while (_audio_queue.size() > 0) { + obs_source_output_audio(_self, &((_audio_queue.front()).osa)); + _audio_queue.pop(); + } +} + +mirror_factory::mirror_factory() +{ + _info.id = S_PREFIX "source-mirror"; + _info.type = OBS_SOURCE_TYPE_INPUT; + _info.output_flags = OBS_SOURCE_VIDEO | OBS_SOURCE_CUSTOM_DRAW | OBS_SOURCE_AUDIO; + + support_active_child_sources(true); + support_child_sources(true); + finish_setup(); + register_proxy("obs-stream-effects-source-mirror"); +} + +mirror_factory::~mirror_factory() {} + +const char* mirror_factory::get_name() +{ + return D_TRANSLATE(ST_I18N); +} + +void mirror_factory::get_defaults2(obs_data_t* data) +{ + obs_data_set_default_string(data, ST_KEY_SOURCE, ""); + obs_data_set_default_bool(data, ST_KEY_SOURCE_AUDIO, false); + obs_data_set_default_int(data, ST_KEY_SOURCE_AUDIO_LAYOUT, static_cast(SPEAKERS_UNKNOWN)); +} + +static bool modified_properties(obs_properties_t* pr, obs_property_t* p, obs_data_t* data) noexcept +{ + try { + if (obs_properties_get(pr, ST_KEY_SOURCE_AUDIO) == p) { + bool show = obs_data_get_bool(data, ST_KEY_SOURCE_AUDIO); + obs_property_set_visible(obs_properties_get(pr, ST_KEY_SOURCE_AUDIO_LAYOUT), show); + return true; + } + return false; + } catch (...) { + return false; + } +} + +obs_properties_t* mirror_factory::get_properties2(mirror_instance* data) +{ + obs_properties_t* pr = obs_properties_create(); + obs_property_t* p = nullptr; + + { + obs_properties_add_button2(pr, S_MANUAL_OPEN, D_TRANSLATE(S_MANUAL_OPEN), streamfx::source::mirror::mirror_factory::on_manual_open, nullptr); + } + + { + p = obs_properties_add_list(pr, ST_KEY_SOURCE, D_TRANSLATE(ST_I18N_SOURCE), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + obs_property_set_modified_callback(p, modified_properties); + + obs_property_list_add_string(p, "", ""); + obs::source_tracker::instance()->enumerate( + [&p](std::string name, ::streamfx::obs::source) { + std::stringstream sstr; + sstr << name << " (" << D_TRANSLATE(S_SOURCETYPE_SOURCE) << ")"; + obs_property_list_add_string(p, sstr.str().c_str(), name.c_str()); + return false; + }, + obs::source_tracker::filter_sources); + obs::source_tracker::instance()->enumerate( + [&p](std::string name, ::streamfx::obs::source) { + std::stringstream sstr; + sstr << name << " (" << D_TRANSLATE(S_SOURCETYPE_SCENE) << ")"; + obs_property_list_add_string(p, sstr.str().c_str(), name.c_str()); + return false; + }, + obs::source_tracker::filter_scenes); + } + + { + p = obs_properties_add_bool(pr, ST_KEY_SOURCE_AUDIO, D_TRANSLATE(ST_I18N_SOURCE_AUDIO)); + obs_property_set_modified_callback(p, modified_properties); + } + + { + p = obs_properties_add_list(pr, ST_KEY_SOURCE_AUDIO_LAYOUT, D_TRANSLATE(ST_I18N_SOURCE_AUDIO_LAYOUT), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); + obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_SOURCE_AUDIO_LAYOUT_(Unknown)), static_cast(SPEAKERS_UNKNOWN)); + obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_SOURCE_AUDIO_LAYOUT_(Mono)), static_cast(SPEAKERS_MONO)); + obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_SOURCE_AUDIO_LAYOUT_(Stereo)), static_cast(SPEAKERS_STEREO)); + obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_SOURCE_AUDIO_LAYOUT_(StereoLFE)), static_cast(SPEAKERS_2POINT1)); + obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_SOURCE_AUDIO_LAYOUT_(Quadraphonic)), static_cast(SPEAKERS_4POINT0)); + obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_SOURCE_AUDIO_LAYOUT_(QuadraphonicLFE)), static_cast(SPEAKERS_4POINT1)); + obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_SOURCE_AUDIO_LAYOUT_(Surround)), static_cast(SPEAKERS_5POINT1)); + obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_SOURCE_AUDIO_LAYOUT_(FullSurround)), static_cast(SPEAKERS_7POINT1)); + } + + return pr; +} + +bool mirror_factory::on_manual_open(obs_properties_t* props, obs_property_t* property, void* data) +{ + try { + streamfx::open_url(HELP_URL); + return false; + } catch (const std::exception& ex) { + D_LOG_ERROR("Failed to open manual due to error: %s", ex.what()); + return false; + } catch (...) { + D_LOG_ERROR("Failed to open manual due to unknown error.", ""); + return false; + } +} + +std::shared_ptr mirror_factory::instance() +{ + static std::weak_ptr winst; + static std::mutex mtx; + + std::unique_lock lock(mtx); + auto instance = winst.lock(); + if (!instance) { + instance = std::shared_ptr(new mirror_factory()); + winst = instance; + } + return instance; +} + +static std::shared_ptr loader_instance; + +static auto loader = streamfx::component( + "source_mirror", + []() { // Initializer + loader_instance = mirror_factory::instance(); + }, + []() { // Finalizer + loader_instance.reset(); + }, + {"core::source_tracker"}); diff --git a/components/mirror/source/sources/source-mirror.hpp b/components/mirror/source/sources/source-mirror.hpp new file mode 100644 index 0000000..865f63d --- /dev/null +++ b/components/mirror/source/sources/source-mirror.hpp @@ -0,0 +1,93 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "common.hpp" +#include "gfx/gfx-source-texture.hpp" +#include "obs/gs/gs-texrender.hpp" +#include "obs/gs/gs-sampler.hpp" +#include "obs/obs-signal-handler.hpp" +#include "obs/obs-source-active-child.hpp" +#include "obs/obs-source-factory.hpp" +#include "obs/obs-source.hpp" +#include "obs/obs-tools.hpp" + +#include "warning-disable.hpp" +#include +#include +#include +#include +#include +#include "warning-enable.hpp" + +namespace streamfx::source::mirror { + struct mirror_audio_data { + mirror_audio_data(const audio_data*, speaker_layout); + + obs_source_audio osa; + std::vector> data; + }; + + class mirror_instance : public obs::source_instance { + // Source + ::streamfx::obs::source _source; + std::shared_ptr<::streamfx::obs::source_active_child> _source_child; + std::shared_ptr _signal_rename; + std::shared_ptr _signal_audio; + std::pair _source_size; + + // Audio + bool _audio_enabled; + speaker_layout _audio_layout; + std::mutex _audio_queue_lock; + std::queue _audio_queue; + + public: + mirror_instance(obs_data_t* settings, obs_source_t* self); + virtual ~mirror_instance(); + + virtual uint32_t get_width() override; + virtual uint32_t get_height() override; + + virtual void load(obs_data_t*) override; + virtual void migrate(obs_data_t*, uint64_t) override; + virtual void update(obs_data_t*) override; + virtual void save(obs_data_t*) override; + + virtual void video_tick(float) override; + virtual void video_render(gs_effect_t*) override; + + virtual void enum_active_sources(obs_source_enum_proc_t, void*) override; + virtual void enum_all_sources(obs_source_enum_proc_t, void*) override; + + private: + void acquire(std::string source_name); + void release(); + + void on_audio(::streamfx::obs::source, const struct audio_data*, bool); + + void audio_output(std::shared_ptr data); + }; + + class mirror_factory : public obs::source_factory { + public: + mirror_factory(); + virtual ~mirror_factory() override; + + virtual const char* get_name() override; + + virtual void get_defaults2(obs_data_t* data) override; + + virtual obs_properties_t* get_properties2(source::mirror::mirror_instance* data) override; + + static bool on_manual_open(obs_properties_t* props, obs_property_t* property, void* data); + + public: // Singleton + static void initialize(); + + static void finalize(); + + static std::shared_ptr instance(); + }; +} // namespace streamfx::source::mirror diff --git a/components/nvidia/CMakeLists.txt b/components/nvidia/CMakeLists.txt new file mode 100644 index 0000000..f0565c0 --- /dev/null +++ b/components/nvidia/CMakeLists.txt @@ -0,0 +1,48 @@ +# AUTOGENERATED COPYRIGHT HEADER START +# Copyright (C) 2023 Michael Fabian 'Xaymar' Dirks +# AUTOGENERATED COPYRIGHT HEADER END + +cmake_minimum_required(VERSION 3.26) +project("NVIDIA") +list(APPEND CMAKE_MESSAGE_INDENT "[${PROJECT_NAME}] ") + +#- NVIDIA Audio Effects SDK +if(NOT TARGET NVIDIA::AFX) + add_library(NVIDIA::AFX IMPORTED INTERFACE) + target_include_directories(NVIDIA::AFX + INTERFACE + "${StreamFX_SOURCE_DIR}/third-party/nvidia-maxine-afx-sdk/nvafx/include/" + ) +endif() + +#- NVIDIA Augmented Reality SDK +if(NOT TARGET NVIDIA::AR) + add_library(NVIDIA::AR IMPORTED INTERFACE) + target_include_directories(NVIDIA::AR + INTERFACE + "${StreamFX_SOURCE_DIR}/third-party/nvidia-maxine-ar-sdk/nvar/include/" + "${StreamFX_SOURCE_DIR}/third-party/nvidia-maxine-ar-sdk/nvar/src/" + ) +endif() + +#- NVIDIA Video Effects SDK +if(NOT TARGET NVIDIA::VFX) + add_library(NVIDIA::VFX IMPORTED INTERFACE) + target_include_directories(NVIDIA::VFX + INTERFACE + "${StreamFX_SOURCE_DIR}/third-party/nvidia-maxine-vfx-sdk/nvvfx/include/" + "${StreamFX_SOURCE_DIR}/third-party/nvidia-maxine-vfx-sdk/nvvfx/src/" + ) +endif() + +streamfx_add_component("NVIDIA") +target_link_libraries(${COMPONENT_TARGET} + PRIVATE + NVIDIA::AFX + NVIDIA::AR + NVIDIA::VFX +) + +if(NOT D_PLATFORM_WINDOWS) + streamfx_disable_component("NVIDIA" REASON "NVIDIA integration is (currently) only available for Windows under Direct3D11.") +endif() diff --git a/components/nvidia/include/nvidia/ar/nvidia-ar-facedetection.hpp b/components/nvidia/include/nvidia/ar/nvidia-ar-facedetection.hpp new file mode 100644 index 0000000..478083a --- /dev/null +++ b/components/nvidia/include/nvidia/ar/nvidia-ar-facedetection.hpp @@ -0,0 +1,54 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2021-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "nvidia-ar-feature.hpp" +#include "nvidia/cuda/nvidia-cuda-gs-texture.hpp" +#include "nvidia/cuda/nvidia-cuda-obs.hpp" +#include "nvidia/cuda/nvidia-cuda.hpp" +#include "nvidia/cv/nvidia-cv-image.hpp" +#include "nvidia/cv/nvidia-cv-texture.hpp" +#include "obs/gs/gs-texture.hpp" + +namespace streamfx::nvidia::ar { + class facedetection : public feature { + std::shared_ptr<::streamfx::nvidia::cv::texture> _input; + std::shared_ptr<::streamfx::nvidia::cv::image> _source; + std::shared_ptr<::streamfx::nvidia::cv::image> _tmp; + + std::vector _rects; + std::vector _rects_confidence; + bounds_t _bboxes; + + bool _dirty; + + public: + ~facedetection(); + + /** Create a new face detection feature. + * + * Must be in a graphics and CUDA context when calling. + */ + facedetection(); + + std::pair tracking_limit_range(); + + size_t tracking_limit(); + + void set_tracking_limit(size_t v); + + void process(std::shared_ptr<::streamfx::obs::gs::texture> in); + + size_t count(); + + rect_t const& at(size_t index); + + rect_t const& at(size_t index, float& confidence); + + private: + void resize(uint32_t width, uint32_t height); + + void load(); + }; +} // namespace streamfx::nvidia::ar diff --git a/components/nvidia/include/nvidia/ar/nvidia-ar-feature.hpp b/components/nvidia/include/nvidia/ar/nvidia-ar-feature.hpp new file mode 100644 index 0000000..4e63009 --- /dev/null +++ b/components/nvidia/include/nvidia/ar/nvidia-ar-feature.hpp @@ -0,0 +1,207 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "nvidia/ar/nvidia-ar.hpp" +#include "nvidia/cuda/nvidia-cuda-obs.hpp" +#include "nvidia/cv/nvidia-cv-image.hpp" +#include "nvidia/cv/nvidia-cv-texture.hpp" +#include "nvidia/cv/nvidia-cv.hpp" + +#include "warning-disable.hpp" +#include +#include "warning-enable.hpp" + +namespace streamfx::nvidia::ar { + class feature { + protected: + std::shared_ptr<::streamfx::nvidia::cuda::obs> _nvcuda; + std::shared_ptr<::streamfx::nvidia::cv::cv> _nvcv; + std::shared_ptr<::streamfx::nvidia::ar::ar> _nvar; + std::shared_ptr _fx; + std::u8string _model_path; + + public: + ~feature(); + feature(feature_t feature); + + ::streamfx::nvidia::ar::handle_t get() + { + return _fx.get(); + } + + public /* Int32 */: + inline cv::result set_uint32(parameter_t param, uint32_t const value) + { + return _nvar->NvAR_SetU32(_fx.get(), param, value); + } + inline cv::result get_uint32(parameter_t param, uint32_t* value) + { + return _nvar->NvAR_GetU32(_fx.get(), param, value); + } + + inline cv::result set_int32(parameter_t param, int32_t const value) + { + return _nvar->NvAR_SetS32(_fx.get(), param, value); + } + inline cv::result get_int32(parameter_t param, int32_t* value) + { + return _nvar->NvAR_GetS32(_fx.get(), param, value); + } + + public /* Int64 */: + inline cv::result set_uint64(parameter_t param, uint64_t const value) + { + return _nvar->NvAR_SetU64(_fx.get(), param, value); + } + inline cv::result get_uint64(parameter_t param, uint64_t* value) + { + return _nvar->NvAR_GetU64(_fx.get(), param, value); + } + + public /* Float32 */: + inline cv::result set_float32(parameter_t param, float const value) + { + return _nvar->NvAR_SetF32(_fx.get(), param, value); + } + inline cv::result get_float32(parameter_t param, float* value) + { + return _nvar->NvAR_GetF32(_fx.get(), param, value); + } + + inline cv::result set_float32array(parameter_t param, float* const value, int32_t size) + { + return _nvar->NvAR_SetF32Array(_fx.get(), param, value, static_cast(size)); + } + inline cv::result get_float32array(parameter_t param, const float* value, int32_t size) + { + return _nvar->NvAR_GetF32Array(_fx.get(), param, &value, &size); + } + + inline cv::result set_float32array(parameter_t param, std::vector const& value) + { + return _nvar->NvAR_SetF32Array(_fx.get(), param, value.data(), static_cast(value.size())); + } + inline cv::result get_float32array(parameter_t param, std::vector& value) + { + const float* data; + int32_t size; + cv::result result; + + result = _nvar->NvAR_GetF32Array(_fx.get(), param, &data, &size); + + value.resize(static_cast(size)); + memcpy(value.data(), data, size * sizeof(float)); + + return result; + } + + public /* Float64 */: + inline cv::result set_float64(parameter_t param, double const value) + { + return _nvar->NvAR_SetF64(_fx.get(), param, value); + } + inline cv::result get_float64(parameter_t param, double* value) + { + return _nvar->NvAR_GetF64(_fx.get(), param, value); + } + + public /* String */: + inline cv::result set_string(parameter_t param, const char* const value) + { + return _nvar->NvAR_SetString(_fx.get(), param, value); + }; + inline cv::result get_string(parameter_t param, const char*& value) + { + return _nvar->NvAR_GetString(_fx.get(), param, &value); + }; + + inline cv::result set_string(parameter_t param, const char8_t* const value) + { + return _nvar->NvAR_SetString(_fx.get(), param, reinterpret_cast(value)); + }; + inline cv::result get_string(parameter_t param, const char8_t*& value) + { + return _nvar->NvAR_GetString(_fx.get(), param, reinterpret_cast(&value)); + }; + + inline cv::result set_string(parameter_t param, std::string_view const value) + { + return _nvar->NvAR_SetString(_fx.get(), param, value.data()); + }; + cv::result get(parameter_t param, std::string_view& value); + + inline cv::result set_string(parameter_t param, std::string const& value) + { + return _nvar->NvAR_SetString(_fx.get(), param, value.c_str()); + }; + cv::result get(parameter_t param, std::string& value); + + inline cv::result set_string(parameter_t param, std::u8string const& value) + { + return _nvar->NvAR_SetString(_fx.get(), param, reinterpret_cast(value.c_str())); + }; + cv::result get(parameter_t param, std::u8string& value); + + public /* CUDA Stream */: + inline cv::result set_cuda_stream(parameter_t param, cuda::stream_t const value) + { + return _nvar->NvAR_SetCudaStream(_fx.get(), param, value); + }; + inline cv::result get_cuda_stream(parameter_t param, cuda::stream_t& value) + { + return _nvar->NvAR_GetCudaStream(_fx.get(), param, &value); + }; + + inline cv::result set_cuda_stream(parameter_t param, std::shared_ptr<::streamfx::nvidia::cuda::stream> const value) + { + return _nvar->NvAR_SetCudaStream(_fx.get(), param, value->get()); + } + //inline cv::result get(parameter_t param, std::shared_ptr<::streamfx::nvidia::cuda::stream> value); + + public /* CV Image */: + inline cv::result set_image(parameter_t param, cv::image_t& value) + { + return _nvar->NvAR_SetObject(_fx.get(), param, &value, sizeof(cv::image_t)); + }; + inline cv::result get_image(parameter_t param, cv::image_t*& value) + { + return _nvar->NvAR_GetObject(_fx.get(), param, reinterpret_cast(&value), sizeof(cv::image_t)); + }; + + inline cv::result set_image(parameter_t param, std::shared_ptr const value) + { + return _nvar->NvAR_SetObject(_fx.get(), param, value->get_image(), sizeof(cv::image_t)); + }; + //inline cv::result get(parameter_t param, std::shared_ptr& value); + + public /* CV Texture */: + inline cv::result set_image(parameter_t param, std::shared_ptr const value) + { + return _nvar->NvAR_SetObject(_fx.get(), param, value->get_image(), sizeof(cv::image_t)); + }; + //inline cv::result get(parameter_t param, std::shared_ptr& value); + + public /* Objects */: + inline cv::result set_object(parameter_t param, void* const data, size_t size) + { + return _nvar->NvAR_SetObject(_fx.get(), param, data, static_cast(size)); + } + inline cv::result get_object(parameter_t param, void*& data, size_t size) + { + return _nvar->NvAR_GetObject(_fx.get(), param, &data, static_cast(size)); + } + + public /* Control */: + inline cv::result load() + { + return _nvar->NvAR_Load(_fx.get()); + } + + inline cv::result run() + { + return _nvar->NvAR_Run(_fx.get()); + } + }; +} // namespace streamfx::nvidia::ar diff --git a/components/nvidia/include/nvidia/ar/nvidia-ar.hpp b/components/nvidia/include/nvidia/ar/nvidia-ar.hpp new file mode 100644 index 0000000..63cb539 --- /dev/null +++ b/components/nvidia/include/nvidia/ar/nvidia-ar.hpp @@ -0,0 +1,165 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "nvidia/cv/nvidia-cv.hpp" + +#include "warning-disable.hpp" +#include +#include "warning-enable.hpp" + +#define P_NVAR_DEFINE_FUNCTION(name, ...) \ + private: \ + typedef ::streamfx::nvidia::cv::result (*t##name)(__VA_ARGS__); \ + \ + public: \ + t##name name = nullptr; + +#define P_NVAR_INPUT "NvAR_Parameter_Input_" +#define P_NVAR_OUTPUT "NvAR_Parameter_Output_" +#define P_NVAR_CONFIG "NvAR_Parameter_Config_" + +/* + * Config Parameters: + * P_NVAR_CONFIG "BatchSize" + * P_NVAR_CONFIG "UseCudaGraph" + * P_NVAR_CONFIG "CUDAStream" + * P_NVAR_CONFIG "ExpressionCount" + * P_NVAR_CONFIG "FeatureDescription" + * P_NVAR_CONFIG "FocalLength" + * P_NVAR_CONFIG "GPU" + * P_NVAR_CONFIG "Landmarks_Size" + * P_NVAR_CONFIG "LandmarksConfidence_Size" + * P_NVAR_CONFIG "Mode" + * P_NVAR_CONFIG "TRTModelDir" + * P_NVAR_CONFIG "ModelDir" + * P_NVAR_CONFIG "ModelName" + * P_NVAR_CONFIG "NumKeyPoints" + * P_NVAR_CONFIG "ReferencePose" + * P_NVAR_CONFIG "ShapeEigenValueCount" + * P_NVAR_CONFIG "Temporal" + * P_NVAR_CONFIG "TriangleCount" + * P_NVAR_CONFIG "VertexCount" + * + * Input Parameters: + * P_NVAR_INPUT "Image" + * P_NVAR_INPUT "Width" + * P_NVAR_INPUT "Height" + * P_NVAR_INPUT "BoundingBoxes" + * P_NVAR_INPUT "BoundingBoxesConfidence" + * P_NVAR_INPUT "Landmarks" + * + * Output Parameters + * P_NVAR_OUTPUT "BoundingBoxes" + * P_NVAR_OUTPUT "BoundingBoxesConfidence" + * P_NVAR_OUTPUT "ExpressionCoefficients" + * P_NVAR_OUTPUT "FaceMesh" + * P_NVAR_OUTPUT "JointAngles" + * P_NVAR_OUTPUT "KeyPoints" + * P_NVAR_OUTPUT "KeyPoints3D" + * P_NVAR_OUTPUT "KeyPointsConfidence" + * P_NVAR_OUTPUT "Landmarks" + * P_NVAR_OUTPUT "LandmarksConfidence" + * P_NVAR_OUTPUT "Pose" + * P_NVAR_OUTPUT "RenderingParams" + * P_NVAR_OUTPUT "ShapeEigenValues" + */ + +namespace streamfx::nvidia::ar { + typedef const char* feature_t; + typedef const char* parameter_t; + typedef void* object_t; + typedef void* handle_t; + + static constexpr feature_t FEATURE_BODY_DETECTION = "BodyDetection"; + static constexpr feature_t FEATURE_BODY_POSE_ESTIMATION = "BodyPoseEstimation"; + static constexpr feature_t FEATURE_FACE_DETECTION = "FaceDetection"; + static constexpr feature_t FEATURE_FACE_BOX_DETECTION = "FaceBoxDetection"; + static constexpr feature_t FEATURE_FACE_RECONSTRUCTION = "Face3DReconstruction"; + static constexpr feature_t FEATURE_LANDMARK_DETECTION = "LandMarkDetection"; + + template + struct vec2 { + T x; + T y; + }; + + template + struct vec3 : public vec2 { + T z; + }; + + template + struct vec4 : public vec3 { + T w; + }; + + typedef vec2 point_t; + typedef vec4 frustum_t; + typedef vec4 quaternion_t; + typedef vec4 rect_t; + + struct bounds_t { + rect_t* rects; + uint8_t current; + uint8_t maximum; + }; + + struct face_mesh_t { + vec3* vertices; + size_t num_vertices; + vec3 indices; + size_t num_indices; + }; + + struct rendering_params_t { + frustum_t frustum; + quaternion_t rotation; + vec3 translation; + }; + + class ar { + std::shared_ptr<::streamfx::util::library> _library; + std::filesystem::path _model_path; +#ifdef WIN32 + void* _extra; +#endif + + public: + ~ar(); + ar(); + + std::filesystem::path const& get_model_path(); + + public: + P_NVAR_DEFINE_FUNCTION(NvAR_GetVersion, uint32_t* version); + + P_NVAR_DEFINE_FUNCTION(NvAR_Create, feature_t feature_id, handle_t* ptr); + P_NVAR_DEFINE_FUNCTION(NvAR_Destroy, handle_t ptr); + P_NVAR_DEFINE_FUNCTION(NvAR_Run, handle_t ptr); + P_NVAR_DEFINE_FUNCTION(NvAR_Load, handle_t ptr); + + P_NVAR_DEFINE_FUNCTION(NvAR_GetS32, handle_t ptr, parameter_t parameter, int32_t* value); + P_NVAR_DEFINE_FUNCTION(NvAR_SetS32, handle_t ptr, parameter_t parameter, int32_t value); + P_NVAR_DEFINE_FUNCTION(NvAR_GetU32, handle_t ptr, parameter_t parameter, uint32_t* value); + P_NVAR_DEFINE_FUNCTION(NvAR_SetU32, handle_t ptr, parameter_t parameter, uint32_t value); + P_NVAR_DEFINE_FUNCTION(NvAR_GetU64, handle_t ptr, parameter_t parameter, uint64_t* value); + P_NVAR_DEFINE_FUNCTION(NvAR_SetU64, handle_t ptr, parameter_t parameter, uint64_t value); + P_NVAR_DEFINE_FUNCTION(NvAR_GetF32, handle_t ptr, parameter_t parameter, float* value); + P_NVAR_DEFINE_FUNCTION(NvAR_SetF32, handle_t ptr, parameter_t parameter, float value); + P_NVAR_DEFINE_FUNCTION(NvAR_GetF64, handle_t ptr, parameter_t parameter, double* value); + P_NVAR_DEFINE_FUNCTION(NvAR_SetF64, handle_t ptr, parameter_t parameter, double value); + P_NVAR_DEFINE_FUNCTION(NvAR_GetString, handle_t ptr, parameter_t parameter, const char** value); + P_NVAR_DEFINE_FUNCTION(NvAR_SetString, handle_t ptr, parameter_t parameter, const char* value); + P_NVAR_DEFINE_FUNCTION(NvAR_GetCudaStream, handle_t ptr, parameter_t parameter, ::streamfx::nvidia::cuda::stream_t* value); + P_NVAR_DEFINE_FUNCTION(NvAR_SetCudaStream, handle_t ptr, parameter_t parameter, ::streamfx::nvidia::cuda::stream_t value); + P_NVAR_DEFINE_FUNCTION(NvAR_GetObject, handle_t ptr, parameter_t parameter, object_t* value, uint32_t size); + P_NVAR_DEFINE_FUNCTION(NvAR_SetObject, handle_t ptr, parameter_t parameter, object_t value, uint32_t size); + P_NVAR_DEFINE_FUNCTION(NvAR_GetF32Array, handle_t ptr, parameter_t parameter, const float** values, int32_t* size); + P_NVAR_DEFINE_FUNCTION(NvAR_SetF32Array, handle_t ptr, parameter_t parameter, const float* values, int32_t size); + + public: + static std::shared_ptr<::streamfx::nvidia::ar::ar> get(); + }; +} // namespace streamfx::nvidia::ar diff --git a/components/nvidia/include/nvidia/cuda/nvidia-cuda-context.hpp b/components/nvidia/include/nvidia/cuda/nvidia-cuda-context.hpp new file mode 100644 index 0000000..0a85942 --- /dev/null +++ b/components/nvidia/include/nvidia/cuda/nvidia-cuda-context.hpp @@ -0,0 +1,57 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2022 lainon +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "nvidia-cuda.hpp" + +#include "warning-disable.hpp" +#include +#include "warning-enable.hpp" + +namespace streamfx::nvidia::cuda { + class context_stack; + + class context : public std::enable_shared_from_this<::streamfx::nvidia::cuda::context> { + std::shared_ptr<::streamfx::nvidia::cuda::cuda> _cuda; + ::streamfx::nvidia::cuda::context_t _ctx; + bool _has_device; + ::streamfx::nvidia::cuda::device_t _device; + + public: + ~context(); + + private: + context(); + + public: +#ifdef WIN32 + context(ID3D11Device* device); +#endif + + ::streamfx::nvidia::cuda::context_t get(); + + void push(); + void pop(); + + void synchronize(); + + public: + std::shared_ptr<::streamfx::nvidia::cuda::context_stack> enter(); + }; + + class context_stack { + std::shared_ptr<::streamfx::nvidia::cuda::context> _ctx; + + public: + inline ~context_stack() + { + _ctx->pop(); + } + inline context_stack(std::shared_ptr<::streamfx::nvidia::cuda::context> ctx) : _ctx(std::move(ctx)) + { + _ctx->push(); + } + }; +} // namespace streamfx::nvidia::cuda diff --git a/components/nvidia/include/nvidia/cuda/nvidia-cuda-gs-texture.hpp b/components/nvidia/include/nvidia/cuda/nvidia-cuda-gs-texture.hpp new file mode 100644 index 0000000..d091337 --- /dev/null +++ b/components/nvidia/include/nvidia/cuda/nvidia-cuda-gs-texture.hpp @@ -0,0 +1,35 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "nvidia-cuda-stream.hpp" +#include "nvidia-cuda.hpp" +#include "obs/gs/gs-texture.hpp" + +#include "warning-disable.hpp" +#include +#include +#include "warning-enable.hpp" + +namespace streamfx::nvidia::cuda { + class gstexture { + std::shared_ptr<::streamfx::nvidia::cuda::cuda> _cuda; + std::shared_ptr _texture; + graphics_resource_t _resource; + + bool _is_mapped; + array_t _pointer; + std::shared_ptr _stream; + + public: + ~gstexture(); + gstexture(std::shared_ptr texture); + + array_t map(std::shared_ptr stream); + void unmap(); + + std::shared_ptr get_texture(); + ::streamfx::nvidia::cuda::graphics_resource_t get(); + }; +} // namespace streamfx::nvidia::cuda diff --git a/components/nvidia/include/nvidia/cuda/nvidia-cuda-memory.hpp b/components/nvidia/include/nvidia/cuda/nvidia-cuda-memory.hpp new file mode 100644 index 0000000..44157e9 --- /dev/null +++ b/components/nvidia/include/nvidia/cuda/nvidia-cuda-memory.hpp @@ -0,0 +1,27 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "nvidia-cuda.hpp" + +#include "warning-disable.hpp" +#include +#include +#include "warning-enable.hpp" + +namespace streamfx::nvidia::cuda { + class memory { + std::shared_ptr<::streamfx::nvidia::cuda::cuda> _cuda; + device_ptr_t _pointer; + size_t _size; + + public: + ~memory(); + memory(size_t size); + + device_ptr_t get(); + + std::size_t size(); + }; +} // namespace streamfx::nvidia::cuda diff --git a/components/nvidia/include/nvidia/cuda/nvidia-cuda-obs.hpp b/components/nvidia/include/nvidia/cuda/nvidia-cuda-obs.hpp new file mode 100644 index 0000000..23f3bd8 --- /dev/null +++ b/components/nvidia/include/nvidia/cuda/nvidia-cuda-obs.hpp @@ -0,0 +1,31 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "nvidia-cuda-context.hpp" +#include "nvidia-cuda-stream.hpp" +#include "nvidia-cuda.hpp" + +#include "warning-disable.hpp" +#include +#include "warning-enable.hpp" + +namespace streamfx::nvidia::cuda { + class obs { + std::shared_ptr<::streamfx::nvidia::cuda::cuda> _cuda; + std::shared_ptr<::streamfx::nvidia::cuda::context> _context; + std::shared_ptr<::streamfx::nvidia::cuda::stream> _stream; + + public: + ~obs(); + obs(); + + std::shared_ptr<::streamfx::nvidia::cuda::cuda> get_cuda(); + std::shared_ptr<::streamfx::nvidia::cuda::context> get_context(); + std::shared_ptr<::streamfx::nvidia::cuda::stream> get_stream(); + + public: + static std::shared_ptr<::streamfx::nvidia::cuda::obs> get(); + }; +} // namespace streamfx::nvidia::cuda diff --git a/components/nvidia/include/nvidia/cuda/nvidia-cuda-stream.hpp b/components/nvidia/include/nvidia/cuda/nvidia-cuda-stream.hpp new file mode 100644 index 0000000..941fb7f --- /dev/null +++ b/components/nvidia/include/nvidia/cuda/nvidia-cuda-stream.hpp @@ -0,0 +1,25 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "nvidia-cuda.hpp" + +#include "warning-disable.hpp" +#include +#include "warning-enable.hpp" + +namespace streamfx::nvidia::cuda { + class stream { + std::shared_ptr<::streamfx::nvidia::cuda::cuda> _cuda; + ::streamfx::nvidia::cuda::stream_t _stream; + + public: + ~stream(); + stream(::streamfx::nvidia::cuda::stream_flags flags = ::streamfx::nvidia::cuda::stream_flags::DEFAULT, int32_t priority = 0); + + ::streamfx::nvidia::cuda::stream_t get(); + + void synchronize(); + }; +} // namespace streamfx::nvidia::cuda diff --git a/components/nvidia/include/nvidia/cuda/nvidia-cuda.hpp b/components/nvidia/include/nvidia/cuda/nvidia-cuda.hpp new file mode 100644 index 0000000..0e2b3cd --- /dev/null +++ b/components/nvidia/include/nvidia/cuda/nvidia-cuda.hpp @@ -0,0 +1,339 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "util/util-bitmask.hpp" +#include "util/util-library.hpp" + +#include "warning-disable.hpp" +#include +#include +#include "warning-enable.hpp" + +#ifdef WIN32 +#include "warning-disable.hpp" +#include +#include +#include "warning-enable.hpp" +#endif + +#define P_CUDA_DEFINE_FUNCTION(name, ...) \ + private: \ + typedef ::streamfx::nvidia::cuda::result (*t##name)(__VA_ARGS__); \ + \ + public: \ + t##name name = nullptr; + +namespace streamfx::nvidia::cuda { + enum class result : std::size_t { + SUCCESS = 0, + INVALID_VALUE = 1, + OUT_OF_MEMORY = 2, + NOT_INITIALIZED = 3, + DEINITIALIZED = 4, + NO_DEVICE = 100, + INVALID_DEVICE = 101, + INVALID_CONTEXT = 201, + MAP_FAILED = 205, + UNMAP_FAILED = 206, + ARRAY_IS_MAPPED = 207, + ALREADY_MAPPED = 208, + NOT_MAPPED = 211, + INVALID_GRAPHICS_CONTEXT = 219, + // Still missing some. + }; + + enum class memory_type : uint32_t { + HOST = 1, + DEVICE = 2, + ARRAY = 3, + UNIFIED = 4, + }; + + enum class array_format : uint32_t { + UNSIGNED_INT8 = 0b00000001, + UNSIGNED_INT16 = 0b00000010, + UNSIGNED_INT32 = 0b00000011, + SIGNED_INT8 = 0b00001000, + SIGNED_INT16 = 0b00001001, + SIGNED_INT32 = 0b00001010, + HALF = 0b00010000, + FLOAT = 0b00100000, + }; + + enum class context_flags : uint32_t { + SCHEDULER_AUTO = 0x0, + SCHEDULER_SPIN = 0x1, + SCHEDULER_YIELD = 0x2, + SCHEDULER_BLOCKING_SYNC = 0x4, + MAP_HOST = 0x8, + LOCAL_MEMORY_RESIZE_TO_MAXIMUM = 0x10, + }; + + enum class external_memory_handle_type : uint32_t { + INVALID = 0, + FILE_DESCRIPTOR = 1, + WIN32_SHARED_HANDLE = 2, + WIN32_GLOBAL_SHARED_HANDLE = 3, + D3D12_HEAP = 4, + D3D12_RESOURCE = 5, + D3D11_SHARED_RESOURCE = 6, + D3D11_GLOBAL_SHARED_RESOURCE = 7, + NVSCIBUF = 8, + }; + + enum class stream_flags : uint32_t { + DEFAULT = 0x0, + NON_BLOCKING = 0x1, + }; + + typedef void* array_t; + typedef void* context_t; + typedef uint64_t device_ptr_t; + typedef void* external_memory_t; + typedef void* graphics_resource_t; + typedef void* stream_t; + typedef int32_t device_t; + + struct memcpy2d_v2_t { + std::size_t src_x_in_bytes; + std::size_t src_y; + + memory_type src_memory_type; + const void* src_host; + device_ptr_t src_device; + array_t src_array; + std::size_t src_pitch; + + std::size_t dst_x_in_bytes; + std::size_t dst_y; + + memory_type dst_memory_type; + const void* dst_host; + device_ptr_t dst_device; + array_t dst_array; + std::size_t dst_pitch; + + std::size_t width_in_bytes; + std::size_t height; + }; + + struct array_descriptor_v2_t { + std::size_t width; + std::size_t height; + uint32_t num_channels; + array_format format; + }; + + struct external_memory_buffer_info_v1_t { + uint64_t offset; + uint64_t size; + uint32_t flags; + uint32_t reserved[16]; + }; + + struct external_memory_handle_info_v1_t { + external_memory_handle_type type; + union { + int32_t file; + struct { + void* handle; + const void* name; + }; + const void* nvscibuf; + }; + uint64_t size; + uint32_t flags; + uint32_t reserved[16]; + }; + + struct uuid_t { + union { + char bytes[16]; + struct { + uint32_t a; + uint16_t b; + uint16_t c; + uint16_t d; + uint16_t e; + uint32_t f; + } uuid; + }; + }; + + struct luid_t { + union { + char bytes[8]; + struct { + uint32_t low; + int32_t high; + } parts; + uint64_t luid; + }; + }; + + class cuda_error : public std::exception { + ::streamfx::nvidia::cuda::result _code; + + public: + ~cuda_error(){}; + cuda_error(::streamfx::nvidia::cuda::result code) : _code(code) {} + + ::streamfx::nvidia::cuda::result code() + { + return _code; + } + }; + + class cuda { + std::shared_ptr _library; + + public: + ~cuda(); + cuda(); + + int32_t version(); + + public: + // Initialization + P_CUDA_DEFINE_FUNCTION(cuInit, int32_t flags); + + // Version Management + P_CUDA_DEFINE_FUNCTION(cuDriverGetVersion, int32_t* driverVersion); + + // Device Management + P_CUDA_DEFINE_FUNCTION(cuDeviceGetName, char* name, int32_t length, device_t device); + P_CUDA_DEFINE_FUNCTION(cuDeviceGetLuid, luid_t* luid, uint32_t* device_node_mask, device_t device); + P_CUDA_DEFINE_FUNCTION(cuDeviceGetUuid, uuid_t* uuid, device_t device); + // - Not yet needed. + + // Primary Context Management + P_CUDA_DEFINE_FUNCTION(cuDevicePrimaryCtxRelease, device_t device); + P_CUDA_DEFINE_FUNCTION(cuDevicePrimaryCtxRetain, context_t* ctx, device_t device); + P_CUDA_DEFINE_FUNCTION(cuDevicePrimaryCtxSetFlags, device_t device, context_flags flags); + + // Context Management + P_CUDA_DEFINE_FUNCTION(cuCtxCreate, context_t* ctx, context_flags flags, device_t device); + P_CUDA_DEFINE_FUNCTION(cuCtxDestroy, context_t ctx); + P_CUDA_DEFINE_FUNCTION(cuCtxGetCurrent, context_t* ctx); + P_CUDA_DEFINE_FUNCTION(cuCtxGetStreamPriorityRange, int32_t* lowestPriority, int32_t* highestPriority); + P_CUDA_DEFINE_FUNCTION(cuCtxPopCurrent, context_t* ctx); + P_CUDA_DEFINE_FUNCTION(cuCtxPushCurrent, context_t ctx); + P_CUDA_DEFINE_FUNCTION(cuCtxSetCurrent, context_t ctx); + P_CUDA_DEFINE_FUNCTION(cuCtxSynchronize); + + // Module Management + // - Not yet needed. + + // Memory Management + P_CUDA_DEFINE_FUNCTION(cuArrayGetDescriptor, array_descriptor_v2_t* pArrayDescripter, array_t array); + P_CUDA_DEFINE_FUNCTION(cuMemAlloc, device_ptr_t* ptr, std::size_t bytes); + P_CUDA_DEFINE_FUNCTION(cuMemAllocPitch, device_ptr_t* ptr, std::size_t* pitch, std::size_t width_in_bytes, std::size_t height, uint32_t element_size_bytes); + P_CUDA_DEFINE_FUNCTION(cuMemFree, device_ptr_t ptr); + P_CUDA_DEFINE_FUNCTION(cuMemHostGetDevicePointer, device_ptr_t* devptr, void* ptr, uint32_t flags); + P_CUDA_DEFINE_FUNCTION(cuMemcpy, device_ptr_t dst, device_ptr_t src, std::size_t bytes); + P_CUDA_DEFINE_FUNCTION(cuMemcpy2D, const memcpy2d_v2_t* copy); + P_CUDA_DEFINE_FUNCTION(cuMemcpy2DAsync, const memcpy2d_v2_t* copy, stream_t stream); + P_CUDA_DEFINE_FUNCTION(cuMemcpyAtoA, array_t dst, std::size_t dstOffset, array_t src, std::size_t srcOffset, std::size_t byteCount); + P_CUDA_DEFINE_FUNCTION(cuMemcpyAtoD, device_ptr_t dst, array_t src, std::size_t srcOffset, std::size_t byteCount); + P_CUDA_DEFINE_FUNCTION(cuMemcpyAtoH, void* dst, array_t src, std::size_t srcOffset, std::size_t byteCount); + P_CUDA_DEFINE_FUNCTION(cuMemcpyAtoHAsync, void* dst, array_t src, std::size_t srcOffset, std::size_t byteCount); + P_CUDA_DEFINE_FUNCTION(cuMemcpyDtoA, array_t dst, std::size_t dstOffset, device_ptr_t src, std::size_t byteCount); + P_CUDA_DEFINE_FUNCTION(cuMemcpyDtoD, device_ptr_t dst, array_t srcArray, std::size_t byteCount); + P_CUDA_DEFINE_FUNCTION(cuMemcpyDtoH, void* dst, array_t src, std::size_t byteCount); + P_CUDA_DEFINE_FUNCTION(cuMemcpyDtoHAsync, void* dst, array_t src, std::size_t byteCount); + P_CUDA_DEFINE_FUNCTION(cuMemcpyHtoA, array_t dst, std::size_t dstOffset, void* src, std::size_t byteCount); + P_CUDA_DEFINE_FUNCTION(cuMemcpyHtoAAsync, array_t dst, std::size_t dstOffset, void* src, std::size_t byteCount); + P_CUDA_DEFINE_FUNCTION(cuMemcpyHtoD, device_ptr_t dst, void* src, std::size_t byteCount); + P_CUDA_DEFINE_FUNCTION(cuMemcpyHtoDAsync, device_ptr_t dst, void* src, std::size_t byteCount); + P_CUDA_DEFINE_FUNCTION(cuMemsetD8, device_ptr_t dst, uint8_t d, size_t byteCount); + P_CUDA_DEFINE_FUNCTION(cuMemsetD8Async, device_ptr_t dst, uint8_t d, size_t byteCount, stream_t stream); + P_CUDA_DEFINE_FUNCTION(cuMemsetD16, device_ptr_t dst, uint16_t d, size_t byteCount); + P_CUDA_DEFINE_FUNCTION(cuMemsetD16Async, device_ptr_t dst, uint16_t d, size_t byteCount, stream_t stream); + P_CUDA_DEFINE_FUNCTION(cuMemsetD32, device_ptr_t dst, uint32_t d, size_t byteCount); + P_CUDA_DEFINE_FUNCTION(cuMemsetD32Async, device_ptr_t dst, uint32_t d, size_t byteCount, stream_t stream); + + // Virtual Memory Management + // - Not yet needed. + + // Stream Ordered Memory Allocator + // - Not yet needed. + + // Unified Addressing + // - Not yet needed. + + // Stream Managment + P_CUDA_DEFINE_FUNCTION(cuStreamCreate, stream_t* stream, stream_flags flags); + P_CUDA_DEFINE_FUNCTION(cuStreamCreateWithPriority, stream_t* stream, stream_flags flags, int32_t priority); + P_CUDA_DEFINE_FUNCTION(cuStreamDestroy, stream_t stream); + P_CUDA_DEFINE_FUNCTION(cuStreamSynchronize, stream_t stream); + P_CUDA_DEFINE_FUNCTION(cuStreamGetPriority, stream_t stream, int32_t* priority); + + // Event Management + // - Not yet needed. + + // External Resource Interoperability (CUDA 11.1+) + // - Not yet needed. + + // Stream Memory Operations + // - Not yet needed. + + // Execution Control + // - Not yet needed. + + // Graph Management + // - Not yet needed. + + // Occupancy + // - Not yet needed. + + // Texture Object Management + // - Not yet needed. + + // Surface Object Management + // - Not yet needed. + + // Peer Context Memory Access + // - Not yet needed. + + // Graphics Interoperability + P_CUDA_DEFINE_FUNCTION(cuGraphicsMapResources, uint32_t count, graphics_resource_t* resources, stream_t stream); + P_CUDA_DEFINE_FUNCTION(cuGraphicsSubResourceGetMappedArray, array_t* array, graphics_resource_t resource, uint32_t index, uint32_t level); + P_CUDA_DEFINE_FUNCTION(cuGraphicsUnmapResources, uint32_t count, graphics_resource_t* resources, stream_t stream); + P_CUDA_DEFINE_FUNCTION(cuGraphicsUnregisterResource, graphics_resource_t resource); + + // Driver Entry Point Access + // - Not yet needed. + + // Profiler Control + // - Not yet needed. + + // OpenGL Interoperability + // - Not yet needed. + + // VDPAU Interoperability + // - Not yet needed. + + // EGL Interoperability + // - Not yet needed. + +#ifdef WIN32 + // Direct3D9 Interoperability + // - Not yet needed. + + // Direct3D10 Interoperability + P_CUDA_DEFINE_FUNCTION(cuD3D10GetDevice, device_t* device, IDXGIAdapter* adapter); + P_CUDA_DEFINE_FUNCTION(cuGraphicsD3D10RegisterResource, graphics_resource_t* resource, ID3D10Resource* d3dresource, uint32_t flags); + + // Direct3D11 Interoperability + P_CUDA_DEFINE_FUNCTION(cuD3D11GetDevice, device_t* device, IDXGIAdapter* adapter); + P_CUDA_DEFINE_FUNCTION(cuGraphicsD3D11RegisterResource, graphics_resource_t* resource, ID3D11Resource* d3dresource, uint32_t flags); +#endif + public: + static std::shared_ptr<::streamfx::nvidia::cuda::cuda> get(); + }; +} // namespace streamfx::nvidia::cuda + +P_ENABLE_BITMASK_OPERATORS(::streamfx::nvidia::cuda::context_flags) +P_ENABLE_BITMASK_OPERATORS(::streamfx::nvidia::cuda::stream_flags) diff --git a/components/nvidia/include/nvidia/cv/nvidia-cv-image.hpp b/components/nvidia/include/nvidia/cv/nvidia-cv-image.hpp new file mode 100644 index 0000000..349203c --- /dev/null +++ b/components/nvidia/include/nvidia/cv/nvidia-cv-image.hpp @@ -0,0 +1,40 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "nvidia/cv/nvidia-cv.hpp" + +#include "warning-disable.hpp" +#include +#include "warning-enable.hpp" + +namespace streamfx::nvidia::cv { + using ::streamfx::nvidia::cv::component_layout; + using ::streamfx::nvidia::cv::component_type; + using ::streamfx::nvidia::cv::memory_location; + using ::streamfx::nvidia::cv::pixel_format; + + class image { + protected: + std::shared_ptr<::streamfx::nvidia::cv::cv> _cv; + image_t _image; + uint32_t _alignment; + + public: + virtual ~image(); + + protected: + image(); + + public: + image(uint32_t width, uint32_t height, pixel_format pix_fmt, component_type cmp_type, component_layout cmp_layout, memory_location location, uint32_t alignment); + + virtual void reallocate(uint32_t width, uint32_t height, pixel_format pix_fmt, component_type cmp_type, component_layout cmp_layout, memory_location location, uint32_t alignment); + + virtual void resize(uint32_t width, uint32_t height); + + virtual ::streamfx::nvidia::cv::image_t* get_image(); + }; + +} // namespace streamfx::nvidia::cv diff --git a/components/nvidia/include/nvidia/cv/nvidia-cv-texture.hpp b/components/nvidia/include/nvidia/cv/nvidia-cv-texture.hpp new file mode 100644 index 0000000..611a46c --- /dev/null +++ b/components/nvidia/include/nvidia/cv/nvidia-cv-texture.hpp @@ -0,0 +1,36 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "nvidia/cv/nvidia-cv-image.hpp" +#include "obs/gs/gs-texture.hpp" + +#include "warning-disable.hpp" +#include +#include "warning-enable.hpp" + +namespace streamfx::nvidia::cv { + using ::streamfx::nvidia::cv::component_layout; + using ::streamfx::nvidia::cv::component_type; + using ::streamfx::nvidia::cv::image; + using ::streamfx::nvidia::cv::memory_location; + using ::streamfx::nvidia::cv::pixel_format; + + class texture : public image { + std::shared_ptr<::streamfx::obs::gs::texture> _texture; + + public: + ~texture() override; + texture(uint32_t width, uint32_t height, gs_color_format pix_fmt); + + void resize(uint32_t width, uint32_t height) override; + + std::shared_ptr<::streamfx::obs::gs::texture> get_texture(); + + private: + void alloc(); + void free(); + }; + +} // namespace streamfx::nvidia::cv diff --git a/components/nvidia/include/nvidia/cv/nvidia-cv.hpp b/components/nvidia/include/nvidia/cv/nvidia-cv.hpp new file mode 100644 index 0000000..41bfdf9 --- /dev/null +++ b/components/nvidia/include/nvidia/cv/nvidia-cv.hpp @@ -0,0 +1,261 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2021-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2022 lainon +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "nvidia/cuda/nvidia-cuda.hpp" +#include "util/util-bitmask.hpp" +#include "util/util-library.hpp" + +#include "warning-disable.hpp" +#include +#ifdef WIN32 +#include +#include +#endif +#include "warning-enable.hpp" + +#define NVCVI_DEFINE_FUNCTION(name, ...) \ + private: \ + typedef ::streamfx::nvidia::cv::result(__cdecl* t##name)(__VA_ARGS__); \ + \ + public: \ + t##name name = nullptr; + +#define NVCVI_DEFINE_FUNCTION_EX(ret, name, ...) \ + private: \ + typedef ret(__cdecl* t##name)(__VA_ARGS__); \ + \ + public: \ + t##name name = nullptr; + +namespace streamfx::nvidia::cv { + enum class result { + // NVIDIA uses negative codes, but we use positive. + SUCCESS = 0, + ERROR_GENERAL = -1, + ERROR_UNIMPLEMENTED = -2, + ERROR_MEMORY = -3, + ERROR_EFFECT = -4, + ERROR_SELECTOR = -5, + ERROR_BUFFER = -6, + ERROR_PARAMETER = -7, + ERROR_MISMATCH = -8, + ERROR_PIXELFORMAT = -9, + ERROR_MODEL = -10, + ERROR_LIBRARY = -11, + ERROR_INITIALIZATION = -12, + ERROR_FILE = -13, + ERROR_FEATURENOTFOUND = -14, + ERROR_MISSINGINPUT = -15, + ERROR_RESOLUTION = -16, + ERROR_UNSUPPORTEDGPU = -17, + ERROR_WRONGGPU = -18, + ERROR_UNSUPPORTEDDRIVER = -19, + ERROR_MODELDEPENDENCIES = -20, + ERROR_PARSE = -21, + ERROR_MODELSUBSTITUTION = -22, + ERROR_READ = -23, + ERROR_WRITE = -24, + ERROR_PARAMREADONLY = -25, + ERROR_TRT_ENQUEUE = -26, + ERROR_TRT_BINDINGS = -27, + ERROR_TRT_CONTEXT = -28, + ERROR_TRT_INFER = -29, + ERROR_TRT_ENGINE = -30, + ERROR_NPP = -31, + ERROR_CONFIG = -32, + + // Error from Graphics API + ERROR_DIRECT3D = -99, + + // Error from CUDA + ERROR_CUDA_BASE = -100, + ERROR_CUDA_VALUE = -101, + ERROR_CUDA_MEMORY = -102, + ERROR_CUDA_PITCH = -112, + ERROR_CUDA_INIT = -127, + ERROR_CUDA_LAUNCH = -819, + ERROR_CUDA_KERNEL = -309, + ERROR_CUDA_DRIVER = -135, + ERROR_CUDA_UNSUPPORTED = -901, + ERROR_CUDA_ILLEGAL_ADDRESS = -800, + ERROR_CUDA = -1099, + }; + + enum class pixel_format { + UNKNOWN = 0, + Y = 1, + A = 2, + YA = 3, + RGB = 4, + BGR = 5, + RGBA = 6, + BGRA = 7, + ARGB = 8, + ABGR = 9, + YUV420 = 10, + YUV422 = 11, + YUV444 = 12, + }; + + enum class component_type { + UKNOWN = 0, + UINT8 = 1, + UINT16 = 2, + SINT16 = 3, + FP16 = 4, + UINT32 = 5, + SINT = 6, + FP32 = 7, + UINT64 = 8, + SINT64 = 9, + FP64 = 10, + }; + + enum class component_layout { + INTERLEAVED = 0, + PLANAR = 1, + UYVY = 2, + YUV = 3, + VYUY = 4, + YVU = 5, + YUYV = 6, + YCUV = 7, + YVYU = 8, + YCVU = 9, + CYUV = 10, + _RESERVED11 = 11, + CYVU = 12, + CHUNKY = INTERLEAVED, + I420 = YUV, + IYUV = YUV, + YV12 = YVU, + NV12 = YCUV, + NV21 = YCVU, + YUY2 = YUYV, + I444 = YUV, + YM24 = YUV, + YM42 = YVU, + NV24 = YCUV, + NV42 = YCVU, + }; + + enum class color_information { + SPACE_BT_601 = 0x00, + SPACE_BT_709 = 0x01, + SPACE_BT_2020 = 0x02, + RANGE_PARTIAL = 0x00, + RANGE_FULL = 0x04, + CHROMA_LOCATION_COSITED = 0x00, + CHROMA_LOCATION_INTERSTITIAL = 0x08, + CHROMA_LOCATION_TOPLEFT = 0x10, + }; + + enum class memory_location { + CPU = 0, + GPU = 1, + CPU_PINNED = 2, + CUDA_ARRAY = 3, + }; + + struct image_t { + uint32_t width; + uint32_t height; + int32_t pitch; + pixel_format pxl_format; + component_type comp_type; + uint8_t pixel_bytes; + uint8_t component_bytes; + uint8_t num_components; + unsigned char comp_layout; + unsigned char mem_location; + unsigned char color_info; + uint8_t reserved[2]; + void* pixels; + void* delete_pointer; + void (*delete_function)(void* delete_pointer); + uint64_t buffer_bytes; + }; + + template + struct point { + T x, y; + }; + + template + struct rect { + T x, y; + T w, h; + }; + + class cv { + std::shared_ptr<::streamfx::util::library> _library; +#ifdef WIN32 + void* _extra; +#endif + + public: + ~cv(); + cv(); + + public: + NVCVI_DEFINE_FUNCTION(NvCVImage_Init, image_t* image, uint32_t width, uint32_t height, uint32_t pitch, void* pixels, pixel_format format, component_type comp_type, component_layout comp_layout, memory_location mem_location); + NVCVI_DEFINE_FUNCTION(NvCVImage_InitView, image_t* sub_image, image_t* image, int32_t x, int32_t y, uint32_t width, uint32_t height); + NVCVI_DEFINE_FUNCTION(NvCVImage_Alloc, image_t* image, uint32_t width, uint32_t height, pixel_format format, component_type comp_type, uint32_t comp_layout, uint32_t mem_location, uint32_t alignment); + NVCVI_DEFINE_FUNCTION(NvCVImage_Realloc, image_t* image, uint32_t width, uint32_t height, pixel_format format, component_type comp_type, uint32_t comp_layout, uint32_t mem_location, uint32_t alignment); + NVCVI_DEFINE_FUNCTION_EX(void, NvCVImage_Dealloc, image_t* image); + NVCVI_DEFINE_FUNCTION(NvCVImage_Create, uint32_t width, uint32_t height, pixel_format format, component_type comp_type, component_layout comp_layout, memory_location mem_location, uint32_t alignment, image_t** image); + NVCVI_DEFINE_FUNCTION_EX(void, NvCVImage_Destroy, image_t* image); + NVCVI_DEFINE_FUNCTION_EX(void, NvCVImage_ComponentOffsets, pixel_format format, int32_t* red_offset, int32_t* green_offset, int32_t* blue_offset, int32_t* alpha_offset, int32_t* y_offset); + NVCVI_DEFINE_FUNCTION(NvCVImage_Transfer, const image_t* source, image_t* destination, float scale, ::streamfx::nvidia::cuda::stream_t stream, image_t* buffer); + NVCVI_DEFINE_FUNCTION(NvCVImage_TransferRect, const image_t* source, const rect* source_rect, image_t* destination, const point* destination_point, float scale, ::streamfx::nvidia::cuda::stream_t stream, image_t* buffer); + NVCVI_DEFINE_FUNCTION(NvCVImage_TransferFromYUV, const void* y, int32_t yPixBytes, int32_t yPitch, const void* u, const void* v, int32_t uvPixBytes, int32_t uvPitch, pixel_format yuvFormat, component_type yuvType, color_information yuvColorSpace, memory_location yuvMemSpace, image_t* destination, const rect* destination_area, float scale, ::streamfx::nvidia::cuda::stream_t stream, image_t* tmp); + NVCVI_DEFINE_FUNCTION(NvCVImage_TransferToYUV, const image_t* source, const rect* source_area, const void* y, int32_t yPixBytes, int32_t yPitch, const void* u, const void* v, int uvPixBytes, int32_t uvPitch, pixel_format yuvFormat, component_type yuvType, color_information yuvColorSpace, memory_location yuvMemSpace, float scale, ::streamfx::nvidia::cuda::stream_t stream, image_t* tmp); + NVCVI_DEFINE_FUNCTION(NvCVImage_MapResource, image_t* image, ::streamfx::nvidia::cuda::stream_t stream); + NVCVI_DEFINE_FUNCTION(NvCVImage_UnmapResource, image_t* image, ::streamfx::nvidia::cuda::stream_t stream); + NVCVI_DEFINE_FUNCTION(NvCVImage_Composite, const image_t* foreground, const image_t* background, const image_t* matte, image_t* destination, ::streamfx::nvidia::cuda::stream_t stream); + NVCVI_DEFINE_FUNCTION(NvCVImage_CompositeRect, const image_t* foreground, const point foreground_origin, const image_t* background, const point background_origin, const image_t* matte, uint32_t mode, image_t* destination, const point destination_origin, ::streamfx::nvidia::cuda::stream_t stream); + NVCVI_DEFINE_FUNCTION(NvCVImage_CompositeOverConstant, const image_t* source, const image_t* matte, const uint8_t background_color[3], image_t* destination); + NVCVI_DEFINE_FUNCTION(NvCVImage_FlipY, const image_t* source, image_t* destination); + NVCVI_DEFINE_FUNCTION(NvCVImage_GetYUVPointers, image_t* image, uint8_t** y, uint8_t** u, uint8_t** v, int32_t* y_pixel_bytes, int32_t* c_pixel_bytes, int32_t* y_row_bytes, int32_t* c_row_bytes); + + NVCVI_DEFINE_FUNCTION_EX(const char*, NvCV_GetErrorStringFromCode, result code); + +#ifdef WIN32 + NVCVI_DEFINE_FUNCTION(NvCVImage_InitFromD3D11Texture, image_t* image, struct ID3D11Texture2D* texture); + NVCVI_DEFINE_FUNCTION(NvCVImage_ToD3DFormat, pixel_format format, component_type comp_type, component_layout comp_layout, DXGI_FORMAT* dxgi_format); + NVCVI_DEFINE_FUNCTION(NvCVImage_FromD3DFormat, DXGI_FORMAT d3dFormat, pixel_format* format, component_type* comp_type, component_layout* comp_layout); + +#ifdef __dxgicommon_h__ + NVCVI_DEFINE_FUNCTION(NvCVImage_ToD3DColorSpace, color_information nvcvColorSpace, DXGI_COLOR_SPACE_TYPE* pD3dColorSpace); + NVCVI_DEFINE_FUNCTION(NvCVImage_FromD3DColorSpace, DXGI_COLOR_SPACE_TYPE d3dColorSpace, color_information* pNvcvColorSpace); +#endif +#endif + + public: + static std::shared_ptr<::streamfx::nvidia::cv::cv> get(); + }; + + class exception : public std::runtime_error { + result _code; + + public: + exception(const char* what, result code) : std::runtime_error(what), _code(code) {} + exception(std::string_view what, result code) : std::runtime_error(what.data()), _code(code) {} + ~exception(){}; + + inline result code() + { + return _code; + } + + inline const char* description() + { + return ::streamfx::nvidia::cv::cv::get()->NvCV_GetErrorStringFromCode(_code); + } + }; +} // namespace streamfx::nvidia::cv + +P_ENABLE_BITMASK_OPERATORS(::streamfx::nvidia::cv::color_information); diff --git a/components/nvidia/include/nvidia/vfx/nvidia-vfx-denoising.hpp b/components/nvidia/include/nvidia/vfx/nvidia-vfx-denoising.hpp new file mode 100644 index 0000000..31c85bd --- /dev/null +++ b/components/nvidia/include/nvidia/vfx/nvidia-vfx-denoising.hpp @@ -0,0 +1,49 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2021-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "nvidia-vfx-effect.hpp" +#include "nvidia-vfx.hpp" +#include "nvidia/cuda/nvidia-cuda-gs-texture.hpp" +#include "nvidia/cuda/nvidia-cuda-obs.hpp" +#include "nvidia/cuda/nvidia-cuda.hpp" +#include "nvidia/cv/nvidia-cv-image.hpp" +#include "nvidia/cv/nvidia-cv-texture.hpp" +#include "obs/gs/gs-texture.hpp" + +namespace streamfx::nvidia::vfx { + class denoising : protected effect { + bool _dirty; + + std::shared_ptr<::streamfx::nvidia::cv::texture> _input; + std::shared_ptr<::streamfx::nvidia::cv::image> _convert_to_fp32; + std::shared_ptr<::streamfx::nvidia::cv::image> _source; + std::shared_ptr<::streamfx::nvidia::cv::image> _destination; + std::shared_ptr<::streamfx::nvidia::cv::image> _convert_to_u8; + std::shared_ptr<::streamfx::nvidia::cv::texture> _output; + std::shared_ptr<::streamfx::nvidia::cv::image> _tmp; + + void* _states[1]; + ::streamfx::nvidia::cuda::device_ptr_t _state; + uint32_t _state_size; + + float _strength; + + public: + ~denoising(); + denoising(); + + void set_strength(float strength); + float strength(); + + void size(std::pair& size); + + std::shared_ptr<::streamfx::obs::gs::texture> process(std::shared_ptr<::streamfx::obs::gs::texture> in); + + private: + void resize(uint32_t width, uint32_t height); + + void load(); + }; +} // namespace streamfx::nvidia::vfx diff --git a/components/nvidia/include/nvidia/vfx/nvidia-vfx-effect.hpp b/components/nvidia/include/nvidia/vfx/nvidia-vfx-effect.hpp new file mode 100644 index 0000000..c485d10 --- /dev/null +++ b/components/nvidia/include/nvidia/vfx/nvidia-vfx-effect.hpp @@ -0,0 +1,190 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2021-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "nvidia-vfx.hpp" +#include "nvidia/cuda/nvidia-cuda-obs.hpp" +#include "nvidia/cuda/nvidia-cuda-stream.hpp" +#include "nvidia/cuda/nvidia-cuda.hpp" +#include "nvidia/cv/nvidia-cv-image.hpp" +#include "nvidia/cv/nvidia-cv-texture.hpp" +#include "nvidia/cv/nvidia-cv.hpp" +#include "nvidia/vfx/nvidia-vfx.hpp" + +#include "warning-disable.hpp" +#include +#include +#include +#include "warning-enable.hpp" + +namespace streamfx::nvidia::vfx { + using namespace ::streamfx::nvidia; + + class effect { + protected: + std::shared_ptr _nvcuda; + std::shared_ptr _nvcvi; + std::shared_ptr _nvvfx; + std::shared_ptr _fx; + std::u8string _model_path; + + public: + ~effect(); + effect(effect_t name); + + ::streamfx::nvidia::vfx::handle_t get() + { + return _fx.get(); + } + + public /* Int32 */: + inline cv::result set_uint32(parameter_t param, uint32_t const value) + { + return _nvvfx->NvVFX_SetU32(_fx.get(), param, value); + } + inline cv::result get_uint32(parameter_t param, uint32_t& value) + { + return _nvvfx->NvVFX_GetU32(_fx.get(), param, &value); + } + + inline cv::result set_int32(parameter_t param, int32_t const value) + { + return _nvvfx->NvVFX_SetS32(_fx.get(), param, value); + } + inline cv::result get_int32(parameter_t param, int32_t& value) + { + return _nvvfx->NvVFX_GetS32(_fx.get(), param, &value); + } + + public /* Int64 */: + inline cv::result set_uint64(parameter_t param, uint64_t const value) + { + return _nvvfx->NvVFX_SetU64(_fx.get(), param, value); + } + inline cv::result get_uint64(parameter_t param, uint64_t& value) + { + return _nvvfx->NvVFX_GetU64(_fx.get(), param, &value); + } + + public /* Float32 */: + inline cv::result set_float32(parameter_t param, float const value) + { + return _nvvfx->NvVFX_SetF32(_fx.get(), param, value); + } + inline cv::result get_float32(parameter_t param, float& value) + { + return _nvvfx->NvVFX_GetF32(_fx.get(), param, &value); + } + + public /* Float64 */: + inline cv::result set_float64(parameter_t param, double const value) + { + return _nvvfx->NvVFX_SetF64(_fx.get(), param, value); + } + inline cv::result get_float64(parameter_t param, double& value) + { + return _nvvfx->NvVFX_GetF64(_fx.get(), param, &value); + } + + public /* String */: + inline cv::result set_string(parameter_t param, const char* const value) + { + return _nvvfx->NvVFX_SetString(_fx.get(), param, value); + } + inline cv::result get_string(parameter_t param, const char*& value) + { + return _nvvfx->NvVFX_GetString(_fx.get(), param, &value); + } + + inline cv::result set_string(parameter_t param, const char8_t* const value) + { + return _nvvfx->NvVFX_SetString(_fx.get(), param, reinterpret_cast(value)); + } + inline cv::result get_string(parameter_t param, const char8_t*& value) + { + return _nvvfx->NvVFX_GetString(_fx.get(), param, reinterpret_cast(&value)); + } + + inline cv::result set_string(parameter_t param, std::string_view const& value) + { + return _nvvfx->NvVFX_SetString(_fx.get(), param, value.data()); + } + cv::result get_string(parameter_t param, std::string_view& value); + + inline cv::result set_string(parameter_t param, std::string const& value) + { + return _nvvfx->NvVFX_SetString(_fx.get(), param, value.c_str()); + } + cv::result get_string(parameter_t param, std::string& value); + + inline cv::result set_string(parameter_t param, std::u8string const& value) + { + return _nvvfx->NvVFX_SetString(_fx.get(), param, reinterpret_cast(value.c_str())); + } + cv::result get_string(parameter_t param, std::u8string& value); + + public /* CUDA Stream */: + inline cv::result set_cuda_stream(parameter_t param, cuda::stream_t const& value) + { + return _nvvfx->NvVFX_SetCudaStream(_fx.get(), param, value); + } + inline cv::result get_cuda_stream(parameter_t param, cuda::stream_t& value) + { + return _nvvfx->NvVFX_GetCudaStream(_fx.get(), param, &value); + } + + inline cv::result set_cuda_stream(parameter_t param, std::shared_ptr const& value) + { + return _nvvfx->NvVFX_SetCudaStream(_fx.get(), param, value->get()); + } + //cv::result get_stream(parameter_t param, std::shared_ptr& value); + + public /* CV Image */: + inline cv::result set_image(parameter_t param, cv::image_t* value) + { + return _nvvfx->NvVFX_SetImage(_fx.get(), param, value); + } + inline cv::result get_image(parameter_t param, cv::image_t* value) + { + return _nvvfx->NvVFX_GetImage(_fx.get(), param, value); + } + + inline cv::result set_image(parameter_t param, std::shared_ptr const& value) + { + return _nvvfx->NvVFX_SetImage(_fx.get(), param, value->get_image()); + } + inline cv::result get_image(parameter_t param, std::shared_ptr& value) + { + return _nvvfx->NvVFX_GetImage(_fx.get(), param, value->get_image()); + } + + public /* CV Texture */: + inline cv::result set_image(parameter_t param, std::shared_ptr const& value) + { + return _nvvfx->NvVFX_SetImage(_fx.get(), param, value->get_image()); + } + //cv::result get(parameter_t param, std::shared_ptr& value); + + public /* Objects */: + inline cv::result set_object(parameter_t param, void* const value) + { + return _nvvfx->NvVFX_SetObject(_fx.get(), param, value); + } + inline cv::result get_object(parameter_t param, void*& value) + { + return _nvvfx->NvVFX_GetObject(_fx.get(), param, &value); + } + + public /* Control */: + inline cv::result load() + { + return _nvvfx->NvVFX_Load(_fx.get()); + } + + inline cv::result run(bool async = false) + { + return _nvvfx->NvVFX_Run(_fx.get(), async ? 1 : 0); + } + }; +} // namespace streamfx::nvidia::vfx diff --git a/components/nvidia/include/nvidia/vfx/nvidia-vfx-greenscreen.hpp b/components/nvidia/include/nvidia/vfx/nvidia-vfx-greenscreen.hpp new file mode 100644 index 0000000..7b92b34 --- /dev/null +++ b/components/nvidia/include/nvidia/vfx/nvidia-vfx-greenscreen.hpp @@ -0,0 +1,49 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2021-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "nvidia-vfx-effect.hpp" +#include "nvidia-vfx.hpp" +#include "nvidia/cuda/nvidia-cuda-gs-texture.hpp" +#include "nvidia/cuda/nvidia-cuda-obs.hpp" +#include "nvidia/cuda/nvidia-cuda.hpp" +#include "nvidia/cv/nvidia-cv-image.hpp" +#include "nvidia/cv/nvidia-cv-texture.hpp" +#include "obs/gs/gs-texture.hpp" + +namespace streamfx::nvidia::vfx { + enum class greenscreen_mode { + QUALITY = 0, + PERFORMANCE = 1, + }; + + class greenscreen : protected effect { + bool _dirty; + std::list> _buffer; + std::shared_ptr<::streamfx::nvidia::cv::texture> _input; + std::shared_ptr<::streamfx::nvidia::cv::image> _source; + std::shared_ptr<::streamfx::nvidia::cv::image> _destination; + std::shared_ptr<::streamfx::nvidia::cv::texture> _output; + std::shared_ptr<::streamfx::nvidia::cv::image> _tmp; + + public: + ~greenscreen(); + greenscreen(); + + void size(std::pair& size); + + void set_mode(greenscreen_mode mode); + + std::shared_ptr<::streamfx::obs::gs::texture> process(std::shared_ptr<::streamfx::obs::gs::texture> in); + + std::shared_ptr<::streamfx::obs::gs::texture> get_color(); + + std::shared_ptr<::streamfx::obs::gs::texture> get_mask(); + + private: + void resize(uint32_t width, uint32_t height); + + void load(); + }; +} // namespace streamfx::nvidia::vfx diff --git a/components/nvidia/include/nvidia/vfx/nvidia-vfx-superresolution.hpp b/components/nvidia/include/nvidia/vfx/nvidia-vfx-superresolution.hpp new file mode 100644 index 0000000..4cc1828 --- /dev/null +++ b/components/nvidia/include/nvidia/vfx/nvidia-vfx-superresolution.hpp @@ -0,0 +1,52 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2021-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "nvidia-vfx-effect.hpp" +#include "nvidia-vfx.hpp" +#include "nvidia/cuda/nvidia-cuda-gs-texture.hpp" +#include "nvidia/cuda/nvidia-cuda-obs.hpp" +#include "nvidia/cuda/nvidia-cuda.hpp" +#include "nvidia/cv/nvidia-cv-image.hpp" +#include "nvidia/cv/nvidia-cv-texture.hpp" +#include "obs/gs/gs-texture.hpp" + +namespace streamfx::nvidia::vfx { + class superresolution : protected effect { + bool _dirty; + std::shared_ptr<::streamfx::nvidia::cv::texture> _input; + std::shared_ptr<::streamfx::nvidia::cv::image> _convert_to_fp32; + std::shared_ptr<::streamfx::nvidia::cv::image> _source; + std::shared_ptr<::streamfx::nvidia::cv::image> _destination; + std::shared_ptr<::streamfx::nvidia::cv::image> _convert_to_u8; + std::shared_ptr<::streamfx::nvidia::cv::texture> _output; + std::shared_ptr<::streamfx::nvidia::cv::image> _tmp; + + float _strength; + float _scale; + + std::pair _cache_input_size; + std::pair _cache_output_size; + float _cache_scale; + + public: + ~superresolution(); + superresolution(); + + void set_strength(float strength); + float strength(); + + void set_scale(float scale); + float scale(); + + void size(std::pair const& size, std::pair& input_size, std::pair& output_size); + + std::shared_ptr<::streamfx::obs::gs::texture> process(std::shared_ptr<::streamfx::obs::gs::texture> in); + + private: + void resize(uint32_t width, uint32_t height); + + void load(); + }; +} // namespace streamfx::nvidia::vfx diff --git a/components/nvidia/include/nvidia/vfx/nvidia-vfx.hpp b/components/nvidia/include/nvidia/vfx/nvidia-vfx.hpp new file mode 100644 index 0000000..5b9562c --- /dev/null +++ b/components/nvidia/include/nvidia/vfx/nvidia-vfx.hpp @@ -0,0 +1,91 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2021-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "nvidia/cv/nvidia-cv.hpp" + +#include "warning-disable.hpp" +#include +#include "warning-enable.hpp" + +#define NVVFX_DEFINE_FUNCTION(name, ...) \ + private: \ + typedef ::streamfx::nvidia::cv::result(__cdecl* t##name)(__VA_ARGS__); \ + \ + public: \ + t##name name = nullptr; + +namespace streamfx::nvidia::vfx { + typedef const char* effect_t; + typedef const char* parameter_t; + typedef void* object_t; + typedef object_t handle_t; + + static constexpr effect_t EFFECT_TRANSFER = "Transfer"; + static constexpr effect_t EFFECT_GREEN_SCREEN = "GreenScreen"; + static constexpr effect_t EFFECT_BACKGROUND_BLUR = "BackgroundBlur"; + static constexpr effect_t EFFECT_ARTIFACT_REDUCTION = "ArtifactReduction"; + static constexpr effect_t EFFECT_SUPERRESOLUTION = "SuperRes"; + static constexpr effect_t EFFECT_UPSCALE = "Upscale"; + static constexpr effect_t EFFECT_DENOISING = "Denoising"; + + static constexpr parameter_t PARAMETER_INPUT_IMAGE_0 = "SrcImage0"; + static constexpr parameter_t PARAMETER_INPUT_IMAGE_1 = "SrcImage1"; + static constexpr parameter_t PARAMETER_OUTPUT_IMAGE_0 = "DstImage0"; + static constexpr parameter_t PARAMETER_MODEL_DIRECTORY = "ModelDir"; + static constexpr parameter_t PARAMETER_CUDA_STREAM = "CudaStream"; + static constexpr parameter_t PARAMETER_INFO = "Info"; + static constexpr parameter_t PARAMETER_SCALE = "Scale"; + static constexpr parameter_t PARAMETER_STRENGTH = "Strength"; + static constexpr parameter_t PARAMETER_STRENGTH_LEVELS = "StrengthLevels"; + static constexpr parameter_t PARAMETER_MODE = "Mode"; + static constexpr parameter_t PARAMETER_TEMPORAL = "Temporal"; + static constexpr parameter_t PARAMETER_GPU = "GPU"; + static constexpr parameter_t PARAMETER_BATCH_SIZE = "BatchSize"; + static constexpr parameter_t PARAMETER_MODEL_BATCH = "ModelBatch"; + static constexpr parameter_t PARAMETER_STATE = "State"; + static constexpr parameter_t PARAMETER_STATE_SIZE = "StateSize"; + + class vfx { + std::shared_ptr<::streamfx::util::library> _library; +#ifdef WIN32 + void* _extra; +#endif + std::filesystem::path _model_path; + + public: + ~vfx(); + vfx(); + + std::filesystem::path const& model_path(); + + public: + NVVFX_DEFINE_FUNCTION(NvVFX_GetVersion, uint32_t* version); + NVVFX_DEFINE_FUNCTION(NvVFX_CreateEffect, effect_t effect, handle_t* handle); + NVVFX_DEFINE_FUNCTION(NvVFX_DestroyEffect, handle_t handle); + NVVFX_DEFINE_FUNCTION(NvVFX_SetU32, handle_t effect, parameter_t paramName, uint32_t val); + NVVFX_DEFINE_FUNCTION(NvVFX_SetS32, handle_t effect, parameter_t paramName, int32_t val); + NVVFX_DEFINE_FUNCTION(NvVFX_SetF32, handle_t effect, parameter_t paramName, float val); + NVVFX_DEFINE_FUNCTION(NvVFX_SetF64, handle_t effect, parameter_t paramName, double val); + NVVFX_DEFINE_FUNCTION(NvVFX_SetU64, handle_t effect, parameter_t paramName, uint64_t val); + NVVFX_DEFINE_FUNCTION(NvVFX_SetObject, handle_t effect, parameter_t paramName, void* ptr); + NVVFX_DEFINE_FUNCTION(NvVFX_SetCudaStream, handle_t effect, parameter_t paramName, ::streamfx::nvidia::cuda::stream_t stream); + NVVFX_DEFINE_FUNCTION(NvVFX_SetImage, handle_t effect, parameter_t paramName, ::streamfx::nvidia::cv::image_t* im); + NVVFX_DEFINE_FUNCTION(NvVFX_SetString, handle_t effect, parameter_t paramName, const char* str); + NVVFX_DEFINE_FUNCTION(NvVFX_GetU32, handle_t effect, parameter_t paramName, uint32_t* val); + NVVFX_DEFINE_FUNCTION(NvVFX_GetS32, handle_t effect, parameter_t paramName, int32_t* val); + NVVFX_DEFINE_FUNCTION(NvVFX_GetF32, handle_t effect, parameter_t paramName, float* val); + NVVFX_DEFINE_FUNCTION(NvVFX_GetF64, handle_t effect, parameter_t paramName, double* val); + NVVFX_DEFINE_FUNCTION(NvVFX_GetU64, handle_t effect, parameter_t paramName, uint64_t* val); + NVVFX_DEFINE_FUNCTION(NvVFX_GetObject, handle_t effect, parameter_t paramName, void** ptr); + NVVFX_DEFINE_FUNCTION(NvVFX_GetCudaStream, handle_t effect, parameter_t paramName, ::streamfx::nvidia::cuda::stream_t stream); + NVVFX_DEFINE_FUNCTION(NvVFX_GetImage, handle_t effect, parameter_t paramName, ::streamfx::nvidia::cv::image_t* im); + NVVFX_DEFINE_FUNCTION(NvVFX_GetString, handle_t effect, parameter_t paramName, const char** str); + NVVFX_DEFINE_FUNCTION(NvVFX_Run, handle_t effect, int32_t async); + NVVFX_DEFINE_FUNCTION(NvVFX_Load, handle_t effect); + + public: + static std::shared_ptr<::streamfx::nvidia::vfx::vfx> get(); + }; +} // namespace streamfx::nvidia::vfx diff --git a/components/nvidia/source/nvidia/ar/nvidia-ar-facedetection.cpp b/components/nvidia/source/nvidia/ar/nvidia-ar-facedetection.cpp new file mode 100644 index 0000000..1ef87a4 --- /dev/null +++ b/components/nvidia/source/nvidia/ar/nvidia-ar-facedetection.cpp @@ -0,0 +1,211 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2021-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2022 lainon +// AUTOGENERATED COPYRIGHT HEADER END + +#include "nvidia/ar/nvidia-ar-facedetection.hpp" +#include "obs/gs/gs-helper.hpp" +#include "util/util-logging.hpp" + +#include "warning-disable.hpp" +#include +#include +#include "warning-enable.hpp" + +#ifdef _DEBUG +#define ST_PREFIX "<%s> " +#define D_LOG_ERROR(x, ...) P_LOG_ERROR(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_WARNING(x, ...) P_LOG_WARN(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_INFO(x, ...) P_LOG_INFO(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_DEBUG(x, ...) P_LOG_DEBUG(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#else +#define ST_PREFIX " " +#define D_LOG_ERROR(...) P_LOG_ERROR(ST_PREFIX __VA_ARGS__) +#define D_LOG_WARNING(...) P_LOG_WARN(ST_PREFIX __VA_ARGS__) +#define D_LOG_INFO(...) P_LOG_INFO(ST_PREFIX __VA_ARGS__) +#define D_LOG_DEBUG(...) P_LOG_DEBUG(ST_PREFIX __VA_ARGS__) +#endif + +// At the current moment, both FaceDetection and FaceBoxDetection only support 8 faces. +#define ST_MAX_TRACKED_FACES 8 + +using namespace ::streamfx::nvidia; + +streamfx::nvidia::ar::facedetection::~facedetection() +{ + D_LOG_DEBUG("Finalizing... (Addr: 0x%" PRIuPTR ")", this); +} + +streamfx::nvidia::ar::facedetection::facedetection() : feature(FEATURE_FACE_DETECTION), _input(), _source(), _tmp(), _rects(), _rects_confidence(), _bboxes(), _dirty(true) +{ + D_LOG_DEBUG("Initializing... (Addr: 0x%" PRIuPTR ")", this); + + // Prepare initial memory + _rects.resize(ST_MAX_TRACKED_FACES); + _rects_confidence.resize(ST_MAX_TRACKED_FACES); + + // Set up initial configuration + set_tracking_limit(1); + + // Attempt to load the feature. + if (auto err = feature::load(); err != cv::result::SUCCESS) { + throw cv::exception("Load", err); + } +} + +std::pair ar::facedetection::tracking_limit_range() +{ + return {1, ST_MAX_TRACKED_FACES}; +} + +size_t ar::facedetection::tracking_limit() +{ + return _rects.size(); +} + +void ar::facedetection::set_tracking_limit(size_t v) +{ + // Ensure there is always at least one face being tracked. + v = std::max(v, 1); + + // Resize all data. + _rects.resize(v); + _rects_confidence.resize(v); + + // Update bounding boxes structure. + _bboxes.rects = _rects.data(); + _bboxes.maximum = static_cast(v); + _bboxes.current = 0; + + // Update feature. + if (auto err = set_object(P_NVAR_OUTPUT "BoundingBoxes", reinterpret_cast(&_bboxes), sizeof(bounds_t)); err != cv::result::SUCCESS) { + throw cv::exception("BoundingBoxes", err); + } + if (auto err = set_float32array(P_NVAR_OUTPUT "BoundingBoxesConfidence", _rects_confidence); err != cv::result::SUCCESS) { + throw cv::exception("BoundingBoxesConfidence", err); + } + if (auto err = set_uint32(P_NVAR_CONFIG "Temporal", (v == 1)); err != cv::result::SUCCESS) { + throw cv::exception("Temporal", err); + } + + // Mark effect dirty for reload. + _dirty = true; +} + +void ar::facedetection::process(std::shared_ptr<::streamfx::obs::gs::texture> in) +{ + // Enter Graphics and CUDA context. + auto gctx = ::streamfx::obs::gs::context(); + auto cctx = _nvcuda->get_context()->enter(); + +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + ::streamfx::obs::gs::debug_marker profiler1{::streamfx::obs::gs::debug_color_magenta, "NvAR Face Detection"}; +#endif + + // Resize if the size or scale was changed. + resize(in->width(), in->height()); + + // Reload effect if dirty. + if (_dirty) { + load(); + } + + { // Copy parameter to input. +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + ::streamfx::obs::gs::debug_marker profiler1{::streamfx::obs::gs::debug_color_copy, "Copy In -> Input"}; +#endif + gs_copy_texture(*(_input->get_texture()), *in); + } + + { // Convert Input to Source format +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + ::streamfx::obs::gs::debug_marker profiler1{::streamfx::obs::gs::debug_color_convert, "Copy Input -> Source"}; +#endif + if (auto res = _nvcv->NvCVImage_Transfer(_input->get_image(), _source->get_image(), 1.f, _nvcuda->get_stream()->get(), _tmp->get_image()); res != ::streamfx::nvidia::cv::result::SUCCESS) { + D_LOG_ERROR("Failed to transfer input to processing source due to error: %s", _nvcv->NvCV_GetErrorStringFromCode(res)); + throw std::runtime_error("Transfer failed."); + } + } + + { // Run +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + ::streamfx::obs::gs::debug_marker profiler1{::streamfx::obs::gs::debug_color_cache, "Run"}; +#endif + if (auto err = run(); err != cv::result::SUCCESS) { + throw cv::exception("Run", err); + } + } +} + +size_t streamfx::nvidia::ar::facedetection::count() +{ + return _bboxes.current; +} + +streamfx::nvidia::ar::rect_t const& streamfx::nvidia::ar::facedetection::at(size_t index) +{ + float v; + return at(index, v); +} + +streamfx::nvidia::ar::rect_t const& streamfx::nvidia::ar::facedetection::at(size_t index, float& confidence) +{ + if (_bboxes.current == 0) + throw std::runtime_error("no tracked faces"); + if (index > _bboxes.current) + throw std::out_of_range("index too large"); + auto& ref = _rects.at(index); + confidence = _rects_confidence.at(index); + return ref; +} + +void ar::facedetection::resize(uint32_t width, uint32_t height) +{ + auto gctx = ::streamfx::obs::gs::context(); + auto cctx = ::streamfx::nvidia::cuda::obs::get()->get_context()->enter(); + + if (!_tmp) { + _tmp = std::make_shared<::streamfx::nvidia::cv::image>(width, height, ::streamfx::nvidia::cv::pixel_format::RGBA, ::streamfx::nvidia::cv::component_type::UINT8, ::streamfx::nvidia::cv::component_layout::PLANAR, ::streamfx::nvidia::cv::memory_location::GPU, 1); + } + + if (!_input || (width != _input->get_texture()->width()) || (height != _input->get_texture()->height())) { + if (_input) { + _input->resize(width, height); + } else { + _input = std::make_shared<::streamfx::nvidia::cv::texture>(width, height, GS_RGBA_UNORM); + } + _dirty = true; + } + + if (!_source || (width != _source->get_image()->width) || (height != _source->get_image()->height)) { + if (_source) { + _source->resize(width, height); + } else { + _source = std::make_shared<::streamfx::nvidia::cv::image>(width, height, ::streamfx::nvidia::cv::pixel_format::BGR, ::streamfx::nvidia::cv::component_type::UINT8, ::streamfx::nvidia::cv::component_layout::INTERLEAVED, ::streamfx::nvidia::cv::memory_location::GPU, 1); + } + + if (auto err = set_image(P_NVAR_INPUT "Image", _source); err != cv::result::SUCCESS) { + throw cv::exception("Image", err); + } + + _dirty = true; + } +} + +void streamfx::nvidia::ar::facedetection::load() +{ + auto gctx = ::streamfx::obs::gs::context(); + auto cctx = ::streamfx::nvidia::cuda::obs::get()->get_context()->enter(); + + // Assign CUDA Stream object. + if (auto err = set_cuda_stream(P_NVAR_CONFIG "CUDAStream", _nvcuda->get_stream()); err != cv::result::SUCCESS) { + throw cv::exception("CUDAStream", err); + } + + // Attempt to load the feature. + if (auto err = feature::load(); err != cv::result::SUCCESS) { + throw cv::exception("Load", err); + } + + _dirty = false; +} diff --git a/components/nvidia/source/nvidia/ar/nvidia-ar-feature.cpp b/components/nvidia/source/nvidia/ar/nvidia-ar-feature.cpp new file mode 100644 index 0000000..6210d43 --- /dev/null +++ b/components/nvidia/source/nvidia/ar/nvidia-ar-feature.cpp @@ -0,0 +1,88 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2021-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#include "nvidia/ar/nvidia-ar-feature.hpp" +#include "obs/gs/gs-helper.hpp" +#include "util/util-logging.hpp" +#include "util/util-platform.hpp" + +#ifdef _DEBUG +#define ST_PREFIX "<%s> " +#define D_LOG_ERROR(x, ...) P_LOG_ERROR(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_WARNING(x, ...) P_LOG_WARN(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_INFO(x, ...) P_LOG_INFO(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_DEBUG(x, ...) P_LOG_DEBUG(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#else +#define ST_PREFIX " " +#define D_LOG_ERROR(...) P_LOG_ERROR(ST_PREFIX __VA_ARGS__) +#define D_LOG_WARNING(...) P_LOG_WARN(ST_PREFIX __VA_ARGS__) +#define D_LOG_INFO(...) P_LOG_INFO(ST_PREFIX __VA_ARGS__) +#define D_LOG_DEBUG(...) P_LOG_DEBUG(ST_PREFIX __VA_ARGS__) +#endif + +streamfx::nvidia::ar::feature::~feature() +{ + D_LOG_DEBUG("Finalizing... (Addr: 0x%" PRIuPTR ")", this); +} + +streamfx::nvidia::ar::feature::feature(feature_t feature) : _nvcuda(::streamfx::nvidia::cuda::obs::get()), _nvcv(::streamfx::nvidia::cv::cv::get()), _nvar(::streamfx::nvidia::ar::ar::get()), _fx() +{ + D_LOG_DEBUG("Initializating... (Addr: 0x%" PRIuPTR ")", this); + auto gctx = ::streamfx::obs::gs::context(); + auto cctx = cuda::obs::get()->get_context()->enter(); + + // Create the Effect/Feature. + ::streamfx::nvidia::ar::handle_t handle; + if (cv::result res = _nvar->NvAR_Create(feature, &handle); res != cv::result::SUCCESS) { + throw cv::exception("Failed to create feature.", res); + } + _fx = std::shared_ptr(handle, [this](::streamfx::nvidia::ar::handle_t handle) { _nvar->NvAR_Destroy(handle); }); + + // Set CUDA stream and model directory. + set_cuda_stream(P_NVAR_CONFIG "CUDAStream", _nvcuda->get_stream()); + _model_path = _nvar->get_model_path().generic_u8string(); + set_string(P_NVAR_CONFIG "ModelDir", _model_path); +} + +streamfx::nvidia::cv::result streamfx::nvidia::ar::feature::get(parameter_t param, std::string_view& value) +{ + const char* cvalue = nullptr; + cv::result res = get_string(param, cvalue); + if (res == cv::result::SUCCESS) { + if (cvalue) { + value = std::string_view(cvalue); + } else { + value = std::string_view(); + } + } + return res; +} + +streamfx::nvidia::cv::result streamfx::nvidia::ar::feature::get(parameter_t param, std::string& value) +{ + const char* cvalue = nullptr; + cv::result res = get_string(param, cvalue); + if (res == cv::result::SUCCESS) { + if (cvalue) { + value = cvalue; + } else { + value.clear(); + } + } + return res; +} + +streamfx::nvidia::cv::result streamfx::nvidia::ar::feature::get(parameter_t param, std::u8string& value) +{ + const char8_t* cvalue = nullptr; + cv::result res = get_string(param, cvalue); + if (res == cv::result::SUCCESS) { + if (cvalue) { + value = cvalue; + } else { + value.clear(); + } + } + return res; +} diff --git a/components/nvidia/source/nvidia/ar/nvidia-ar.cpp b/components/nvidia/source/nvidia/ar/nvidia-ar.cpp new file mode 100644 index 0000000..8a7982b --- /dev/null +++ b/components/nvidia/source/nvidia/ar/nvidia-ar.cpp @@ -0,0 +1,217 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2021-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#include "nvidia/ar/nvidia-ar.hpp" +#include "nvidia/cuda/nvidia-cuda-obs.hpp" +#include "obs/gs/gs-helper.hpp" +#include "util/util-logging.hpp" +#include "util/util-platform.hpp" + +#include "warning-disable.hpp" +#include +#include +#include "warning-enable.hpp" + +#ifdef _DEBUG +#define ST_PREFIX "<%s> " +#define D_LOG_ERROR(x, ...) P_LOG_ERROR(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_WARNING(x, ...) P_LOG_WARN(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_INFO(x, ...) P_LOG_INFO(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_DEBUG(x, ...) P_LOG_DEBUG(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#else +#define ST_PREFIX " " +#define D_LOG_ERROR(...) P_LOG_ERROR(ST_PREFIX __VA_ARGS__) +#define D_LOG_WARNING(...) P_LOG_WARN(ST_PREFIX __VA_ARGS__) +#define D_LOG_INFO(...) P_LOG_INFO(ST_PREFIX __VA_ARGS__) +#define D_LOG_DEBUG(...) P_LOG_DEBUG(ST_PREFIX __VA_ARGS__) +#endif + +#ifdef WIN32 +#include "warning-disable.hpp" +#include +#include +#include +#include "warning-enable.hpp" + +#define ST_LIBRARY_NAME "nvARPose.dll" +#else +#define ST_LIBRARY_NAME "libnvARPose.so" +#endif + +#define P_NVAR_LOAD_SYMBOL(NAME) \ + { \ + NAME = reinterpret_cast(_library->load_symbol(#NAME)); \ + if (!NAME) \ + throw std::runtime_error("Failed to load '" #NAME "' from '" ST_LIBRARY_NAME "'."); \ + } + +streamfx::nvidia::ar::ar::~ar() +{ + D_LOG_DEBUG("Finalizing... (Addr: 0x%" PRIuPTR ")", this); + +#ifdef WIN32 + // Remove the DLL directory from the library loader paths. + if (_extra != nullptr) { + RemoveDllDirectory(reinterpret_cast(_extra)); + } +#endif + + { // The library may need to release Graphics and CUDA resources. + auto gctx = ::streamfx::obs::gs::context(); + auto cctx = ::streamfx::nvidia::cuda::obs::get()->get_context()->enter(); + _library.reset(); + } +} + +streamfx::nvidia::ar::ar::ar() : _library(), _model_path() +{ + std::filesystem::path sdk_path; + auto gctx = ::streamfx::obs::gs::context(); + auto cctx = ::streamfx::nvidia::cuda::obs::get()->get_context()->enter(); + + D_LOG_DEBUG("Initializating... (Addr: 0x%" PRIuPTR ")", this); + + // Figure out where the Augmented Reality SDK is, if it is installed. +#ifdef WIN32 + { + // NVAR SDK only defines NVAR_MODEL_PATH, so we'll use that as our baseline. + DWORD env_size = GetEnvironmentVariableW(L"NVAR_MODEL_PATH", nullptr, 0); + if (env_size > 0) { + std::vector buffer(static_cast(env_size) + 1, 0); + env_size = GetEnvironmentVariableW(L"NVAR_MODEL_PATH", buffer.data(), static_cast(buffer.size())); + _model_path = std::wstring(buffer.data(), buffer.size()); + + // The SDK is location one directory "up" from the model path. + sdk_path = std::filesystem::path(_model_path) / ".."; + } + + // If the environment variable wasn't set and our model path is still undefined, guess! + if (sdk_path.empty()) { + PWSTR str = nullptr; + HRESULT res = SHGetKnownFolderPath(FOLDERID_ProgramFiles, KF_FLAG_DEFAULT, nullptr, &str); + if (res == S_OK) { + sdk_path = std::wstring(str); + sdk_path /= "NVIDIA Corporation"; + sdk_path /= "NVIDIA AR SDK"; + CoTaskMemFree(str); + + // Model path is in 'models' subdirectory. + _model_path = sdk_path; + _model_path /= "models"; + } + } + + // Figure out absolute paths to everything. + _model_path = streamfx::util::platform::native_to_utf8(std::filesystem::absolute(_model_path)); + sdk_path = streamfx::util::platform::native_to_utf8(std::filesystem::absolute(sdk_path)); + } +#else + throw std::runtime_error("Not yet implemented."); +#endif + + // Check if any of the found paths are valid. + if (!std::filesystem::exists(sdk_path)) { + D_LOG_ERROR("No supported NVIDIA SDK is installed to provide '%s'.", ST_LIBRARY_NAME); + throw std::runtime_error("Failed to load '" ST_LIBRARY_NAME "'."); + } + + // Try and load the library. + { +#ifdef WIN32 + // On platforms where it is possible, modify the linker directories. + DLL_DIRECTORY_COOKIE ck = AddDllDirectory(sdk_path.wstring().c_str()); + _extra = reinterpret_cast(ck); + if (ck == 0) { + DWORD ec = GetLastError(); + std::string error; + { + LPWSTR str; + FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_IGNORE_INSERTS, nullptr, ec, MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US), reinterpret_cast(&str), 0, nullptr); + error = ::streamfx::util::platform::native_to_utf8(std::wstring(str)); + LocalFree(str); + } + D_LOG_WARNING("Failed to add '%'s to the library loader paths with error: %s (Code %" PRIu32 ")", sdk_path.string().c_str(), error.c_str(), ec); + } +#endif + + std::filesystem::path paths[] = { + ST_LIBRARY_NAME, + std::filesystem::path(sdk_path) / ST_LIBRARY_NAME, + }; + + for (auto path : paths) { + try { + _library = ::streamfx::util::library::load(path); + } catch (std::exception const& ex) { + D_LOG_ERROR("Failed to load '%s' with error: %s", path.string().c_str(), ex.what()); + } catch (...) { + D_LOG_ERROR("Failed to load '%s'.", path.string().c_str()); + } + + if (_library) { + break; + } + } + + if (!_library) { +#ifdef WIN32 + // Remove the DLL directory from the library loader paths. + if (_extra != nullptr) { + RemoveDllDirectory(reinterpret_cast(_extra)); + } +#endif + throw std::runtime_error("Failed to load " ST_LIBRARY_NAME "."); + } + } + + { // Load Symbols + P_NVAR_LOAD_SYMBOL(NvAR_GetVersion); + P_NVAR_LOAD_SYMBOL(NvAR_Create); + P_NVAR_LOAD_SYMBOL(NvAR_Destroy); + P_NVAR_LOAD_SYMBOL(NvAR_Run); + P_NVAR_LOAD_SYMBOL(NvAR_Load); + P_NVAR_LOAD_SYMBOL(NvAR_GetS32); + P_NVAR_LOAD_SYMBOL(NvAR_SetS32); + P_NVAR_LOAD_SYMBOL(NvAR_GetU32); + P_NVAR_LOAD_SYMBOL(NvAR_SetU32); + P_NVAR_LOAD_SYMBOL(NvAR_GetU64); + P_NVAR_LOAD_SYMBOL(NvAR_SetU64); + P_NVAR_LOAD_SYMBOL(NvAR_GetF32); + P_NVAR_LOAD_SYMBOL(NvAR_SetF32); + P_NVAR_LOAD_SYMBOL(NvAR_GetF64); + P_NVAR_LOAD_SYMBOL(NvAR_SetF64); + P_NVAR_LOAD_SYMBOL(NvAR_GetString); + P_NVAR_LOAD_SYMBOL(NvAR_SetString); + P_NVAR_LOAD_SYMBOL(NvAR_GetCudaStream); + P_NVAR_LOAD_SYMBOL(NvAR_SetCudaStream); + P_NVAR_LOAD_SYMBOL(NvAR_GetObject); + P_NVAR_LOAD_SYMBOL(NvAR_SetObject); + P_NVAR_LOAD_SYMBOL(NvAR_GetF32Array); + P_NVAR_LOAD_SYMBOL(NvAR_SetF32Array); + } + + { // Assign proper GPU. + auto cctx = ::streamfx::nvidia::cuda::obs::get()->get_context()->enter(); + NvAR_SetU32(nullptr, P_NVAR_CONFIG "GPU", 0); + } +} + +std::filesystem::path const& streamfx::nvidia::ar::ar::get_model_path() +{ + return _model_path; +} + +std::shared_ptr streamfx::nvidia::ar::ar::get() +{ + static std::weak_ptr instance; + static std::mutex lock; + + std::unique_lock ul(lock); + if (instance.expired()) { + auto hard_instance = std::make_shared(); + instance = hard_instance; + return hard_instance; + } + return instance.lock(); +} diff --git a/components/nvidia/source/nvidia/cuda/nvidia-cuda-context.cpp b/components/nvidia/source/nvidia/cuda/nvidia-cuda-context.cpp new file mode 100644 index 0000000..f988d72 --- /dev/null +++ b/components/nvidia/source/nvidia/cuda/nvidia-cuda-context.cpp @@ -0,0 +1,143 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#include "nvidia/cuda/nvidia-cuda-context.hpp" +#include "util/util-logging.hpp" + +#include "warning-disable.hpp" +#include +#include +#include "warning-enable.hpp" + +#ifdef _DEBUG +#define ST_PREFIX "<%s> " +#define D_LOG_ERROR(x, ...) P_LOG_ERROR(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_WARNING(x, ...) P_LOG_WARN(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_INFO(x, ...) P_LOG_INFO(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_DEBUG(x, ...) P_LOG_DEBUG(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#else +#define ST_PREFIX " " +#define D_LOG_ERROR(...) P_LOG_ERROR(ST_PREFIX __VA_ARGS__) +#define D_LOG_WARNING(...) P_LOG_WARN(ST_PREFIX __VA_ARGS__) +#define D_LOG_INFO(...) P_LOG_INFO(ST_PREFIX __VA_ARGS__) +#define D_LOG_DEBUG(...) P_LOG_DEBUG(ST_PREFIX __VA_ARGS__) +#endif + +#ifdef WIN32 +#include "warning-disable.hpp" +#include +#include "warning-enable.hpp" +#endif + +#define ENABLE_STACK_CHECKS + +streamfx::nvidia::cuda::context::~context() +{ + D_LOG_DEBUG("Finalizing... (Addr: 0x%" PRIuPTR ")", this); + + if (_has_device) { + _cuda->cuDevicePrimaryCtxRelease(_device); + } else { + _cuda->cuCtxDestroy(_ctx); + } +} + +streamfx::nvidia::cuda::context::context() : _cuda(::streamfx::nvidia::cuda::cuda::get()), _ctx(), _has_device(false), _device() +{ + D_LOG_DEBUG("Initializating... (Addr: 0x%" PRIuPTR ")", this); +} + +#ifdef WIN32 +streamfx::nvidia::cuda::context::context(ID3D11Device* device) : context() +{ + using namespace streamfx::nvidia::cuda; + + if (!device) + throw std::invalid_argument("device"); + // Get DXGI Device + IDXGIDevice* dxgi_device; // Don't use ATL::CComPtr + device->QueryInterface(__uuidof(IDXGIDevice), (void**)&dxgi_device); + + // Get DXGI Adapter + ATL::CComPtr dxgi_adapter; + dxgi_device->GetAdapter(&dxgi_adapter); + + // Get Device Index + if (result res = _cuda->cuD3D11GetDevice(&_device, dxgi_adapter); res != result::SUCCESS) { + throw std::runtime_error("Failed to get device index for device."); + } + + _cuda->cuDevicePrimaryCtxSetFlags(_device, context_flags::SCHEDULER_BLOCKING_SYNC); + + // Acquire Context + if (result res = _cuda->cuDevicePrimaryCtxRetain(&_ctx, _device); res != result::SUCCESS) { + throw std::runtime_error("Failed to acquire primary device context."); + } + + // Log some information. + std::string device_name; + uuid_t device_uuid; + luid_t device_luid; + uint32_t device_luid_mask; + { + // Device Name + std::vector name(256, 0); + _cuda->cuDeviceGetName(name.data(), static_cast(name.size() - 1), _device); + device_name = std::string(name.data(), name.data() + strlen(name.data())); + + // Device LUID + _cuda->cuDeviceGetLuid(&device_luid, &device_luid_mask, _device); + + // Device UUID + _cuda->cuDeviceGetUuid(&device_uuid, _device); + } + + D_LOG_INFO("Initialized CUDA on device '%s' (%08" PRIx32 "-%04" PRIx16 "-%04" PRIx16 "-%04" PRIx16 "-%04" PRIx16 "%08" PRIx32 ", %08" PRIx64 ", %" PRIu32 ").", device_name.c_str(), device_uuid.uuid.a, device_uuid.uuid.b, device_uuid.uuid.c, device_uuid.uuid.d, device_uuid.uuid.e, device_uuid.uuid.f, device_luid.luid, device_luid_mask); + + _has_device = true; +} +#endif + +::streamfx::nvidia::cuda::context_t streamfx::nvidia::cuda::context::get() +{ + return _ctx; +} + +std::shared_ptr<::streamfx::nvidia::cuda::context_stack> streamfx::nvidia::cuda::context::enter() +{ + return std::make_shared<::streamfx::nvidia::cuda::context_stack>(shared_from_this()); +} + +void streamfx::nvidia::cuda::context::push() +{ + if (auto res = _cuda->cuCtxPushCurrent(_ctx); res != ::streamfx::nvidia::cuda::result::SUCCESS) { + throw ::streamfx::nvidia::cuda::cuda_error(res); + } +} + +void streamfx::nvidia::cuda::context::pop() +{ +#ifdef ENABLE_STACK_CHECKS + ::streamfx::nvidia::cuda::context_t ctx; + if (_cuda->cuCtxGetCurrent(&ctx) == ::streamfx::nvidia::cuda::result::SUCCESS) + assert(ctx == _ctx); +#endif + + assert(_cuda->cuCtxPopCurrent(&ctx) == ::streamfx::nvidia::cuda::result::SUCCESS); +} + +void streamfx::nvidia::cuda::context::synchronize() +{ + //D_LOG_DEBUG("Synchronizing... (Addr: 0x%" PRIuPTR ")", this); + +#ifdef ENABLE_STACK_CHECKS + ::streamfx::nvidia::cuda::context_t ctx; + if (_cuda->cuCtxGetCurrent(&ctx) == ::streamfx::nvidia::cuda::result::SUCCESS) + assert(ctx == _ctx); +#endif + + if (auto res = _cuda->cuCtxSynchronize(); res != ::streamfx::nvidia::cuda::result::SUCCESS) { + throw ::streamfx::nvidia::cuda::cuda_error(res); + } +} diff --git a/components/nvidia/source/nvidia/cuda/nvidia-cuda-gs-texture.cpp b/components/nvidia/source/nvidia/cuda/nvidia-cuda-gs-texture.cpp new file mode 100644 index 0000000..43fa442 --- /dev/null +++ b/components/nvidia/source/nvidia/cuda/nvidia-cuda-gs-texture.cpp @@ -0,0 +1,117 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2022 lainon +// AUTOGENERATED COPYRIGHT HEADER END + +#include "nvidia/cuda/nvidia-cuda-gs-texture.hpp" +#include "obs/gs/gs-helper.hpp" +#include "util/util-logging.hpp" + +#ifdef _DEBUG +#define ST_PREFIX "<%s> " +#define D_LOG_ERROR(x, ...) P_LOG_ERROR(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_WARNING(x, ...) P_LOG_WARN(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_INFO(x, ...) P_LOG_INFO(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_DEBUG(x, ...) P_LOG_DEBUG(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#else +#define ST_PREFIX " " +#define D_LOG_ERROR(...) P_LOG_ERROR(ST_PREFIX __VA_ARGS__) +#define D_LOG_WARNING(...) P_LOG_WARN(ST_PREFIX __VA_ARGS__) +#define D_LOG_INFO(...) P_LOG_INFO(ST_PREFIX __VA_ARGS__) +#define D_LOG_DEBUG(...) P_LOG_DEBUG(ST_PREFIX __VA_ARGS__) +#endif + +streamfx::nvidia::cuda::gstexture::~gstexture() +{ + D_LOG_DEBUG("Finalizing... (Addr: 0x%" PRIuPTR ")", this); + + unmap(); + _cuda->cuGraphicsUnregisterResource(_resource); +} + +streamfx::nvidia::cuda::gstexture::gstexture(std::shared_ptr texture) : _cuda(::streamfx::nvidia::cuda::cuda::get()), _texture(texture), _resource(), _is_mapped(false), _pointer() +{ + D_LOG_DEBUG("Initializating... (Addr: 0x%" PRIuPTR ")", this); + + if (!texture) + throw std::invalid_argument("texture"); + + streamfx::obs::gs::context gctx; + int dev_type = gs_get_device_type(); + + if (dev_type == GS_DEVICE_OPENGL) { + // ToDo + } +#ifdef WIN32 + if (dev_type == GS_DEVICE_DIRECT3D_11) { + ID3D11Resource* resource = static_cast(gs_texture_get_obj(_texture->get_object())); + + if (!resource) { + throw std::runtime_error("nvidia::cuda::gstexture: Failed to get resource from gs::texture."); + } + + switch (_cuda->cuGraphicsD3D11RegisterResource(&_resource, resource, 0)) { + case streamfx::nvidia::cuda::result::SUCCESS: + break; + default: + throw std::runtime_error("nvidia::cuda::gstexture: Failed to register resource."); + } + } +#endif +} + +streamfx::nvidia::cuda::array_t streamfx::nvidia::cuda::gstexture::map(std::shared_ptr stream) +{ + if (_is_mapped) { + return _pointer; + } + + graphics_resource_t resources[] = {_resource}; + switch (_cuda->cuGraphicsMapResources(1, resources, stream->get())) { + case streamfx::nvidia::cuda::result::SUCCESS: + break; + default: + throw std::runtime_error("nvidia::cuda::gstexture: Mapping failed."); + } + + _stream = std::move(stream); + _is_mapped = true; + + switch (_cuda->cuGraphicsSubResourceGetMappedArray(&_pointer, _resource, 0, 0)) { + case streamfx::nvidia::cuda::result::SUCCESS: + break; + default: + unmap(); + throw std::runtime_error("nvidia::cuda::gstexture: Mapping pointer failed."); + } + + return _pointer; +} + +void streamfx::nvidia::cuda::gstexture::unmap() +{ + if (!_is_mapped) + return; + + graphics_resource_t resources[] = {_resource}; + switch (_cuda->cuGraphicsUnmapResources(1, resources, _stream->get())) { + case streamfx::nvidia::cuda::result::SUCCESS: + break; + default: + throw std::runtime_error("nvidia::cuda::gstexture: Unmapping failed."); + } + + _is_mapped = false; + _pointer = nullptr; + _stream.reset(); +} + +std::shared_ptr streamfx::nvidia::cuda::gstexture::get_texture() +{ + return _texture; +} + +::streamfx::nvidia::cuda::graphics_resource_t streamfx::nvidia::cuda::gstexture::get() +{ + return _resource; +} diff --git a/components/nvidia/source/nvidia/cuda/nvidia-cuda-memory.cpp b/components/nvidia/source/nvidia/cuda/nvidia-cuda-memory.cpp new file mode 100644 index 0000000..6705dd4 --- /dev/null +++ b/components/nvidia/source/nvidia/cuda/nvidia-cuda-memory.cpp @@ -0,0 +1,54 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#include "nvidia/cuda/nvidia-cuda-memory.hpp" +#include "util/util-logging.hpp" + +#include "warning-disable.hpp" +#include +#include "warning-enable.hpp" + +#ifdef _DEBUG +#define ST_PREFIX "<%s> " +#define D_LOG_ERROR(x, ...) P_LOG_ERROR(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_WARNING(x, ...) P_LOG_WARN(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_INFO(x, ...) P_LOG_INFO(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_DEBUG(x, ...) P_LOG_DEBUG(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#else +#define ST_PREFIX " " +#define D_LOG_ERROR(...) P_LOG_ERROR(ST_PREFIX __VA_ARGS__) +#define D_LOG_WARNING(...) P_LOG_WARN(ST_PREFIX __VA_ARGS__) +#define D_LOG_INFO(...) P_LOG_INFO(ST_PREFIX __VA_ARGS__) +#define D_LOG_DEBUG(...) P_LOG_DEBUG(ST_PREFIX __VA_ARGS__) +#endif + +streamfx::nvidia::cuda::memory::~memory() +{ + D_LOG_DEBUG("Finalizing... (Addr: 0x%" PRIuPTR ")", this); + + _cuda->cuMemFree(_pointer); +} + +streamfx::nvidia::cuda::memory::memory(size_t size) : _cuda(::streamfx::nvidia::cuda::cuda::get()), _pointer(), _size(size) +{ + D_LOG_DEBUG("Initializating... (Addr: 0x%" PRIuPTR ")", this); + + ::streamfx::nvidia::cuda::result res = _cuda->cuMemAlloc(&_pointer, _size); + switch (res) { + case ::streamfx::nvidia::cuda::result::SUCCESS: + break; + default: + throw std::runtime_error("nvidia::cuda::memory: cuMemAlloc failed."); + } +} + +streamfx::nvidia::cuda::device_ptr_t streamfx::nvidia::cuda::memory::get() +{ + return _pointer; +} + +std::size_t streamfx::nvidia::cuda::memory::size() +{ + return _size; +} diff --git a/components/nvidia/source/nvidia/cuda/nvidia-cuda-obs.cpp b/components/nvidia/source/nvidia/cuda/nvidia-cuda-obs.cpp new file mode 100644 index 0000000..500546c --- /dev/null +++ b/components/nvidia/source/nvidia/cuda/nvidia-cuda-obs.cpp @@ -0,0 +1,87 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#include "nvidia/cuda/nvidia-cuda-obs.hpp" +#include "obs/gs/gs-helper.hpp" +#include "util/util-logging.hpp" + +#ifdef _DEBUG +#define ST_PREFIX "<%s> " +#define D_LOG_ERROR(x, ...) P_LOG_ERROR(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_WARNING(x, ...) P_LOG_WARN(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_INFO(x, ...) P_LOG_INFO(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_DEBUG(x, ...) P_LOG_DEBUG(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#else +#define ST_PREFIX " " +#define D_LOG_ERROR(...) P_LOG_ERROR(ST_PREFIX __VA_ARGS__) +#define D_LOG_WARNING(...) P_LOG_WARN(ST_PREFIX __VA_ARGS__) +#define D_LOG_INFO(...) P_LOG_INFO(ST_PREFIX __VA_ARGS__) +#define D_LOG_DEBUG(...) P_LOG_DEBUG(ST_PREFIX __VA_ARGS__) +#endif + +streamfx::nvidia::cuda::obs::~obs() +{ + D_LOG_DEBUG("Finalizing... (Addr: 0x%" PRIuPTR ")", this); + + auto gctx = streamfx::obs::gs::context{}; + { + auto stack = _context->enter(); + _stream->synchronize(); + _context->synchronize(); + _stream.reset(); + } + _context.reset(); + _cuda.reset(); +} + +streamfx::nvidia::cuda::obs::obs() : _cuda(::streamfx::nvidia::cuda::cuda::get()), _context() +{ + D_LOG_DEBUG("Initializating... (Addr: 0x%" PRIuPTR ")", this); + + auto gctx = streamfx::obs::gs::context{}; + + // Create Context +#ifdef WIN32 + if (gs_get_device_type() == GS_DEVICE_DIRECT3D_11) { + _context = std::make_shared<::streamfx::nvidia::cuda::context>(reinterpret_cast(gs_get_device_obj())); + } +#endif + if (gs_get_device_type() == GS_DEVICE_OPENGL) { + throw std::runtime_error("Not yet implemented."); + } + + // Create Stream + auto stack = _context->enter(); + _stream = std::make_shared<::streamfx::nvidia::cuda::stream>(); +} + +std::shared_ptr streamfx::nvidia::cuda::obs::get() +{ + static std::weak_ptr instance; + static std::mutex lock; + + std::unique_lock ul(lock); + if (instance.expired()) { + std::shared_ptr hard_instance; + hard_instance = std::make_shared(); + instance = hard_instance; + return hard_instance; + } + return instance.lock(); +} + +std::shared_ptr streamfx::nvidia::cuda::obs::get_cuda() +{ + return _cuda; +} + +std::shared_ptr streamfx::nvidia::cuda::obs::get_context() +{ + return _context; +} + +std::shared_ptr streamfx::nvidia::cuda::obs::get_stream() +{ + return _stream; +} diff --git a/components/nvidia/source/nvidia/cuda/nvidia-cuda-stream.cpp b/components/nvidia/source/nvidia/cuda/nvidia-cuda-stream.cpp new file mode 100644 index 0000000..f9ed360 --- /dev/null +++ b/components/nvidia/source/nvidia/cuda/nvidia-cuda-stream.cpp @@ -0,0 +1,62 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#include "nvidia/cuda/nvidia-cuda-stream.hpp" +#include "util/util-logging.hpp" + +#include "warning-disable.hpp" +#include +#include "warning-enable.hpp" + +#ifdef _DEBUG +#define ST_PREFIX "<%s> " +#define D_LOG_ERROR(x, ...) P_LOG_ERROR(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_WARNING(x, ...) P_LOG_WARN(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_INFO(x, ...) P_LOG_INFO(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_DEBUG(x, ...) P_LOG_DEBUG(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#else +#define ST_PREFIX " " +#define D_LOG_ERROR(...) P_LOG_ERROR(ST_PREFIX __VA_ARGS__) +#define D_LOG_WARNING(...) P_LOG_WARN(ST_PREFIX __VA_ARGS__) +#define D_LOG_INFO(...) P_LOG_INFO(ST_PREFIX __VA_ARGS__) +#define D_LOG_DEBUG(...) P_LOG_DEBUG(ST_PREFIX __VA_ARGS__) +#endif + +streamfx::nvidia::cuda::stream::~stream() +{ + D_LOG_DEBUG("Finalizing... (Addr: 0x%" PRIuPTR ")", this); + + _cuda->cuStreamDestroy(_stream); +} + +streamfx::nvidia::cuda::stream::stream(::streamfx::nvidia::cuda::stream_flags flags, int32_t priority) : _cuda(::streamfx::nvidia::cuda::cuda::get()) +{ + D_LOG_DEBUG("Initializating... (Addr: 0x%" PRIuPTR ")", this); + + streamfx::nvidia::cuda::result res; + if (priority == 0) { + res = _cuda->cuStreamCreate(&_stream, flags); + } else { + res = _cuda->cuStreamCreateWithPriority(&_stream, flags, priority); + } + switch (res) { + case streamfx::nvidia::cuda::result::SUCCESS: + break; + default: + throw std::runtime_error("Failed to create CUstream object."); + } +} + +::streamfx::nvidia::cuda::stream_t streamfx::nvidia::cuda::stream::get() +{ + return _stream; +} + +void streamfx::nvidia::cuda::stream::synchronize() +{ + //D_LOG_DEBUG("Synchronizing... (Addr: 0x%" PRIuPTR ")", this); + if (auto res = _cuda->cuStreamSynchronize(_stream); res != ::streamfx::nvidia::cuda::result::SUCCESS) { + throw ::streamfx::nvidia::cuda::cuda_error(res); + } +} diff --git a/components/nvidia/source/nvidia/cuda/nvidia-cuda.cpp b/components/nvidia/source/nvidia/cuda/nvidia-cuda.cpp new file mode 100644 index 0000000..69d1e9c --- /dev/null +++ b/components/nvidia/source/nvidia/cuda/nvidia-cuda.cpp @@ -0,0 +1,256 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#include "nvidia/cuda/nvidia-cuda.hpp" +#include "util/util-logging.hpp" + +#include "warning-disable.hpp" +#include +#include "warning-enable.hpp" + +#ifdef _DEBUG +#define ST_PREFIX "<%s> " +#define D_LOG_ERROR(x, ...) P_LOG_ERROR(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_WARNING(x, ...) P_LOG_WARN(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_INFO(x, ...) P_LOG_INFO(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_DEBUG(x, ...) P_LOG_DEBUG(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#else +#define ST_PREFIX " " +#define D_LOG_ERROR(...) P_LOG_ERROR(ST_PREFIX __VA_ARGS__) +#define D_LOG_WARNING(...) P_LOG_WARN(ST_PREFIX __VA_ARGS__) +#define D_LOG_INFO(...) P_LOG_INFO(ST_PREFIX __VA_ARGS__) +#define D_LOG_DEBUG(...) P_LOG_DEBUG(ST_PREFIX __VA_ARGS__) +#endif + +#if defined(_WIN32) || defined(_WIN64) +#define ST_CUDA_NAME "nvcuda.dll" +#else +#define ST_CUDA_NAME "libcuda.so.1" +#endif + +#define P_CUDA_LOAD_SYMBOL(NAME) \ + { \ + NAME = reinterpret_cast(_library->load_symbol(#NAME)); \ + if (!NAME) \ + throw std::runtime_error("Failed to load '" #NAME "' from '" ST_CUDA_NAME "'."); \ + } +#define P_CUDA_LOAD_SYMBOL_OPT(NAME) \ + { \ + NAME = reinterpret_cast(_library->load_symbol(#NAME)); \ + if (!NAME) \ + D_LOG_WARNING("Loading of optional symbol '" #NAME "' failed.", 0); \ + } + +#define P_CUDA_LOAD_SYMBOL_EX(NAME, OVERRIDE) \ + { \ + NAME = reinterpret_cast(_library->load_symbol(#OVERRIDE)); \ + if (!NAME) \ + throw std::runtime_error("Failed to load '" #NAME "' from '" ST_CUDA_NAME "'."); \ + } +#define P_CUDA_LOAD_SYMBOL_OPT_EX(NAME, OVERRIDE) \ + { \ + NAME = reinterpret_cast(_library->load_symbol(#OVERRIDE)); \ + if (!NAME) \ + D_LOG_WARNING("Loading of optional symbol '" #NAME "' failed.", 0); \ + } + +#define P_CUDA_LOAD_SYMBOL_V2(NAME) \ + { \ + NAME = reinterpret_cast(_library->load_symbol(#NAME "_v2")); \ + if (!NAME) \ + throw std::runtime_error("Failed to load '" #NAME "' from '" ST_CUDA_NAME "'."); \ + } +#define P_CUDA_LOAD_SYMBOL_OPT_V2(NAME) \ + { \ + NAME = reinterpret_cast(_library->load_symbol(#NAME "_v2")); \ + if (!NAME) \ + D_LOG_WARNING("Loading of optional symbol '" #NAME "' failed.", 0); \ + } + +streamfx::nvidia::cuda::cuda::~cuda() +{ + D_LOG_DEBUG("Finalizing... (Addr: 0x%" PRIuPTR ")", this); +} + +streamfx::nvidia::cuda::cuda::cuda() : _library() +{ + int32_t cuda_version = 0; + + D_LOG_DEBUG("Initializing... (Addr: 0x%" PRIuPTR ")", this); + + _library = streamfx::util::library::load(std::string_view(ST_CUDA_NAME)); + + { // 1. Load critical initialization functions. + // Initialization + P_CUDA_LOAD_SYMBOL(cuInit); + + // Version Management + P_CUDA_LOAD_SYMBOL(cuDriverGetVersion); + } + + { // 2. Get the CUDA Driver version and log it. + if (cuDriverGetVersion(&cuda_version) == result::SUCCESS) { + int32_t major = cuda_version / 1000; + int32_t minor = (cuda_version % 1000) / 10; + int32_t patch = (cuda_version % 10); + D_LOG_INFO("Driver reported CUDA version: %" PRId32 ".%" PRId32 ".%" PRId32, major, minor, patch); + } else { + D_LOG_WARNING("Failed to query NVIDIA CUDA Driver for version.", 0); + } + } + + { // 3. Load remaining functions. + // Device Management + P_CUDA_LOAD_SYMBOL(cuDeviceGetName); + P_CUDA_LOAD_SYMBOL(cuDeviceGetLuid); + P_CUDA_LOAD_SYMBOL(cuDeviceGetUuid); + + // Primary Context Management + P_CUDA_LOAD_SYMBOL(cuDevicePrimaryCtxRetain); + P_CUDA_LOAD_SYMBOL_V2(cuDevicePrimaryCtxRelease); + P_CUDA_LOAD_SYMBOL_OPT_V2(cuDevicePrimaryCtxSetFlags); + + // Context Management + P_CUDA_LOAD_SYMBOL_V2(cuCtxCreate); + P_CUDA_LOAD_SYMBOL_V2(cuCtxDestroy); + P_CUDA_LOAD_SYMBOL_V2(cuCtxPushCurrent); + P_CUDA_LOAD_SYMBOL_V2(cuCtxPopCurrent); + P_CUDA_LOAD_SYMBOL_OPT(cuCtxGetCurrent); + P_CUDA_LOAD_SYMBOL_OPT(cuCtxSetCurrent); + P_CUDA_LOAD_SYMBOL(cuCtxGetStreamPriorityRange); + P_CUDA_LOAD_SYMBOL(cuCtxSynchronize); + + // Module Management + // - Not yet needed. + + // Memory Management + P_CUDA_LOAD_SYMBOL_V2(cuMemAlloc); + P_CUDA_LOAD_SYMBOL_V2(cuMemAllocPitch); + P_CUDA_LOAD_SYMBOL_V2(cuMemFree); + P_CUDA_LOAD_SYMBOL(cuMemcpy); + P_CUDA_LOAD_SYMBOL_V2(cuMemcpy2D); + P_CUDA_LOAD_SYMBOL_V2(cuMemcpy2DAsync); + P_CUDA_LOAD_SYMBOL_OPT_V2(cuArrayGetDescriptor); + P_CUDA_LOAD_SYMBOL_OPT_V2(cuMemcpyAtoA); + P_CUDA_LOAD_SYMBOL_OPT_V2(cuMemcpyAtoD); + P_CUDA_LOAD_SYMBOL_OPT_V2(cuMemcpyAtoH); + P_CUDA_LOAD_SYMBOL_OPT_V2(cuMemcpyAtoHAsync); + P_CUDA_LOAD_SYMBOL_OPT_V2(cuMemcpyDtoA); + P_CUDA_LOAD_SYMBOL_OPT_V2(cuMemcpyDtoD); + P_CUDA_LOAD_SYMBOL_OPT_V2(cuMemcpyDtoH); + P_CUDA_LOAD_SYMBOL_OPT_V2(cuMemcpyDtoHAsync); + P_CUDA_LOAD_SYMBOL_OPT_V2(cuMemcpyHtoA); + P_CUDA_LOAD_SYMBOL_OPT_V2(cuMemcpyHtoAAsync); + P_CUDA_LOAD_SYMBOL_OPT_V2(cuMemcpyHtoD); + P_CUDA_LOAD_SYMBOL_OPT_V2(cuMemcpyHtoDAsync); + P_CUDA_LOAD_SYMBOL_OPT_V2(cuMemHostGetDevicePointer); + P_CUDA_LOAD_SYMBOL_V2(cuMemsetD8); + P_CUDA_LOAD_SYMBOL(cuMemsetD8Async); + P_CUDA_LOAD_SYMBOL_OPT_V2(cuMemsetD16); + P_CUDA_LOAD_SYMBOL_OPT(cuMemsetD16Async); + P_CUDA_LOAD_SYMBOL_OPT_V2(cuMemsetD32); + P_CUDA_LOAD_SYMBOL_OPT(cuMemsetD32Async); + + // Virtual Memory Management + // - Not yet needed. + + // Stream Ordered Memory Allocator + // - Not yet needed. + + // Unified Addressing + // - Not yet needed. + + // Stream Management + P_CUDA_LOAD_SYMBOL(cuStreamCreate); + P_CUDA_LOAD_SYMBOL_V2(cuStreamDestroy); + P_CUDA_LOAD_SYMBOL(cuStreamSynchronize); + P_CUDA_LOAD_SYMBOL_OPT(cuStreamCreateWithPriority); + P_CUDA_LOAD_SYMBOL_OPT(cuStreamGetPriority); + + // Event Management + // - Not yet needed. + + // External Resource Interoperability (CUDA 11.1+) + // - Not yet needed. + + // Stream Memory Operations + // - Not yet needed. + + // Execution Control + // - Not yet needed. + + // Graph Management + // - Not yet needed. + + // Occupancy + // - Not yet needed. + + // Texture Object Management + // - Not yet needed. + + // Surface Object Management + // - Not yet needed. + + // Peer Context Memory Access + // - Not yet needed. + + // Graphics Interoperability + P_CUDA_LOAD_SYMBOL(cuGraphicsMapResources); + P_CUDA_LOAD_SYMBOL(cuGraphicsSubResourceGetMappedArray); + P_CUDA_LOAD_SYMBOL(cuGraphicsUnmapResources); + P_CUDA_LOAD_SYMBOL(cuGraphicsUnregisterResource); + + // Driver Entry Point Access + // - Not yet needed. + + // Profiler Control + // - Not yet needed. + + // OpenGL Interoperability + // - Not yet needed. + + // VDPAU Interoperability + // - Not yet needed. + + // EGL Interoperability + // - Not yet needed. + +#ifdef WIN32 + // Direct3D9 Interoperability + // - Not yet needed. + + // Direct3D10 Interoperability + P_CUDA_LOAD_SYMBOL(cuD3D10GetDevice); + P_CUDA_LOAD_SYMBOL_OPT(cuGraphicsD3D10RegisterResource); + + // Direct3D11 Interoperability + P_CUDA_LOAD_SYMBOL(cuD3D11GetDevice); + P_CUDA_LOAD_SYMBOL_OPT(cuGraphicsD3D11RegisterResource); +#endif + } + + // Initialize CUDA + cuInit(0); +} + +int32_t streamfx::nvidia::cuda::cuda::version() +{ + int32_t v = 0; + cuDriverGetVersion(&v); + return v; +} + +std::shared_ptr streamfx::nvidia::cuda::cuda::get() +{ + static std::weak_ptr instance; + static std::mutex lock; + + std::unique_lock ul(lock); + if (instance.expired()) { + auto hard_instance = std::make_shared(); + instance = hard_instance; + return hard_instance; + } + return instance.lock(); +} diff --git a/components/nvidia/source/nvidia/cv/nvidia-cv-image.cpp b/components/nvidia/source/nvidia/cv/nvidia-cv-image.cpp new file mode 100644 index 0000000..a4b6241 --- /dev/null +++ b/components/nvidia/source/nvidia/cv/nvidia-cv-image.cpp @@ -0,0 +1,74 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2021-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +// NVIDIA CVImage is part of: +// - NVIDIA Video Effects SDK +// - NVIDIA Augmented Reality SDK + +#include "nvidia/cv/nvidia-cv-image.hpp" +#include "nvidia/cuda/nvidia-cuda-obs.hpp" +#include "obs/gs/gs-helper.hpp" + +#ifdef _DEBUG +#define ST_PREFIX "<%s> " +#define D_LOG_ERROR(x, ...) P_LOG_ERROR(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_WARNING(x, ...) P_LOG_WARN(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_INFO(x, ...) P_LOG_INFO(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_DEBUG(x, ...) P_LOG_DEBUG(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#else +#define ST_PREFIX " " +#define D_LOG_ERROR(...) P_LOG_ERROR(ST_PREFIX __VA_ARGS__) +#define D_LOG_WARNING(...) P_LOG_WARN(ST_PREFIX __VA_ARGS__) +#define D_LOG_INFO(...) P_LOG_INFO(ST_PREFIX __VA_ARGS__) +#define D_LOG_DEBUG(...) P_LOG_DEBUG(ST_PREFIX __VA_ARGS__) +#endif + +using ::streamfx::nvidia::cv::image; +using ::streamfx::nvidia::cv::result; + +image::~image() +{ + auto gctx = ::streamfx::obs::gs::context(); + auto cctx = ::streamfx::nvidia::cuda::obs::get()->get_context()->enter(); + + _cv->NvCVImage_Dealloc(&_image); +} + +image::image() : _cv(::streamfx::nvidia::cv::cv::get()), _image(), _alignment(1) +{ + // Forcefully clear the image storage. + memset(&_image, sizeof(_image), 0); +} + +image::image(uint32_t width, uint32_t height, pixel_format pix_fmt, component_type cmp_type, component_layout cmp_layout, memory_location location, uint32_t alignment) : image() +{ + auto gctx = ::streamfx::obs::gs::context(); + auto cctx = ::streamfx::nvidia::cuda::obs::get()->get_context()->enter(); + + _alignment = alignment; + if (auto res = _cv->NvCVImage_Alloc(&_image, width, height, pix_fmt, cmp_type, static_cast(cmp_layout), static_cast(location), _alignment); res != result::SUCCESS) { + throw std::runtime_error(_cv->NvCV_GetErrorStringFromCode(res)); + } +} + +void streamfx::nvidia::cv::image::reallocate(uint32_t width, uint32_t height, pixel_format pix_fmt, component_type cmp_type, component_layout cmp_layout, memory_location location, uint32_t alignment) +{ + auto gctx = ::streamfx::obs::gs::context(); + auto cctx = ::streamfx::nvidia::cuda::obs::get()->get_context()->enter(); + + if (auto res = _cv->NvCVImage_Realloc(&_image, width, height, pix_fmt, cmp_type, static_cast(cmp_layout), static_cast(location), alignment); res != result::SUCCESS) { + throw std::runtime_error(_cv->NvCV_GetErrorStringFromCode(res)); + } + _alignment = alignment; +} + +void streamfx::nvidia::cv::image::resize(uint32_t width, uint32_t height) +{ + reallocate(width, height, _image.pxl_format, _image.comp_type, static_cast(_image.comp_layout), static_cast(_image.mem_location), _alignment); +} + +streamfx::nvidia::cv::image_t* streamfx::nvidia::cv::image::get_image() +{ + return &_image; +} diff --git a/components/nvidia/source/nvidia/cv/nvidia-cv-texture.cpp b/components/nvidia/source/nvidia/cv/nvidia-cv-texture.cpp new file mode 100644 index 0000000..00661ab --- /dev/null +++ b/components/nvidia/source/nvidia/cv/nvidia-cv-texture.cpp @@ -0,0 +1,100 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2021-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +// NVIDIA CVImage is part of: +// - NVIDIA Video Effects SDK +// - NVIDIA Augmented Reality SDK + +#include "nvidia/cv/nvidia-cv-texture.hpp" +#include "nvidia/cuda/nvidia-cuda-obs.hpp" +#include "obs/gs/gs-helper.hpp" +#include "util/util-logging.hpp" + +#ifdef _DEBUG +#define ST_PREFIX "<%s> " +#define D_LOG_ERROR(x, ...) P_LOG_ERROR(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_WARNING(x, ...) P_LOG_WARN(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_INFO(x, ...) P_LOG_INFO(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_DEBUG(x, ...) P_LOG_DEBUG(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#else +#define ST_PREFIX " " +#define D_LOG_ERROR(...) P_LOG_ERROR(ST_PREFIX __VA_ARGS__) +#define D_LOG_WARNING(...) P_LOG_WARN(ST_PREFIX __VA_ARGS__) +#define D_LOG_INFO(...) P_LOG_INFO(ST_PREFIX __VA_ARGS__) +#define D_LOG_DEBUG(...) P_LOG_DEBUG(ST_PREFIX __VA_ARGS__) +#endif + +using ::streamfx::nvidia::cv::component_layout; +using ::streamfx::nvidia::cv::component_type; +using ::streamfx::nvidia::cv::pixel_format; +using ::streamfx::nvidia::cv::result; +using ::streamfx::nvidia::cv::texture; + +texture::~texture() +{ + auto gctx = ::streamfx::obs::gs::context(); + auto cctx = ::streamfx::nvidia::cuda::obs::get()->get_context()->enter(); + + free(); + _texture.reset(); +} + +texture::texture(uint32_t width, uint32_t height, gs_color_format pix_fmt) +{ + auto gctx = ::streamfx::obs::gs::context(); + auto cctx = ::streamfx::nvidia::cuda::obs::get()->get_context()->enter(); + + // Allocate a new Texture + _texture = std::make_shared<::streamfx::obs::gs::texture>(width, height, pix_fmt, 1, nullptr, ::streamfx::obs::gs::texture_flags::None); + alloc(); +} + +void texture::resize(uint32_t width, uint32_t height) +{ + auto gctx = ::streamfx::obs::gs::context(); + auto cctx = ::streamfx::nvidia::cuda::obs::get()->get_context()->enter(); + + D_LOG_DEBUG("Resizing object 0x%" PRIxPTR " to %" PRIu32 "x%" PRIu32 "...", this, width, height); + + // Allocate a new Texture + free(); + _texture = std::make_shared<::streamfx::obs::gs::texture>(width, height, _texture->color_format(), 1, nullptr, ::streamfx::obs::gs::texture_flags::None); + alloc(); +} + +std::shared_ptr<::streamfx::obs::gs::texture> texture::get_texture() +{ + return _texture; +} + +void streamfx::nvidia::cv::texture::alloc() +{ + auto gctx = ::streamfx::obs::gs::context(); + auto cctx = ::streamfx::nvidia::cuda::obs::get()->get_context()->enter(); + auto nvobs = ::streamfx::nvidia::cuda::obs::get(); + + // Allocate any relevant CV buffers and Map it. + if (auto res = _cv->NvCVImage_InitFromD3D11Texture(&_image, reinterpret_cast(gs_texture_get_obj(_texture->get_object()))); res != result::SUCCESS) { + D_LOG_ERROR("Object 0x%" PRIxPTR " failed NvCVImage_InitFromD3D11Texture call with error: %s", this, _cv->NvCV_GetErrorStringFromCode(res)); + throw std::runtime_error("NvCVImage_InitFromD3D11Texture"); + } + if (auto res = _cv->NvCVImage_MapResource(&_image, nvobs->get_stream()->get()); res != result::SUCCESS) { + D_LOG_ERROR("Object 0x%" PRIxPTR " failed NvCVImage_MapResource call with error: %s", this, _cv->NvCV_GetErrorStringFromCode(res)); + throw std::runtime_error("NvCVImage_MapResource"); + } +} + +void streamfx::nvidia::cv::texture::free() +{ + auto gctx = ::streamfx::obs::gs::context(); + auto cctx = ::streamfx::nvidia::cuda::obs::get()->get_context()->enter(); + auto nvobs = ::streamfx::nvidia::cuda::obs::get(); + + // Unmap and deallocate any relevant CV buffers. + if (auto res = _cv->NvCVImage_UnmapResource(&_image, nvobs->get_stream()->get()); res != result::SUCCESS) { + D_LOG_ERROR("Object 0x%" PRIxPTR " failed NvCVImage_UnmapResource call with error: %s", this, _cv->NvCV_GetErrorStringFromCode(res)); + throw std::runtime_error("NvCVImage_UnmapResource"); + } + _cv->NvCVImage_Dealloc(&_image); +} diff --git a/components/nvidia/source/nvidia/cv/nvidia-cv.cpp b/components/nvidia/source/nvidia/cv/nvidia-cv.cpp new file mode 100644 index 0000000..38737b4 --- /dev/null +++ b/components/nvidia/source/nvidia/cv/nvidia-cv.cpp @@ -0,0 +1,237 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2021-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +// NVIDIA CVImage is part of: +// - NVIDIA Video Effects SDK +// - NVIDIA Augmented Reality SDK + +#include "nvidia/cv/nvidia-cv.hpp" +#include "nvidia/cuda/nvidia-cuda-obs.hpp" +#include "obs/gs/gs-helper.hpp" +#include "util/util-logging.hpp" +#include "util/util-platform.hpp" + +#include "warning-disable.hpp" +#include +#include +#include "warning-enable.hpp" + +#ifdef _DEBUG +#define ST_PREFIX "<%s> " +#define D_LOG_ERROR(x, ...) P_LOG_ERROR(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_WARNING(x, ...) P_LOG_WARN(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_INFO(x, ...) P_LOG_INFO(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_DEBUG(x, ...) P_LOG_DEBUG(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#else +#define ST_PREFIX " " +#define D_LOG_ERROR(...) P_LOG_ERROR(ST_PREFIX __VA_ARGS__) +#define D_LOG_WARNING(...) P_LOG_WARN(ST_PREFIX __VA_ARGS__) +#define D_LOG_INFO(...) P_LOG_INFO(ST_PREFIX __VA_ARGS__) +#define D_LOG_DEBUG(...) P_LOG_DEBUG(ST_PREFIX __VA_ARGS__) +#endif + +#if defined(WIN32) +#include "warning-disable.hpp" +#include +#include +#include +#include "warning-enable.hpp" + +#define LIB_NAME "NVCVImage.dll" +#else +#define LIB_NAME "libNVCVImage.so" +#endif + +#define ST_ENV_NVIDIA_AR_SDK_PATH L"NV_AR_SDK_PATH" +#define ST_ENV_NVIDIA_VIDEO_EFFECTS_SDK_PATH L"NV_VIDEO_EFFECTS_PATH" + +#define NVCVI_LOAD_SYMBOL(NAME) \ + { \ + NAME = reinterpret_cast(_library->load_symbol(#NAME)); \ + if (!NAME) \ + throw std::runtime_error("Failed to load '" #NAME "' from '" LIB_NAME "'."); \ + } + +streamfx::nvidia::cv::cv::~cv() +{ + D_LOG_DEBUG("Finalizing... (Addr: 0x%" PRIuPTR ")", this); + +#ifdef WIN32 + // Remove the DLL directory from the library loader paths. + if (_extra != nullptr) { + RemoveDllDirectory(reinterpret_cast(_extra)); + } +#endif +} + +streamfx::nvidia::cv::cv::cv() +{ + std::filesystem::path vfx_sdk_path; + std::filesystem::path ar_sdk_path; + std::vector lib_paths; + + auto gctx = ::streamfx::obs::gs::context(); + auto cctx = ::streamfx::nvidia::cuda::obs::get()->get_context()->enter(); + + D_LOG_DEBUG("Initializing... (Addr: 0x%" PRIuPTR ")", this); + + // Figure out the location of supported SDKs. + { +#ifdef WIN32 + DWORD env_size; + std::vector buffer; + env_size = GetEnvironmentVariableW(ST_ENV_NVIDIA_VIDEO_EFFECTS_SDK_PATH, nullptr, 0); + if (env_size > 0) { + buffer.resize(static_cast(env_size) + 1); + env_size = GetEnvironmentVariableW(ST_ENV_NVIDIA_VIDEO_EFFECTS_SDK_PATH, buffer.data(), static_cast(buffer.size())); + vfx_sdk_path = std::wstring(buffer.data(), buffer.size()); + } else { + PWSTR str = nullptr; + HRESULT res = SHGetKnownFolderPath(FOLDERID_ProgramFiles, KF_FLAG_DEFAULT, nullptr, &str); + if (res == S_OK) { + vfx_sdk_path = std::wstring(str); + vfx_sdk_path /= "NVIDIA Corporation"; + vfx_sdk_path /= "NVIDIA Video Effects"; + CoTaskMemFree(str); + } + } +#else + throw std::runtime_error("Not yet implemented."); +#endif + + // Check if any of the found paths are valid. + if (std::filesystem::exists(vfx_sdk_path)) { + lib_paths.push_back(vfx_sdk_path); + } + } + { +#ifdef WIN32 + DWORD env_size; + std::vector buffer; + env_size = GetEnvironmentVariableW(ST_ENV_NVIDIA_AR_SDK_PATH, nullptr, 0); + if (env_size > 0) { + buffer.resize(static_cast(env_size) + 1); + env_size = GetEnvironmentVariableW(ST_ENV_NVIDIA_AR_SDK_PATH, buffer.data(), static_cast(buffer.size())); + ar_sdk_path = std::wstring(buffer.data(), buffer.size()); + } else { + PWSTR str = nullptr; + HRESULT res = SHGetKnownFolderPath(FOLDERID_ProgramFiles, KF_FLAG_DEFAULT, nullptr, &str); + if (res == S_OK) { + ar_sdk_path = std::wstring(str); + ar_sdk_path /= "NVIDIA Corporation"; + ar_sdk_path /= "NVIDIA AR SDK"; + CoTaskMemFree(str); + } + } +#else + throw std::runtime_error("Not yet implemented."); +#endif + + // Check if any of the found paths are valid. + if (std::filesystem::exists(ar_sdk_path)) { + lib_paths.push_back(ar_sdk_path); + } + } + + // Check if we have any found paths. + if (lib_paths.size() == 0) { + D_LOG_ERROR("No supported NVIDIA SDK is installed to provide '%s'.", LIB_NAME); + throw std::runtime_error("Failed to load '" LIB_NAME "'."); + } + + // Try and load any available NvCVImage library. + for (auto path : lib_paths) { +#ifdef WIN32 + // Add the DLL directory to the library loader paths, if possible. + DLL_DIRECTORY_COOKIE ck = AddDllDirectory(vfx_sdk_path.wstring().c_str()); + _extra = reinterpret_cast(ck); + if (ck == 0) { + DWORD ec = GetLastError(); + std::string error; + { + LPWSTR str; + FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_IGNORE_INSERTS, nullptr, ec, MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US), reinterpret_cast(&str), 0, nullptr); + error = ::streamfx::util::platform::native_to_utf8(std::wstring(str)); + LocalFree(str); + } + D_LOG_WARNING("Failed to add '%'s to the library loader paths with error: %s (Code %" PRIu32 ")", vfx_sdk_path.string().c_str(), error.c_str(), ec); + } +#endif + + // Try to load it directly first, it may be on the search path already. + try { + _library = ::streamfx::util::library::load(std::string_view(LIB_NAME)); + } catch (...) { + auto pathu8 = util::platform::native_to_utf8(path / LIB_NAME); + try { + _library = ::streamfx::util::library::load(pathu8); + } catch (...) { + D_LOG_WARNING("Failed to load '%s' from '%s'.", LIB_NAME, pathu8.string().c_str()); + } + } + + // If we were successful at loading it, break out. + if (_library) { + break; + } + +#ifdef WIN32 + // Remove the DLL directory from the library loader paths if we added it. + if (ck != 0) { + RemoveDllDirectory(ck); + } +#endif + } + + if (!_library) { + D_LOG_ERROR("No installed NVIDIA SDK provides '%s'.", LIB_NAME); + throw std::runtime_error("Failed to load '" LIB_NAME "'."); + } + + { // Load Symbols + NVCVI_LOAD_SYMBOL(NvCVImage_Init); + NVCVI_LOAD_SYMBOL(NvCVImage_InitView); + NVCVI_LOAD_SYMBOL(NvCVImage_Alloc); + NVCVI_LOAD_SYMBOL(NvCVImage_Realloc); + NVCVI_LOAD_SYMBOL(NvCVImage_Dealloc); + NVCVI_LOAD_SYMBOL(NvCVImage_Create); + NVCVI_LOAD_SYMBOL(NvCVImage_Destroy); + NVCVI_LOAD_SYMBOL(NvCVImage_ComponentOffsets); + NVCVI_LOAD_SYMBOL(NvCVImage_Transfer); + NVCVI_LOAD_SYMBOL(NvCVImage_TransferRect); + NVCVI_LOAD_SYMBOL(NvCVImage_TransferFromYUV); + NVCVI_LOAD_SYMBOL(NvCVImage_TransferToYUV); + NVCVI_LOAD_SYMBOL(NvCVImage_MapResource); + NVCVI_LOAD_SYMBOL(NvCVImage_UnmapResource); + NVCVI_LOAD_SYMBOL(NvCVImage_Composite); + NVCVI_LOAD_SYMBOL(NvCVImage_CompositeRect); + NVCVI_LOAD_SYMBOL(NvCVImage_CompositeOverConstant); + NVCVI_LOAD_SYMBOL(NvCVImage_FlipY); + NVCVI_LOAD_SYMBOL(NvCVImage_GetYUVPointers); + NVCVI_LOAD_SYMBOL(NvCV_GetErrorStringFromCode); +#ifdef WIN32 + NVCVI_LOAD_SYMBOL(NvCVImage_InitFromD3D11Texture); + NVCVI_LOAD_SYMBOL(NvCVImage_ToD3DFormat); + NVCVI_LOAD_SYMBOL(NvCVImage_FromD3DFormat); +#ifdef __dxgicommon_h__ + NVCVI_LOAD_SYMBOL(NvCVImage_ToD3DColorSpace); + NVCVI_LOAD_SYMBOL(NvCVImage_FromD3DColorSpace); +#endif +#endif + } +} + +std::shared_ptr streamfx::nvidia::cv::cv::get() +{ + static std::weak_ptr instance; + static std::mutex lock; + + std::unique_lock ul(lock); + if (instance.expired()) { + auto hard_instance = std::make_shared(); + instance = hard_instance; + return hard_instance; + } + return instance.lock(); +} diff --git a/components/nvidia/source/nvidia/vfx/nvidia-vfx-denoising.cpp b/components/nvidia/source/nvidia/vfx/nvidia-vfx-denoising.cpp new file mode 100644 index 0000000..c62a773 --- /dev/null +++ b/components/nvidia/source/nvidia/vfx/nvidia-vfx-denoising.cpp @@ -0,0 +1,285 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2021-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#include "nvidia/vfx/nvidia-vfx-denoising.hpp" +#include "obs/gs/gs-helper.hpp" +#include "util/util-logging.hpp" +#include "util/utility.hpp" + +#include "warning-disable.hpp" +#include +#include +#include "warning-enable.hpp" + +#ifdef _DEBUG +#define ST_PREFIX "<%s> " +#define D_LOG_ERROR(x, ...) P_LOG_ERROR(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_WARNING(x, ...) P_LOG_WARN(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_INFO(x, ...) P_LOG_INFO(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_DEBUG(x, ...) P_LOG_DEBUG(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#else +#define ST_PREFIX " " +#define D_LOG_ERROR(...) P_LOG_ERROR(ST_PREFIX __VA_ARGS__) +#define D_LOG_WARNING(...) P_LOG_WARN(ST_PREFIX __VA_ARGS__) +#define D_LOG_INFO(...) P_LOG_INFO(ST_PREFIX __VA_ARGS__) +#define D_LOG_DEBUG(...) P_LOG_DEBUG(ST_PREFIX __VA_ARGS__) +#endif + +streamfx::nvidia::vfx::denoising::~denoising() +{ + auto gctx = ::streamfx::obs::gs::context(); + auto cctx = ::streamfx::nvidia::cuda::obs::get()->get_context()->enter(); + + // Clean up state buffer. + _nvcuda->get_cuda()->cuMemFree(_state); + + // Clean up any CUDA resources in use. + _input.reset(); + _convert_to_fp32.reset(); + _source.reset(); + _destination.reset(); + _convert_to_u8.reset(); + _output.reset(); + _tmp.reset(); +} + +streamfx::nvidia::vfx::denoising::denoising() : effect(EFFECT_DENOISING), _dirty(true), _input(), _convert_to_fp32(), _source(), _destination(), _convert_to_u8(), _output(), _tmp(), _state(0), _state_size(0), _strength(1.) +{ + // Enter Graphics and CUDA context. + auto gctx = ::streamfx::obs::gs::context(); + auto cctx = ::streamfx::nvidia::cuda::obs::get()->get_context()->enter(); + + // Set the strength, scale and buffers. + set_strength(_strength); + resize(160, 90); + + // Load the effect. + load(); +} + +void streamfx::nvidia::vfx::denoising::set_strength(float strength) +{ + std::swap(_strength, strength); + + // If anything was changed, flag the effect as dirty. + if (!::streamfx::util::math::is_close(_strength, strength, 0.01f)) + _dirty = true; + + // Update Effect + auto gctx = ::streamfx::obs::gs::context(); + auto cctx = _nvcuda->get_context()->enter(); + if (auto res = set_float32(PARAMETER_STRENGTH, _strength); res != ::streamfx::nvidia::cv::result::SUCCESS) { + D_LOG_ERROR("Failed to set '%s' to %1.3f.", PARAMETER_STRENGTH, _strength); + }; +} + +float streamfx::nvidia::vfx::denoising::strength() +{ + return _strength; +} + +void streamfx::nvidia::vfx::denoising::size(std::pair& size) +{ + constexpr uint32_t min_width = 142; + constexpr uint32_t min_height = 80; + uint32_t max_width = 1920; + uint32_t max_height = 1080; + + // Calculate Size + if (size.first > size.second) { + // Dominant Width + double ar = static_cast(size.second) / static_cast(size.first); + size.first = std::clamp(size.first, min_width, max_width); + size.second = std::clamp(static_cast(std::lround(static_cast(size.first) * ar)), min_height, max_height); + } else { + // Dominant Height + double ar = static_cast(size.first) / static_cast(size.second); + size.second = std::clamp(size.second, min_height, max_height); + size.first = std::clamp(static_cast(std::lround(static_cast(size.second) * ar)), min_width, max_width); + } +} + +std::shared_ptr<::streamfx::obs::gs::texture> streamfx::nvidia::vfx::denoising::process(std::shared_ptr<::streamfx::obs::gs::texture> in) +{ + // Enter Graphics and CUDA context. + auto gctx = ::streamfx::obs::gs::context(); + auto cctx = _nvcuda->get_context()->enter(); + +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + ::streamfx::obs::gs::debug_marker profiler1{::streamfx::obs::gs::debug_color_magenta, "NvVFX Denoising"}; +#endif + + // Resize if the size or scale was changed. + resize(in->width(), in->height()); + + // Reload effect if dirty. + if (_dirty) { + load(); + } + + { // Copy parameter to input. +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + ::streamfx::obs::gs::debug_marker profiler1{::streamfx::obs::gs::debug_color_copy, "Copy In -> Input"}; +#endif + gs_copy_texture(_input->get_texture()->get_object(), in->get_object()); + } + + { // Convert Input to Source format +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + ::streamfx::obs::gs::debug_marker profiler1{::streamfx::obs::gs::debug_color_convert, "Convert Input -> Source"}; +#endif + if (auto res = _nvcvi->NvCVImage_Transfer(_input->get_image(), _convert_to_fp32->get_image(), 1.f / 255.f, _nvcuda->get_stream()->get(), _tmp->get_image()); res != ::streamfx::nvidia::cv::result::SUCCESS) { + D_LOG_ERROR("Failed to transfer input to processing source due to error: %s", _nvcvi->NvCV_GetErrorStringFromCode(res)); + throw std::runtime_error("Transfer failed."); + } + } + + { // Copy input to source. +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + ::streamfx::obs::gs::debug_marker profiler1{::streamfx::obs::gs::debug_color_copy, "Copy Input -> Source"}; +#endif + if (auto res = _nvcvi->NvCVImage_Transfer(_convert_to_fp32->get_image(), _source->get_image(), 1.f, _nvcuda->get_stream()->get(), _tmp->get_image()); res != ::streamfx::nvidia::cv::result::SUCCESS) { + D_LOG_ERROR("Failed to transfer input to processing source due to error: %s", _nvcvi->NvCV_GetErrorStringFromCode(res)); + throw std::runtime_error("Transfer failed."); + } + } + + { // Process source to destination. +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + ::streamfx::obs::gs::debug_marker profiler1{::streamfx::obs::gs::debug_color_cache, "Process"}; +#endif + if (auto res = _nvvfx->NvVFX_Run(_fx.get(), 0); res != ::streamfx::nvidia::cv::result::SUCCESS) { + D_LOG_ERROR("Failed to process due to error: %s", _nvcvi->NvCV_GetErrorStringFromCode(res)); + throw std::runtime_error("Run failed."); + } + } + + { // Convert Destination to Output format +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + ::streamfx::obs::gs::debug_marker profiler1{::streamfx::obs::gs::debug_color_convert, "Convert Destination -> Output"}; +#endif + if (auto res = _nvcvi->NvCVImage_Transfer(_destination->get_image(), _convert_to_u8->get_image(), 255.f, _nvcuda->get_stream()->get(), _tmp->get_image()); res != ::streamfx::nvidia::cv::result::SUCCESS) { + D_LOG_ERROR("Failed to transfer processing result to output due to error: %s", _nvcvi->NvCV_GetErrorStringFromCode(res)); + throw std::runtime_error("Transfer failed."); + } + } + + { // Copy destination to output. +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + ::streamfx::obs::gs::debug_marker profiler1{::streamfx::obs::gs::debug_color_copy, "Copy Destination -> Output"}; +#endif + if (auto res = _nvcvi->NvCVImage_Transfer(_convert_to_u8->get_image(), _output->get_image(), 1., _nvcuda->get_stream()->get(), _tmp->get_image()); res != ::streamfx::nvidia::cv::result::SUCCESS) { + D_LOG_ERROR("Failed to transfer processing result to output due to error: %s", _nvcvi->NvCV_GetErrorStringFromCode(res)); + throw std::runtime_error("Transfer failed."); + } + } + + // Return output. + return _output->get_texture(); +} + +void streamfx::nvidia::vfx::denoising::resize(uint32_t width, uint32_t height) +{ + auto gctx = ::streamfx::obs::gs::context(); + auto cctx = ::streamfx::nvidia::cuda::obs::get()->get_context()->enter(); + + if (!_tmp) { + _tmp = std::make_shared<::streamfx::nvidia::cv::image>(width, height, ::streamfx::nvidia::cv::pixel_format::RGBA, ::streamfx::nvidia::cv::component_type::UINT8, ::streamfx::nvidia::cv::component_layout::PLANAR, ::streamfx::nvidia::cv::memory_location::GPU, 1); + } + + if (!_input || (_input->get_image()->width != width) || (_input->get_image()->height != height)) { + if (_input) { + _input->resize(width, height); + } else { + _input = std::make_shared<::streamfx::nvidia::cv::texture>(width, height, GS_RGBA_UNORM); + } + } + + if (!_convert_to_fp32 || (_convert_to_fp32->get_image()->width != width) || (_convert_to_fp32->get_image()->height != height)) { + if (_convert_to_fp32) { + _convert_to_fp32->resize(width, height); + } else { + _convert_to_fp32 = std::make_shared<::streamfx::nvidia::cv::image>(width, height, ::streamfx::nvidia::cv::pixel_format::RGBA, ::streamfx::nvidia::cv::component_type::FP32, ::streamfx::nvidia::cv::component_layout::PLANAR, ::streamfx::nvidia::cv::memory_location::GPU, 1); + } + } + + if (!_source || (_source->get_image()->width != width) || (_source->get_image()->height != height)) { + if (_source) { + _source->resize(width, height); + } else { + _source = std::make_shared<::streamfx::nvidia::cv::image>(width, height, ::streamfx::nvidia::cv::pixel_format::BGR, ::streamfx::nvidia::cv::component_type::FP32, ::streamfx::nvidia::cv::component_layout::PLANAR, ::streamfx::nvidia::cv::memory_location::GPU, 1); + } + + if (auto res = set_image(::streamfx::nvidia::vfx::PARAMETER_INPUT_IMAGE_0, _source); res != ::streamfx::nvidia::cv::result::SUCCESS) { + D_LOG_ERROR("Failed to set input image due to error: %s", _nvcvi->NvCV_GetErrorStringFromCode(res)); + _source.reset(); + throw std::runtime_error("SetImage failed."); + } + + _dirty = true; + } + + if (!_destination || (_destination->get_image()->width != width) || (_destination->get_image()->height != height)) { + if (_destination) { + _destination->resize(width, height); + } else { + _destination = std::make_shared<::streamfx::nvidia::cv::image>(width, height, ::streamfx::nvidia::cv::pixel_format::BGR, ::streamfx::nvidia::cv::component_type::FP32, ::streamfx::nvidia::cv::component_layout::PLANAR, ::streamfx::nvidia::cv::memory_location::GPU, 1); + } + + if (auto res = set_image(::streamfx::nvidia::vfx::PARAMETER_OUTPUT_IMAGE_0, _destination); res != ::streamfx::nvidia::cv::result::SUCCESS) { + D_LOG_ERROR("Failed to set output image due to error: %s", _nvcvi->NvCV_GetErrorStringFromCode(res)); + _destination.reset(); + throw std::runtime_error("SetImage failed."); + } + + _dirty = true; + } + + if (!_convert_to_u8 || (_convert_to_u8->get_image()->width != width) || (_convert_to_u8->get_image()->height != height)) { + if (_convert_to_u8) { + _convert_to_u8->resize(width, height); + } else { + _convert_to_u8 = std::make_shared<::streamfx::nvidia::cv::image>(width, height, ::streamfx::nvidia::cv::pixel_format::RGBA, ::streamfx::nvidia::cv::component_type::UINT8, ::streamfx::nvidia::cv::component_layout::INTERLEAVED, ::streamfx::nvidia::cv::memory_location::GPU, 1); + } + } + + if (!_output || (_output->get_image()->width != width) || (_output->get_image()->height != height)) { + if (_output) { + _output->resize(width, height); + } else { + _output = std::make_shared<::streamfx::nvidia::cv::texture>(width, height, GS_RGBA_UNORM); + } + } + + if (!_state || _dirty) { // Reallocate and clean state. + if (_state) { + _nvcuda->get_cuda()->cuMemFree(_state); + } + + _nvvfx->NvVFX_GetU32(_fx.get(), ::streamfx::nvidia::vfx::PARAMETER_STATE_SIZE, &_state_size); + _nvcuda->get_cuda()->cuMemAlloc(&_state, _state_size); + _nvcuda->get_cuda()->cuMemsetD8(_state, 0, _state_size); + + _states[0] = reinterpret_cast(_state); + if (auto res = _nvvfx->NvVFX_SetObject(_fx.get(), ::streamfx::nvidia::vfx::PARAMETER_STATE, reinterpret_cast(_states)); res != ::streamfx::nvidia::cv::result::SUCCESS) { + D_LOG_ERROR("Failed to set state due to error: %s", _nvcvi->NvCV_GetErrorStringFromCode(res)); + throw std::runtime_error("SetObject failed."); + } + + _dirty = true; + } +} + +void streamfx::nvidia::vfx::denoising::load() +{ + auto gctx = ::streamfx::obs::gs::context(); + auto cctx = ::streamfx::nvidia::cuda::obs::get()->get_context()->enter(); + + if (auto res = effect::load(); res != ::streamfx::nvidia::cv::result::SUCCESS) { + D_LOG_ERROR("Failed to initialize effect due to error: %s", _nvcvi->NvCV_GetErrorStringFromCode(res)); + throw std::runtime_error("Load failed."); + } + + _dirty = false; +} diff --git a/components/nvidia/source/nvidia/vfx/nvidia-vfx-effect.cpp b/components/nvidia/source/nvidia/vfx/nvidia-vfx-effect.cpp new file mode 100644 index 0000000..44621d5 --- /dev/null +++ b/components/nvidia/source/nvidia/vfx/nvidia-vfx-effect.cpp @@ -0,0 +1,105 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2021-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#include "nvidia/vfx/nvidia-vfx-effect.hpp" +#include "obs/gs/gs-helper.hpp" +#include "util/util-logging.hpp" + +#include "warning-disable.hpp" +#include +#include "warning-enable.hpp" + +#ifdef _DEBUG +#define ST_PREFIX "<%s> " +#define D_LOG_ERROR(x, ...) P_LOG_ERROR(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_WARNING(x, ...) P_LOG_WARN(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_INFO(x, ...) P_LOG_INFO(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_DEBUG(x, ...) P_LOG_DEBUG(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#else +#define ST_PREFIX " " +#define D_LOG_ERROR(...) P_LOG_ERROR(ST_PREFIX __VA_ARGS__) +#define D_LOG_WARNING(...) P_LOG_WARN(ST_PREFIX __VA_ARGS__) +#define D_LOG_INFO(...) P_LOG_INFO(ST_PREFIX __VA_ARGS__) +#define D_LOG_DEBUG(...) P_LOG_DEBUG(ST_PREFIX __VA_ARGS__) +#endif + +using namespace ::streamfx::nvidia; + +streamfx::nvidia::vfx::effect::~effect() +{ + auto gctx = ::streamfx::obs::gs::context(); + auto cctx = cuda::obs::get()->get_context()->enter(); + + _fx.reset(); + _nvvfx.reset(); + _nvcvi.reset(); + _nvcuda.reset(); +} + +streamfx::nvidia::vfx::effect::effect(effect_t effect) : _nvcuda(cuda::obs::get()), _nvcvi(cv::cv::get()), _nvvfx(vfx::vfx::get()), _fx() +{ + auto gctx = ::streamfx::obs::gs::context(); + auto cctx = cuda::obs::get()->get_context()->enter(); + + // Create the Effect/Feature. + ::vfx::handle_t handle; + if (cv::result res = _nvvfx->NvVFX_CreateEffect(effect, &handle); res != cv::result::SUCCESS) { + D_LOG_ERROR("Unable to create effect: %s", _nvcvi->NvCV_GetErrorStringFromCode(res)); + throw std::runtime_error("Unable to create effect."); + } + _fx = std::shared_ptr(handle, [](::vfx::handle_t handle) { ::vfx::vfx::get()->NvVFX_DestroyEffect(handle); }); + + // Assign CUDA Stream object. + if (auto v = set_cuda_stream(PARAMETER_CUDA_STREAM, _nvcuda->get_stream()); v != cv::result::SUCCESS) { + throw ::streamfx::nvidia::cv::exception(PARAMETER_CUDA_STREAM, v); + } + + // Assign Model Directory. + _model_path = _nvvfx->model_path().generic_u8string(); + if (auto v = set_string(PARAMETER_MODEL_DIRECTORY, _model_path); v != cv::result::SUCCESS) { + throw ::streamfx::nvidia::cv::exception(PARAMETER_MODEL_DIRECTORY, v); + } +} + +cv::result streamfx::nvidia::vfx::effect::get_string(parameter_t param, std::string_view& value) +{ + const char* cvalue = nullptr; + cv::result res = get_string(param, cvalue); + if (res == cv::result::SUCCESS) { + if (cvalue) { + value = std::string_view(cvalue); + } else { + value = std::string_view(); + } + } + return res; +} + +cv::result streamfx::nvidia::vfx::effect::get_string(parameter_t param, std::string& value) +{ + const char* cvalue = nullptr; + cv::result res = get_string(param, cvalue); + if (res == cv::result::SUCCESS) { + if (cvalue) { + value = cvalue; + } else { + value.clear(); + } + } + return res; +} + +streamfx::nvidia::cv::result vfx::effect::get_string(parameter_t param, std::u8string& value) +{ + const char8_t* cvalue = nullptr; + cv::result res = get_string(param, cvalue); + if (res == cv::result::SUCCESS) { + if (cvalue) { + value = cvalue; + } else { + value.clear(); + } + } + return res; +} diff --git a/components/nvidia/source/nvidia/vfx/nvidia-vfx-greenscreen.cpp b/components/nvidia/source/nvidia/vfx/nvidia-vfx-greenscreen.cpp new file mode 100644 index 0000000..042491f --- /dev/null +++ b/components/nvidia/source/nvidia/vfx/nvidia-vfx-greenscreen.cpp @@ -0,0 +1,246 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2021-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#include "nvidia/vfx/nvidia-vfx-greenscreen.hpp" +#include "nvidia/cv/nvidia-cv.hpp" +#include "obs/gs/gs-helper.hpp" +#include "util/util-logging.hpp" +#include "util/utility.hpp" + +#include "warning-disable.hpp" +#include +#include +#include "warning-enable.hpp" + +#ifdef _DEBUG +#define ST_PREFIX "<%s> " +#define D_LOG_ERROR(x, ...) P_LOG_ERROR(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_WARNING(x, ...) P_LOG_WARN(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_INFO(x, ...) P_LOG_INFO(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_DEBUG(x, ...) P_LOG_DEBUG(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#else +#define ST_PREFIX " " +#define D_LOG_ERROR(...) P_LOG_ERROR(ST_PREFIX __VA_ARGS__) +#define D_LOG_WARNING(...) P_LOG_WARN(ST_PREFIX __VA_ARGS__) +#define D_LOG_INFO(...) P_LOG_INFO(ST_PREFIX __VA_ARGS__) +#define D_LOG_DEBUG(...) P_LOG_DEBUG(ST_PREFIX __VA_ARGS__) +#endif + +// TODO: Figure out actual latency, appears to be either 2 or 3 frames. +#define LATENCY_BUFFER 2 + +streamfx::nvidia::vfx::greenscreen::~greenscreen() +{ + // Enter Contexts. + auto gctx = ::streamfx::obs::gs::context(); + auto cctx = ::streamfx::nvidia::cuda::obs::get()->get_context()->enter(); + + _tmp.reset(); + _output.reset(); + _destination.reset(); + _source.reset(); + _input.reset(); + _buffer.clear(); +} + +streamfx::nvidia::vfx::greenscreen::greenscreen() : effect(EFFECT_GREEN_SCREEN), _dirty(true), _input(), _source(), _destination(), _output(), _tmp() +{ + // Enter Contexts. + auto gctx = ::streamfx::obs::gs::context(); + auto cctx = ::streamfx::nvidia::cuda::obs::get()->get_context()->enter(); + + // Mode + set_mode(greenscreen_mode::QUALITY); + + // Allocate resources + resize(512, 288); +} + +void streamfx::nvidia::vfx::greenscreen::size(std::pair& size) +{ + constexpr uint32_t min_width = 512; + constexpr uint32_t min_height = 288; + + // Calculate Size + if (size.first > size.second) { + // Dominant Width + double ar = static_cast(size.second) / static_cast(size.first); + size.first = std::max(size.first, min_width); + size.second = std::max(static_cast(std::lround(static_cast(size.first) * ar)), min_height); + } else { + // Dominant Height + double ar = static_cast(size.first) / static_cast(size.second); + size.second = std::max(size.second, min_height); + size.first = std::max(static_cast(std::lround(static_cast(size.second) * ar)), min_width); + } +} + +void streamfx::nvidia::vfx::greenscreen::set_mode(greenscreen_mode mode) +{ + set_uint32(PARAMETER_MODE, static_cast(mode)); + _dirty = true; +} + +std::shared_ptr streamfx::nvidia::vfx::greenscreen::process(std::shared_ptr<::streamfx::obs::gs::texture> in) +{ + // Enter Graphics and CUDA context. + auto gctx = ::streamfx::obs::gs::context(); + auto cctx = _nvcuda->get_context()->enter(); + +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + ::streamfx::obs::gs::debug_marker profiler1{::streamfx::obs::gs::debug_color_magenta, "NvVFX Background Removal"}; +#endif + + // Resize if the size or scale was changed. + resize(in->width(), in->height()); + + // Reload effect if dirty. + if (_dirty) { + load(); + } + + { // Copy parameter to input. +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + ::streamfx::obs::gs::debug_marker profiler1{::streamfx::obs::gs::debug_color_copy, "Copy In -> Input"}; +#endif + gs_copy_texture(*(_input->get_texture()), *in); + } + + { // Enqueue into buffer (back is newest). + auto el = _buffer.front(); + gs_copy_texture(*el, *in); + _buffer.push_back(el); + _buffer.pop_front(); + } + + { // Copy input to source. +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + ::streamfx::obs::gs::debug_marker profiler1{::streamfx::obs::gs::debug_color_copy, "Copy Input -> Source"}; +#endif + if (auto res = _nvcvi->NvCVImage_Transfer(_input->get_image(), _source->get_image(), 1.f, _nvcuda->get_stream()->get(), _tmp->get_image()); res != ::streamfx::nvidia::cv::result::SUCCESS) { + D_LOG_ERROR("Failed to transfer input to processing source due to error: %s", _nvcvi->NvCV_GetErrorStringFromCode(res)); + throw std::runtime_error("Transfer failed."); + } + } + + { // Process source to destination. +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + ::streamfx::obs::gs::debug_marker profiler1{::streamfx::obs::gs::debug_color_cache, "Process"}; +#endif + if (auto res = run(); res != ::streamfx::nvidia::cv::result::SUCCESS) { + D_LOG_ERROR("Failed to process due to error: %s", _nvcvi->NvCV_GetErrorStringFromCode(res)); + throw std::runtime_error("Run failed."); + } + } + + { // Copy destination to output. +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + ::streamfx::obs::gs::debug_marker profiler1{::streamfx::obs::gs::debug_color_copy, "Copy Destination -> Output"}; +#endif + if (auto res = _nvcvi->NvCVImage_Transfer(_destination->get_image(), _output->get_image(), 1., _nvcuda->get_stream()->get(), _tmp->get_image()); res != ::streamfx::nvidia::cv::result::SUCCESS) { + D_LOG_ERROR("Failed to transfer processing result to output due to error: %s", _nvcvi->NvCV_GetErrorStringFromCode(res)); + throw std::runtime_error("Transfer failed."); + } + } + + // Return output. + return _output->get_texture(); +} + +std::shared_ptr streamfx::nvidia::vfx::greenscreen::get_color() +{ + //return _input->get_texture(); + return _buffer.front(); +} + +std::shared_ptr streamfx::nvidia::vfx::greenscreen::get_mask() +{ + return _output->get_texture(); +} + +void streamfx::nvidia::vfx::greenscreen::resize(uint32_t width, uint32_t height) +{ + auto gctx = ::streamfx::obs::gs::context(); + auto cctx = ::streamfx::nvidia::cuda::obs::get()->get_context()->enter(); + + std::pair in_size = {width, height}; + size(in_size); + + if (!_tmp) { + _tmp = std::make_shared<::streamfx::nvidia::cv::image>(width, height, ::streamfx::nvidia::cv::pixel_format::RGBA, ::streamfx::nvidia::cv::component_type::UINT8, ::streamfx::nvidia::cv::component_layout::PLANAR, ::streamfx::nvidia::cv::memory_location::GPU, 1); + } + + if (!_input || (in_size.first != _input->get_texture()->width()) || (in_size.second != _input->get_texture()->height())) { + { + _buffer.clear(); + for (size_t idx = 0; idx < LATENCY_BUFFER; idx++) { + auto el = std::make_shared<::streamfx::obs::gs::texture>(width, height, GS_RGBA_UNORM, 1, nullptr, ::streamfx::obs::gs::texture_flags::None); + _buffer.push_back(el); + } + } + + if (_input) { + _input->resize(in_size.first, in_size.second); + } else { + _input = std::make_shared<::streamfx::nvidia::cv::texture>(in_size.first, in_size.second, GS_RGBA_UNORM); + } + + _dirty = true; + } + + if (!_source || (in_size.first != _source->get_image()->width) || (in_size.second != _source->get_image()->height)) { + if (_source) { + _source->resize(in_size.first, in_size.second); + } else { + _source = std::make_shared<::streamfx::nvidia::cv::image>(in_size.first, in_size.second, ::streamfx::nvidia::cv::pixel_format::BGR, ::streamfx::nvidia::cv::component_type::UINT8, ::streamfx::nvidia::cv::component_layout::INTERLEAVED, ::streamfx::nvidia::cv::memory_location::GPU, 1); + } + + if (auto v = set_image(PARAMETER_INPUT_IMAGE_0, _source); v != ::streamfx::nvidia::cv::result::SUCCESS) { + throw ::streamfx::nvidia::cv::exception(PARAMETER_INPUT_IMAGE_0, v); + } + + _dirty = true; + } + + if (!_destination || (in_size.first != _destination->get_image()->width) || (in_size.second != _destination->get_image()->height)) { + if (_destination) { + _destination->resize(in_size.first, in_size.second); + } else { + _destination = std::make_shared<::streamfx::nvidia::cv::image>(in_size.first, in_size.second, ::streamfx::nvidia::cv::pixel_format::A, ::streamfx::nvidia::cv::component_type::UINT8, ::streamfx::nvidia::cv::component_layout::INTERLEAVED, ::streamfx::nvidia::cv::memory_location::GPU, 1); + } + + if (auto v = set_image(PARAMETER_OUTPUT_IMAGE_0, _destination); v != ::streamfx::nvidia::cv::result::SUCCESS) { + throw ::streamfx::nvidia::cv::exception(PARAMETER_OUTPUT_IMAGE_0, v); + } + + _dirty = true; + } + + if (!_output || (in_size.first != _output->get_texture()->width()) || (in_size.second != _output->get_texture()->height())) { + if (_output) { + _output->resize(in_size.first, in_size.second); + } else { + _output = std::make_shared<::streamfx::nvidia::cv::texture>(in_size.first, in_size.second, GS_A8); + } + + _dirty = true; + } +} + +void streamfx::nvidia::vfx::greenscreen::load() +{ + auto gctx = ::streamfx::obs::gs::context(); + auto cctx = _nvcuda->get_context()->enter(); + + // Assign CUDA Stream object. + if (auto v = set_cuda_stream(PARAMETER_CUDA_STREAM, _nvcuda->get_stream()); v != cv::result::SUCCESS) { + throw ::streamfx::nvidia::cv::exception(PARAMETER_CUDA_STREAM, v); + } + + if (auto v = effect::load(); v != ::streamfx::nvidia::cv::result::SUCCESS) { + throw ::streamfx::nvidia::cv::exception("load", v); + } + + _dirty = false; +} diff --git a/components/nvidia/source/nvidia/vfx/nvidia-vfx-superresolution.cpp b/components/nvidia/source/nvidia/vfx/nvidia-vfx-superresolution.cpp new file mode 100644 index 0000000..4a40bdc --- /dev/null +++ b/components/nvidia/source/nvidia/vfx/nvidia-vfx-superresolution.cpp @@ -0,0 +1,365 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2021-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2022 lainon +// AUTOGENERATED COPYRIGHT HEADER END + +#include "nvidia/vfx/nvidia-vfx-superresolution.hpp" +#include "obs/gs/gs-helper.hpp" +#include "util/util-logging.hpp" +#include "util/utility.hpp" + +#include "warning-disable.hpp" +#include +#include +#include +#include "warning-enable.hpp" + +#ifdef _DEBUG +#define ST_PREFIX "<%s> " +#define D_LOG_ERROR(x, ...) P_LOG_ERROR(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_WARNING(x, ...) P_LOG_WARN(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_INFO(x, ...) P_LOG_INFO(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_DEBUG(x, ...) P_LOG_DEBUG(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#else +#define ST_PREFIX " " +#define D_LOG_ERROR(...) P_LOG_ERROR(ST_PREFIX __VA_ARGS__) +#define D_LOG_WARNING(...) P_LOG_WARN(ST_PREFIX __VA_ARGS__) +#define D_LOG_INFO(...) P_LOG_INFO(ST_PREFIX __VA_ARGS__) +#define D_LOG_DEBUG(...) P_LOG_DEBUG(ST_PREFIX __VA_ARGS__) +#endif + +static std::vector supported_scale_factors{4. / 3., 1.5, 2., 3., 4.}; + +static float find_closest_scale_factor(float factor) +{ + std::pair minimal = {0.f, std::numeric_limits::max()}; + for (float delta : supported_scale_factors) { + float value = abs(delta - factor); + if (minimal.second > value) { + minimal.first = delta; + minimal.second = value; + } + } + + return minimal.first; +} + +static size_t find_closest_scale_factor_index(float factor) +{ + std::pair minimal = {0, std::numeric_limits::max()}; + for (size_t idx = 0; idx < supported_scale_factors.size(); idx++) { + float delta = supported_scale_factors[idx]; + float value = abs(delta - factor); + if (minimal.second > value) { + minimal.first = idx; + minimal.second = value; + } + } + + return minimal.first; +} + +streamfx::nvidia::vfx::superresolution::~superresolution() +{ + auto gctx = ::streamfx::obs::gs::context(); + auto cctx = ::streamfx::nvidia::cuda::obs::get()->get_context()->enter(); + + // Clean up any CUDA resources in use. + _input.reset(); + _convert_to_fp32.reset(); + _source.reset(); + _destination.reset(); + _convert_to_u8.reset(); + _output.reset(); + _tmp.reset(); +} + +streamfx::nvidia::vfx::superresolution::superresolution() : effect(EFFECT_SUPERRESOLUTION), _dirty(true), _input(), _convert_to_fp32(), _source(), _destination(), _convert_to_u8(), _output(), _tmp(), _strength(1.), _scale(1.5), _cache_input_size(), _cache_output_size(), _cache_scale() +{ + // Enter Graphics and CUDA context. + auto gctx = ::streamfx::obs::gs::context(); + auto cctx = ::streamfx::nvidia::cuda::obs::get()->get_context()->enter(); + + // Set the strength, scale and buffers. + set_strength(_strength); + set_scale(_scale); + resize(160, 90); + + // Load the effect. + load(); +} + +void streamfx::nvidia::vfx::superresolution::set_strength(float strength) +{ + strength = (strength >= .5f) ? 1.f : 0.f; + std::swap(_strength, strength); + + // If anything was changed, flag the effect as dirty. + if (!::streamfx::util::math::is_close(_strength, strength, 0.01f)) + _dirty = true; + + // Update Effect + uint32_t value = (_strength >= .5f) ? 1u : 0u; + auto gctx = ::streamfx::obs::gs::context(); + auto cctx = ::streamfx::nvidia::cuda::obs::get()->get_context()->enter(); + if (auto res = set_float32(::streamfx::nvidia::vfx::PARAMETER_STRENGTH, value); res != ::streamfx::nvidia::cv::result::SUCCESS) { + D_LOG_ERROR("Failed to set '%s' to %lu.", ::streamfx::nvidia::vfx::PARAMETER_STRENGTH, value); + }; +} + +float streamfx::nvidia::vfx::superresolution::strength() +{ + return _strength; +} + +void streamfx::nvidia::vfx::superresolution::set_scale(float scale) +{ + // Limit to acceptable range. + scale = std::clamp(scale, 1., 4.); + + // Match to nearest scale. + float factor = static_cast(find_closest_scale_factor(scale)); + + // If anything was changed, flag the effect as dirty. + if (!::streamfx::util::math::is_close(_scale, factor, 0.01f)) + _dirty = true; + + // Save new scale factor. + _scale = factor; +} + +float streamfx::nvidia::vfx::superresolution::scale() +{ + return _scale; +} + +void streamfx::nvidia::vfx::superresolution::size(std::pair const& size, std::pair& input_size, std::pair& output_size) +{ + // Check if the size has actually changed at all. + if ((input_size.first == _cache_input_size.first) && (input_size.second == _cache_input_size.second) && (_scale == _cache_scale)) { + input_size = _cache_input_size; + output_size = _cache_output_size; + _scale = _cache_scale; + return; + } + + // Define lower and upper boundaries for resolution. + constexpr uint32_t min_width = 160; + constexpr uint32_t min_height = 90; + uint32_t max_width = 0; + uint32_t max_height = 0; + if (_scale > 3.0) { + max_width = 960; + max_height = 540; + } else if (_scale > 2.0) { + max_width = 1280; + max_height = 720; + } else { + max_width = 1920; + max_height = 1080; + } + + // Restore Input Size + input_size.first = size.first; + input_size.second = size.second; + + // Calculate Input Size + if (input_size.first > input_size.second) { + // Dominant Width + double ar = static_cast(input_size.second) / static_cast(input_size.first); + input_size.first = std::clamp(input_size.first, min_width, max_width); + input_size.second = std::clamp(static_cast(std::lround(static_cast(input_size.first) * ar)), min_height, max_height); + } else { + // Dominant Height + double ar = static_cast(input_size.first) / static_cast(input_size.second); + input_size.second = std::clamp(input_size.second, min_height, max_height); + input_size.first = std::clamp(static_cast(std::lround(static_cast(input_size.second) * ar)), min_width, max_width); + } + + // Calculate Output Size. + output_size.first = static_cast(std::lround(static_cast(input_size.first) * _scale)); + output_size.second = static_cast(std::lround(static_cast(input_size.second) * _scale)); + + // Verify that this is a valid scale factor. + float width_mul = (static_cast(output_size.first) / static_cast(input_size.first)); + float height_mul = (static_cast(output_size.second) / static_cast(input_size.second)); + if (!::streamfx::util::math::is_close(width_mul, _scale, 0.00001) || !::streamfx::util::math::is_close(height_mul, _scale, 0.00001)) { + size_t scale_idx = find_closest_scale_factor_index(_scale); + if (scale_idx < supported_scale_factors.size()) { + _scale = supported_scale_factors[scale_idx + 1]; + this->size(size, input_size, output_size); + } + } + + // Update last stored values. + _cache_input_size = input_size; + _cache_output_size = output_size; + _cache_scale = _scale; +} + +std::shared_ptr<::streamfx::obs::gs::texture> streamfx::nvidia::vfx::superresolution::process(std::shared_ptr<::streamfx::obs::gs::texture> in) +{ + // Enter Graphics and CUDA context. + auto gctx = ::streamfx::obs::gs::context(); + auto cctx = _nvcuda->get_context()->enter(); + +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + ::streamfx::obs::gs::debug_marker profiler1{::streamfx::obs::gs::debug_color_magenta, "NvVFX Super-Resolution"}; +#endif + + // Resize if the size or scale was changed. + resize(in->width(), in->height()); + + // Reload effect if dirty. + if (_dirty) { + load(); + } + + { // Copy parameter to input. +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + ::streamfx::obs::gs::debug_marker profiler1{::streamfx::obs::gs::debug_color_copy, "Copy In -> Input"}; +#endif + gs_copy_texture(*(_input->get_texture()), *in); + } + + { // Convert Input to Source format +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + ::streamfx::obs::gs::debug_marker profiler1{::streamfx::obs::gs::debug_color_convert, "Convert Input -> Source"}; +#endif + if (auto res = _nvcvi->NvCVImage_Transfer(_input->get_image(), _convert_to_fp32->get_image(), 1.f, _nvcuda->get_stream()->get(), _tmp->get_image()); res != ::streamfx::nvidia::cv::result::SUCCESS) { + D_LOG_ERROR("Failed to transfer processing result to output due to error: %s", _nvcvi->NvCV_GetErrorStringFromCode(res)); + throw std::runtime_error("Transfer failed."); + } + } + + { // Copy input to source. +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + ::streamfx::obs::gs::debug_marker profiler1{::streamfx::obs::gs::debug_color_copy, "Copy Input -> Source"}; +#endif + if (auto res = _nvcvi->NvCVImage_Transfer(_convert_to_fp32->get_image(), _source->get_image(), 1.f, _nvcuda->get_stream()->get(), _tmp->get_image()); res != ::streamfx::nvidia::cv::result::SUCCESS) { + D_LOG_ERROR("Failed to transfer input to processing source due to error: %s", _nvcvi->NvCV_GetErrorStringFromCode(res)); + throw std::runtime_error("Transfer failed."); + } + } + + { // Process source to destination. +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + ::streamfx::obs::gs::debug_marker profiler1{::streamfx::obs::gs::debug_color_cache, "Process"}; +#endif + if (auto res = run(); res != ::streamfx::nvidia::cv::result::SUCCESS) { + D_LOG_ERROR("Failed to process due to error: %s", _nvcvi->NvCV_GetErrorStringFromCode(res)); + throw std::runtime_error("Run failed."); + } + } + + { // Convert Destination to Output format +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + ::streamfx::obs::gs::debug_marker profiler1{::streamfx::obs::gs::debug_color_convert, "Convert Destination -> Output"}; +#endif + if (auto res = _nvcvi->NvCVImage_Transfer(_destination->get_image(), _convert_to_u8->get_image(), 1.f, _nvcuda->get_stream()->get(), _tmp->get_image()); res != ::streamfx::nvidia::cv::result::SUCCESS) { + D_LOG_ERROR("Failed to transfer processing result to output due to error: %s", _nvcvi->NvCV_GetErrorStringFromCode(res)); + throw std::runtime_error("Transfer failed."); + } + } + + { // Copy destination to output. +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + ::streamfx::obs::gs::debug_marker profiler1{::streamfx::obs::gs::debug_color_copy, "Copy Destination -> Output"}; +#endif + if (auto res = _nvcvi->NvCVImage_Transfer(_convert_to_u8->get_image(), _output->get_image(), 1., _nvcuda->get_stream()->get(), _tmp->get_image()); res != ::streamfx::nvidia::cv::result::SUCCESS) { + D_LOG_ERROR("Failed to transfer processing result to output due to error: %s", _nvcvi->NvCV_GetErrorStringFromCode(res)); + throw std::runtime_error("Transfer failed."); + } + } + + // Return output. + return _output->get_texture(); +} + +void streamfx::nvidia::vfx::superresolution::resize(uint32_t width, uint32_t height) +{ + auto gctx = ::streamfx::obs::gs::context(); + auto cctx = ::streamfx::nvidia::cuda::obs::get()->get_context()->enter(); + + _cache_input_size = {width, height}; + this->size(_cache_input_size, _cache_input_size, _cache_output_size); + + if (!_tmp) { + _tmp = std::make_shared<::streamfx::nvidia::cv::image>(_cache_output_size.first, _cache_output_size.second, ::streamfx::nvidia::cv::pixel_format::RGBA, ::streamfx::nvidia::cv::component_type::UINT8, ::streamfx::nvidia::cv::component_layout::PLANAR, ::streamfx::nvidia::cv::memory_location::GPU, 1); + } + + if (!_input || (_input->get_image()->width != _cache_input_size.first) || (_input->get_image()->height != _cache_input_size.second)) { + if (_input) { + _input->resize(_cache_input_size.first, _cache_input_size.second); + } else { + _input = std::make_shared<::streamfx::nvidia::cv::texture>(_cache_input_size.first, _cache_input_size.second, GS_RGBA_UNORM); + } + } + + if (!_convert_to_fp32 || (_convert_to_fp32->get_image()->width != _cache_input_size.first) || (_convert_to_fp32->get_image()->height != _cache_input_size.second)) { + if (_convert_to_fp32) { + _convert_to_fp32->resize(_cache_input_size.first, _cache_input_size.second); + } else { + _convert_to_fp32 = std::make_shared<::streamfx::nvidia::cv::image>(_cache_input_size.first, _cache_input_size.second, ::streamfx::nvidia::cv::pixel_format::RGBA, ::streamfx::nvidia::cv::component_type::FP32, ::streamfx::nvidia::cv::component_layout::PLANAR, ::streamfx::nvidia::cv::memory_location::GPU, 1); + } + } + + if (!_source || (_source->get_image()->width != _cache_input_size.first) || (_source->get_image()->height != _cache_input_size.second)) { + if (_source) { + _source->resize(_cache_input_size.first, _cache_input_size.second); + } else { + _source = std::make_shared<::streamfx::nvidia::cv::image>(_cache_input_size.first, _cache_input_size.second, ::streamfx::nvidia::cv::pixel_format::BGR, ::streamfx::nvidia::cv::component_type::FP32, ::streamfx::nvidia::cv::component_layout::PLANAR, ::streamfx::nvidia::cv::memory_location::GPU, 1); + } + + if (auto res = set_image(::streamfx::nvidia::vfx::PARAMETER_INPUT_IMAGE_0, _source); res != ::streamfx::nvidia::cv::result::SUCCESS) { + D_LOG_ERROR("Failed to set input image due to error: %s", _nvcvi->NvCV_GetErrorStringFromCode(res)); + throw std::runtime_error("SetImage failed."); + } + + _dirty = true; + } + + if (!_destination || (_destination->get_image()->width != _cache_output_size.first) || (_destination->get_image()->height != _cache_output_size.second)) { + if (_destination) { + _destination->resize(_cache_output_size.first, _cache_output_size.second); + } else { + _destination = std::make_shared<::streamfx::nvidia::cv::image>(_cache_output_size.first, _cache_output_size.second, ::streamfx::nvidia::cv::pixel_format::BGR, ::streamfx::nvidia::cv::component_type::FP32, ::streamfx::nvidia::cv::component_layout::PLANAR, ::streamfx::nvidia::cv::memory_location::GPU, 1); + } + + if (auto res = set_image(::streamfx::nvidia::vfx::PARAMETER_OUTPUT_IMAGE_0, _destination); res != ::streamfx::nvidia::cv::result::SUCCESS) { + D_LOG_ERROR("Failed to set output image due to error: %s", _nvcvi->NvCV_GetErrorStringFromCode(res)); + throw std::runtime_error("SetImage failed."); + } + + _dirty = true; + } + + if (!_convert_to_u8 || (_convert_to_u8->get_image()->width != _cache_output_size.first) || (_convert_to_u8->get_image()->height != _cache_output_size.second)) { + if (_convert_to_u8) { + _convert_to_u8->resize(_cache_output_size.first, _cache_output_size.second); + } else { + _convert_to_u8 = std::make_shared<::streamfx::nvidia::cv::image>(_cache_output_size.first, _cache_output_size.second, ::streamfx::nvidia::cv::pixel_format::RGBA, ::streamfx::nvidia::cv::component_type::UINT8, ::streamfx::nvidia::cv::component_layout::INTERLEAVED, ::streamfx::nvidia::cv::memory_location::GPU, 1); + } + } + + if (!_output || (_output->get_image()->width != _cache_output_size.first) || (_output->get_image()->height != _cache_output_size.second)) { + if (_output) { + _output->resize(_cache_output_size.first, _cache_output_size.second); + } else { + _output = std::make_shared<::streamfx::nvidia::cv::texture>(_cache_output_size.first, _cache_output_size.second, GS_RGBA_UNORM); + } + } +} + +void streamfx::nvidia::vfx::superresolution::load() +{ + auto gctx = ::streamfx::obs::gs::context(); + auto cctx = ::streamfx::nvidia::cuda::obs::get()->get_context()->enter(); + + if (auto res = effect::load(); res != ::streamfx::nvidia::cv::result::SUCCESS) { + D_LOG_ERROR("Failed to initialize effect due to error: %s", _nvcvi->NvCV_GetErrorStringFromCode(res)); + throw std::runtime_error("Load failed."); + } + + _dirty = false; +} diff --git a/components/nvidia/source/nvidia/vfx/nvidia-vfx.cpp b/components/nvidia/source/nvidia/vfx/nvidia-vfx.cpp new file mode 100644 index 0000000..78c70af --- /dev/null +++ b/components/nvidia/source/nvidia/vfx/nvidia-vfx.cpp @@ -0,0 +1,210 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2021-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#include "nvidia/vfx/nvidia-vfx.hpp" +#include "nvidia/cuda/nvidia-cuda-obs.hpp" +#include "obs/gs/gs-helper.hpp" +#include "util/util-logging.hpp" +#include "util/util-platform.hpp" + +#include "warning-disable.hpp" +#include +#include +#include "warning-enable.hpp" + +#ifdef _DEBUG +#define ST_PREFIX "<%s> " +#define D_LOG_ERROR(x, ...) P_LOG_ERROR(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_WARNING(x, ...) P_LOG_WARN(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_INFO(x, ...) P_LOG_INFO(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_DEBUG(x, ...) P_LOG_DEBUG(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#else +#define ST_PREFIX " " +#define D_LOG_ERROR(...) P_LOG_ERROR(ST_PREFIX __VA_ARGS__) +#define D_LOG_WARNING(...) P_LOG_WARN(ST_PREFIX __VA_ARGS__) +#define D_LOG_INFO(...) P_LOG_INFO(ST_PREFIX __VA_ARGS__) +#define D_LOG_DEBUG(...) P_LOG_DEBUG(ST_PREFIX __VA_ARGS__) +#endif + +#if defined(WIN32) +#include "warning-disable.hpp" +#include +#include +#include +#include "warning-enable.hpp" + +#define LIB_NAME "NVVideoEffects.dll" +#else +#define LIB_NAME "libNVVideoEffects.so" +#endif + +#define ST_ENV_NVIDIA_VIDEO_EFFECTS_SDK_PATH L"NV_VIDEO_EFFECTS_PATH" + +#define NVVFX_LOAD_SYMBOL(NAME) \ + { \ + NAME = reinterpret_cast(_library->load_symbol(#NAME)); \ + if (!NAME) \ + throw std::runtime_error("Failed to load '" #NAME "' from '" LIB_NAME "'."); \ + } + +streamfx::nvidia::vfx::vfx::~vfx() +{ + D_LOG_DEBUG("Finalizing... (Addr: 0x%" PRIuPTR ")", this); + +#ifdef WIN32 + // Remove the DLL directory from the library loader paths. + if (_extra != nullptr) { + RemoveDllDirectory(reinterpret_cast(_extra)); + } +#endif + + { // The library may need to release Graphics and CUDA resources. + auto gctx = ::streamfx::obs::gs::context(); + auto cctx = ::streamfx::nvidia::cuda::obs::get()->get_context()->enter(); + _library.reset(); + } +} + +streamfx::nvidia::vfx::vfx::vfx() +{ + std::filesystem::path sdk_path; + auto gctx = ::streamfx::obs::gs::context(); + auto cctx = ::streamfx::nvidia::cuda::obs::get()->get_context()->enter(); + + D_LOG_DEBUG("Initializing... (Addr: 0x%" PRIuPTR ")", this); + + // Figure out the location of the Video Effects SDK, if it is installed. +#ifdef WIN32 + { + DWORD env_size; + std::vector buffer; + + env_size = GetEnvironmentVariableW(ST_ENV_NVIDIA_VIDEO_EFFECTS_SDK_PATH, nullptr, 0); + if (env_size > 0) { + buffer.resize(static_cast(env_size) + 1); + env_size = GetEnvironmentVariableW(ST_ENV_NVIDIA_VIDEO_EFFECTS_SDK_PATH, buffer.data(), static_cast(buffer.size())); + sdk_path = std::wstring(buffer.data(), buffer.size()); + } else { + PWSTR str = nullptr; + HRESULT res = SHGetKnownFolderPath(FOLDERID_ProgramFiles, KF_FLAG_DEFAULT, nullptr, &str); + if (res == S_OK) { + sdk_path = std::wstring(str); + CoTaskMemFree(str); + sdk_path /= "NVIDIA Corporation"; + sdk_path /= "NVIDIA Video Effects"; + } + } + } +#else + throw std::runtime_error("Not yet implemented."); +#endif + + // Check if any of the found paths are valid. + if (!std::filesystem::exists(sdk_path)) { + D_LOG_ERROR("No supported NVIDIA SDK is installed to provide '%s'.", LIB_NAME); + throw std::runtime_error("Failed to load '" LIB_NAME "'."); + } + + // Try and load the library. + { +#ifdef WIN32 + // On platforms where it is possible, modify the linker directories. + DLL_DIRECTORY_COOKIE ck = AddDllDirectory(sdk_path.wstring().c_str()); + _extra = reinterpret_cast(ck); + if (ck == 0) { + DWORD ec = GetLastError(); + std::string error; + { + LPWSTR str; + FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_IGNORE_INSERTS, nullptr, ec, MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US), reinterpret_cast(&str), 0, nullptr); + error = ::streamfx::util::platform::native_to_utf8(std::wstring(str)); + LocalFree(str); + } + D_LOG_WARNING("Failed to add '%'s to the library loader paths with error: %s (Code %" PRIu32 ")", sdk_path.string().c_str(), error.c_str(), ec); + } +#endif + + std::filesystem::path paths[] = { + LIB_NAME, + util::platform::native_to_utf8(std::filesystem::path(sdk_path) / LIB_NAME), + }; + + for (auto path : paths) { + try { + _library = ::streamfx::util::library::load(path); + } catch (std::exception const& ex) { + D_LOG_ERROR("Failed to load '%s' with error: %s", path.string().c_str(), ex.what()); + } catch (...) { + D_LOG_ERROR("Failed to load '%s'.", path.string().c_str()); + } + + if (_library) { + break; + } + } + + if (!_library) { +#ifdef WIN32 + // Remove the DLL directory from the library loader paths. + if (_extra != nullptr) { + RemoveDllDirectory(reinterpret_cast(_extra)); + } +#endif + throw std::runtime_error("Failed to load " LIB_NAME "."); + } + } + + // Store the model path for later use. + _model_path = std::filesystem::path(sdk_path) / "models"; + + { // Load Symbols + NVVFX_LOAD_SYMBOL(NvVFX_GetVersion); + NVVFX_LOAD_SYMBOL(NvVFX_CreateEffect); + NVVFX_LOAD_SYMBOL(NvVFX_DestroyEffect); + NVVFX_LOAD_SYMBOL(NvVFX_SetU32); + NVVFX_LOAD_SYMBOL(NvVFX_SetS32); + NVVFX_LOAD_SYMBOL(NvVFX_SetF32); + NVVFX_LOAD_SYMBOL(NvVFX_SetF64); + NVVFX_LOAD_SYMBOL(NvVFX_SetU64); + NVVFX_LOAD_SYMBOL(NvVFX_SetImage); + NVVFX_LOAD_SYMBOL(NvVFX_SetObject); + NVVFX_LOAD_SYMBOL(NvVFX_SetString); + NVVFX_LOAD_SYMBOL(NvVFX_SetCudaStream); + NVVFX_LOAD_SYMBOL(NvVFX_GetU32); + NVVFX_LOAD_SYMBOL(NvVFX_GetS32); + NVVFX_LOAD_SYMBOL(NvVFX_GetF32); + NVVFX_LOAD_SYMBOL(NvVFX_GetF64); + NVVFX_LOAD_SYMBOL(NvVFX_GetU64); + NVVFX_LOAD_SYMBOL(NvVFX_GetImage); + NVVFX_LOAD_SYMBOL(NvVFX_GetObject); + NVVFX_LOAD_SYMBOL(NvVFX_GetString); + NVVFX_LOAD_SYMBOL(NvVFX_GetCudaStream); + NVVFX_LOAD_SYMBOL(NvVFX_Run); + NVVFX_LOAD_SYMBOL(NvVFX_Load); + } + + { // Assign proper GPU. + auto cctx = ::streamfx::nvidia::cuda::obs::get()->get_context()->enter(); + NvVFX_SetU32(nullptr, PARAMETER_GPU, 0); + } +} + +std::shared_ptr<::streamfx::nvidia::vfx::vfx> streamfx::nvidia::vfx::vfx::get() +{ + static std::weak_ptr instance; + static std::mutex lock; + + std::unique_lock ul(lock); + if (instance.expired()) { + auto hard_instance = std::make_shared(); + instance = hard_instance; + return hard_instance; + } + return instance.lock(); +} + +std::filesystem::path const& streamfx::nvidia::vfx::vfx::model_path() +{ + return _model_path; +} diff --git a/components/sdf-effects/CMakeLists.txt b/components/sdf-effects/CMakeLists.txt new file mode 100644 index 0000000..69439f6 --- /dev/null +++ b/components/sdf-effects/CMakeLists.txt @@ -0,0 +1,9 @@ +# AUTOGENERATED COPYRIGHT HEADER START +# Copyright (C) 2023 Michael Fabian 'Xaymar' Dirks +# AUTOGENERATED COPYRIGHT HEADER END + +cmake_minimum_required(VERSION 3.26) +project("SDFFx") +list(APPEND CMAKE_MESSAGE_INDENT "[${PROJECT_NAME}] ") + +streamfx_add_component("SDF Effects") diff --git a/components/sdf-effects/source/filters/filter-sdf-effects.cpp b/components/sdf-effects/source/filters/filter-sdf-effects.cpp new file mode 100644 index 0000000..9fe5faa --- /dev/null +++ b/components/sdf-effects/source/filters/filter-sdf-effects.cpp @@ -0,0 +1,708 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2018-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#include "filter-sdf-effects.hpp" +#include "strings.hpp" +#include "obs/gs/gs-helper.hpp" +#include "util/util-logging.hpp" + +#include "warning-disable.hpp" +#include +#include "warning-enable.hpp" + +#ifdef _DEBUG +#define ST_PREFIX "<%s> " +#define D_LOG_ERROR(x, ...) P_LOG_ERROR(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_WARNING(x, ...) P_LOG_WARN(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_INFO(x, ...) P_LOG_INFO(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_DEBUG(x, ...) P_LOG_DEBUG(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#else +#define ST_PREFIX " " +#define D_LOG_ERROR(...) P_LOG_ERROR(ST_PREFIX __VA_ARGS__) +#define D_LOG_WARNING(...) P_LOG_WARN(ST_PREFIX __VA_ARGS__) +#define D_LOG_INFO(...) P_LOG_INFO(ST_PREFIX __VA_ARGS__) +#define D_LOG_DEBUG(...) P_LOG_DEBUG(ST_PREFIX __VA_ARGS__) +#endif + +// Translation Strings +#define ST_I18N "Filter.SDFEffects" + +#define ST_I18N_SHADOW_INNER "Filter.SDFEffects.Shadow.Inner" +#define ST_KEY_SHADOW_INNER "Filter.SDFEffects.Shadow.Inner" +#define ST_I18N_SHADOW_INNER_RANGE_MINIMUM "Filter.SDFEffects.Shadow.Inner.Range.Minimum" +#define ST_KEY_SHADOW_INNER_RANGE_MINIMUM "Filter.SDFEffects.Shadow.Inner.Range.Minimum" +#define ST_I18N_SHADOW_INNER_RANGE_MAXIMUM "Filter.SDFEffects.Shadow.Inner.Range.Maximum" +#define ST_KEY_SHADOW_INNER_RANGE_MAXIMUM "Filter.SDFEffects.Shadow.Inner.Range.Maximum" +#define ST_I18N_SHADOW_INNER_OFFSET_X "Filter.SDFEffects.Shadow.Inner.Offset.X" +#define ST_KEY_SHADOW_INNER_OFFSET_X "Filter.SDFEffects.Shadow.Inner.Offset.X" +#define ST_I18N_SHADOW_INNER_OFFSET_Y "Filter.SDFEffects.Shadow.Inner.Offset.Y" +#define ST_KEY_SHADOW_INNER_OFFSET_Y "Filter.SDFEffects.Shadow.Inner.Offset.Y" +#define ST_I18N_SHADOW_INNER_COLOR "Filter.SDFEffects.Shadow.Inner.Color" +#define ST_KEY_SHADOW_INNER_COLOR "Filter.SDFEffects.Shadow.Inner.Color" +#define ST_I18N_SHADOW_INNER_ALPHA "Filter.SDFEffects.Shadow.Inner.Alpha" +#define ST_KEY_SHADOW_INNER_ALPHA "Filter.SDFEffects.Shadow.Inner.Alpha" + +#define ST_I18N_SHADOW_OUTER "Filter.SDFEffects.Shadow.Outer" +#define ST_KEY_SHADOW_OUTER "Filter.SDFEffects.Shadow.Outer" +#define ST_I18N_SHADOW_OUTER_RANGE_MINIMUM "Filter.SDFEffects.Shadow.Outer.Range.Minimum" +#define ST_KEY_SHADOW_OUTER_RANGE_MINIMUM "Filter.SDFEffects.Shadow.Outer.Range.Minimum" +#define ST_I18N_SHADOW_OUTER_RANGE_MAXIMUM "Filter.SDFEffects.Shadow.Outer.Range.Maximum" +#define ST_KEY_SHADOW_OUTER_RANGE_MAXIMUM "Filter.SDFEffects.Shadow.Outer.Range.Maximum" +#define ST_I18N_SHADOW_OUTER_OFFSET_X "Filter.SDFEffects.Shadow.Outer.Offset.X" +#define ST_KEY_SHADOW_OUTER_OFFSET_X "Filter.SDFEffects.Shadow.Outer.Offset.X" +#define ST_I18N_SHADOW_OUTER_OFFSET_Y "Filter.SDFEffects.Shadow.Outer.Offset.Y" +#define ST_KEY_SHADOW_OUTER_OFFSET_Y "Filter.SDFEffects.Shadow.Outer.Offset.Y" +#define ST_I18N_SHADOW_OUTER_COLOR "Filter.SDFEffects.Shadow.Outer.Color" +#define ST_KEY_SHADOW_OUTER_COLOR "Filter.SDFEffects.Shadow.Outer.Color" +#define ST_I18N_SHADOW_OUTER_ALPHA "Filter.SDFEffects.Shadow.Outer.Alpha" +#define ST_KEY_SHADOW_OUTER_ALPHA "Filter.SDFEffects.Shadow.Outer.Alpha" + +#define ST_I18N_GLOW_INNER "Filter.SDFEffects.Glow.Inner" +#define ST_KEY_GLOW_INNER "Filter.SDFEffects.Glow.Inner" +#define ST_I18N_GLOW_INNER_COLOR "Filter.SDFEffects.Glow.Inner.Color" +#define ST_KEY_GLOW_INNER_COLOR "Filter.SDFEffects.Glow.Inner.Color" +#define ST_I18N_GLOW_INNER_ALPHA "Filter.SDFEffects.Glow.Inner.Alpha" +#define ST_KEY_GLOW_INNER_ALPHA "Filter.SDFEffects.Glow.Inner.Alpha" +#define ST_I18N_GLOW_INNER_WIDTH "Filter.SDFEffects.Glow.Inner.Width" +#define ST_KEY_GLOW_INNER_WIDTH "Filter.SDFEffects.Glow.Inner.Width" +#define ST_I18N_GLOW_INNER_SHARPNESS "Filter.SDFEffects.Glow.Inner.Sharpness" +#define ST_KEY_GLOW_INNER_SHARPNESS "Filter.SDFEffects.Glow.Inner.Sharpness" + +#define ST_I18N_GLOW_OUTER "Filter.SDFEffects.Glow.Outer" +#define ST_KEY_GLOW_OUTER "Filter.SDFEffects.Glow.Outer" +#define ST_I18N_GLOW_OUTER_COLOR "Filter.SDFEffects.Glow.Outer.Color" +#define ST_KEY_GLOW_OUTER_COLOR "Filter.SDFEffects.Glow.Outer.Color" +#define ST_I18N_GLOW_OUTER_ALPHA "Filter.SDFEffects.Glow.Outer.Alpha" +#define ST_KEY_GLOW_OUTER_ALPHA "Filter.SDFEffects.Glow.Outer.Alpha" +#define ST_I18N_GLOW_OUTER_WIDTH "Filter.SDFEffects.Glow.Outer.Width" +#define ST_KEY_GLOW_OUTER_WIDTH "Filter.SDFEffects.Glow.Outer.Width" +#define ST_I18N_GLOW_OUTER_SHARPNESS "Filter.SDFEffects.Glow.Outer.Sharpness" +#define ST_KEY_GLOW_OUTER_SHARPNESS "Filter.SDFEffects.Glow.Outer.Sharpness" + +#define ST_I18N_OUTLINE "Filter.SDFEffects.Outline" +#define ST_KEY_OUTLINE "Filter.SDFEffects.Outline" +#define ST_I18N_OUTLINE_COLOR "Filter.SDFEffects.Outline.Color" +#define ST_KEY_OUTLINE_COLOR "Filter.SDFEffects.Outline.Color" +#define ST_I18N_OUTLINE_ALPHA "Filter.SDFEffects.Outline.Alpha" +#define ST_KEY_OUTLINE_ALPHA "Filter.SDFEffects.Outline.Alpha" +#define ST_I18N_OUTLINE_WIDTH "Filter.SDFEffects.Outline.Width" +#define ST_KEY_OUTLINE_WIDTH "Filter.SDFEffects.Outline.Width" +#define ST_I18N_OUTLINE_OFFSET "Filter.SDFEffects.Outline.Offset" +#define ST_KEY_OUTLINE_OFFSET "Filter.SDFEffects.Outline.Offset" +#define ST_I18N_OUTLINE_SHARPNESS "Filter.SDFEffects.Outline.Sharpness" +#define ST_KEY_OUTLINE_SHARPNESS "Filter.SDFEffects.Outline.Sharpness" + +#define ST_I18N_SDF_SCALE "Filter.SDFEffects.SDF.Scale" +#define ST_KEY_SDF_SCALE "Filter.SDFEffects.SDF.Scale" +#define ST_I18N_SDF_THRESHOLD "Filter.SDFEffects.SDF.Threshold" +#define ST_KEY_SDF_THRESHOLD "Filter.SDFEffects.SDF.Threshold" + +using namespace streamfx::filter::sdf_effects; + +static constexpr std::string_view HELP_URL = "https://github.com/Xaymar/obs-StreamFX/wiki/Filter-SDF-Effects"; + +sdf_effects_instance::sdf_effects_instance(obs_data_t* settings, obs_source_t* self) : obs::source_instance(settings, self), _gfx_util(::streamfx::gfx::util::get()), _source_rendered(false), _sdf_scale(1.0), _sdf_threshold(), _output_rendered(false), _inner_shadow(false), _inner_shadow_color(), _inner_shadow_range_min(), _inner_shadow_range_max(), _inner_shadow_offset_x(), _inner_shadow_offset_y(), _outer_shadow(false), _outer_shadow_color(), _outer_shadow_range_min(), _outer_shadow_range_max(), _outer_shadow_offset_x(), _outer_shadow_offset_y(), _inner_glow(false), _inner_glow_color(), _inner_glow_width(), _inner_glow_sharpness(), _inner_glow_sharpness_inv(), _outer_glow(false), _outer_glow_color(), _outer_glow_width(), _outer_glow_sharpness(), _outer_glow_sharpness_inv(), _outline(false), _outline_color(), _outline_width(), _outline_offset(), _outline_sharpness(), _outline_sharpness_inv() +{ + { + auto gctx = streamfx::obs::gs::context(); + vec4 transparent = {0, 0, 0, 0}; + + _source_rt = std::make_shared(GS_RGBA, GS_ZS_NONE); + _sdf_write = std::make_shared(GS_RGBA32F, GS_ZS_NONE); + _sdf_read = std::make_shared(GS_RGBA32F, GS_ZS_NONE); + _output_rt = std::make_shared(GS_RGBA, GS_ZS_NONE); + + std::shared_ptr initialize_rts[] = {_source_rt, _sdf_write, _sdf_read, _output_rt}; + for (auto rt : initialize_rts) { + auto op = rt->render(1, 1); + gs_clear(GS_CLEAR_COLOR | GS_CLEAR_DEPTH, &transparent, 0, 0); + } + + std::pair load_arr[] = { + {"effects/sdf/sdf-producer.effect", _sdf_producer_effect}, + {"effects/sdf/sdf-consumer.effect", _sdf_consumer_effect}, + }; + for (auto& kv : load_arr) { + auto file = streamfx::data_file_path(kv.first); + try { + kv.second = streamfx::obs::gs::effect::create(file); + } catch (std::exception& ex) { + D_LOG_ERROR("Error loading '%s': %s", file.u8string().c_str(), ex.what()); + throw; + } + } + } + + update(settings); +} + +sdf_effects_instance::~sdf_effects_instance() {} + +void sdf_effects_instance::load(obs_data_t* settings) +{ + update(settings); +} + +void sdf_effects_instance::migrate(obs_data_t* data, uint64_t version) {} + +void sdf_effects_instance::update(obs_data_t* data) +{ + { + _outer_shadow = obs_data_get_bool(data, ST_KEY_SHADOW_OUTER) && (obs_data_get_double(data, ST_KEY_SHADOW_OUTER_ALPHA) >= std::numeric_limits::epsilon()); + { + struct cs { + uint8_t r, g, b, a; + }; + union { + uint32_t color; + uint8_t channel[4]; + cs c; + }; + color = uint32_t(obs_data_get_int(data, ST_KEY_SHADOW_OUTER_COLOR)); + _outer_shadow_color.x = float(c.r / 255.0); + _outer_shadow_color.y = float(c.g / 255.0); + _outer_shadow_color.z = float(c.b / 255.0); + _outer_shadow_color.w = float(obs_data_get_double(data, ST_KEY_SHADOW_OUTER_ALPHA) / 100.0); + } + _outer_shadow_range_min = float(obs_data_get_double(data, ST_KEY_SHADOW_OUTER_RANGE_MINIMUM)); + _outer_shadow_range_max = float(obs_data_get_double(data, ST_KEY_SHADOW_OUTER_RANGE_MAXIMUM)); + _outer_shadow_offset_x = float(obs_data_get_double(data, ST_KEY_SHADOW_OUTER_OFFSET_X)); + _outer_shadow_offset_y = float(obs_data_get_double(data, ST_KEY_SHADOW_OUTER_OFFSET_Y)); + } + + { + _inner_shadow = obs_data_get_bool(data, ST_KEY_SHADOW_INNER) && (obs_data_get_double(data, ST_KEY_SHADOW_INNER_ALPHA) >= std::numeric_limits::epsilon()); + { + struct cs { + uint8_t r, g, b, a; + }; + union { + uint32_t color; + uint8_t channel[4]; + cs c; + }; + color = uint32_t(obs_data_get_int(data, ST_KEY_SHADOW_INNER_COLOR)); + _inner_shadow_color.x = float(c.r / 255.0); + _inner_shadow_color.y = float(c.g / 255.0); + _inner_shadow_color.z = float(c.b / 255.0); + _inner_shadow_color.w = float(obs_data_get_double(data, ST_KEY_SHADOW_INNER_ALPHA) / 100.0); + } + _inner_shadow_range_min = float(obs_data_get_double(data, ST_KEY_SHADOW_INNER_RANGE_MINIMUM)); + _inner_shadow_range_max = float(obs_data_get_double(data, ST_KEY_SHADOW_INNER_RANGE_MAXIMUM)); + _inner_shadow_offset_x = float(obs_data_get_double(data, ST_KEY_SHADOW_INNER_OFFSET_X)); + _inner_shadow_offset_y = float(obs_data_get_double(data, ST_KEY_SHADOW_INNER_OFFSET_Y)); + } + + { + _outer_glow = obs_data_get_bool(data, ST_KEY_GLOW_OUTER) && (obs_data_get_double(data, ST_KEY_GLOW_OUTER_ALPHA) >= std::numeric_limits::epsilon()); + { + struct cs { + uint8_t r, g, b, a; + }; + union { + uint32_t color; + uint8_t channel[4]; + cs c; + }; + color = uint32_t(obs_data_get_int(data, ST_KEY_GLOW_OUTER_COLOR)); + _outer_glow_color.x = float(c.r / 255.0); + _outer_glow_color.y = float(c.g / 255.0); + _outer_glow_color.z = float(c.b / 255.0); + _outer_glow_color.w = float(obs_data_get_double(data, ST_KEY_GLOW_OUTER_ALPHA) / 100.0); + } + _outer_glow_width = float(obs_data_get_double(data, ST_KEY_GLOW_OUTER_WIDTH)); + _outer_glow_sharpness = float(obs_data_get_double(data, ST_KEY_GLOW_OUTER_SHARPNESS) / 100.0); + _outer_glow_sharpness_inv = float(1.0f / (1.0f - _outer_glow_sharpness)); + if (_outer_glow_sharpness >= (1.0f - std::numeric_limits::epsilon())) { + _outer_glow_sharpness = 1.0f - std::numeric_limits::epsilon(); + } + } + + { + _inner_glow = obs_data_get_bool(data, ST_KEY_GLOW_INNER) && (obs_data_get_double(data, ST_KEY_GLOW_INNER_ALPHA) >= std::numeric_limits::epsilon()); + { + struct cs { + uint8_t r, g, b, a; + }; + union { + uint32_t color; + uint8_t channel[4]; + cs c; + }; + color = uint32_t(obs_data_get_int(data, ST_KEY_GLOW_INNER_COLOR)); + _inner_glow_color.x = float(c.r / 255.0); + _inner_glow_color.y = float(c.g / 255.0); + _inner_glow_color.z = float(c.b / 255.0); + _inner_glow_color.w = float(obs_data_get_double(data, ST_KEY_GLOW_INNER_ALPHA) / 100.0); + } + _inner_glow_width = float(obs_data_get_double(data, ST_KEY_GLOW_INNER_WIDTH)); + _inner_glow_sharpness = float(obs_data_get_double(data, ST_KEY_GLOW_INNER_SHARPNESS) / 100.0); + _inner_glow_sharpness_inv = float(1.0f / (1.0f - _inner_glow_sharpness)); + if (_inner_glow_sharpness >= (1.0f - std::numeric_limits::epsilon())) { + _inner_glow_sharpness = 1.0f - std::numeric_limits::epsilon(); + } + } + + { + _outline = obs_data_get_bool(data, ST_KEY_OUTLINE) && (obs_data_get_double(data, ST_KEY_OUTLINE_ALPHA) >= std::numeric_limits::epsilon()); + { + struct cs { + uint8_t r, g, b, a; + }; + union { + uint32_t color; + uint8_t channel[4]; + cs c; + }; + color = uint32_t(obs_data_get_int(data, ST_KEY_OUTLINE_COLOR)); + _outline_color.x = float(c.r / 255.0); + _outline_color.y = float(c.g / 255.0); + _outline_color.z = float(c.b / 255.0); + _outline_color.w = float(obs_data_get_double(data, ST_KEY_OUTLINE_ALPHA) / 100.0); + } + _outline_width = float(obs_data_get_double(data, ST_KEY_OUTLINE_WIDTH)); + _outline_offset = float(obs_data_get_double(data, ST_KEY_OUTLINE_OFFSET)); + _outline_sharpness = float(obs_data_get_double(data, ST_KEY_OUTLINE_SHARPNESS) / 100.0); + _outline_sharpness_inv = float(1.0f / (1.0f - _outline_sharpness)); + if (_outline_sharpness >= (1.0f - std::numeric_limits::epsilon())) { + _outline_sharpness = 1.0f - std::numeric_limits::epsilon(); + } + } + + _sdf_scale = double_t(obs_data_get_double(data, ST_KEY_SDF_SCALE) / 100.0); + _sdf_threshold = float(obs_data_get_double(data, ST_KEY_SDF_THRESHOLD) / 100.0); +} + +void sdf_effects_instance::video_tick(float) +{ + if (obs_source_t* target = obs_filter_get_target(_self); target != nullptr) { + _source_rendered = false; + _output_rendered = false; + } +} + +void sdf_effects_instance::video_render(gs_effect_t* effect) +{ + obs_source_t* parent = obs_filter_get_parent(_self); + obs_source_t* target = obs_filter_get_target(_self); + uint32_t baseW = obs_source_get_base_width(target); + uint32_t baseH = obs_source_get_base_height(target); + gs_effect_t* final_effect = effect ? effect : obs_get_base_effect(obs_base_effect::OBS_EFFECT_DEFAULT); + gs_effect_t* default_effect = obs_get_base_effect(obs_base_effect::OBS_EFFECT_DEFAULT); + + if (!_self || !parent || !target || !baseW || !baseH || !final_effect) { + obs_source_skip_video_filter(_self); + return; + } + +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + streamfx::obs::gs::debug_marker gdmp{streamfx::obs::gs::debug_color_source, "SDF Effects '%s' on '%s'", obs_source_get_name(_self), obs_source_get_name(obs_filter_get_parent(_self))}; +#endif + + auto gctx = streamfx::obs::gs::context(); + vec4 color_transparent = {0, 0, 0, 0}; + + try { + gs_blend_state_push(); + gs_reset_blend_state(); + gs_enable_blending(false); + gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO); + + gs_set_cull_mode(GS_NEITHER); + gs_enable_color(true, true, true, true); + gs_enable_depth_test(false); + gs_depth_function(GS_ALWAYS); + gs_enable_stencil_test(false); + gs_enable_stencil_write(false); + gs_stencil_function(GS_STENCIL_BOTH, GS_ALWAYS); + gs_stencil_op(GS_STENCIL_BOTH, GS_ZERO, GS_ZERO, GS_ZERO); + + if (!_source_rendered) { + // Store input texture. + { +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + streamfx::obs::gs::debug_marker gdm{streamfx::obs::gs::debug_color_cache, "Cache"}; +#endif + + auto op = _source_rt->render(baseW, baseH); + gs_ortho(0, static_cast(baseW), 0, static_cast(baseH), -1, 1); + gs_clear(GS_CLEAR_COLOR | GS_CLEAR_DEPTH, &color_transparent, 0, 0); + + if (obs_source_process_filter_begin(_self, GS_RGBA, OBS_ALLOW_DIRECT_RENDERING)) { + obs_source_process_filter_end(_self, final_effect, baseW, baseH); + } else { + throw std::runtime_error("failed to process source"); + } + } + _source_rt->get_texture(_source_texture); + if (!_source_texture) { + throw std::runtime_error("failed to draw source"); + } + + // Generate SDF Buffers + { + _sdf_read->get_texture(_sdf_texture); + if (!_sdf_texture) { + throw std::runtime_error("SDF Backbuffer empty"); + } + + if (!_sdf_producer_effect) { + throw std::runtime_error("SDF Effect no loaded"); + } + + // Scale SDF Size + double_t sdfW, sdfH; + sdfW = baseW * _sdf_scale; + sdfH = baseH * _sdf_scale; + if (sdfW <= 1) { + sdfW = 1.0; + } + if (sdfH <= 1) { + sdfH = 1.0; + } + + { +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + streamfx::obs::gs::debug_marker gdm{streamfx::obs::gs::debug_color_convert, "Update Distance Field"}; +#endif + + auto op = _sdf_write->render(uint32_t(sdfW), uint32_t(sdfH)); + gs_ortho(0, 1, 0, 1, -1, 1); + gs_clear(GS_CLEAR_COLOR | GS_CLEAR_DEPTH, &color_transparent, 0, 0); + + _sdf_producer_effect.get_parameter("_image").set_texture(_source_texture); + _sdf_producer_effect.get_parameter("_size").set_float2(float(sdfW), float(sdfH)); + _sdf_producer_effect.get_parameter("_sdf").set_texture(_sdf_texture); + _sdf_producer_effect.get_parameter("_threshold").set_float(_sdf_threshold); + + while (gs_effect_loop(_sdf_producer_effect.get_object(), "Draw")) { + _gfx_util->draw_fullscreen_triangle(); + } + } + std::swap(_sdf_read, _sdf_write); + _sdf_read->get_texture(_sdf_texture); + if (!_sdf_texture) { + throw std::runtime_error("SDF Backbuffer empty"); + } + } + + _source_rendered = true; + } + + gs_blend_state_pop(); + } catch (...) { + gs_blend_state_pop(); + obs_source_skip_video_filter(_self); + return; + } + + if (!_output_rendered) { + _output_texture = _source_texture; + + if (!_sdf_consumer_effect) { + obs_source_skip_video_filter(_self); + return; + } + + gs_blend_state_push(); + gs_reset_blend_state(); + gs_enable_color(true, true, true, true); + gs_enable_depth_test(false); + gs_set_cull_mode(GS_NEITHER); + + // SDF Effects Stack: + // Normal Source + // Outer Shadow + // Inner Shadow + // Outer Glow + // Inner Glow + // Outline + + // Optimized Render path. + try { +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + streamfx::obs::gs::debug_marker gdm{streamfx::obs::gs::debug_color_convert, "Calculate"}; +#endif + + auto op = _output_rt->render(baseW, baseH); + gs_ortho(0, 1, 0, 1, 0, 1); + + gs_enable_blending(false); + gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO); + auto param = gs_effect_get_param_by_name(default_effect, "image"); + if (param) { + gs_effect_set_texture(param, *_output_texture); + } + while (gs_effect_loop(default_effect, "Draw")) { + _gfx_util->draw_fullscreen_triangle(); + } + + gs_enable_blending(true); + gs_blend_function_separate(GS_BLEND_SRCALPHA, GS_BLEND_INVSRCALPHA, GS_BLEND_ONE, GS_BLEND_ONE); + if (_outer_shadow) { + _sdf_consumer_effect.get_parameter("pSDFTexture").set_texture(_sdf_texture); + _sdf_consumer_effect.get_parameter("pSDFThreshold").set_float(_sdf_threshold); + _sdf_consumer_effect.get_parameter("pImageTexture").set_texture(*_source_texture); + _sdf_consumer_effect.get_parameter("pShadowColor").set_float4(_outer_shadow_color); + _sdf_consumer_effect.get_parameter("pShadowMin").set_float(_outer_shadow_range_min); + _sdf_consumer_effect.get_parameter("pShadowMax").set_float(_outer_shadow_range_max); + _sdf_consumer_effect.get_parameter("pShadowOffset").set_float2(_outer_shadow_offset_x / float(baseW), _outer_shadow_offset_y / float(baseH)); + while (gs_effect_loop(_sdf_consumer_effect.get_object(), "ShadowOuter")) { + _gfx_util->draw_fullscreen_triangle(); + } + } + if (_inner_shadow) { + _sdf_consumer_effect.get_parameter("pSDFTexture").set_texture(_sdf_texture); + _sdf_consumer_effect.get_parameter("pSDFThreshold").set_float(_sdf_threshold); + _sdf_consumer_effect.get_parameter("pImageTexture").set_texture(*_source_texture); + _sdf_consumer_effect.get_parameter("pShadowColor").set_float4(_inner_shadow_color); + _sdf_consumer_effect.get_parameter("pShadowMin").set_float(_inner_shadow_range_min); + _sdf_consumer_effect.get_parameter("pShadowMax").set_float(_inner_shadow_range_max); + _sdf_consumer_effect.get_parameter("pShadowOffset").set_float2(_inner_shadow_offset_x / float(baseW), _inner_shadow_offset_y / float(baseH)); + while (gs_effect_loop(_sdf_consumer_effect.get_object(), "ShadowInner")) { + _gfx_util->draw_fullscreen_triangle(); + } + } + if (_outer_glow) { + _sdf_consumer_effect.get_parameter("pSDFTexture").set_texture(_sdf_texture); + _sdf_consumer_effect.get_parameter("pSDFThreshold").set_float(_sdf_threshold); + _sdf_consumer_effect.get_parameter("pImageTexture").set_texture(*_source_texture); + _sdf_consumer_effect.get_parameter("pGlowColor").set_float4(_outer_glow_color); + _sdf_consumer_effect.get_parameter("pGlowWidth").set_float(_outer_glow_width); + _sdf_consumer_effect.get_parameter("pGlowSharpness").set_float(_outer_glow_sharpness); + _sdf_consumer_effect.get_parameter("pGlowSharpnessInverse").set_float(_outer_glow_sharpness_inv); + while (gs_effect_loop(_sdf_consumer_effect.get_object(), "GlowOuter")) { + _gfx_util->draw_fullscreen_triangle(); + } + } + if (_inner_glow) { + _sdf_consumer_effect.get_parameter("pSDFTexture").set_texture(_sdf_texture); + _sdf_consumer_effect.get_parameter("pSDFThreshold").set_float(_sdf_threshold); + _sdf_consumer_effect.get_parameter("pImageTexture").set_texture(*_source_texture); + _sdf_consumer_effect.get_parameter("pGlowColor").set_float4(_inner_glow_color); + _sdf_consumer_effect.get_parameter("pGlowWidth").set_float(_inner_glow_width); + _sdf_consumer_effect.get_parameter("pGlowSharpness").set_float(_inner_glow_sharpness); + _sdf_consumer_effect.get_parameter("pGlowSharpnessInverse").set_float(_inner_glow_sharpness_inv); + while (gs_effect_loop(_sdf_consumer_effect.get_object(), "GlowInner")) { + _gfx_util->draw_fullscreen_triangle(); + } + } + if (_outline) { + _sdf_consumer_effect.get_parameter("pSDFTexture").set_texture(_sdf_texture); + _sdf_consumer_effect.get_parameter("pSDFThreshold").set_float(_sdf_threshold); + _sdf_consumer_effect.get_parameter("pImageTexture").set_texture(*_source_texture); + _sdf_consumer_effect.get_parameter("pOutlineColor").set_float4(_outline_color); + _sdf_consumer_effect.get_parameter("pOutlineWidth").set_float(_outline_width); + _sdf_consumer_effect.get_parameter("pOutlineOffset").set_float(_outline_offset); + _sdf_consumer_effect.get_parameter("pOutlineSharpness").set_float(_outline_sharpness); + _sdf_consumer_effect.get_parameter("pOutlineSharpnessInverse").set_float(_outline_sharpness_inv); + while (gs_effect_loop(_sdf_consumer_effect.get_object(), "Outline")) { + _gfx_util->draw_fullscreen_triangle(); + } + } + } catch (...) { + } + + _output_rt->get_texture(_output_texture); + + gs_blend_state_pop(); + _output_rendered = true; + } + + if (!_output_texture) { + obs_source_skip_video_filter(_self); + return; + } + + { +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + streamfx::obs::gs::debug_marker gdm{streamfx::obs::gs::debug_color_render, "Render"}; +#endif + + gs_eparam_t* ep = gs_effect_get_param_by_name(final_effect, "image"); + if (ep) { + gs_effect_set_texture(ep, _output_texture->get_object()); + } + while (gs_effect_loop(final_effect, "Draw")) { + gs_draw_sprite(0, 0, baseW, baseH); + } + } +} + +sdf_effects_factory::sdf_effects_factory() +{ + _info.id = S_PREFIX "filter-sdf-effects"; + _info.type = OBS_SOURCE_TYPE_FILTER; + _info.output_flags = OBS_SOURCE_VIDEO; + + support_size(false); + finish_setup(); + register_proxy("obs-stream-effects-filter-sdf-effects"); +} + +sdf_effects_factory::~sdf_effects_factory() {} + +const char* sdf_effects_factory::get_name() +{ + return D_TRANSLATE(ST_I18N); +} + +void sdf_effects_factory::get_defaults2(obs_data_t* data) +{ + obs_data_set_default_bool(data, ST_KEY_SHADOW_OUTER, false); + obs_data_set_default_int(data, ST_KEY_SHADOW_OUTER_COLOR, 0x00000000); + obs_data_set_default_double(data, ST_KEY_SHADOW_OUTER_ALPHA, 100.0); + obs_data_set_default_double(data, ST_KEY_SHADOW_OUTER_RANGE_MINIMUM, 0.0); + obs_data_set_default_double(data, ST_KEY_SHADOW_OUTER_RANGE_MAXIMUM, 4.0); + obs_data_set_default_double(data, ST_KEY_SHADOW_OUTER_OFFSET_X, 0.0); + obs_data_set_default_double(data, ST_KEY_SHADOW_OUTER_OFFSET_Y, 0.0); + + obs_data_set_default_bool(data, ST_KEY_SHADOW_INNER, false); + obs_data_set_default_int(data, ST_KEY_SHADOW_INNER_COLOR, 0x00000000); + obs_data_set_default_double(data, ST_KEY_SHADOW_INNER_ALPHA, 100.0); + obs_data_set_default_double(data, ST_KEY_SHADOW_INNER_RANGE_MINIMUM, 0.0); + obs_data_set_default_double(data, ST_KEY_SHADOW_INNER_RANGE_MAXIMUM, 4.0); + obs_data_set_default_double(data, ST_KEY_SHADOW_INNER_OFFSET_X, 0.0); + obs_data_set_default_double(data, ST_KEY_SHADOW_INNER_OFFSET_Y, 0.0); + + obs_data_set_default_bool(data, ST_KEY_GLOW_OUTER, false); + obs_data_set_default_int(data, ST_KEY_GLOW_OUTER_COLOR, 0xFFFFFFFF); + obs_data_set_default_double(data, ST_KEY_GLOW_OUTER_ALPHA, 100.0); + obs_data_set_default_double(data, ST_KEY_GLOW_OUTER_WIDTH, 4.0); + obs_data_set_default_double(data, ST_KEY_GLOW_OUTER_SHARPNESS, 50.0); + + obs_data_set_default_bool(data, ST_KEY_GLOW_INNER, false); + obs_data_set_default_int(data, ST_KEY_GLOW_INNER_COLOR, 0xFFFFFFFF); + obs_data_set_default_double(data, ST_KEY_GLOW_INNER_ALPHA, 100.0); + obs_data_set_default_double(data, ST_KEY_GLOW_INNER_WIDTH, 4.0); + obs_data_set_default_double(data, ST_KEY_GLOW_INNER_SHARPNESS, 50.0); + + obs_data_set_default_bool(data, ST_KEY_OUTLINE, false); + obs_data_set_default_int(data, ST_KEY_OUTLINE_COLOR, 0x00000000); + obs_data_set_default_double(data, ST_KEY_OUTLINE_ALPHA, 100.0); + obs_data_set_default_double(data, ST_KEY_OUTLINE_WIDTH, 4.0); + obs_data_set_default_double(data, ST_KEY_OUTLINE_OFFSET, 0.0); + obs_data_set_default_double(data, ST_KEY_OUTLINE_SHARPNESS, 50.0); + + obs_data_set_default_double(data, ST_KEY_SDF_SCALE, 100.0); + obs_data_set_default_double(data, ST_KEY_SDF_THRESHOLD, 50.0); +} + +obs_properties_t* sdf_effects_factory::get_properties2(sdf_effects_instance* data) +{ + obs_properties_t* prs = obs_properties_create(); + obs_property_t* p = nullptr; + + { + obs_properties_add_button2(prs, S_MANUAL_OPEN, D_TRANSLATE(S_MANUAL_OPEN), streamfx::filter::sdf_effects::sdf_effects_factory::on_manual_open, nullptr); + } + + { // Shadow Outer + auto pr = obs_properties_create(); + obs_properties_add_group(prs, ST_KEY_SHADOW_OUTER, D_TRANSLATE(ST_I18N_SHADOW_OUTER), OBS_GROUP_CHECKABLE, pr); + + obs_properties_add_float_slider(pr, ST_KEY_SHADOW_OUTER_RANGE_MINIMUM, D_TRANSLATE(ST_I18N_SHADOW_OUTER_RANGE_MINIMUM), -16.0, 16.0, 0.01); + obs_properties_add_float_slider(pr, ST_KEY_SHADOW_OUTER_RANGE_MAXIMUM, D_TRANSLATE(ST_I18N_SHADOW_OUTER_RANGE_MAXIMUM), -16.0, 16.0, 0.01); + obs_properties_add_float_slider(pr, ST_KEY_SHADOW_OUTER_OFFSET_X, D_TRANSLATE(ST_I18N_SHADOW_OUTER_OFFSET_X), -100.0, 100.0, 0.01); + obs_properties_add_float_slider(pr, ST_KEY_SHADOW_OUTER_OFFSET_Y, D_TRANSLATE(ST_I18N_SHADOW_OUTER_OFFSET_Y), -100.0, 100.0, 0.01); + obs_properties_add_color(pr, ST_KEY_SHADOW_OUTER_COLOR, D_TRANSLATE(ST_I18N_SHADOW_OUTER_COLOR)); + obs_properties_add_float_slider(pr, ST_KEY_SHADOW_OUTER_ALPHA, D_TRANSLATE(ST_I18N_SHADOW_OUTER_ALPHA), 0.0, 100.0, 0.1); + } + + { // Shadow Inner + auto pr = obs_properties_create(); + obs_properties_add_group(prs, ST_KEY_SHADOW_INNER, D_TRANSLATE(ST_I18N_SHADOW_INNER), OBS_GROUP_CHECKABLE, pr); + + obs_properties_add_float_slider(pr, ST_KEY_SHADOW_INNER_RANGE_MINIMUM, D_TRANSLATE(ST_I18N_SHADOW_INNER_RANGE_MINIMUM), -16.0, 16.0, 0.01); + obs_properties_add_float_slider(pr, ST_KEY_SHADOW_INNER_RANGE_MAXIMUM, D_TRANSLATE(ST_I18N_SHADOW_INNER_RANGE_MAXIMUM), -16.0, 16.0, 0.01); + obs_properties_add_float_slider(pr, ST_KEY_SHADOW_INNER_OFFSET_X, D_TRANSLATE(ST_I18N_SHADOW_INNER_OFFSET_X), -100.0, 100.0, 0.01); + obs_properties_add_float_slider(pr, ST_KEY_SHADOW_INNER_OFFSET_Y, D_TRANSLATE(ST_I18N_SHADOW_INNER_OFFSET_Y), -100.0, 100.0, 0.01); + obs_properties_add_color(pr, ST_KEY_SHADOW_INNER_COLOR, D_TRANSLATE(ST_I18N_SHADOW_INNER_COLOR)); + obs_properties_add_float_slider(pr, ST_KEY_SHADOW_INNER_ALPHA, D_TRANSLATE(ST_I18N_SHADOW_INNER_ALPHA), 0.0, 100.0, 0.1); + } + + { // Glow Outer + auto pr = obs_properties_create(); + obs_properties_add_group(prs, ST_KEY_GLOW_OUTER, D_TRANSLATE(ST_I18N_GLOW_OUTER), OBS_GROUP_CHECKABLE, pr); + + obs_properties_add_color(pr, ST_KEY_GLOW_OUTER_COLOR, D_TRANSLATE(ST_I18N_GLOW_OUTER_COLOR)); + obs_properties_add_float_slider(pr, ST_KEY_GLOW_OUTER_ALPHA, D_TRANSLATE(ST_I18N_GLOW_OUTER_ALPHA), 0.0, 100.0, 0.1); + obs_properties_add_float_slider(pr, ST_KEY_GLOW_OUTER_WIDTH, D_TRANSLATE(ST_I18N_GLOW_OUTER_WIDTH), 0.0, 16.0, 0.01); + obs_properties_add_float_slider(pr, ST_KEY_GLOW_OUTER_SHARPNESS, D_TRANSLATE(ST_I18N_GLOW_OUTER_SHARPNESS), 0.00, 100.0, 0.01); + } + + { // Glow Inner + auto pr = obs_properties_create(); + obs_properties_add_group(prs, ST_KEY_GLOW_INNER, D_TRANSLATE(ST_I18N_GLOW_INNER), OBS_GROUP_CHECKABLE, pr); + + obs_properties_add_color(pr, ST_KEY_GLOW_INNER_COLOR, D_TRANSLATE(ST_I18N_GLOW_INNER_COLOR)); + obs_properties_add_float_slider(pr, ST_KEY_GLOW_INNER_ALPHA, D_TRANSLATE(ST_I18N_GLOW_INNER_ALPHA), 0.0, 100.0, 0.1); + obs_properties_add_float_slider(pr, ST_KEY_GLOW_INNER_WIDTH, D_TRANSLATE(ST_I18N_GLOW_INNER_WIDTH), 0.0, 16.0, 0.01); + obs_properties_add_float_slider(pr, ST_KEY_GLOW_INNER_SHARPNESS, D_TRANSLATE(ST_I18N_GLOW_INNER_SHARPNESS), 0.00, 100.0, 0.01); + } + + { // Outline + auto pr = obs_properties_create(); + obs_properties_add_group(prs, ST_KEY_OUTLINE, D_TRANSLATE(ST_I18N_OUTLINE), OBS_GROUP_CHECKABLE, pr); + + obs_properties_add_color(pr, ST_KEY_OUTLINE_COLOR, D_TRANSLATE(ST_I18N_OUTLINE_COLOR)); + obs_properties_add_float_slider(pr, ST_KEY_OUTLINE_ALPHA, D_TRANSLATE(ST_I18N_OUTLINE_ALPHA), 0.0, 100.0, 0.1); + + obs_properties_add_float_slider(pr, ST_KEY_OUTLINE_WIDTH, D_TRANSLATE(ST_I18N_OUTLINE_WIDTH), 0.0, 16.0, 0.01); + + obs_properties_add_float_slider(pr, ST_KEY_OUTLINE_OFFSET, D_TRANSLATE(ST_I18N_OUTLINE_OFFSET), -16.0, 16.0, 0.01); + + obs_properties_add_float_slider(pr, ST_KEY_OUTLINE_SHARPNESS, D_TRANSLATE(ST_I18N_OUTLINE_SHARPNESS), 0.00, 100.0, 0.01); + } + + { // Advanced Options + auto pr = obs_properties_create(); + obs_properties_add_group(prs, S_ADVANCED, D_TRANSLATE(S_ADVANCED), OBS_GROUP_NORMAL, pr); + + obs_properties_add_float_slider(pr, ST_KEY_SDF_SCALE, D_TRANSLATE(ST_I18N_SDF_SCALE), 0.1, 500.0, 0.1); + obs_properties_add_float_slider(pr, ST_KEY_SDF_THRESHOLD, D_TRANSLATE(ST_I18N_SDF_THRESHOLD), 0.0, 100.0, 0.01); + } + + return prs; +} + +bool sdf_effects_factory::on_manual_open(obs_properties_t* props, obs_property_t* property, void* data) +{ + try { + streamfx::open_url(HELP_URL); + return false; + } catch (const std::exception& ex) { + D_LOG_ERROR("Failed to open manual due to error: %s", ex.what()); + return false; + } catch (...) { + D_LOG_ERROR("Failed to open manual due to unknown error.", ""); + return false; + } +} + +std::shared_ptr sdf_effects_factory::instance() +{ + static std::weak_ptr winst; + static std::mutex mtx; + + std::unique_lock lock(mtx); + auto instance = winst.lock(); + if (!instance) { + instance = std::shared_ptr(new sdf_effects_factory()); + winst = instance; + } + return instance; +} + +static std::shared_ptr loader_instance; + +static auto loader = streamfx::component( + "sdf_effects", + []() { // Initializer + loader_instance = sdf_effects_factory::instance(); + }, + []() { // Finalizer + loader_instance.reset(); + }, + {}); diff --git a/components/sdf-effects/source/filters/filter-sdf-effects.hpp b/components/sdf-effects/source/filters/filter-sdf-effects.hpp new file mode 100644 index 0000000..577042b --- /dev/null +++ b/components/sdf-effects/source/filters/filter-sdf-effects.hpp @@ -0,0 +1,104 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2018-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "common.hpp" +#include "gfx/gfx-util.hpp" +#include "obs/gs/gs-effect.hpp" +#include "obs/gs/gs-texrender.hpp" +#include "obs/gs/gs-sampler.hpp" +#include "obs/gs/gs-texture.hpp" +#include "obs/gs/gs-vertexbuffer.hpp" +#include "obs/obs-source-factory.hpp" + +namespace streamfx::filter::sdf_effects { + class sdf_effects_instance : public obs::source_instance { + streamfx::obs::gs::effect _sdf_producer_effect; + streamfx::obs::gs::effect _sdf_consumer_effect; + std::shared_ptr _gfx_util; + + // Input + std::shared_ptr _source_rt; + std::shared_ptr _source_texture; + bool _source_rendered; + + // Distance Field + std::shared_ptr _sdf_write; + std::shared_ptr _sdf_read; + std::shared_ptr _sdf_texture; + double_t _sdf_scale; + float _sdf_threshold; + + // Effects + bool _output_rendered; + std::shared_ptr _output_texture; + std::shared_ptr _output_rt; + /// Inner Shadow + bool _inner_shadow; + vec4 _inner_shadow_color; + float _inner_shadow_range_min; + float _inner_shadow_range_max; + float _inner_shadow_offset_x; + float _inner_shadow_offset_y; + /// Outer Shadow + bool _outer_shadow; + vec4 _outer_shadow_color; + float _outer_shadow_range_min; + float _outer_shadow_range_max; + float _outer_shadow_offset_x; + float _outer_shadow_offset_y; + /// Inner Glow + bool _inner_glow; + vec4 _inner_glow_color; + float _inner_glow_width; + float _inner_glow_sharpness; + float _inner_glow_sharpness_inv; + /// Outer Glow + bool _outer_glow; + vec4 _outer_glow_color; + float _outer_glow_width; + float _outer_glow_sharpness; + float _outer_glow_sharpness_inv; + /// Outline + bool _outline; + vec4 _outline_color; + float _outline_width; + float _outline_offset; + float _outline_sharpness; + float _outline_sharpness_inv; + + public: + sdf_effects_instance(obs_data_t* settings, obs_source_t* self); + virtual ~sdf_effects_instance(); + + virtual void load(obs_data_t* settings) override; + virtual void migrate(obs_data_t* data, uint64_t version) override; + virtual void update(obs_data_t* settings) override; + + virtual void video_tick(float) override; + virtual void video_render(gs_effect_t*) override; + }; + + class sdf_effects_factory : public obs::source_factory { + public: + sdf_effects_factory(); + virtual ~sdf_effects_factory(); + + virtual const char* get_name() override; + + virtual void get_defaults2(obs_data_t* data) override; + + virtual obs_properties_t* get_properties2(filter::sdf_effects::sdf_effects_instance* data) override; + + static bool on_manual_open(obs_properties_t* props, obs_property_t* property, void* data); + + public: // Singleton + static void initialize(); + + static void finalize(); + + static std::shared_ptr instance(); + }; + +} // namespace streamfx::filter::sdf_effects diff --git a/components/shader/CMakeLists.txt b/components/shader/CMakeLists.txt new file mode 100644 index 0000000..b7e0281 --- /dev/null +++ b/components/shader/CMakeLists.txt @@ -0,0 +1,9 @@ +# AUTOGENERATED COPYRIGHT HEADER START +# Copyright (C) 2023 Michael Fabian 'Xaymar' Dirks +# AUTOGENERATED COPYRIGHT HEADER END + +cmake_minimum_required(VERSION 3.26) +project("Shader") +list(APPEND CMAKE_MESSAGE_INDENT "[${PROJECT_NAME}] ") + +streamfx_add_component(${PROJECT_NAME}) diff --git a/components/shader/source/filters/filter-shader.cpp b/components/shader/source/filters/filter-shader.cpp new file mode 100644 index 0000000..3763819 --- /dev/null +++ b/components/shader/source/filters/filter-shader.cpp @@ -0,0 +1,240 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2021 coolsoft.rf +// AUTOGENERATED COPYRIGHT HEADER END + +#include "filter-shader.hpp" +#include "strings.hpp" +#include "obs/gs/gs-helper.hpp" +#include "util/util-logging.hpp" + +#include "warning-disable.hpp" +#include +#include "warning-enable.hpp" + +#ifdef _DEBUG +#define ST_PREFIX "<%s> " +#define D_LOG_ERROR(x, ...) P_LOG_ERROR(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_WARNING(x, ...) P_LOG_WARN(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_INFO(x, ...) P_LOG_INFO(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_DEBUG(x, ...) P_LOG_DEBUG(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#else +#define ST_PREFIX " " +#define D_LOG_ERROR(...) P_LOG_ERROR(ST_PREFIX __VA_ARGS__) +#define D_LOG_WARNING(...) P_LOG_WARN(ST_PREFIX __VA_ARGS__) +#define D_LOG_INFO(...) P_LOG_INFO(ST_PREFIX __VA_ARGS__) +#define D_LOG_DEBUG(...) P_LOG_DEBUG(ST_PREFIX __VA_ARGS__) +#endif + +#define ST_I18N "Filter.Shader" + +using namespace streamfx::filter::shader; + +static constexpr std::string_view HELP_URL = "https://github.com/Xaymar/obs-StreamFX/wiki/Source-Filter-Transition-Shader"; + +shader_instance::shader_instance(obs_data_t* data, obs_source_t* self) : obs::source_instance(data, self) +{ + _fx = std::make_shared(self, streamfx::gfx::shader::shader_mode::Filter); + _rt = std::make_shared(GS_RGBA, GS_ZS_NONE); + + update(data); +} + +shader_instance::~shader_instance() {} + +uint32_t shader_instance::get_width() +{ + return _fx->width(); +} + +uint32_t shader_instance::get_height() +{ + return _fx->height(); +} + +void shader_instance::properties(obs_properties_t* props) +{ + _fx->properties(props); +} + +void shader_instance::load(obs_data_t* data) +{ + update(data); +} + +void shader_instance::migrate(obs_data_t* data, uint64_t version) {} + +void shader_instance::update(obs_data_t* data) +{ + _fx->update(data); +} + +void shader_instance::video_tick(float sec_since_last) +{ + if (_fx->tick(sec_since_last)) { + obs_data_t* data = obs_source_get_settings(_self); + _fx->update(data); + obs_data_release(data); + } + + if (obs_source_t* tgt = obs_filter_get_target(_self); tgt != nullptr) { + _fx->set_size(obs_source_get_base_width(tgt), obs_source_get_base_height(tgt)); + } else if (obs_source* src = obs_filter_get_parent(_self); src != nullptr) { + _fx->set_size(obs_source_get_base_width(src), obs_source_get_base_height(src)); + } +} + +void shader_instance::video_render(gs_effect_t* effect) +{ + try { + if (!_fx || !_fx->base_width() || !_fx->base_height()) { + throw std::runtime_error("No effect, or invalid base size."); + } + +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + streamfx::obs::gs::debug_marker gdmp{streamfx::obs::gs::debug_color_source, "Shader Filter '%s' on '%s'", obs_source_get_name(_self), obs_source_get_name(obs_filter_get_parent(_self))}; +#endif + + { +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + streamfx::obs::gs::debug_marker gdm{streamfx::obs::gs::debug_color_source, "Cache"}; +#endif + + auto op = _rt->render(_fx->base_width(), _fx->base_height()); + + gs_ortho(0, 1, 0, 1, -1, 1); + + /// Render original source + if (obs_source_process_filter_begin(_self, GS_RGBA, OBS_NO_DIRECT_RENDERING)) { + gs_blend_state_push(); + gs_reset_blend_state(); + gs_blend_function_separate(GS_BLEND_ONE, GS_BLEND_ZERO, GS_BLEND_SRCALPHA, GS_BLEND_ZERO); + gs_enable_blending(false); + gs_enable_depth_test(false); + gs_enable_stencil_test(false); + gs_enable_stencil_write(false); + gs_enable_color(true, true, true, true); + gs_set_cull_mode(GS_NEITHER); + + obs_source_process_filter_end(_self, obs_get_base_effect(OBS_EFFECT_DEFAULT), 1, 1); + + gs_blend_state_pop(); + } else { + throw std::runtime_error("Failed to render previous source."); + } + } + + { +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + streamfx::obs::gs::debug_marker gdm{streamfx::obs::gs::debug_color_render, "Render"}; +#endif + + _fx->prepare_render(); + _fx->set_input_a(_rt->get_texture()); + _fx->render(effect); + } + } catch (const std::exception& ex) { + obs_source_skip_video_filter(_self); + throw ex; + } +} + +void streamfx::filter::shader::shader_instance::show() +{ + _fx->set_visible(true); +} + +void streamfx::filter::shader::shader_instance::hide() +{ + _fx->set_visible(false); +} + +void streamfx::filter::shader::shader_instance::activate() +{ + _fx->set_active(true); +} + +void streamfx::filter::shader::shader_instance::deactivate() +{ + _fx->set_active(false); +} + +shader_factory::shader_factory() +{ + _info.id = S_PREFIX "filter-shader"; + _info.type = OBS_SOURCE_TYPE_FILTER; + _info.output_flags = OBS_SOURCE_VIDEO; + + support_activity_tracking(true); + support_visibility_tracking(true); + finish_setup(); + register_proxy("obs-stream-effects-filter-shader"); +} + +shader_factory::~shader_factory() {} + +const char* shader_factory::get_name() +{ + return D_TRANSLATE(ST_I18N); +} + +void shader_factory::get_defaults2(obs_data_t* data) +{ + streamfx::gfx::shader::shader::defaults(data); +} + +obs_properties_t* shader_factory::get_properties2(shader::shader_instance* data) +{ + auto pr = obs_properties_create(); + obs_properties_set_param(pr, data, nullptr); + + { + auto p = obs_properties_add_button2(pr, S_MANUAL_OPEN, D_TRANSLATE(S_MANUAL_OPEN), streamfx::filter::shader::shader_factory::on_manual_open, nullptr); + } + + if (data) { + reinterpret_cast(data)->properties(pr); + } + + return pr; +} + +bool shader_factory::on_manual_open(obs_properties_t* props, obs_property_t* property, void* data) +{ + try { + streamfx::open_url(HELP_URL); + return false; + } catch (const std::exception& ex) { + D_LOG_ERROR("Failed to open manual due to error: %s", ex.what()); + return false; + } catch (...) { + D_LOG_ERROR("Failed to open manual due to unknown error.", ""); + return false; + } +} + +std::shared_ptr shader_factory::instance() +{ + static std::weak_ptr winst; + static std::mutex mtx; + + std::unique_lock lock(mtx); + auto instance = winst.lock(); + if (!instance) { + instance = std::shared_ptr(new shader_factory()); + winst = instance; + } + return instance; +} + +static std::shared_ptr loader_instance; + +static auto loader = streamfx::component( + "shader::filter", + []() { // Initializer + loader_instance = shader_factory::instance(); + }, + []() { // Finalizer + loader_instance.reset(); + }, + {"core::source_tracker", "core::gs::texture", "core::gs::texrender", "core::gs::sampler"}); diff --git a/components/shader/source/filters/filter-shader.hpp b/components/shader/source/filters/filter-shader.hpp new file mode 100644 index 0000000..bc4984e --- /dev/null +++ b/components/shader/source/filters/filter-shader.hpp @@ -0,0 +1,60 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2021 coolsoft.rf +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "common.hpp" +#include "gfx/shader/gfx-shader.hpp" +#include "obs/gs/gs-texrender.hpp" +#include "obs/obs-source-factory.hpp" + +namespace streamfx::filter::shader { + class shader_instance : public obs::source_instance { + std::shared_ptr _fx; + std::shared_ptr _rt; + + public: + shader_instance(obs_data_t* data, obs_source_t* self); + virtual ~shader_instance(); + + virtual uint32_t get_width() override; + virtual uint32_t get_height() override; + + void properties(obs_properties_t* props); + + virtual void load(obs_data_t* data) override; + virtual void migrate(obs_data_t* data, uint64_t version) override; + virtual void update(obs_data_t* data) override; + + virtual void video_tick(float sec_since_last) override; + virtual void video_render(gs_effect_t* effect) override; + + void show() override; + void hide() override; + + void activate() override; + void deactivate() override; + }; + + class shader_factory : public obs::source_factory { + public: + shader_factory(); + virtual ~shader_factory(); + + virtual const char* get_name() override; + + virtual void get_defaults2(obs_data_t* data) override; + + virtual obs_properties_t* get_properties2(filter::shader::shader_instance* data) override; + + static bool on_manual_open(obs_properties_t* props, obs_property_t* property, void* data); + + public: // Singleton + static void initialize(); + + static void finalize(); + + static std::shared_ptr instance(); + }; +} // namespace streamfx::filter::shader diff --git a/components/shader/source/gfx/shader/gfx-shader-param-audio.cpp b/components/shader/source/gfx/shader/gfx-shader-param-audio.cpp new file mode 100644 index 0000000..fc9d506 --- /dev/null +++ b/components/shader/source/gfx/shader/gfx-shader-param-audio.cpp @@ -0,0 +1,3 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END diff --git a/components/shader/source/gfx/shader/gfx-shader-param-audio.hpp b/components/shader/source/gfx/shader/gfx-shader-param-audio.hpp new file mode 100644 index 0000000..fc9d506 --- /dev/null +++ b/components/shader/source/gfx/shader/gfx-shader-param-audio.hpp @@ -0,0 +1,3 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END diff --git a/components/shader/source/gfx/shader/gfx-shader-param-basic.cpp b/components/shader/source/gfx/shader/gfx-shader-param-basic.cpp new file mode 100644 index 0000000..154490f --- /dev/null +++ b/components/shader/source/gfx/shader/gfx-shader-param-basic.cpp @@ -0,0 +1,400 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2022 lainon +// AUTOGENERATED COPYRIGHT HEADER END + +#include "gfx-shader-param-basic.hpp" +#include "strings.hpp" + +#include "warning-disable.hpp" +#include +#include +#include +#include +#include "warning-enable.hpp" + +static const std::string_view _annotation_field_type = "field_type"; +static const std::string_view _annotation_suffix = "suffix"; +static const std::string_view _annotation_minimum = "minimum"; +static const std::string_view _annotation_maximum = "maximum"; +static const std::string_view _annotation_step = "step"; +static const std::string_view _annotation_scale = "scale"; +static const std::string_view _annotation_enum_entry = "enum_%zu"; +static const std::string_view _annotation_enum_entry_name = "enum_%zu_name"; + +inline bool get_annotation_string(streamfx::obs::gs::effect_parameter param, std::string_view anno_name, std::string& out) +{ + if (!param) + return false; + + if (auto el = param.get_annotation(anno_name); el != nullptr) { + if (auto val = el.get_default_string(); val.length() > 0) { + out = val; + return true; + } + } + + return false; +} + +inline bool get_annotation_float(streamfx::obs::gs::effect_parameter param, std::string_view anno_name, float& out) +{ + if (!param) { + return false; + } + + if (auto el = param.get_annotation(anno_name); el != nullptr) { + out = el.get_default_float(); + return true; + } + + return false; +} + +streamfx::gfx::shader::basic_field_type streamfx::gfx::shader::get_field_type_from_string(std::string_view v) +{ + std::map matches = { + {"input", basic_field_type::Input}, + {"slider", basic_field_type::Slider}, + {"enum", basic_field_type::Enum}, + {"enumeration", basic_field_type::Enum}, + }; + + auto fnd = matches.find(v.data()); + if (fnd != matches.end()) + return fnd->second; + + return basic_field_type::Input; +} + +streamfx::gfx::shader::basic_parameter::basic_parameter(streamfx::gfx::shader::shader* parent, streamfx::obs::gs::effect_parameter param, std::string prefix) : parameter(parent, param, prefix), _field_type(basic_field_type::Input), _suffix(), _keys(), _names(), _min(), _max(), _step(), _values() +{ + char string_buffer[256]; + + _keys.resize(get_size()); + _names.resize(get_size()); + + _min.resize(get_size()); + _max.resize(get_size()); + _step.resize(get_size()); + _scale.resize(get_size()); + + // Build sub-keys + if (get_size() == 1) { + _names[0] = get_name(); + _keys[0] = get_key(); + } else { + for (std::size_t idx = 0; idx < get_size(); idx++) { + snprintf(string_buffer, sizeof(string_buffer), "[%" PRId32 "]", static_cast(idx)); + _names[idx] = std::string(string_buffer, string_buffer + strnlen(string_buffer, sizeof(string_buffer))); + snprintf(string_buffer, sizeof(string_buffer), "%s[%" PRId32 "]", get_key().data(), static_cast(idx)); + _keys[idx] = std::string(string_buffer, string_buffer + strnlen(string_buffer, sizeof(string_buffer))); + } + } + + // Detect Field Types + if (auto anno = get_parameter().get_annotation(_annotation_field_type); anno) { + _field_type = get_field_type_from_string(anno.get_default_string()); + } + + // Read Suffix Data + if (auto anno = get_parameter().get_annotation(_annotation_suffix); anno) { + if (anno.get_type() == streamfx::obs::gs::effect_parameter::type::String) + _suffix = anno.get_default_string(); + } + + // Read Enumeration Data if Enumeration + if (field_type() == basic_field_type::Enum) { + for (std::size_t idx = 0; idx < std::numeric_limits::max(); idx++) { + // Build key. + std::string key_name; + std::string key_value; + { + snprintf(string_buffer, sizeof(string_buffer), _annotation_enum_entry.data(), idx); + key_value = std::string(string_buffer); + snprintf(string_buffer, sizeof(string_buffer), _annotation_enum_entry_name.data(), idx); + key_name = std::string(string_buffer); + } + + // Value must be given, name is optional. + if (auto eanno = get_parameter().get_annotation(key_value); eanno && (get_type_from_effect_type(eanno.get_type()) == get_type())) { + basic_enum_data entry; + + load_parameter_data(eanno, entry.data); + if (auto nanno = get_parameter().get_annotation(key_name); nanno && (nanno.get_type() == streamfx::obs::gs::effect_parameter::type::String)) { + entry.name = nanno.get_default_string(); + } else { + entry.name = "Unnamed Entry"; + } + + _values.push_back(entry); + } else { + break; + } + } + + if (_values.size() == 0) { + _field_type = basic_field_type::Input; + } + } +} + +streamfx::gfx::shader::basic_parameter::~basic_parameter() {} + +void streamfx::gfx::shader::basic_parameter::load_parameter_data(streamfx::obs::gs::effect_parameter parameter, basic_data& data) +{ + parameter.get_default_value(&data.i32, 1); +} + +streamfx::gfx::shader::bool_parameter::bool_parameter(streamfx::gfx::shader::shader* parent, streamfx::obs::gs::effect_parameter param, std::string prefix) : basic_parameter(parent, param, prefix) +{ + _min.resize(0); + _max.resize(0); + _step.resize(0); + _scale.resize(0); + + _data.resize(get_size(), 1); +} + +streamfx::gfx::shader::bool_parameter::~bool_parameter() {} + +void streamfx::gfx::shader::bool_parameter::defaults(obs_data_t* settings) +{ + // TODO: Support for bool[] + if (get_size() == 1) { + obs_data_set_default_int(settings, get_key().data(), get_parameter().get_default_bool() ? 1 : 0); + } +} + +void streamfx::gfx::shader::bool_parameter::properties(obs_properties_t* props, obs_data_t* settings) +{ + if (!is_visible()) + return; + + // TODO: Support for bool[] + if (get_size() == 1) { + auto p = obs_properties_add_list(props, get_key().data(), get_name().data(), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); + if (has_description()) + obs_property_set_long_description(p, get_description().data()); + obs_property_list_add_int(p, D_TRANSLATE(S_STATE_DISABLED), 0); + obs_property_list_add_int(p, D_TRANSLATE(S_STATE_ENABLED), 1); + } +} + +void streamfx::gfx::shader::bool_parameter::update(obs_data_t* settings) +{ + if (is_automatic()) + return; + + // TODO: Support for bool[] + if (get_size() == 1) { + _data[0] = static_cast(obs_data_get_int(settings, get_key().data())); + } +} + +void streamfx::gfx::shader::bool_parameter::assign() +{ + get_parameter().set_value(_data.data(), _data.size()); +} + +streamfx::gfx::shader::float_parameter::float_parameter(streamfx::gfx::shader::shader* parent, streamfx::obs::gs::effect_parameter param, std::string prefix) : basic_parameter(parent, param, prefix) +{ + _data.resize(get_size()); + + // Reset minimum, maximum, step and scale. + for (std::size_t idx = 0; idx < get_size(); idx++) { + _min[idx].f32 = std::numeric_limits::lowest(); + _max[idx].f32 = std::numeric_limits::max(); + _step[idx].f32 = 0.01f; + _scale[idx].f32 = 1.00f; + } + + // Load Limits + if (auto anno = get_parameter().get_annotation(_annotation_minimum); anno) { + if (anno.get_type() == get_parameter().get_type()) { + anno.get_default_value(_min.data(), get_size()); + } + } + if (auto anno = get_parameter().get_annotation(_annotation_maximum); anno) { + if (anno.get_type() == get_parameter().get_type()) { + anno.get_default_value(_max.data(), get_size()); + } + } + if (auto anno = get_parameter().get_annotation(_annotation_step); anno) { + if (anno.get_type() == get_parameter().get_type()) { + anno.get_default_value(_step.data(), get_size()); + } + } + if (auto anno = get_parameter().get_annotation(_annotation_scale); anno) { + if (anno.get_type() == get_parameter().get_type()) { + anno.get_default_value(_scale.data(), get_size()); + } + } +} + +streamfx::gfx::shader::float_parameter::~float_parameter() {} + +void streamfx::gfx::shader::float_parameter::defaults(obs_data_t* settings) +{ + std::vector defaults; + defaults.resize(get_size()); + get_parameter().get_default_value(defaults.data(), get_size()); + + for (std::size_t idx = 0; idx < get_size(); idx++) { + obs_data_set_default_double(settings, key_at(idx).data(), static_cast(defaults[idx])); + } +} + +static inline obs_property_t* build_float_property(streamfx::gfx::shader::basic_field_type ft, obs_properties_t* props, const char* key, const char* name, float min, float max, float step, std::list edata) +{ + switch (ft) { + case streamfx::gfx::shader::basic_field_type::Enum: { + auto p = obs_properties_add_list(props, key, name, OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_FLOAT); + for (auto& el : edata) { + obs_property_list_add_float(p, el.name.c_str(), el.data.f32); + } + return p; + } + case streamfx::gfx::shader::basic_field_type::Slider: + return obs_properties_add_float_slider(props, key, name, min, max, step); + default: + case streamfx::gfx::shader::basic_field_type::Input: + return obs_properties_add_float(props, key, name, min, max, step); + } +} + +void streamfx::gfx::shader::float_parameter::properties(obs_properties_t* props, obs_data_t* settings) +{ + if (!is_visible()) + return; + + obs_properties_t* pr = props; + if (get_size() > 1) { + pr = obs_properties_create(); + auto p = obs_properties_add_group(props, get_key().data(), has_name() ? get_name().data() : get_key().data(), OBS_GROUP_NORMAL, pr); + if (has_description()) + obs_property_set_long_description(p, get_description().data()); + } + + for (std::size_t idx = 0; idx < get_size(); idx++) { + auto p = build_float_property(field_type(), pr, key_at(idx).data(), name_at(idx).data(), _min[idx].f32, _max[idx].f32, _step[idx].f32, _values); + if (has_description()) + obs_property_set_long_description(p, get_description().data()); + obs_property_float_set_suffix(p, suffix().data()); + } +} + +void streamfx::gfx::shader::float_parameter::update(obs_data_t* settings) +{ + for (std::size_t idx = 0; idx < get_size(); idx++) { + _data[idx].f32 = static_cast(obs_data_get_double(settings, key_at(idx).data())) * _scale[idx].f32; + } +} + +void streamfx::gfx::shader::float_parameter::assign() +{ + if (is_automatic()) + return; + + get_parameter().set_value(_data.data(), get_size()); +} +static inline obs_property_t* build_int_property(streamfx::gfx::shader::basic_field_type ft, obs_properties_t* props, const char* key, const char* name, int32_t min, int32_t max, int32_t step, std::list edata) +{ + switch (ft) { + case streamfx::gfx::shader::basic_field_type::Enum: { + auto p = obs_properties_add_list(props, key, name, OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); + for (auto& el : edata) { + obs_property_list_add_int(p, el.name.c_str(), el.data.i32); + } + return p; + } + case streamfx::gfx::shader::basic_field_type::Slider: + return obs_properties_add_int_slider(props, key, name, min, max, step); + default: + case streamfx::gfx::shader::basic_field_type::Input: + return obs_properties_add_int(props, key, name, min, max, step); + } +} + +streamfx::gfx::shader::int_parameter::int_parameter(streamfx::gfx::shader::shader* parent, streamfx::obs::gs::effect_parameter param, std::string prefix) : basic_parameter(parent, param, prefix) +{ + _data.resize(get_size()); + + // Reset minimum, maximum, step and scale. + for (std::size_t idx = 0; idx < get_size(); idx++) { + _min[idx].i32 = std::numeric_limits::lowest(); + _max[idx].i32 = std::numeric_limits::max(); + _step[idx].i32 = 1; + _scale[idx].i32 = 1; + } + + // Load Limits + if (auto anno = get_parameter().get_annotation(_annotation_minimum); anno) { + if (anno.get_type() == get_parameter().get_type()) { + anno.get_default_value(_min.data(), get_size()); + } + } + if (auto anno = get_parameter().get_annotation(_annotation_maximum); anno) { + if (anno.get_type() == get_parameter().get_type()) { + anno.get_default_value(_max.data(), get_size()); + } + } + if (auto anno = get_parameter().get_annotation(_annotation_step); anno) { + if (anno.get_type() == get_parameter().get_type()) { + anno.get_default_value(_step.data(), get_size()); + } + } + if (auto anno = get_parameter().get_annotation(_annotation_scale); anno) { + if (anno.get_type() == get_parameter().get_type()) { + anno.get_default_value(_scale.data(), get_size()); + } + } +} + +streamfx::gfx::shader::int_parameter::~int_parameter() {} + +void streamfx::gfx::shader::int_parameter::defaults(obs_data_t* settings) +{ + std::vector defaults; + defaults.resize(get_size()); + get_parameter().get_default_value(defaults.data(), get_size()); + for (std::size_t idx = 0; idx < get_size(); idx++) { + obs_data_set_default_int(settings, key_at(idx).data(), defaults[idx]); + } +} + +void streamfx::gfx::shader::int_parameter::properties(obs_properties_t* props, obs_data_t* settings) +{ + if (!is_visible()) + return; + + obs_properties_t* pr = props; + if (get_size() > 1) { + pr = obs_properties_create(); + auto p = obs_properties_add_group(props, get_key().data(), has_name() ? get_name().data() : get_key().data(), OBS_GROUP_NORMAL, pr); + if (has_description()) + obs_property_set_long_description(p, get_description().data()); + } + + for (std::size_t idx = 0; idx < get_size(); idx++) { + auto p = build_int_property(field_type(), pr, key_at(idx).data(), name_at(idx).data(), _min[idx].i32, _max[idx].i32, _step[idx].i32, _values); + if (has_description()) + obs_property_set_long_description(p, get_description().data()); + obs_property_int_set_suffix(p, suffix().data()); + } +} + +void streamfx::gfx::shader::int_parameter::update(obs_data_t* settings) +{ + for (std::size_t idx = 0; idx < get_size(); idx++) { + _data[idx].i32 = static_cast(obs_data_get_int(settings, key_at(idx).data()) * _scale[idx].i32); + } +} + +void streamfx::gfx::shader::int_parameter::assign() +{ + if (is_automatic()) + return; + + get_parameter().set_value(_data.data(), get_size()); +} diff --git a/components/shader/source/gfx/shader/gfx-shader-param-basic.hpp b/components/shader/source/gfx/shader/gfx-shader-param-basic.hpp new file mode 100644 index 0000000..ce8bbd9 --- /dev/null +++ b/components/shader/source/gfx/shader/gfx-shader-param-basic.hpp @@ -0,0 +1,137 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2022 lainon +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "common.hpp" +#include "gfx-shader-param.hpp" +#include "obs/gs/gs-effect-parameter.hpp" + +#include "warning-disable.hpp" +#include +#include "warning-enable.hpp" + +namespace streamfx::gfx { + namespace shader { + enum class basic_field_type { + Input, + Slider, + Enum, + }; + + basic_field_type get_field_type_from_string(std::string_view v); + + struct basic_data { + union { + int32_t i32; + uint32_t ui32; + float f32; + }; + }; + + struct basic_enum_data { + std::string name; + basic_data data; + }; + + class basic_parameter : public parameter { + // Descriptor + basic_field_type _field_type; + std::string _suffix; + std::vector _keys; + std::vector _names; + + protected: + // Limits + std::vector _min; + std::vector _max; + std::vector _step; + std::vector _scale; + + // Enumeration Information + std::list _values; + + public: + basic_parameter(streamfx::gfx::shader::shader* parent, streamfx::obs::gs::effect_parameter param, std::string prefix); + virtual ~basic_parameter(); + + virtual void load_parameter_data(streamfx::obs::gs::effect_parameter parameter, basic_data& data); + + public: + inline basic_field_type field_type() + { + return _field_type; + } + + inline std::string_view suffix() + { + return _suffix; + } + + inline std::string_view key_at(std::size_t idx) + { + if (idx >= get_size()) + throw std::out_of_range("Index out of range."); + return _keys[idx]; + } + + inline std::string_view name_at(std::size_t idx) + { + if (idx >= get_size()) + throw std::out_of_range("Index out of range."); + return _names[idx]; + } + }; + + struct bool_parameter : public basic_parameter { + // std::vector doesn't allow .data() + std::vector _data; + + public: + bool_parameter(streamfx::gfx::shader::shader* parent, streamfx::obs::gs::effect_parameter param, std::string prefix); + virtual ~bool_parameter(); + + void defaults(obs_data_t* settings) override; + + void properties(obs_properties_t* props, obs_data_t* settings) override; + + void update(obs_data_t* settings) override; + + void assign() override; + }; + + struct float_parameter : public basic_parameter { + std::vector _data; + + public: + float_parameter(streamfx::gfx::shader::shader* parent, streamfx::obs::gs::effect_parameter param, std::string prefix); + virtual ~float_parameter(); + + void defaults(obs_data_t* settings) override; + + void properties(obs_properties_t* props, obs_data_t* settings) override; + + void update(obs_data_t* settings) override; + + void assign() override; + }; + + struct int_parameter : public basic_parameter { + std::vector _data; + + public: + int_parameter(streamfx::gfx::shader::shader* parent, streamfx::obs::gs::effect_parameter param, std::string prefix); + virtual ~int_parameter(); + + void defaults(obs_data_t* settings) override; + + void properties(obs_properties_t* props, obs_data_t* settings) override; + + void update(obs_data_t* settings) override; + + void assign() override; + }; + + } // namespace shader +} // namespace streamfx::gfx diff --git a/components/shader/source/gfx/shader/gfx-shader-param-matrix.cpp b/components/shader/source/gfx/shader/gfx-shader-param-matrix.cpp new file mode 100644 index 0000000..fc9d506 --- /dev/null +++ b/components/shader/source/gfx/shader/gfx-shader-param-matrix.cpp @@ -0,0 +1,3 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END diff --git a/components/shader/source/gfx/shader/gfx-shader-param-matrix.hpp b/components/shader/source/gfx/shader/gfx-shader-param-matrix.hpp new file mode 100644 index 0000000..fc9d506 --- /dev/null +++ b/components/shader/source/gfx/shader/gfx-shader-param-matrix.hpp @@ -0,0 +1,3 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END diff --git a/components/shader/source/gfx/shader/gfx-shader-param-texture.cpp b/components/shader/source/gfx/shader/gfx-shader-param-texture.cpp new file mode 100644 index 0000000..20a0521 --- /dev/null +++ b/components/shader/source/gfx/shader/gfx-shader-param-texture.cpp @@ -0,0 +1,385 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2021 coolsoft.rf +// Copyright (C) 2022 lainon +// AUTOGENERATED COPYRIGHT HEADER END + +#include "gfx-shader-param-texture.hpp" +#include "strings.hpp" +#include "gfx-shader.hpp" +#include "gfx/gfx-util.hpp" +#include "obs/gs/gs-helper.hpp" +#include "obs/obs-source-tracker.hpp" +#include "util/util-platform.hpp" + +#include "warning-disable.hpp" +#include +#include +#include +#include "warning-enable.hpp" + +// TODO: +// - FFT Audio Conversion +// - FFT Variable Size... + +// UI: +// Name/Key { +// Type = File/Source/Sink +// File = ... +// Source = ... +// } + +#define ST_I18N "Shader.Parameter.Texture" +#define ST_KEY_TYPE ".Type" +#define ST_I18N_TYPE ST_I18N ".Type" +#define ST_I18N_TYPE_FILE ST_I18N_TYPE ".File" +#define ST_I18N_TYPE_SOURCE ST_I18N_TYPE ".Source" +#define ST_KEY_FILE ".File" +#define ST_I18N_FILE ST_I18N ".File" +#define ST_KEY_SOURCE ".Source" +#define ST_I18N_SOURCE ST_I18N ".Source" + +static constexpr std::string_view _annotation_field_type = "field_type"; +static constexpr std::string_view _annotation_default = "default"; +static constexpr std::string_view _annotation_enum_entry = "enum_%zu"; +static constexpr std::string_view _annotation_enum_entry_name = "enum_%zu_name"; + +streamfx::gfx::shader::texture_field_type streamfx::gfx::shader::get_texture_field_type_from_string(std::string_view v) +{ + std::map matches = { + {"input", texture_field_type::Input}, + {"enum", texture_field_type::Enum}, + {"enumeration", texture_field_type::Enum}, + }; + + auto fnd = matches.find(v.data()); + if (fnd != matches.end()) + return fnd->second; + + return texture_field_type::Input; +} + +streamfx::gfx::shader::texture_parameter::texture_parameter(streamfx::gfx::shader::shader* parent, streamfx::obs::gs::effect_parameter param, std::string prefix) : parameter(parent, param, prefix), _field_type(texture_field_type::Input), _keys(), _values(), _type(texture_type::File), _active(false), _visible(false), _dirty(true), _dirty_ts(std::chrono::high_resolution_clock::now()), _file_path(), _file_texture(), _source_name(), _source(), _source_child(), _source_active(), _source_visible(), _source_rendertarget() +{ + char string_buffer[256]; + + // Build keys and names. + { + _keys.reserve(3); + { // Type + snprintf(string_buffer, sizeof(string_buffer), "%s%s", get_key().data(), ST_KEY_TYPE); + _keys.emplace_back(string_buffer); + } + { // File + snprintf(string_buffer, sizeof(string_buffer), "%s%s", get_key().data(), ST_KEY_FILE); + _keys.emplace_back(string_buffer); + } + { // Source + snprintf(string_buffer, sizeof(string_buffer), "%s%s", get_key().data(), ST_KEY_SOURCE); + _keys.emplace_back(string_buffer); + } + } + + // Detect Field Types + if (auto anno = get_parameter().get_annotation(_annotation_field_type); anno) { + _field_type = get_texture_field_type_from_string(anno.get_default_string()); + } + if (auto anno = get_parameter().get_annotation(_annotation_default); anno) { + _default = std::filesystem::path(anno.get_default_string()); + } + + if (field_type() == texture_field_type::Enum) { + for (std::size_t idx = 0; idx < std::numeric_limits::max(); idx++) { + // Build key. + std::string key_name; + std::string key_value; + { + snprintf(string_buffer, sizeof(string_buffer), _annotation_enum_entry.data(), idx); + key_value = std::string(string_buffer); + snprintf(string_buffer, sizeof(string_buffer), _annotation_enum_entry_name.data(), idx); + key_name = std::string(string_buffer); + } + + // Value must be given, name is optional. + if (auto eanno = get_parameter().get_annotation(key_value); eanno && (get_type_from_effect_type(eanno.get_type()) == streamfx::gfx::shader::parameter_type::String)) { + texture_enum_data entry; + + entry.data.file = std::filesystem::path(eanno.get_default_string()); + + if (auto nanno = get_parameter().get_annotation(key_name); nanno && (nanno.get_type() == streamfx::obs::gs::effect_parameter::type::String)) { + entry.name = nanno.get_default_string(); + } else { + entry.name = "Unnamed Entry"; + } + + _values.push_back(entry); + } else { + break; + } + } + + if (_values.size() == 0) { + _field_type = texture_field_type::Input; + } else { + _keys[1] = get_key(); + } + } + + if (field_type() == texture_field_type::Input) { + // Special code for Input-only fields. + } +} + +streamfx::gfx::shader::texture_parameter::~texture_parameter() {} + +void streamfx::gfx::shader::texture_parameter::defaults(obs_data_t* settings) +{ + if (field_type() == texture_field_type::Input) { + obs_data_set_default_int(settings, _keys[0].c_str(), static_cast(texture_type::File)); + obs_data_set_default_string(settings, _keys[1].c_str(), reinterpret_cast(_default.generic_u8string().c_str())); + obs_data_set_default_string(settings, _keys[2].c_str(), ""); + } else { + obs_data_set_default_string(settings, _keys[1].c_str(), reinterpret_cast(_default.generic_u8string().c_str())); + } +} + +bool streamfx::gfx::shader::texture_parameter::modified_type(void* priv, obs_properties_t* props, obs_property_t*, obs_data_t* settings) +{ + auto self = reinterpret_cast(priv); + if (self->field_type() == texture_field_type::Input) { + auto type = static_cast(obs_data_get_int(settings, self->_keys[0].c_str())); + obs_property_set_visible(obs_properties_get(props, self->_keys[1].c_str()), type == texture_type::File); + obs_property_set_visible(obs_properties_get(props, self->_keys[2].c_str()), type == texture_type::Source); + return true; + } + return false; +} + +void streamfx::gfx::shader::texture_parameter::properties(obs_properties_t* props, obs_data_t* settings) +{ + if (!is_visible()) + return; + + if (field_type() == texture_field_type::Enum) { + auto p = obs_properties_add_list(props, get_key().data(), has_name() ? get_name().data() : get_key().data(), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + for (auto v : _values) { + obs_property_list_add_string(p, v.name.c_str(), reinterpret_cast(v.data.file.generic_u8string().c_str())); + } + } else { + obs_properties_t* pr = obs_properties_create(); + { + auto p = obs_properties_add_group(props, get_key().data(), has_name() ? get_name().data() : get_key().data(), OBS_GROUP_NORMAL, pr); + if (has_description()) + obs_property_set_long_description(p, get_description().data()); + } + + { + auto p = obs_properties_add_list(pr, _keys[0].c_str(), D_TRANSLATE(ST_I18N_TYPE), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); + obs_property_set_modified_callback2(p, modified_type, this); + obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_TYPE_FILE), static_cast(texture_type::File)); + obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_TYPE_SOURCE), static_cast(texture_type::Source)); + } + + { + // ToDo: Filter and Default Path. + auto p = obs_properties_add_path(pr, _keys[1].c_str(), D_TRANSLATE(ST_I18N_FILE), OBS_PATH_FILE, "* (*.*)", nullptr); + } + + { + auto p = obs_properties_add_list(pr, _keys[2].c_str(), D_TRANSLATE(ST_I18N_SOURCE), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + obs_property_list_add_string(p, "", ""); + obs::source_tracker::instance()->enumerate( + [&p](std::string name, ::streamfx::obs::source) { + std::stringstream sstr; + sstr << name << " (" << D_TRANSLATE(S_SOURCETYPE_SOURCE) << ")"; + obs_property_list_add_string(p, sstr.str().c_str(), name.c_str()); + return false; + }, + obs::source_tracker::filter_video_sources); + obs::source_tracker::instance()->enumerate( + [&p](std::string name, ::streamfx::obs::source) { + std::stringstream sstr; + sstr << name << " (" << D_TRANSLATE(S_SOURCETYPE_SCENE) << ")"; + obs_property_list_add_string(p, sstr.str().c_str(), name.c_str()); + return false; + }, + obs::source_tracker::filter_scenes); + } + + modified_type(this, props, nullptr, settings); + } +} + +std::filesystem::path make_absolute_to(const std::filesystem::path& origin, const std::filesystem::path& destination) +{ + return std::filesystem::absolute(destination / origin); +} + +void streamfx::gfx::shader::texture_parameter::update(obs_data_t* settings) +{ + // Value is assigned elsewhere. + if (is_automatic()) + return; + + if (field_type() == texture_field_type::Input) { + _type = static_cast(obs_data_get_int(settings, _keys[0].c_str())); + } else { + _type = texture_type::File; + } + + if (_type == texture_type::File) { + auto file_path = std::filesystem::path(obs_data_get_string(settings, _keys[1].c_str())); + if (file_path.is_relative()) { + file_path = make_absolute_to(file_path, get_parent()->get_shader_file()); + } + + if (_file_path != file_path) { + _file_path = file_path; + _dirty = true; + _dirty_ts = std::chrono::high_resolution_clock::now() - std::chrono::milliseconds(1); + } + } else if (_type == texture_type::Source) { + const char* source_name = obs_data_get_string(settings, _keys[2].c_str()); + + if (_source_name != source_name) { + _source_name = source_name; + _dirty = true; + _dirty_ts = std::chrono::high_resolution_clock::now() - std::chrono::milliseconds(1); + } + } +} + +void streamfx::gfx::shader::texture_parameter::assign() +{ + if (is_automatic()) + return; + + // If the data has been marked dirty, and the future timestamp minus the now is smaller than 0ms. + if (_dirty && ((_dirty_ts - std::chrono::high_resolution_clock::now()) < std::chrono::milliseconds(0))) { + // Reload or Reacquire everything necessary. + try { + // Remove now unused references. + _source.reset(); + _source_child.reset(); + _source_active.reset(); + _source_visible.reset(); + _source_rendertarget.reset(); + _file_texture.reset(); + + if (((field_type() == texture_field_type::Input) && (_type == texture_type::File)) || (field_type() == texture_field_type::Enum)) { + if (!_file_path.empty()) { + _file_texture = std::make_shared(_file_path.string()); + } + } else if ((field_type() == texture_field_type::Input) && (_type == texture_type::Source)) { + // Try and grab the source itself. + auto source = ::streamfx::obs::source(_source_name); + if (!source) { + throw std::runtime_error("Specified Source does not exist."); + } + + // Attach the child to our parent. + auto child = std::make_shared<::streamfx::obs::source_active_child>(source, get_parent()->get()); + + // Create necessary visible and active objects. + decltype(_source_active) active; + decltype(_source_visible) visible; + if (_active) { + active = ::streamfx::obs::source_active_reference::add_active_reference(source); + } + if (_visible) { + visible = ::streamfx::obs::source_showing_reference::add_showing_reference(source); + } + + // Create the necessary render target to capture the source. + auto rt = std::make_shared(GS_RGBA, GS_ZS_NONE); + + // Propagate all of this into the storage. + _source_rendertarget = rt; + _source_visible = std::move(visible); + _source_active = std::move(active); + _source_child = child; + _source = source; + } + + _dirty = false; + } catch (const std::exception&) { + _dirty_ts = std::chrono::high_resolution_clock::now() + std::chrono::milliseconds(5000); + } catch (...) { + _dirty_ts = std::chrono::high_resolution_clock::now() + std::chrono::milliseconds(5000); + } + } + + // If this is a source and active or visible, capture it. + if ((_type == texture_type::Source) && (_active || _visible) && _source_rendertarget) { + auto source = _source.lock(); +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + ::streamfx::obs::gs::debug_marker profiler1{::streamfx::obs::gs::debug_color_capture, "Parameter '%s'", get_key().data()}; + ::streamfx::obs::gs::debug_marker profiler2{::streamfx::obs::gs::debug_color_capture, "Capture '%s'", source.name().data()}; +#endif + uint32_t width = source.width(); + uint32_t height = source.height(); + + auto op = _source_rendertarget->render(width, height); + + gs_matrix_push(); + gs_ortho(0, static_cast(width), 0, static_cast(height), 0, 1); + + // ToDo: Figure out if this breaks some sources. + gs_blend_state_push(); + gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO); + gs_enable_blending(false); + + gs_enable_color(true, true, true, true); + + obs_source_video_render(source.get()); + + gs_blend_state_pop(); + gs_matrix_pop(); + } + + if (_type == texture_type::Source) { + if (_source_rendertarget) { + auto tex = _source_rendertarget->get_texture(); + if (tex) { + get_parameter().set_texture(_source_rendertarget->get_texture(), false); + } else { + get_parameter().set_texture(nullptr, false); + } + } else { + get_parameter().set_texture(nullptr, false); + } + } else if (_type == texture_type::File) { + if (_file_texture) { + // Loaded files are always linear. + get_parameter().set_texture(_file_texture, false); + } else { + get_parameter().set_texture(nullptr, false); + } + } +} + +void streamfx::gfx::shader::texture_parameter::visible(bool visible) +{ + _visible = visible; + if (visible) { + auto source = _source.lock(); + if (source) { + _source_visible = ::streamfx::obs::source_showing_reference::add_showing_reference(source); + } + } else { + _source_visible.reset(); + } +} + +void streamfx::gfx::shader::texture_parameter::active(bool active) +{ + _active = active; + if (active) { + auto source = _source.lock(); + if (source) { + _source_active = ::streamfx::obs::source_active_reference::add_active_reference(source); + } + } else { + _source_active.reset(); + } +} diff --git a/components/shader/source/gfx/shader/gfx-shader-param-texture.hpp b/components/shader/source/gfx/shader/gfx-shader-param-texture.hpp new file mode 100644 index 0000000..25a9dee --- /dev/null +++ b/components/shader/source/gfx/shader/gfx-shader-param-texture.hpp @@ -0,0 +1,101 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2021 coolsoft.rf +// Copyright (C) 2022 lainon +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "common.hpp" +#include "gfx-shader-param.hpp" +#include "obs/gs/gs-texrender.hpp" +#include "obs/gs/gs-texture.hpp" +#include "obs/obs-source-active-child.hpp" +#include "obs/obs-source-active-reference.hpp" +#include "obs/obs-source-showing-reference.hpp" +#include "obs/obs-tools.hpp" +#include "obs/obs-weak-source.hpp" + +#include "warning-disable.hpp" +#include +#include +#include "warning-enable.hpp" + +namespace streamfx::gfx { + namespace shader { + enum class texture_field_type { + Input, + Enum, + }; + + texture_field_type get_texture_field_type_from_string(std::string_view v); + + enum class texture_type { + File, + Source, + }; + + struct texture_data { + std::filesystem::path file; + }; + + struct texture_enum_data { + std::string name; + texture_data data; + }; + + struct texture_parameter : public parameter { + // Descriptor + texture_field_type _field_type; + std::vector _keys; + + // Enumeration Information + std::list _values; + + // Data + texture_type _type; + bool _active; + bool _visible; + std::filesystem::path _default; + + // Data: Dirty state + bool _dirty; + std::chrono::high_resolution_clock::time_point _dirty_ts; + + // Data: File + std::filesystem::path _file_path; + std::shared_ptr _file_texture; + + // Data: Source + std::string _source_name; + ::streamfx::obs::weak_source _source; + std::shared_ptr _source_child; + std::shared_ptr _source_active; + std::shared_ptr _source_visible; + std::shared_ptr _source_rendertarget; + + public: + texture_parameter(streamfx::gfx::shader::shader* parent, streamfx::obs::gs::effect_parameter param, std::string prefix); + virtual ~texture_parameter(); + + void defaults(obs_data_t* settings) override; + + static bool modified_type(void*, obs_properties_t*, obs_property_t*, obs_data_t*); + + void properties(obs_properties_t* props, obs_data_t* settings) override; + + void update(obs_data_t* settings) override; + + void assign() override; + + void visible(bool visible) override; + + void active(bool enabled) override; + + public: + inline texture_field_type field_type() + { + return _field_type; + } + }; + } // namespace shader +} // namespace streamfx::gfx diff --git a/components/shader/source/gfx/shader/gfx-shader-param.cpp b/components/shader/source/gfx/shader/gfx-shader-param.cpp new file mode 100644 index 0000000..d2ee4fc --- /dev/null +++ b/components/shader/source/gfx/shader/gfx-shader-param.cpp @@ -0,0 +1,198 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2021 coolsoft.rf +// Copyright (C) 2022 lainon +// AUTOGENERATED COPYRIGHT HEADER END + +#include "gfx-shader-param.hpp" +#include "gfx-shader-param-basic.hpp" +#include "gfx-shader-param-texture.hpp" + +#include "warning-disable.hpp" +#include +#include +#include "warning-enable.hpp" + +#define ST_ANNO_ORDER "order" +#define ST_ANNO_VISIBILITY "visible" +#define ST_ANNO_AUTOMATIC "automatic" +#define ST_ANNO_NAME "name" +#define ST_ANNO_DESCRIPTION "description" +#define ST_ANNO_TYPE "type" +#define ST_ANNO_SIZE "size" + +typedef streamfx::obs::gs::effect_parameter::type eptype; + +streamfx::gfx::shader::parameter_type streamfx::gfx::shader::get_type_from_effect_type(streamfx::obs::gs::effect_parameter::type type) +{ + switch (type) { + case eptype::Boolean: + return parameter_type::Boolean; + case eptype::Integer: + case eptype::Integer2: + case eptype::Integer3: + case eptype::Integer4: + return parameter_type::Integer; + case eptype::Float: + case eptype::Float2: + case eptype::Float3: + case eptype::Float4: + case eptype::Matrix: + return parameter_type::Float; + case eptype::String: + return parameter_type::String; + case eptype::Texture: + return parameter_type::Texture; + default: + return parameter_type::Unknown; + } +} + +std::size_t streamfx::gfx::shader::get_length_from_effect_type(streamfx::obs::gs::effect_parameter::type type) +{ + switch (type) { + default: + case eptype::Unknown: + case eptype::Invalid: + case eptype::String: + return 0; + case eptype::Boolean: + case eptype::Float: + case eptype::Integer: + case eptype::Texture: + return 1; + case eptype::Float2: + case eptype::Integer2: + return 2; + case eptype::Float3: + case eptype::Integer3: + return 3; + case eptype::Float4: + case eptype::Integer4: + return 4; + case eptype::Matrix: + return 16; + } +} + +streamfx::gfx::shader::parameter_type streamfx::gfx::shader::get_type_from_string(std::string_view v) +{ + if ((v == "bool") || (v == "boolean")) { + return parameter_type::Boolean; + } + if ((v == "float") || (v == "single")) { + return parameter_type::Float; + } + if ((v == "int") || (v == "integer")) { + return parameter_type::Integer; + } + if ((v == "text") || (v == "string")) { + return parameter_type::String; + } + if ((v == "tex") || (v == "texture")) { + return parameter_type::Texture; + } + if ((v == "sampler")) { + return parameter_type::Sampler; + } + /* To decide on in the future: + * - Double support? + * - Half Support? + * - Texture Arrays? (Likely not supported in libobs) + */ + throw std::invalid_argument("Invalid parameter type string."); +} + +streamfx::gfx::shader::parameter::parameter(streamfx::gfx::shader::shader* parent, streamfx::obs::gs::effect_parameter param, std::string key_prefix) : _parent(parent), _param(param), _order(0), _key(_param.get_name()), _visible(true), _automatic(false), _name(_key), _description() +{ + { + std::stringstream ss; + ss << key_prefix << "." << param.get_name(); + _name = (_key); + } + + // Read Order + if (auto anno = _param.get_annotation(ST_ANNO_VISIBILITY); anno) { + _visible = anno.get_default_bool(); + } + if (auto anno = _param.get_annotation(ST_ANNO_AUTOMATIC); anno) { + _automatic = anno.get_default_bool(); + } + + // Read Order + if (auto anno = _param.get_annotation(ST_ANNO_ORDER); anno) { + _order = anno.get_default_int(); + } + + // Read Name + if (auto anno = _param.get_annotation(ST_ANNO_NAME); anno) { + if (std::string v = anno.get_default_string(); v.length() > 0) { + _name = std::move(v); + } else { + throw std::out_of_range("'" ST_ANNO_NAME "' annotation has zero length."); + } + } + + // Read Description + if (auto anno = _param.get_annotation(ST_ANNO_DESCRIPTION); anno) { + if (std::string v = anno.get_default_string(); v.length() > 0) { + _description = std::move(v); + } else { + throw std::out_of_range("'" ST_ANNO_DESCRIPTION "' annotation has zero length."); + } + } + + // Read Type override. + _type = get_type_from_effect_type(_param.get_type()); + if (auto anno = _param.get_annotation(ST_ANNO_TYPE); anno) { + // We have a type override. + _type = get_type_from_string(anno.get_default_string()); + } + + // Read Size override. + _size = get_length_from_effect_type(_param.get_type()); + if (auto anno = _param.get_annotation(ST_ANNO_SIZE); anno) { + std::size_t ov = static_cast(anno.get_default_int()); + if (ov > 0) + _size = ov; + } + _size = std::clamp(_size, size_t{1}, size_t{32}); +} + +void streamfx::gfx::shader::parameter::defaults(obs_data_t* settings) {} + +void streamfx::gfx::shader::parameter::properties(obs_properties_t* props, obs_data_t* settings) {} + +void streamfx::gfx::shader::parameter::update(obs_data_t* settings) {} + +void streamfx::gfx::shader::parameter::assign() {} + +void streamfx::gfx::shader::parameter::visible(bool visible) {} + +void streamfx::gfx::shader::parameter::active(bool active) {} + +std::shared_ptr streamfx::gfx::shader::parameter::make_parameter(streamfx::gfx::shader::shader* parent, streamfx::obs::gs::effect_parameter param, std::string prefix) +{ + if (!parent || !param) { + throw std::runtime_error("Bad call to make_parameter. This is a bug in the plugin."); + } + + parameter_type real_type = get_type_from_effect_type(param.get_type()); + if (auto anno = param.get_annotation(ST_ANNO_TYPE); anno) { + // We have a type override. + real_type = get_type_from_string(param.get_default_string()); + } + + switch (real_type) { + case parameter_type::Boolean: + return std::make_shared(parent, param, prefix); + case parameter_type::Integer: + return std::make_shared(parent, param, prefix); + case parameter_type::Float: + return std::make_shared(parent, param, prefix); + case parameter_type::Texture: + return std::make_shared(parent, param, prefix); + default: + return nullptr; + } +} diff --git a/components/shader/source/gfx/shader/gfx-shader-param.hpp b/components/shader/source/gfx/shader/gfx-shader-param.hpp new file mode 100644 index 0000000..6141ddc --- /dev/null +++ b/components/shader/source/gfx/shader/gfx-shader-param.hpp @@ -0,0 +1,149 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2021 coolsoft.rf +// Copyright (C) 2022 lainon +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "obs/gs/gs-effect-parameter.hpp" + +#include "warning-disable.hpp" +#include +#include +#include "warning-enable.hpp" + +namespace streamfx::gfx { + namespace shader { + class shader; + + enum class parameter_type { + // Unknown type, could be anything. + Unknown, + // Boolean, either false or true. + Boolean, + // Single Floating Point + Float, + // 32-Bit Integer + Integer, + // UTF-8 Character based String. + String, + // Texture with dimensions stored in size (1 = Texture1D, 2 = Texture2D, 3 = Texture3D, 6 = TextureCube). + Texture, + // Sampler for Textures. + Sampler + }; + + parameter_type get_type_from_effect_type(streamfx::obs::gs::effect_parameter::type type); + + std::size_t get_length_from_effect_type(streamfx::obs::gs::effect_parameter::type type); + + parameter_type get_type_from_string(std::string_view v); + + class parameter { + // Parent Shader + streamfx::gfx::shader::shader* _parent; + + // Parameter used for all functionality. + streamfx::obs::gs::effect_parameter _param; + + // Real type of the parameter (libobs gets it wrong often). + parameter_type _type; + + // Real size of the parameter (libobs gets it wrong often). + std::size_t _size; + + // Order of the parameter in a list/map. + int32_t _order; + + // Key for the parameter (group) in a list/map. + std::string _key; + + // Visibility, name and description. + bool _visible; + bool _automatic; + std::string _name; + std::string _description; + + protected: + parameter(streamfx::gfx::shader::shader* parent, streamfx::obs::gs::effect_parameter param, std::string key_prefix); + virtual ~parameter(){}; + + public: + virtual void defaults(obs_data_t* settings); + + virtual void properties(obs_properties_t* props, obs_data_t* settings); + + virtual void update(obs_data_t* settings); + + virtual void assign(); + + virtual void visible(bool visible); + + virtual void active(bool enabled); + + public: + inline streamfx::gfx::shader::shader* get_parent() + { + return _parent; + } + + inline streamfx::obs::gs::effect_parameter get_parameter() + { + return _param; + } + + inline parameter_type get_type() + { + return _type; + } + + inline std::size_t get_size() + { + return _size; + } + + inline int32_t get_order() + { + return _order; + } + + inline std::string_view get_key() + { + return _key; + } + + inline bool is_visible() + { + return _visible && !_automatic; + } + + inline bool is_automatic() + { + return _automatic; + } + + inline bool has_name() + { + return _name.length() > 0; + } + + inline std::string_view get_name() + { + return _name; + } + + inline bool has_description() + { + return _description.length() > 0; + } + + inline std::string_view get_description() + { + return _description; + } + + public: + static std::shared_ptr make_parameter(streamfx::gfx::shader::shader* parent, streamfx::obs::gs::effect_parameter param, std::string prefix); + }; + } // namespace shader +} // namespace streamfx::gfx diff --git a/components/shader/source/gfx/shader/gfx-shader.cpp b/components/shader/source/gfx/shader/gfx-shader.cpp new file mode 100644 index 0000000..5f5ad9f --- /dev/null +++ b/components/shader/source/gfx/shader/gfx-shader.cpp @@ -0,0 +1,631 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2018-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2021 coolsoft.rf +// Copyright (C) 2022 lainon +// AUTOGENERATED COPYRIGHT HEADER END + +#include "gfx-shader.hpp" +#include "obs/gs/gs-helper.hpp" +#include "obs/obs-tools.hpp" +#include "plugin.hpp" + +#include "warning-disable.hpp" +#include +#include +#include +#include "warning-enable.hpp" + +#define ST_I18N "Shader" +#define ST_I18N_REFRESH ST_I18N ".Refresh" +#define ST_KEY_REFRESH "Shader.Refresh" +#define ST_I18N_SHADER ST_I18N ".Shader" +#define ST_KEY_SHADER "Shader.Shader" +#define ST_I18N_SHADER_FILE ST_I18N_SHADER ".File" +#define ST_KEY_SHADER_FILE ST_KEY_SHADER ".File" +#define ST_I18N_SHADER_TECHNIQUE ST_I18N_SHADER ".Technique" +#define ST_KEY_SHADER_TECHNIQUE ST_KEY_SHADER ".Technique" +#define ST_I18N_SHADER_SIZE ST_I18N_SHADER ".Size" +#define ST_KEY_SHADER_SIZE ST_KEY_SHADER ".Size" +#define ST_I18N_SHADER_SIZE_WIDTH ST_I18N_SHADER_SIZE ".Width" +#define ST_KEY_SHADER_SIZE_WIDTH ST_KEY_SHADER_SIZE ".Width" +#define ST_I18N_SHADER_SIZE_HEIGHT ST_I18N_SHADER_SIZE ".Height" +#define ST_KEY_SHADER_SIZE_HEIGHT ST_KEY_SHADER_SIZE ".Height" +#define ST_I18N_SHADER_SEED ST_I18N_SHADER ".Seed" +#define ST_KEY_SHADER_SEED ST_KEY_SHADER ".Seed" +#define ST_I18N_PARAMETERS ST_I18N ".Parameters" +#define ST_KEY_PARAMETERS "Shader.Parameters" + +streamfx::gfx::shader::shader::shader(obs_source_t* self, shader_mode mode) + : _self(self), _gfx_util(::streamfx::gfx::util::get()), _mode(mode), _base_width(1), _base_height(1), _active(true), + + _shader(), _shader_file(), _shader_tech("Draw"), _shader_file_mt(), _shader_file_sz(), _shader_file_tick(0), + + _width_type(size_type::Percent), _width_value(1.0), _height_type(size_type::Percent), _height_value(1.0), + + _have_current_params(false), _time(0), _time_loop(0), _loops(0), _random(), _random_seed(0), + + _rt_up_to_date(false), _rt(std::make_shared(GS_RGBA_UNORM, GS_ZS_NONE)) +{ + // Initialize random values. + _random.seed(static_cast(_random_seed)); + for (size_t idx = 0; idx < 16; idx++) { + _random_values[idx] = static_cast(static_cast(_random()) / static_cast(_random.max())); + } +} + +streamfx::gfx::shader::shader::~shader() = default; + +bool streamfx::gfx::shader::shader::is_shader_different(const std::filesystem::path& file) +{ + try { + if (std::filesystem::exists(file)) { + // Check if the file name differs. + if (file != _shader_file) + return true; + } + + if (std::filesystem::exists(_shader_file)) { + // Is the file write time different? + if (std::filesystem::last_write_time(_shader_file) != _shader_file_mt) + return true; + + // Is the file size different? + if (std::filesystem::file_size(_shader_file) != _shader_file_sz) + return true; + } + + return false; + } catch (const std::exception& ex) { + DLOG_ERROR("Loading shader '%s' failed with error: %s", file.c_str(), ex.what()); + return false; + } +} + +bool streamfx::gfx::shader::shader::is_technique_different(std::string_view tech) +{ + // Is the technique different? + if (tech != _shader_tech) + return true; + + return false; +} + +bool streamfx::gfx::shader::shader::load_shader(const std::filesystem::path& file, std::string_view tech, bool& shader_dirty, bool& param_dirty) +{ + try { + if (!std::filesystem::exists(file)) + return false; + + shader_dirty = is_shader_different(file); + param_dirty = is_technique_different(tech) || shader_dirty; + + // Update Shader + if (shader_dirty) { + _shader = streamfx::obs::gs::effect(file); + _shader_file_mt = std::filesystem::last_write_time(file); + _shader_file_sz = std::filesystem::file_size(file); + _shader_file = file; + _shader_file_tick = 0; + } + + // Update Params + if (param_dirty) { + auto settings = std::shared_ptr(obs_source_get_settings(_self), [](obs_data_t* p) { obs_data_release(p); }); + + bool have_valid_tech = false; + for (std::size_t idx = 0; idx < _shader.count_techniques(); idx++) { + if (_shader.get_technique(idx).name() == tech) { + have_valid_tech = true; + break; + } + } + if (have_valid_tech) { + _shader_tech = tech; + } else { + _shader_tech = _shader.get_technique(0).name(); + + // Update source data. + obs_data_set_string(settings.get(), ST_KEY_SHADER_TECHNIQUE, _shader_tech.c_str()); + } + + // Clear the shader parameters map and rebuild. + _shader_params.clear(); + auto etech = _shader.get_technique(_shader_tech); + for (std::size_t idx = 0; idx < etech.count_passes(); idx++) { + auto pass = etech.get_pass(idx); + auto fetch_params = [&](std::size_t count, std::function get_func) { + for (std::size_t vidx = 0; vidx < count; vidx++) { + auto el = get_func(vidx); + if (!el) + continue; + + auto el_name = el.get_name(); + auto fnd = _shader_params.find(el_name); + if (fnd != _shader_params.end()) + continue; + + auto param = streamfx::gfx::shader::parameter::make_parameter(this, el, ST_KEY_PARAMETERS); + + if (param) { + _shader_params.insert_or_assign(el_name, param); + param->defaults(settings.get()); + param->update(settings.get()); + } + } + }; + + auto gvp = [&](std::size_t idx) { return pass.get_vertex_parameter(idx); }; + fetch_params(pass.count_vertex_parameters(), gvp); + auto gpp = [&](std::size_t idx) { return pass.get_pixel_parameter(idx); }; + fetch_params(pass.count_pixel_parameters(), gpp); + } + } + + return true; + } catch (const std::exception& ex) { + DLOG_ERROR("Loading shader '%s' failed with error: %s", file.c_str(), ex.what()); + return false; + } catch (...) { + return false; + } +} + +void streamfx::gfx::shader::shader::defaults(obs_data_t* data) +{ + obs_data_set_default_string(data, ST_KEY_SHADER_FILE, ""); + obs_data_set_default_string(data, ST_KEY_SHADER_TECHNIQUE, ""); + obs_data_set_default_string(data, ST_KEY_SHADER_SIZE_WIDTH, "100.0 %"); + obs_data_set_default_string(data, ST_KEY_SHADER_SIZE_HEIGHT, "100.0 %"); + obs_data_set_default_int(data, ST_KEY_SHADER_SEED, static_cast(time(NULL))); +} + +void streamfx::gfx::shader::shader::properties(obs_properties_t* pr) +{ + _have_current_params = false; + + { + auto grp = obs_properties_create(); + obs_properties_add_group(pr, ST_KEY_SHADER, D_TRANSLATE(ST_I18N_SHADER), OBS_GROUP_NORMAL, grp); + + { + std::u8string path = u8""; + if (_shader_file.has_parent_path()) { + path = _shader_file.parent_path().generic_u8string(); + } else { + path = streamfx::data_file_path("examples/").generic_u8string(); + } + auto p = obs_properties_add_path(grp, ST_KEY_SHADER_FILE, D_TRANSLATE(ST_I18N_SHADER_FILE), OBS_PATH_FILE, "*.*", reinterpret_cast(path.c_str())); + } + { + auto p = obs_properties_add_list(grp, ST_KEY_SHADER_TECHNIQUE, D_TRANSLATE(ST_I18N_SHADER_TECHNIQUE), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + } + + { + obs_properties_add_button2( + grp, ST_KEY_REFRESH, D_TRANSLATE(ST_I18N_REFRESH), [](obs_properties_t* props, obs_property_t* prop, void* priv) { return reinterpret_cast(priv)->on_refresh_properties(props, prop); }, this); + } + + if (_mode != shader_mode::Transition) { + auto grp2 = obs_properties_create(); + obs_properties_add_group(grp, ST_KEY_SHADER_SIZE, D_TRANSLATE(ST_I18N_SHADER_SIZE), OBS_GROUP_NORMAL, grp2); + + { + auto p = obs_properties_add_text(grp2, ST_KEY_SHADER_SIZE_WIDTH, D_TRANSLATE(ST_I18N_SHADER_SIZE_WIDTH), OBS_TEXT_DEFAULT); + } + { + auto p = obs_properties_add_text(grp2, ST_KEY_SHADER_SIZE_HEIGHT, D_TRANSLATE(ST_I18N_SHADER_SIZE_HEIGHT), OBS_TEXT_DEFAULT); + } + } + + { + auto p = obs_properties_add_int_slider(grp, ST_KEY_SHADER_SEED, D_TRANSLATE(ST_I18N_SHADER_SEED), std::numeric_limits::min(), std::numeric_limits::max(), 1); + } + } + { + auto grp = obs_properties_create(); + obs_properties_add_group(pr, ST_KEY_PARAMETERS, D_TRANSLATE(ST_I18N_PARAMETERS), OBS_GROUP_NORMAL, grp); + } + + // Manually call the refresh. + on_refresh_properties(pr, nullptr); +} + +bool streamfx::gfx::shader::shader::on_refresh_properties(obs_properties_t* props, obs_property_t* prop) +{ + if (_shader) { // Clear list of techniques and rebuild it. + obs_property_t* p_tech_list = obs_properties_get(props, ST_KEY_SHADER_TECHNIQUE); + obs_property_list_clear(p_tech_list); + for (std::size_t idx = 0; idx < _shader.count_techniques(); idx++) { + auto tech = _shader.get_technique(idx); + obs_property_list_add_string(p_tech_list, tech.name().c_str(), tech.name().c_str()); + } + } + + { // Clear parameter options. + auto grp = obs_property_group_content(obs_properties_get(props, ST_KEY_PARAMETERS)); + for (auto p = obs_properties_first(grp); p != nullptr; p = obs_properties_first(grp)) { + obs_properties_remove_by_name(grp, obs_property_name(p)); + } + + // Rebuild new parameters. + obs_data_t* data = obs_source_get_settings(_self); + for (auto kv : _shader_params) { + try { + kv.second->defaults(data); + kv.second->update(data); + kv.second->properties(grp, data); + } catch (...) { + // ToDo: Do something with these? + } + } + //obs_source_update(_self, data); + } + + return true; +} + +bool streamfx::gfx::shader::shader::on_shader_or_technique_modified(obs_properties_t* props, obs_property_t* prop, obs_data_t* data) +{ + bool shader_dirty = false; + bool param_dirty = false; + + if (!update_shader(data, shader_dirty, param_dirty)) + return false; + + { // Clear list of techniques and rebuild it. + obs_property_t* p_tech_list = obs_properties_get(props, ST_KEY_SHADER_TECHNIQUE); + obs_property_list_clear(p_tech_list); + for (std::size_t idx = 0; idx < _shader.count_techniques(); idx++) { + auto tech = _shader.get_technique(idx); + obs_property_list_add_string(p_tech_list, tech.name().c_str(), tech.name().c_str()); + } + } + if (param_dirty || !_have_current_params) { + // Clear parameter options. + auto grp = obs_property_group_content(obs_properties_get(props, ST_KEY_PARAMETERS)); + for (auto p = obs_properties_first(grp); p != nullptr; p = obs_properties_first(grp)) { + obs_properties_remove_by_name(grp, obs_property_name(p)); + } + + // Rebuild new parameters. + for (auto kv : _shader_params) { + kv.second->properties(grp, data); + kv.second->defaults(data); + kv.second->update(data); + } + } + + _have_current_params = true; + return shader_dirty || param_dirty || !_have_current_params; +} + +bool streamfx::gfx::shader::shader::update_shader(obs_data_t* data, bool& shader_dirty, bool& param_dirty) +{ + const char* file_c = obs_data_get_string(data, ST_KEY_SHADER_FILE); + std::string file = file_c ? file_c : ""; + const char* tech_c = obs_data_get_string(data, ST_KEY_SHADER_TECHNIQUE); + std::string tech = tech_c ? tech_c : "Draw"; + + return load_shader(file, tech, shader_dirty, param_dirty); +} + +inline std::pair parse_text_as_size(const char* text) +{ + double_t v = 0; + if (sscanf(text, "%lf", &v) == 1) { + const char* prc_chr = strrchr(text, '%'); + if (prc_chr && (*prc_chr == '%')) { + return {streamfx::gfx::shader::size_type::Percent, v / 100.0}; + } else { + return {streamfx::gfx::shader::size_type::Pixel, v}; + } + } else { + return {streamfx::gfx::shader::size_type::Percent, 1.0}; + } +} + +void streamfx::gfx::shader::shader::update(obs_data_t* data) +{ + bool v1, v2; + update_shader(data, v1, v2); + + { + auto sz_x = parse_text_as_size(obs_data_get_string(data, ST_KEY_SHADER_SIZE_WIDTH)); + _width_type = sz_x.first; + _width_value = std::clamp(sz_x.second, 0.01, 8192.0); + + auto sz_y = parse_text_as_size(obs_data_get_string(data, ST_KEY_SHADER_SIZE_HEIGHT)); + _height_type = sz_y.first; + _height_value = std::clamp(sz_y.second, 0.01, 8192.0); + } + + if (int32_t seed = static_cast(obs_data_get_int(data, ST_KEY_SHADER_SEED)); _random_seed != seed) { + _random_seed = seed; + _random.seed(static_cast(_random_seed)); + for (size_t idx = 0; idx < 16; idx++) { + _random_values[idx] = static_cast(static_cast(_random()) / static_cast(_random.max())); + } + } + + for (auto kv : _shader_params) { + kv.second->defaults(data); + kv.second->update(data); + } +} + +uint32_t streamfx::gfx::shader::shader::width() +{ + switch (_mode) { + case shader_mode::Transition: + return _base_width; + case shader_mode::Source: + case shader_mode::Filter: + switch (_width_type) { + case size_type::Pixel: + return std::clamp(static_cast(_width_value), 1u, 16384u); + case size_type::Percent: + return std::clamp(static_cast(_width_value * _base_width), 1u, 16384u); + } + default: + return 0; + } +} + +uint32_t streamfx::gfx::shader::shader::height() +{ + switch (_mode) { + case shader_mode::Transition: + return _base_height; + case shader_mode::Source: + case shader_mode::Filter: + switch (_height_type) { + case size_type::Pixel: + return std::clamp(static_cast(_height_value), 1u, 16384u); + case size_type::Percent: + return std::clamp(static_cast(_height_value * _base_height), 1u, 16384u); + } + default: + return 0; + } +} + +uint32_t streamfx::gfx::shader::shader::base_width() +{ + return _base_width; +} + +uint32_t streamfx::gfx::shader::shader::base_height() +{ + return _base_height; +} + +bool streamfx::gfx::shader::shader::tick(float time) +{ + _shader_file_tick = static_cast(static_cast(_shader_file_tick) + static_cast(time)); + if (_shader_file_tick >= 1.0f / 3.0f) { + _shader_file_tick -= 1.0f / 3.0f; + bool v1, v2; + load_shader(_shader_file, _shader_tech, v1, v2); + } + + // Update State + _time += time; + _time_loop += time; + if (_time_loop > 1.) { + _time_loop -= 1.; + + // Loops + _loops += 1; + if (_loops >= 4194304) + _loops = -_loops; + } + + // Recreate Per-Activation-Random values. + for (size_t idx = 0; idx < 8; idx++) { + _random_values[8 + idx] = static_cast(static_cast(_random()) / static_cast(_random.max())); + } + + // Flag Render Target as outdated. + _rt_up_to_date = false; + + return false; +} + +void streamfx::gfx::shader::shader::prepare_render() +{ + if (!_shader) + return; + + // Assign user parameters + for (auto kv : _shader_params) { + kv.second->assign(); + } + + // float4 Time: (Time in Seconds), (Time in Current Second), (Time in Seconds only), (Random Value) + if (streamfx::obs::gs::effect_parameter el = _shader.get_parameter("Time"); el != nullptr) { + if (el.get_type() == streamfx::obs::gs::effect_parameter::type::Float4) { + el.set_float4(_time, _time_loop, static_cast(_loops), static_cast(static_cast(_random()) / static_cast(_random.max()))); + } + } + + // float4 ViewSize: (Width), (Height), (1.0 / Width), (1.0 / Height) + if (auto el = _shader.get_parameter("ViewSize"); el != nullptr) { + if (el.get_type() == streamfx::obs::gs::effect_parameter::type::Float4) { + el.set_float4(static_cast(width()), static_cast(height()), 1.0f / static_cast(width()), 1.0f / static_cast(height())); + } + } + + // float4x4 Random: float4[Per-Instance Random], float4[Per-Activation Random], float4x2[Per-Frame Random] + if (auto el = _shader.get_parameter("Random"); el != nullptr) { + if (el.get_type() == streamfx::obs::gs::effect_parameter::type::Matrix) { + el.set_value(_random_values, 16); + } + } + + // int32 RandomSeed: Seed used for random generation + if (auto el = _shader.get_parameter("RandomSeed"); el != nullptr) { + if (el.get_type() == streamfx::obs::gs::effect_parameter::type::Integer) { + el.set_int(_random_seed); + } + } + + return; +} + +void streamfx::gfx::shader::shader::render(gs_effect* effect) +{ + if (!_shader) + return; + + if (!effect) + effect = obs_get_base_effect(OBS_EFFECT_DEFAULT); + + if (!_rt_up_to_date) { +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + ::streamfx::obs::gs::debug_marker profiler1{::streamfx::obs::gs::debug_color_cache, "Render Cache"}; +#endif + + auto op = _rt->render(width(), height()); + + vec4 zero = {0, 0, 0, 0}; + gs_clear(GS_CLEAR_COLOR, &zero, 0, 0); + gs_ortho(0, 1, 0, 1, 0, 1); + + // Update Blend State + gs_blend_state_push(); + gs_reset_blend_state(); + gs_enable_blending(false); + gs_blend_function_separate(GS_BLEND_ONE, GS_BLEND_ZERO, GS_BLEND_ONE, GS_BLEND_ZERO); + + gs_enable_color(true, true, true, true); + + // Fix sRGB Status + bool old_srgb = gs_framebuffer_srgb_enabled(); + gs_enable_framebuffer_srgb(false); + + while (gs_effect_loop(_shader.get_object(), _shader_tech.c_str())) { + _gfx_util->draw_fullscreen_triangle(); + } + + // Restore sRGB Status + gs_enable_framebuffer_srgb(old_srgb); + + // Restore Blend State + gs_blend_state_pop(); + + _rt_up_to_date = true; + } + + if (auto tex = _rt->get_texture(); tex) { +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + ::streamfx::obs::gs::debug_marker profiler1{::streamfx::obs::gs::debug_color_render, "Draw Cache"}; +#endif + + gs_effect_set_texture(gs_effect_get_param_by_name(effect, "image"), *tex); + while (gs_effect_loop(effect, "Draw")) { + gs_draw_sprite(nullptr, 0, width(), height()); + } + } +} + +void streamfx::gfx::shader::shader::set_size(uint32_t w, uint32_t h) +{ + _base_width = w; + _base_height = h; +} + +void streamfx::gfx::shader::shader::set_input_a(std::shared_ptr tex, bool srgb) +{ + if (!_shader) + return; + + std::string_view params[] = { + "InputA", + "image", + "tex_a", + }; + for (auto& name : params) { + if (streamfx::obs::gs::effect_parameter el = _shader.get_parameter(name.data()); el != nullptr) { + if (el.get_type() == streamfx::obs::gs::effect_parameter::type::Texture) { + el.set_texture(tex, srgb); + break; + } + } + } +} + +void streamfx::gfx::shader::shader::set_input_b(std::shared_ptr tex, bool srgb) +{ + if (!_shader) + return; + + std::string_view params[] = { + "InputB", + "image2", + "tex_b", + }; + for (auto& name : params) { + if (streamfx::obs::gs::effect_parameter el = _shader.get_parameter(name.data()); el != nullptr) { + if (el.get_type() == streamfx::obs::gs::effect_parameter::type::Texture) { + el.set_texture(tex, srgb); + break; + } + } + } +} + +void streamfx::gfx::shader::shader::set_transition_time(float t) +{ + if (!_shader) + return; + + if (streamfx::obs::gs::effect_parameter el = _shader.get_parameter("TransitionTime"); el != nullptr) { + if (el.get_type() == streamfx::obs::gs::effect_parameter::type::Float) { + el.set_float(t); + } + } +} + +void streamfx::gfx::shader::shader::set_transition_size(uint32_t w, uint32_t h) +{ + if (!_shader) + return; + if (streamfx::obs::gs::effect_parameter el = _shader.get_parameter("TransitionSize"); el != nullptr) { + if (el.get_type() == streamfx::obs::gs::effect_parameter::type::Integer2) { + el.set_int2(static_cast(w), static_cast(h)); + } + } +} + +void streamfx::gfx::shader::shader::set_visible(bool visible) +{ + _visible = visible; + + for (auto kv : _shader_params) { + kv.second->visible(visible); + } +} + +void streamfx::gfx::shader::shader::set_active(bool active) +{ + _active = active; + + for (auto kv : _shader_params) { + kv.second->active(active); + } + + // Recreate Per-Activation-Random values. + for (size_t idx = 0; idx < 4; idx++) { + _random_values[4 + idx] = static_cast(static_cast(_random()) / static_cast(_random.max())); + } +} + +obs_source_t* streamfx::gfx::shader::shader::get() +{ + return _self; +} + +std::filesystem::path streamfx::gfx::shader::shader::get_shader_file() +{ + return _shader_file; +} diff --git a/components/shader/source/gfx/shader/gfx-shader.hpp b/components/shader/source/gfx/shader/gfx-shader.hpp new file mode 100644 index 0000000..da6dd1e --- /dev/null +++ b/components/shader/source/gfx/shader/gfx-shader.hpp @@ -0,0 +1,132 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2018-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2021 coolsoft.rf +// Copyright (C) 2022 lainon +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "common.hpp" +#include "gfx/gfx-util.hpp" +#include "gfx/shader/gfx-shader-param.hpp" +#include "obs/gs/gs-effect.hpp" +#include "obs/gs/gs-texrender.hpp" + +#include "warning-disable.hpp" +#include +#include +#include +#include +#include "warning-enable.hpp" + +namespace streamfx::gfx { + namespace shader { + enum class size_type { + Pixel, + Percent, + }; + + enum class shader_mode { + Source, + Filter, + Transition, + }; + + typedef std::map> shader_param_map_t; + + class shader { + obs_source_t* _self; + + std::shared_ptr _gfx_util; + + // Inputs + shader_mode _mode; + uint32_t _base_width; + uint32_t _base_height; + bool _active; + bool _visible; + + // Shader + streamfx::obs::gs::effect _shader; + std::filesystem::path _shader_file; + std::string _shader_tech; + std::filesystem::file_time_type _shader_file_mt; + uintmax_t _shader_file_sz; + float _shader_file_tick; + shader_param_map_t _shader_params; + + // Options + size_type _width_type; + double_t _width_value; + size_type _height_type; + double_t _height_value; + + // Cache + bool _have_current_params; + float _time; + float _time_loop; + int32_t _loops; + std::mt19937_64 _random; + int32_t _random_seed; + float _random_values[16]; // 0..4 Per-Instance-Random, 4..8 Per-Activation-Random 9..15 Per-Frame-Random + + // Rendering + bool _rt_up_to_date; + std::shared_ptr _rt; + + public: + shader(obs_source_t* self, shader_mode mode); + ~shader(); + + bool is_shader_different(const std::filesystem::path& file); + + bool is_technique_different(std::string_view tech); + + bool load_shader(const std::filesystem::path& file, std::string_view tech, bool& shader_dirty, bool& param_dirty); + + static void defaults(obs_data_t* data); + + void properties(obs_properties_t* props); + + bool on_refresh_properties(obs_properties_t* props, obs_property_t* prop); + + bool on_shader_or_technique_modified(obs_properties_t* props, obs_property_t* prop, obs_data_t* data); + + bool update_shader(obs_data_t* data, bool& shader_dirty, bool& param_dirty); + + void update(obs_data_t* data); + + uint32_t width(); + + uint32_t height(); + + uint32_t base_width(); + + uint32_t base_height(); + + bool tick(float time); + + void prepare_render(); + + void render(gs_effect* effect); + + obs_source_t* get(); + + std::filesystem::path get_shader_file(); + + public: + void set_size(uint32_t w, uint32_t h); + + void set_input_a(std::shared_ptr tex, bool srgb = false); + + void set_input_b(std::shared_ptr tex, bool srgb = false); + + void set_transition_time(float t); + + void set_transition_size(uint32_t w, uint32_t h); + + void set_visible(bool visible); + + void set_active(bool active); + }; + } // namespace shader +} // namespace streamfx::gfx diff --git a/components/shader/source/sources/source-shader.cpp b/components/shader/source/sources/source-shader.cpp new file mode 100644 index 0000000..81f1d93 --- /dev/null +++ b/components/shader/source/sources/source-shader.cpp @@ -0,0 +1,194 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2021 coolsoft.rf +// AUTOGENERATED COPYRIGHT HEADER END + +#include "source-shader.hpp" +#include "strings.hpp" +#include "obs/gs/gs-helper.hpp" +#include "util/util-logging.hpp" + +#include "warning-disable.hpp" +#include +#include "warning-enable.hpp" + +#ifdef _DEBUG +#define ST_PREFIX "<%s> " +#define D_LOG_ERROR(x, ...) P_LOG_ERROR(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_WARNING(x, ...) P_LOG_WARN(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_INFO(x, ...) P_LOG_INFO(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_DEBUG(x, ...) P_LOG_DEBUG(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#else +#define ST_PREFIX " " +#define D_LOG_ERROR(...) P_LOG_ERROR(ST_PREFIX __VA_ARGS__) +#define D_LOG_WARNING(...) P_LOG_WARN(ST_PREFIX __VA_ARGS__) +#define D_LOG_INFO(...) P_LOG_INFO(ST_PREFIX __VA_ARGS__) +#define D_LOG_DEBUG(...) P_LOG_DEBUG(ST_PREFIX __VA_ARGS__) +#endif + +#define ST_I18N "Source.Shader" + +using namespace streamfx::source::shader; + +static constexpr std::string_view HELP_URL = "https://github.com/Xaymar/obs-StreamFX/wiki/Source-Filter-Transition-Shader"; + +shader_instance::shader_instance(obs_data_t* data, obs_source_t* self) : obs::source_instance(data, self), _fx() +{ + _fx = std::make_shared<::streamfx::gfx::shader::shader>(self, ::streamfx::gfx::shader::shader_mode::Source); + + update(data); +} + +shader_instance::~shader_instance() {} + +uint32_t shader_instance::get_width() +{ + return _fx->width(); +} + +uint32_t shader_instance::get_height() +{ + return _fx->height(); +} + +void shader_instance::properties(obs_properties_t* props) +{ + _fx->properties(props); +} + +void shader_instance::load(obs_data_t* data) +{ + _fx->update(data); +} + +void shader_instance::update(obs_data_t* data) +{ + _fx->update(data); +} + +void shader_instance::video_tick(float sec_since_last) +{ + if (_fx->tick(sec_since_last)) { + obs_data_t* data = obs_source_get_settings(_self); + _fx->update(data); + obs_data_release(data); + } + + obs_video_info ovi; + obs_get_video_info(&ovi); + _fx->set_size(ovi.base_width, ovi.base_height); +} + +void shader_instance::video_render(gs_effect_t* effect) +{ + if (!_fx) { + return; + } + +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + streamfx::obs::gs::debug_marker gdmp{streamfx::obs::gs::debug_color_source, "Shader Source '%s'", obs_source_get_name(_self)}; +#endif + + _fx->prepare_render(); + _fx->render(effect); +} + +void streamfx::source::shader::shader_instance::show() +{ + _fx->set_visible(true); +} + +void streamfx::source::shader::shader_instance::hide() +{ + _fx->set_visible(false); +} + +void streamfx::source::shader::shader_instance::activate() +{ + _fx->set_active(true); +} + +void streamfx::source::shader::shader_instance::deactivate() +{ + _fx->set_active(false); +} + +shader_factory::shader_factory() +{ + _info.id = S_PREFIX "source-shader"; + _info.type = OBS_SOURCE_TYPE_INPUT; + _info.output_flags = OBS_SOURCE_VIDEO | OBS_SOURCE_CUSTOM_DRAW; + + support_activity_tracking(true); + support_visibility_tracking(true); + finish_setup(); + register_proxy("obs-stream-effects-source-shader"); +} + +shader_factory::~shader_factory() {} + +const char* shader_factory::get_name() +{ + return D_TRANSLATE(ST_I18N); +} + +void shader_factory::get_defaults2(obs_data_t* data) +{ + ::streamfx::gfx::shader::shader::defaults(data); +} + +obs_properties_t* shader_factory::get_properties2(shader_instance* data) +{ + auto pr = obs_properties_create(); + obs_properties_set_param(pr, data, nullptr); + + { + obs_properties_add_button2(pr, S_MANUAL_OPEN, D_TRANSLATE(S_MANUAL_OPEN), streamfx::source::shader::shader_factory::on_manual_open, nullptr); + } + + if (data) { + reinterpret_cast(data)->properties(pr); + } + + return pr; +} + +bool shader_factory::on_manual_open(obs_properties_t* props, obs_property_t* property, void* data) +{ + try { + streamfx::open_url(HELP_URL); + return false; + } catch (const std::exception& ex) { + D_LOG_ERROR("Failed to open manual due to error: %s", ex.what()); + return false; + } catch (...) { + D_LOG_ERROR("Failed to open manual due to unknown error.", ""); + return false; + } +} + +std::shared_ptr shader_factory::instance() +{ + static std::weak_ptr winst; + static std::mutex mtx; + + std::unique_lock lock(mtx); + auto instance = winst.lock(); + if (!instance) { + instance = std::shared_ptr(new shader_factory()); + winst = instance; + } + return instance; +} + +static std::shared_ptr loader_instance; + +static auto loader = streamfx::component( + "shader::source", + []() { // Initializer + loader_instance = shader_factory::instance(); + }, + []() { // Finalizer + loader_instance.reset(); + }, + {"core::source_tracker", "core::gs::texture", "core::gs::texrender", "core::gs::sampler"}); diff --git a/components/shader/source/sources/source-shader.hpp b/components/shader/source/sources/source-shader.hpp new file mode 100644 index 0000000..793de95 --- /dev/null +++ b/components/shader/source/sources/source-shader.hpp @@ -0,0 +1,55 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2021 coolsoft.rf +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "common.hpp" +#include "gfx/shader/gfx-shader.hpp" +#include "obs/gs/gs-texrender.hpp" +#include "obs/obs-source-factory.hpp" +#include "plugin.hpp" + +namespace streamfx::source::shader { + class shader_instance : public obs::source_instance { + std::shared_ptr _fx; + + public: + shader_instance(obs_data_t* data, obs_source_t* self); + virtual ~shader_instance(); + + virtual uint32_t get_width() override; + virtual uint32_t get_height() override; + + void properties(obs_properties_t* props); + + virtual void load(obs_data_t* data) override; + virtual void update(obs_data_t* data) override; + + virtual void video_tick(float sec_since_last) override; + virtual void video_render(gs_effect_t* effect) override; + + void show() override; + void hide() override; + + void activate() override; + void deactivate() override; + }; + + class shader_factory : public obs::source_factory { + public: + shader_factory(); + virtual ~shader_factory(); + + virtual const char* get_name() override; + + virtual void get_defaults2(obs_data_t* data) override; + + virtual obs_properties_t* get_properties2(source::shader::shader_instance* data) override; + + static bool on_manual_open(obs_properties_t* props, obs_property_t* property, void* data); + + public: // Singleton + static std::shared_ptr instance(); + }; +} // namespace streamfx::source::shader diff --git a/components/shader/source/transitions/transition-shader.cpp b/components/shader/source/transitions/transition-shader.cpp new file mode 100644 index 0000000..5b9a74b --- /dev/null +++ b/components/shader/source/transitions/transition-shader.cpp @@ -0,0 +1,201 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2021 coolsoft.rf +// AUTOGENERATED COPYRIGHT HEADER END + +#include "transition-shader.hpp" +#include "strings.hpp" +#include "obs/gs/gs-helper.hpp" +#include "util/util-logging.hpp" + +#include "warning-disable.hpp" +#include +#include "warning-enable.hpp" + +#ifdef _DEBUG +#define ST_PREFIX "<%s> " +#define D_LOG_ERROR(x, ...) P_LOG_ERROR(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_WARNING(x, ...) P_LOG_WARN(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_INFO(x, ...) P_LOG_INFO(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_DEBUG(x, ...) P_LOG_DEBUG(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#else +#define ST_PREFIX " " +#define D_LOG_ERROR(...) P_LOG_ERROR(ST_PREFIX __VA_ARGS__) +#define D_LOG_WARNING(...) P_LOG_WARN(ST_PREFIX __VA_ARGS__) +#define D_LOG_INFO(...) P_LOG_INFO(ST_PREFIX __VA_ARGS__) +#define D_LOG_DEBUG(...) P_LOG_DEBUG(ST_PREFIX __VA_ARGS__) +#endif + +#define ST_I18N "Transition.Shader" + +using namespace streamfx::transition::shader; + +static constexpr std::string_view HELP_URL = "https://github.com/Xaymar/obs-StreamFX/wiki/Source-Filter-Transition-Shader"; + +shader_instance::shader_instance(obs_data_t* data, obs_source_t* self) : obs::source_instance(data, self) +{ + _fx = std::make_shared(self, streamfx::gfx::shader::shader_mode::Transition); + + update(data); +} + +shader_instance::~shader_instance() {} + +uint32_t shader_instance::get_width() +{ + return _fx->width(); +} + +uint32_t shader_instance::get_height() +{ + return _fx->height(); +} + +void shader_instance::properties(obs_properties_t* props) +{ + _fx->properties(props); +} + +void shader_instance::load(obs_data_t* data) +{ + update(data); +} + +void shader_instance::update(obs_data_t* data) +{ + _fx->update(data); +} + +void shader_instance::video_tick(float sec_since_last) +{ + if (_fx->tick(sec_since_last)) { + obs_data_t* data = obs_source_get_settings(_self); + _fx->update(data); + obs_data_release(data); + } + + // Update Size from global base resolution. + obs_video_info ovi; + obs_get_video_info(&ovi); + _fx->set_size(ovi.base_width, ovi.base_height); +} + +void shader_instance::video_render(gs_effect_t* effect) +{ + if (!_fx) { + return; + } + +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + streamfx::obs::gs::debug_marker gdmp{streamfx::obs::gs::debug_color_source, "Shader Transition '%s'", obs_source_get_name(_self)}; +#endif + + obs_transition_video_render(_self, [](void* data, gs_texture_t* a, gs_texture_t* b, float t, uint32_t cx, uint32_t cy) { reinterpret_cast(data)->transition_render(a, b, t, cx, cy); }); +} + +void shader_instance::transition_render(gs_texture_t* a, gs_texture_t* b, float t, uint32_t cx, uint32_t cy) +{ + _fx->set_input_a(std::make_shared<::streamfx::obs::gs::texture>(a, false)); + _fx->set_input_b(std::make_shared<::streamfx::obs::gs::texture>(b, false)); + _fx->set_transition_time(t); + _fx->set_transition_size(cx, cy); + _fx->prepare_render(); + _fx->render(nullptr); +} + +bool shader_instance::audio_render(uint64_t* ts_out, obs_source_audio_mix* audio_output, uint32_t mixers, std::size_t channels, std::size_t sample_rate) +{ + return obs_transition_audio_render( + _self, ts_out, audio_output, mixers, channels, sample_rate, [](void*, float t) { return 1.0f - t; }, [](void*, float t) { return t; }); +} + +void shader_instance::transition_start() +{ + _fx->set_visible(true); + _fx->set_active(true); +} + +void shader_instance::transition_stop() +{ + _fx->set_active(false); + _fx->set_visible(false); +} + +shader_factory::shader_factory() +{ + _info.id = S_PREFIX "transition-shader"; + _info.type = OBS_SOURCE_TYPE_TRANSITION; + _info.output_flags = OBS_SOURCE_VIDEO | OBS_SOURCE_CUSTOM_DRAW; + + //support_activity_tracking(true); // Handled via transition start/stop + finish_setup(); + register_proxy("obs-stream-effects-transition-shader"); +} + +shader_factory::~shader_factory() {} + +const char* shader_factory::get_name() +{ + return D_TRANSLATE(ST_I18N); +} + +void shader_factory::get_defaults2(obs_data_t* data) +{ + streamfx::gfx::shader::shader::defaults(data); +} + +obs_properties_t* shader_factory::get_properties2(shader::shader_instance* data) +{ + auto pr = obs_properties_create(); + obs_properties_set_param(pr, data, nullptr); + + { + obs_properties_add_button2(pr, S_MANUAL_OPEN, D_TRANSLATE(S_MANUAL_OPEN), streamfx::transition::shader::shader_factory::on_manual_open, nullptr); + } + + if (data) { + reinterpret_cast(data)->properties(pr); + } + + return pr; +} + +bool shader_factory::on_manual_open(obs_properties_t* props, obs_property_t* property, void* data) +{ + try { + streamfx::open_url(HELP_URL); + return false; + } catch (const std::exception& ex) { + D_LOG_ERROR("Failed to open manual due to error: %s", ex.what()); + return false; + } catch (...) { + D_LOG_ERROR("Failed to open manual due to unknown error.", ""); + return false; + } +} + +std::shared_ptr shader_factory::instance() +{ + static std::weak_ptr winst; + static std::mutex mtx; + + std::unique_lock lock(mtx); + auto instance = winst.lock(); + if (!instance) { + instance = std::shared_ptr(new shader_factory()); + winst = instance; + } + return instance; +} + +static std::shared_ptr loader_instance; + +static auto loader = streamfx::component( + "shader::transition", + []() { // Initializer + loader_instance = shader_factory::instance(); + }, + []() { // Finalizer + loader_instance.reset(); + }, + {"core::source_tracker", "core::gs::texture", "core::gs::texrender", "core::gs::sampler"}); diff --git a/components/shader/source/transitions/transition-shader.hpp b/components/shader/source/transitions/transition-shader.hpp new file mode 100644 index 0000000..839b5f3 --- /dev/null +++ b/components/shader/source/transitions/transition-shader.hpp @@ -0,0 +1,55 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "common.hpp" +#include "gfx/shader/gfx-shader.hpp" +#include "obs/gs/gs-texrender.hpp" +#include "obs/obs-source-factory.hpp" +#include "plugin.hpp" + +namespace streamfx::transition::shader { + class shader_instance : public obs::source_instance { + std::shared_ptr _fx; + + public: + shader_instance(obs_data_t* data, obs_source_t* self); + virtual ~shader_instance(); + + virtual uint32_t get_width() override; + virtual uint32_t get_height() override; + + void properties(obs_properties_t* props); + + virtual void load(obs_data_t* data) override; + virtual void update(obs_data_t* data) override; + + virtual void video_tick(float sec_since_last) override; + virtual void video_render(gs_effect_t* effect) override; + + void transition_render(gs_texture_t* a, gs_texture_t* b, float t, uint32_t cx, uint32_t cy); + + virtual bool audio_render(uint64_t* ts_out, struct obs_source_audio_mix* audio_output, uint32_t mixers, std::size_t channels, std::size_t sample_rate) override; + + virtual void transition_start() override; + virtual void transition_stop() override; + }; + + class shader_factory : public obs::source_factory { + public: + shader_factory(); + virtual ~shader_factory(); + + virtual const char* get_name() override; + + virtual void get_defaults2(obs_data_t* data) override; + + virtual obs_properties_t* get_properties2(transition::shader::shader_instance* data) override; + + static bool on_manual_open(obs_properties_t* props, obs_property_t* property, void* data); + + public: // Singleton + static std::shared_ptr instance(); + }; +} // namespace streamfx::transition::shader diff --git a/components/spoutsink/CMakeLists.txt b/components/spoutsink/CMakeLists.txt new file mode 100644 index 0000000..e2faf5b --- /dev/null +++ b/components/spoutsink/CMakeLists.txt @@ -0,0 +1,8 @@ +# AUTOGENERATED COPYRIGHT HEADER START +# Copyright (C) 2023 Michael Fabian 'Xaymar' Dirks +# AUTOGENERATED COPYRIGHT HEADER END +cmake_minimum_required(VERSION 3.26) +project("SpoutSink") +list(APPEND CMAKE_MESSAGE_INDENT "[Spout/Sink] ") + +streamfx_add_component("Spout/Sink") diff --git a/components/spoutsink/source/filters/filter-spout-asyncvideo.cpp b/components/spoutsink/source/filters/filter-spout-asyncvideo.cpp new file mode 100644 index 0000000..31390f7 --- /dev/null +++ b/components/spoutsink/source/filters/filter-spout-asyncvideo.cpp @@ -0,0 +1,114 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#include "filter-spout-asyncvideo.hpp" +#include "strings.hpp" +#include "plugin.hpp" +#include "util/util-logging.hpp" + +#include "warning-disable.hpp" +#include +#include +#include "warning-enable.hpp" + +// OBS +#include "warning-disable.hpp" +#include "warning-enable.hpp" + +#ifdef _DEBUG +#define ST_PREFIX "<%s> " +#define D_LOG_ERROR(x, ...) P_LOG_ERROR(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_WARNING(x, ...) P_LOG_WARN(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_INFO(x, ...) P_LOG_INFO(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_DEBUG(x, ...) P_LOG_DEBUG(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#else +#define ST_PREFIX " " +#define D_LOG_ERROR(...) P_LOG_ERROR(ST_PREFIX __VA_ARGS__) +#define D_LOG_WARNING(...) P_LOG_WARN(ST_PREFIX __VA_ARGS__) +#define D_LOG_INFO(...) P_LOG_INFO(ST_PREFIX __VA_ARGS__) +#define D_LOG_DEBUG(...) P_LOG_DEBUG(ST_PREFIX __VA_ARGS__) +#endif + +#define ST_I18N "Filter.Spout.AsyncVideo" +#define ST_I18N_NAME ST_I18N ".Name" +#define ST_KEY_NAME "Name" + +using namespace streamfx::filter::spout; + +asyncvideo_instance::asyncvideo_instance(obs_data_t* data, obs_source_t* context) : obs::source_instance(data, context) {} + +asyncvideo_instance::~asyncvideo_instance() {} + +void asyncvideo_instance::load(obs_data_t* settings) {} + +void asyncvideo_instance::migrate(obs_data_t* settings, uint64_t version) {} + +void asyncvideo_instance::update(obs_data_t* settings) {} + +struct obs_audio_data* asyncvideo_instance::filter_audio(struct obs_audio_data* audio) { + return audio; +} + +struct obs_source_frame* asyncvideo_instance::filter_video(struct obs_source_frame* frame) { + return frame; +} + +asyncvideo_factory::asyncvideo_factory() +{ + _info.id = S_PREFIX "filter-spout-asyncvideo"; + _info.type = OBS_SOURCE_TYPE_FILTER; + _info.output_flags = OBS_SOURCE_ASYNC_VIDEO; + + support_size(false); + finish_setup(); +} + +asyncvideo_factory::~asyncvideo_factory() {} + +const char* asyncvideo_factory::get_name() +{ + return D_TRANSLATE(ST_I18N); +} + +void asyncvideo_factory::get_defaults2(obs_data_t* settings) +{ + obs_data_set_default_string(settings, ST_KEY_NAME, "${SOURCE}/${FILTER}"); +} + +obs_properties_t* asyncvideo_factory::get_properties2(asyncvideo_instance* data) +{ + obs_properties_t* pr = obs_properties_create(); + + { + auto p = obs_properties_add_text(pr, ST_KEY_NAME, D_TRANSLATE(ST_I18N_NAME), obs_text_type::OBS_TEXT_DEFAULT); + } + + return pr; +} + +std::shared_ptr asyncvideo_factory::instance() +{ + static std::weak_ptr winst; + static std::mutex mtx; + + std::unique_lock lock(mtx); + auto instance = winst.lock(); + if (!instance) { + instance = std::shared_ptr(new asyncvideo_factory()); + winst = instance; + } + return instance; +} + +static std::shared_ptr loader_instance; + +static auto loader = streamfx::component( + "spoutsink::asyncvideo", + []() { // Initializer + loader_instance = asyncvideo_factory::instance(); + }, + []() { // Finalizer + loader_instance.reset(); + }, + {}); diff --git a/components/spoutsink/source/filters/filter-spout-asyncvideo.hpp b/components/spoutsink/source/filters/filter-spout-asyncvideo.hpp new file mode 100644 index 0000000..b2f4c06 --- /dev/null +++ b/components/spoutsink/source/filters/filter-spout-asyncvideo.hpp @@ -0,0 +1,42 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "common.hpp" +#include "obs/obs-source-factory.hpp" + +#include "warning-disable.hpp" +#include +#include "warning-enable.hpp" + +namespace streamfx::filter::spout { + class asyncvideo_instance : public obs::source_instance { + public: + asyncvideo_instance(obs_data_t*, obs_source_t*); + ~asyncvideo_instance() override; + + void load(obs_data_t* settings) override; + void migrate(obs_data_t* data, uint64_t version) override; + void update(obs_data_t*) override; + + struct obs_audio_data* filter_audio(struct obs_audio_data* audio) override; + + struct obs_source_frame* filter_video(struct obs_source_frame* frame) override; + }; + + class asyncvideo_factory : public obs::source_factory { + public: + asyncvideo_factory(); + ~asyncvideo_factory() override; + + const char* get_name() override; + + void get_defaults2(obs_data_t* data) override; + + obs_properties_t* get_properties2(filter::spout::asyncvideo_instance* data) override; + + public: // Singleton + static std::shared_ptr instance(); + }; +} // namespace streamfx::filter::spout diff --git a/components/spoutsink/source/filters/filter-spout-audio.cpp b/components/spoutsink/source/filters/filter-spout-audio.cpp new file mode 100644 index 0000000..da59635 --- /dev/null +++ b/components/spoutsink/source/filters/filter-spout-audio.cpp @@ -0,0 +1,111 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#include "filter-spout-audio.hpp" +#include "strings.hpp" +#include "plugin.hpp" +#include "util/util-logging.hpp" + +#include "warning-disable.hpp" +#include +#include +#include "warning-enable.hpp" + +// OBS +#include "warning-disable.hpp" +#include "warning-enable.hpp" + +#ifdef _DEBUG +#define ST_PREFIX "<%s> " +#define D_LOG_ERROR(x, ...) P_LOG_ERROR(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_WARNING(x, ...) P_LOG_WARN(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_INFO(x, ...) P_LOG_INFO(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_DEBUG(x, ...) P_LOG_DEBUG(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#else +#define ST_PREFIX " " +#define D_LOG_ERROR(...) P_LOG_ERROR(ST_PREFIX __VA_ARGS__) +#define D_LOG_WARNING(...) P_LOG_WARN(ST_PREFIX __VA_ARGS__) +#define D_LOG_INFO(...) P_LOG_INFO(ST_PREFIX __VA_ARGS__) +#define D_LOG_DEBUG(...) P_LOG_DEBUG(ST_PREFIX __VA_ARGS__) +#endif + +#define ST_I18N "Filter.Spout.Audio" +#define ST_I18N_NAME ST_I18N ".Name" +#define ST_KEY_NAME "Name" + +using namespace streamfx::filter::spout; + +audio_instance::audio_instance(obs_data_t* data, obs_source_t* context) : obs::source_instance(data, context) {} + +audio_instance::~audio_instance() {} + +void audio_instance::load(obs_data_t* settings) {} + +void audio_instance::migrate(obs_data_t* settings, uint64_t version) {} + +void audio_instance::update(obs_data_t* settings) {} + +struct obs_audio_data* audio_instance::filter_audio(struct obs_audio_data* audio) +{ + return audio; +} + +audio_factory::audio_factory() +{ + _info.id = S_PREFIX "filter-spout-audio"; + _info.type = OBS_SOURCE_TYPE_FILTER; + _info.output_flags = OBS_SOURCE_ASYNC_VIDEO; + + support_size(false); + finish_setup(); +} + +audio_factory::~audio_factory() {} + +const char* audio_factory::get_name() +{ + return D_TRANSLATE(ST_I18N); +} + +void audio_factory::get_defaults2(obs_data_t* settings) +{ + obs_data_set_default_string(settings, ST_KEY_NAME, "${SOURCE}/${FILTER}"); +} + +obs_properties_t* audio_factory::get_properties2(audio_instance* data) +{ + obs_properties_t* pr = obs_properties_create(); + + { + auto p = obs_properties_add_text(pr, ST_KEY_NAME, D_TRANSLATE(ST_I18N_NAME), obs_text_type::OBS_TEXT_DEFAULT); + } + + return pr; +} + +std::shared_ptr audio_factory::instance() +{ + static std::weak_ptr winst; + static std::mutex mtx; + + std::unique_lock lock(mtx); + auto instance = winst.lock(); + if (!instance) { + instance = std::shared_ptr(new audio_factory()); + winst = instance; + } + return instance; +} + +static std::shared_ptr loader_instance; + +static auto loader = streamfx::component( + "spoutsink::audio", + []() { // Initializer + loader_instance = audio_factory::instance(); + }, + []() { // Finalizer + loader_instance.reset(); + }, + {}); diff --git a/components/spoutsink/source/filters/filter-spout-audio.hpp b/components/spoutsink/source/filters/filter-spout-audio.hpp new file mode 100644 index 0000000..6c3fd89 --- /dev/null +++ b/components/spoutsink/source/filters/filter-spout-audio.hpp @@ -0,0 +1,40 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "common.hpp" +#include "obs/obs-source-factory.hpp" + +#include "warning-disable.hpp" +#include +#include "warning-enable.hpp" + +namespace streamfx::filter::spout { + class audio_instance : public obs::source_instance { + public: + audio_instance(obs_data_t*, obs_source_t*); + virtual ~audio_instance() override; + + virtual void load(obs_data_t* settings) override; + virtual void migrate(obs_data_t* data, uint64_t version) override; + virtual void update(obs_data_t*) override; + + struct obs_audio_data* filter_audio(struct obs_audio_data* audio) override; + }; + + class audio_factory : public obs::source_factory { + public: + audio_factory(); + virtual ~audio_factory() override; + + virtual const char* get_name() override; + + virtual void get_defaults2(obs_data_t* data) override; + + virtual obs_properties_t* get_properties2(filter::spout::audio_instance* data) override; + + public: // Singleton + static std::shared_ptr instance(); + }; +} // namespace streamfx::filter::spout diff --git a/components/spoutsink/source/filters/filter-spout-video.cpp b/components/spoutsink/source/filters/filter-spout-video.cpp new file mode 100644 index 0000000..15fbf46 --- /dev/null +++ b/components/spoutsink/source/filters/filter-spout-video.cpp @@ -0,0 +1,150 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#include "filter-spout-video.hpp" +#include "strings.hpp" +#include "plugin.hpp" +#include "util/util-logging.hpp" + +#include "warning-disable.hpp" +#include +#include +#include "warning-enable.hpp" + +// OBS +#include "warning-disable.hpp" +#include "warning-enable.hpp" + +#ifdef _DEBUG +#define ST_PREFIX "<%s> " +#define D_LOG_ERROR(x, ...) P_LOG_ERROR(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_WARNING(x, ...) P_LOG_WARN(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_INFO(x, ...) P_LOG_INFO(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_DEBUG(x, ...) P_LOG_DEBUG(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#else +#define ST_PREFIX " " +#define D_LOG_ERROR(...) P_LOG_ERROR(ST_PREFIX __VA_ARGS__) +#define D_LOG_WARNING(...) P_LOG_WARN(ST_PREFIX __VA_ARGS__) +#define D_LOG_INFO(...) P_LOG_INFO(ST_PREFIX __VA_ARGS__) +#define D_LOG_DEBUG(...) P_LOG_DEBUG(ST_PREFIX __VA_ARGS__) +#endif + +#define ST_I18N "Filter.Spout.Video" +#define ST_I18N_NAME ST_I18N ".Name" +#define ST_KEY_NAME "Name" + +using namespace streamfx::filter::spout; + +video_instance::video_instance(obs_data_t* data, obs_source_t* context) : obs::source_instance(data, context) {} + +video_instance::~video_instance() {} + +void video_instance::load(obs_data_t* settings) {} + +void video_instance::migrate(obs_data_t* settings, uint64_t version) {} + +void video_instance::update(obs_data_t* settings) {} + +struct obs_audio_data* video_instance::filter_audio(struct obs_audio_data* audio) +{ + return audio; +} + +void video_instance::video_tick(float) +{ + // What information do we need to provide to downstream consumers? + // - Width, Height (Resolution) + // - Color Space + // - Color Format + + auto parent = _self.get_filter_parent(); + auto target = _self.get_filter_target(); + + // Retrieve resolution. + _resolution = {target.width(), target.height()}; + + // Retrieve both color format and space. + _color_space = GS_CS_SRGB; + _color_space = target.color_space(1, &_color_space); + switch (_color_space) { + case GS_CS_SRGB: + _color_format = GS_RGBA; + break; + default: + _color_format = GS_RGBA16F; + break; + } + + + +} + +void video_instance::video_render(gs_effect_t* effect) +{ + auto parent = _self.get_filter_parent(); + auto target = _self.get_filter_target(); + + //_self.process_filter_begin(_color_format, OBS_ALLOW_DIRECT_RENDERING); + + obs_source_skip_video_filter(_self); +} + +video_factory::video_factory() +{ + _info.id = S_PREFIX "filter-spout-video"; + _info.type = OBS_SOURCE_TYPE_FILTER; + _info.output_flags = OBS_SOURCE_VIDEO; + + support_size(false); + finish_setup(); +} + +video_factory::~video_factory() {} + +const char* video_factory::get_name() +{ + return D_TRANSLATE(ST_I18N); +} + +void video_factory::get_defaults2(obs_data_t* settings) +{ + obs_data_set_default_string(settings, ST_KEY_NAME, "${SOURCE}/${FILTER}"); +} + +obs_properties_t* video_factory::get_properties2(video_instance* data) +{ + obs_properties_t* pr = obs_properties_create(); + + { + auto p = obs_properties_add_text(pr, ST_KEY_NAME, D_TRANSLATE(ST_I18N_NAME), obs_text_type::OBS_TEXT_DEFAULT); + } + + return pr; +} + +std::shared_ptr video_factory::instance() +{ + static std::weak_ptr winst; + static std::mutex mtx; + + std::unique_lock lock(mtx); + auto instance = winst.lock(); + if (!instance) { + instance = std::shared_ptr(new video_factory()); + winst = instance; + } + return instance; +} + +static std::shared_ptr loader_instance; + +static auto loader = streamfx::component( + "spoutsink::video", + []() { // Initializer + loader_instance = video_factory::instance(); + }, + []() { // Finalizer + loader_instance.reset(); + }, + {}); diff --git a/components/spoutsink/source/filters/filter-spout-video.hpp b/components/spoutsink/source/filters/filter-spout-video.hpp new file mode 100644 index 0000000..114603d --- /dev/null +++ b/components/spoutsink/source/filters/filter-spout-video.hpp @@ -0,0 +1,48 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "common.hpp" +#include "obs/obs-source-factory.hpp" + +#include "warning-disable.hpp" +#include +#include +#include "warning-enable.hpp" + +namespace streamfx::filter::spout { + class video_instance : public obs::source_instance { + std::pair _resolution; + gs_color_space _color_space; + gs_color_format _color_format; + + public: + video_instance(obs_data_t*, obs_source_t*); + ~video_instance() override; + + void load(obs_data_t* settings) override; + void migrate(obs_data_t* data, uint64_t version) override; + void update(obs_data_t*) override; + + struct obs_audio_data* filter_audio(struct obs_audio_data* audio) override; + + void video_tick(float) override; + void video_render(gs_effect_t*) override; + }; + + class video_factory : public obs::source_factory { + public: + video_factory(); + ~video_factory() override; + + const char* get_name() override; + + void get_defaults2(obs_data_t* data) override; + + obs_properties_t* get_properties2(filter::spout::video_instance* data) override; + + public: // Singleton + static std::shared_ptr instance(); + }; +} // namespace streamfx::filter::spout diff --git a/components/spoutsink/source/sources/source-sink.cpp b/components/spoutsink/source/sources/source-sink.cpp new file mode 100644 index 0000000..00342f4 --- /dev/null +++ b/components/spoutsink/source/sources/source-sink.cpp @@ -0,0 +1,152 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#include "source-sink.hpp" +#include "strings.hpp" +#include +#include +#include +#include +#include +#include +#include +#include "obs/gs/gs-helper.hpp" +#include "obs/obs-source-tracker.hpp" +#include "obs/obs-tools.hpp" +#include "util/util-logging.hpp" + +#ifdef _DEBUG +#define ST_PREFIX "<%s> " +#define D_LOG_ERROR(x, ...) P_LOG_ERROR(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_WARNING(x, ...) P_LOG_WARN(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_INFO(x, ...) P_LOG_INFO(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_DEBUG(x, ...) P_LOG_DEBUG(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#else +#define ST_PREFIX " " +#define D_LOG_ERROR(...) P_LOG_ERROR(ST_PREFIX __VA_ARGS__) +#define D_LOG_WARNING(...) P_LOG_WARN(ST_PREFIX __VA_ARGS__) +#define D_LOG_INFO(...) P_LOG_INFO(ST_PREFIX __VA_ARGS__) +#define D_LOG_DEBUG(...) P_LOG_DEBUG(ST_PREFIX __VA_ARGS__) +#endif + +#define ST_I18N "Source.Sink" +#define ST_I18N_VIDEO ST_I18N ".Video" +#define ST_KEY_VIDEO "Video" +#define ST_I18N_VIDEO_SOURCE ST_I18N_VIDEO ".Source" +#define ST_KEY_VIDEO_SOURCE ST_KEY_VIDEO ".Source" +#define ST_I18N_AUDIO ST_I18N ".Audio" +#define ST_KEY_AUDIO "Audio" +#define ST_I18N_AUDIO_SOURCE ST_I18N_AUDIO ".Source" +#define ST_KEY_AUDIO_SOURCE ST_KEY_AUDIO ".Source" + +using namespace streamfx::source::sink; + +sink_instance::sink_instance(obs_data_t* settings, obs_source_t* self) : obs::source_instance(settings, self) +{ + update(settings); +} + +sink_instance::~sink_instance() +{ +} + +uint32_t sink_instance::get_width() +{ + return 1; +} + +uint32_t sink_instance::get_height() +{ + return 1; +} + +void sink_instance::load(obs_data_t* data) +{ + update(data); +} + +void sink_instance::migrate(obs_data_t* data, uint64_t version) {} + +void sink_instance::update(obs_data_t* data) {} + +void sink_instance::save(obs_data_t* data) {} + +void sink_instance::video_tick(float_t time) {} + +void sink_instance::video_render(gs_effect_t* effect) {} + +void sink_instance::enum_active_sources(obs_source_enum_proc_t cb, void* ptr) {} + +void sink_instance::enum_all_sources(obs_source_enum_proc_t cb, void* ptr) {} + +sink_factory::sink_factory() +{ + _info.id = S_PREFIX "source-sink"; + _info.type = OBS_SOURCE_TYPE_INPUT; + _info.output_flags = OBS_SOURCE_VIDEO | OBS_SOURCE_ASYNC | OBS_SOURCE_AUDIO; + + finish_setup(); +} + +sink_factory::~sink_factory() {} + +const char* sink_factory::get_name() +{ + return D_TRANSLATE(ST_I18N); +} + +void sink_factory::get_defaults2(obs_data_t* data) +{ +} + +obs_properties_t* sink_factory::get_properties2(sink_instance* data) +{ + obs_properties_t* pr = obs_properties_create(); + + { + auto grp = obs_properties_create(); + auto pgrp = obs_properties_add_group(pr, ST_KEY_VIDEO, D_TRANSLATE(ST_I18N_VIDEO), OBS_GROUP_CHECKABLE, grp); + + { + auto p = obs_properties_add_list(grp, ST_KEY_VIDEO_SOURCE, D_TRANSLATE(ST_KEY_VIDEO_SOURCE), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + } + } + + { + auto grp = obs_properties_create(); + auto pgrp = obs_properties_add_group(pr, ST_KEY_AUDIO, D_TRANSLATE(ST_I18N_AUDIO), OBS_GROUP_CHECKABLE, grp); + + { + auto p = obs_properties_add_list(grp, ST_KEY_AUDIO_SOURCE, D_TRANSLATE(ST_KEY_AUDIO_SOURCE), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + } + } + + return pr; +} + +std::shared_ptr sink_factory::instance() +{ + static std::weak_ptr winst; + static std::mutex mtx; + + std::unique_lock lock(mtx); + auto instance = winst.lock(); + if (!instance) { + instance = std::shared_ptr(new sink_factory()); + winst = instance; + } + return instance; +} + +static std::shared_ptr loader_instance; + +static auto loader = streamfx::component( + "spoutsink::sink", + []() { // Initializer + loader_instance = sink_factory::instance(); + }, + []() { // Finalizer + loader_instance.reset(); + }, + {}); diff --git a/components/spoutsink/source/sources/source-sink.hpp b/components/spoutsink/source/sources/source-sink.hpp new file mode 100644 index 0000000..c44e8f2 --- /dev/null +++ b/components/spoutsink/source/sources/source-sink.hpp @@ -0,0 +1,59 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "common.hpp" +#include "gfx/gfx-source-texture.hpp" +#include "obs/gs/gs-texrender.hpp" +#include "obs/gs/gs-sampler.hpp" +#include "obs/obs-signal-handler.hpp" +#include "obs/obs-source-active-child.hpp" +#include "obs/obs-source-factory.hpp" +#include "obs/obs-source.hpp" +#include "obs/obs-tools.hpp" + +#include "warning-disable.hpp" +#include +#include +#include +#include +#include +#include "warning-enable.hpp" + +namespace streamfx::source::sink { + class sink_instance : public obs::source_instance { + public: + sink_instance(obs_data_t* settings, obs_source_t* self); + virtual ~sink_instance(); + + virtual uint32_t get_width() override; + virtual uint32_t get_height() override; + + virtual void load(obs_data_t*) override; + virtual void migrate(obs_data_t*, uint64_t) override; + virtual void update(obs_data_t*) override; + virtual void save(obs_data_t*) override; + + virtual void video_tick(float_t) override; + virtual void video_render(gs_effect_t*) override; + + virtual void enum_active_sources(obs_source_enum_proc_t, void*) override; + virtual void enum_all_sources(obs_source_enum_proc_t, void*) override; + }; + + class sink_factory : public obs::source_factory { + public: + sink_factory(); + virtual ~sink_factory() override; + + virtual const char* get_name() override; + + virtual void get_defaults2(obs_data_t* data) override; + + virtual obs_properties_t* get_properties2(source::sink::sink_instance* data) override; + + public: // Singleton + static std::shared_ptr instance(); + }; +} // namespace streamfx::source::sink diff --git a/components/transform/CMakeLists.txt b/components/transform/CMakeLists.txt new file mode 100644 index 0000000..ac1f619 --- /dev/null +++ b/components/transform/CMakeLists.txt @@ -0,0 +1,9 @@ +# AUTOGENERATED COPYRIGHT HEADER START +# Copyright (C) 2023 Michael Fabian 'Xaymar' Dirks +# AUTOGENERATED COPYRIGHT HEADER END + +cmake_minimum_required(VERSION 3.26) +project("Transform") +list(APPEND CMAKE_MESSAGE_INDENT "[${PROJECT_NAME}] ") + +streamfx_add_component("Transform") diff --git a/components/transform/source/filter/filter-transform.cpp b/components/transform/source/filter/filter-transform.cpp new file mode 100644 index 0000000..d9beabd --- /dev/null +++ b/components/transform/source/filter/filter-transform.cpp @@ -0,0 +1,802 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2017-2024 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2022 lainon +// AUTOGENERATED COPYRIGHT HEADER END + +#include "filter-transform.hpp" +#include "strings.hpp" +#include "obs/gs/gs-helper.hpp" +#include "util/util-logging.hpp" + +#include "warning-disable.hpp" +#include +#include +#include "warning-enable.hpp" + +// OBS +#include "warning-disable.hpp" +#include +#include +#include +#include "warning-enable.hpp" + +#ifdef _DEBUG +#define ST_PREFIX "<%s> " +#define D_LOG_ERROR(x, ...) P_LOG_ERROR(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_WARNING(x, ...) P_LOG_WARN(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_INFO(x, ...) P_LOG_INFO(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_DEBUG(x, ...) P_LOG_DEBUG(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#else +#define ST_PREFIX " " +#define D_LOG_ERROR(...) P_LOG_ERROR(ST_PREFIX __VA_ARGS__) +#define D_LOG_WARNING(...) P_LOG_WARN(ST_PREFIX __VA_ARGS__) +#define D_LOG_INFO(...) P_LOG_INFO(ST_PREFIX __VA_ARGS__) +#define D_LOG_DEBUG(...) P_LOG_DEBUG(ST_PREFIX __VA_ARGS__) +#endif + +#define ST_I18N "Filter.Transform" +#define ST_I18N_CAMERA ST_I18N ".Camera" +#define ST_I18N_CAMERA_MODE ST_I18N_CAMERA ".Mode" +#define ST_KEY_CAMERA_MODE "Camera.Mode" +#define ST_I18N_CAMERA_MODE_CORNER_PIN ST_I18N_CAMERA_MODE ".CornerPin" +#define ST_I18N_CAMERA_MODE_ORTHOGRAPHIC ST_I18N_CAMERA_MODE ".Orthographic" +#define ST_I18N_CAMERA_MODE_PERSPECTIVE ST_I18N_CAMERA_MODE ".Perspective" +#define ST_I18N_CAMERA_FIELDOFVIEW ST_I18N_CAMERA ".FieldOfView" +#define ST_KEY_CAMERA_FIELDOFVIEW "Camera.FieldOfView" +#define ST_I18N_POSITION ST_I18N ".Position" +#define ST_KEY_POSITION_X "Position.X" +#define ST_KEY_POSITION_Y "Position.Y" +#define ST_KEY_POSITION_Z "Position.Z" +#define ST_I18N_ROTATION ST_I18N ".Rotation" +#define ST_KEY_ROTATION_X "Rotation.X" +#define ST_KEY_ROTATION_Y "Rotation.Y" +#define ST_KEY_ROTATION_Z "Rotation.Z" +#define ST_I18N_SCALE ST_I18N ".Scale" +#define ST_KEY_SCALE_X "Scale.X" +#define ST_KEY_SCALE_Y "Scale.Y" +#define ST_I18N_SHEAR ST_I18N ".Shear" +#define ST_KEY_SHEAR_X "Shear.X" +#define ST_KEY_SHEAR_Y "Shear.Y" +#define ST_I18N_ROTATION_ORDER ST_I18N ".Rotation.Order" +#define ST_KEY_ROTATION_ORDER "Rotation.Order" +#define ST_I18N_ROTATION_ORDER_XYZ ST_I18N_ROTATION_ORDER ".XYZ" +#define ST_I18N_ROTATION_ORDER_XZY ST_I18N_ROTATION_ORDER ".XZY" +#define ST_I18N_ROTATION_ORDER_YXZ ST_I18N_ROTATION_ORDER ".YXZ" +#define ST_I18N_ROTATION_ORDER_YZX ST_I18N_ROTATION_ORDER ".YZX" +#define ST_I18N_ROTATION_ORDER_ZXY ST_I18N_ROTATION_ORDER ".ZXY" +#define ST_I18N_ROTATION_ORDER_ZYX ST_I18N_ROTATION_ORDER ".ZYX" +#define ST_I18N_CORNERS ST_I18N ".Corners" +#define ST_I18N_CORNERS_TOPLEFT ST_I18N_CORNERS ".TopLeft" +#define ST_KEY_CORNERS_TOPLEFT "Corners.TopLeft." +#define ST_I18N_CORNERS_TOPRIGHT ST_I18N_CORNERS ".TopRight" +#define ST_KEY_CORNERS_TOPRIGHT "Corners.TopRight." +#define ST_I18N_CORNERS_BOTTOMLEFT ST_I18N_CORNERS ".BottomLeft" +#define ST_KEY_CORNERS_BOTTOMLEFT "Corners.BottomLeft." +#define ST_I18N_CORNERS_BOTTOMRIGHT ST_I18N_CORNERS ".BottomRight" +#define ST_KEY_CORNERS_BOTTOMRIGHT "Corners.BottomRight." +#define ST_I18N_MIPMAPPING ST_I18N ".Mipmapping" +#define ST_KEY_MIPMAPPING "Mipmapping" + +using namespace streamfx::filter::transform; + +static constexpr std::string_view HELP_URL = "https://github.com/Xaymar/obs-StreamFX/wiki/Filter-3D-Transform"; + +static const float farZ = 2097152.0f; // 2 pow 21 +static const float nearZ = 1.0f / farZ; + +enum RotationOrder : int64_t { + XYZ = 0, + XZY = 1, + YXZ = 2, + YZX = 3, + ZXY = 4, + ZYX = 5, +}; + +transform_instance::transform_instance(obs_data_t* data, obs_source_t* context) : obs::source_instance(data, context), _gfx_util(::streamfx::gfx::util::get()), _standard_effect(), _transform_effect(), _source_size(), _mip_levels(), _mesh(), _sampler(), _mipmapper(), _final_rt(), _final_tex(), _mesh_dirty(true), _is_rendered(false), _cache_size(), _camera_mode(), _camera_fov(), _params(), _corners(), _mipmap_enabled() +{ + { + auto gctx = obs::gs::context(); + + _final_rt = std::make_shared(GS_RGBA, GS_ZS_NONE); + _mesh = std::make_shared(uint32_t(4u), uint8_t(1u)); + { + auto file = streamfx::data_file_path("effects/standard.effect"); + try { + _standard_effect = streamfx::obs::gs::effect::create(file); + } catch (const std::exception& ex) { + DLOG_ERROR("Error loading '%s': %s", file.generic_u8string().c_str(), ex.what()); + } + } + { + auto file = streamfx::data_file_path("effects/transform.effect"); + try { + _transform_effect = streamfx::obs::gs::effect::create(file); + } catch (const std::exception& ex) { + DLOG_ERROR("Error loading '%s': %s", file.generic_u8string().c_str(), ex.what()); + } + } + { + gs_sampler_info si; + si.address_u = si.address_v = si.address_w = GS_ADDRESS_CLAMP; + si.filter = GS_FILTER_ANISOTROPIC; + si.max_anisotropy = 16; + _sampler = streamfx::obs::gs::sampler::pool::instance()->acquire(si); + } + + vec3_set(&_params.position, 0, 0, 0); + vec3_set(&_params.rotation, 0, 0, 0); + vec3_set(&_params.scale, 1, 1, 1); + vec3_set(&_params.shear, 0, 0, 0); + + vec2_set(&_corners.tl, 0, 0); + vec2_set(&_corners.tr, 1, 0); + vec2_set(&_corners.bl, 0, 1); + vec2_set(&_corners.br, 1, 1); + } + + update(data); +} + +transform_instance::~transform_instance() {} + +void transform_instance::load(obs_data_t* settings) +{ + update(settings); +} + +void transform_instance::migrate(obs_data_t* settings, uint64_t version) +{ + // Only test for A.B.C in A.B.C.D + version = version & STREAMFX_MASK_UPDATE; + +#define COPY_UNSET(TYPE, NAME, OLDNAME) \ + if (obs_data_has_user_value(settings, OLDNAME)) { \ + obs_data_set_##TYPE(settings, NAME, obs_data_get_##TYPE(settings, OLDNAME)); \ + obs_data_unset_user_value(settings, OLDNAME); \ + } +#define COPY_IGNORE(TYPE, NAME, OLDNAME) \ + if (obs_data_has_user_value(settings, OLDNAME)) { \ + obs_data_set_##TYPE(settings, NAME, obs_data_get_##TYPE(settings, OLDNAME)); \ + } +#define SET_IF_UNSET(TYPE, NAME, value) \ + if (!obs_data_has_user_value(settings, NAME)) { \ + obs_data_set_##TYPE(settings, NAME, value); \ + } + + if (version < STREAMFX_MAKE_VERSION(0, 8, 0, 0)) { + if (obs_data_has_user_value(settings, "Filter.Transform.Position.X")) { + obs_data_set_double(settings, "Filter.Transform.Position.X", -obs_data_get_double(settings, "Filter.Transform.Position.X")); + } + if (obs_data_has_user_value(settings, "Filter.Transform.Position.Y")) { + obs_data_set_double(settings, "Filter.Transform.Position.Y", -obs_data_get_double(settings, "Filter.Transform.Position.Y")); + } + } + + if (version < STREAMFX_MAKE_VERSION(0, 11, 0, 0)) { + COPY_UNSET(int, ST_KEY_CAMERA_MODE, "Filter.Transform.Camera"); + COPY_UNSET(double, ST_KEY_CAMERA_FIELDOFVIEW, "Filter.Transform.Camera.FieldOfView"); + COPY_UNSET(double, ST_KEY_POSITION_X, "Filter.Transform.Position.X"); + COPY_UNSET(double, ST_KEY_POSITION_Y, "Filter.Transform.Position.Y"); + COPY_UNSET(double, ST_KEY_POSITION_Z, "Filter.Transform.Position.Z"); + COPY_UNSET(double, ST_KEY_ROTATION_X, "Filter.Transform.Rotation.X"); + COPY_UNSET(double, ST_KEY_ROTATION_Y, "Filter.Transform.Rotation.Y"); + COPY_UNSET(double, ST_KEY_ROTATION_Z, "Filter.Transform.Rotation.Z"); + COPY_UNSET(double, ST_KEY_SCALE_X, "Filter.Transform.Scale.X"); + COPY_UNSET(double, ST_KEY_SCALE_Y, "Filter.Transform.Scale.Y"); + COPY_UNSET(double, ST_KEY_SHEAR_X, "Filter.Transform.Shear.X"); + COPY_UNSET(double, ST_KEY_SHEAR_Y, "Filter.Transform.Shear.Y"); + COPY_UNSET(double, ST_KEY_ROTATION_ORDER, "Filter.Transform.Rotation.Order"); + COPY_UNSET(double, ST_KEY_MIPMAPPING, "Filter.Transform.Mipmapping"); + + if (!obs_data_has_user_value(settings, ST_KEY_CAMERA_MODE)) { + SET_IF_UNSET(int, ST_KEY_CAMERA_MODE, static_cast(transform_mode::ORTHOGRAPHIC)); + } + } + +#undef SET_IF_UNSET +#undef COPY_IGNORE +#undef COPY_UNSET +} + +void transform_instance::update(obs_data_t* settings) +{ + // Camera + _camera_mode = static_cast(obs_data_get_int(settings, ST_KEY_CAMERA_MODE)); + _camera_fov = static_cast(obs_data_get_double(settings, ST_KEY_CAMERA_FIELDOFVIEW)); + + { // Parametrized Mesh + _params.position.x = static_cast(obs_data_get_double(settings, ST_KEY_POSITION_X) / 100.0); + _params.position.y = static_cast(obs_data_get_double(settings, ST_KEY_POSITION_Y) / 100.0); + _params.position.z = static_cast(obs_data_get_double(settings, ST_KEY_POSITION_Z) / 100.0); + _params.scale.x = static_cast(obs_data_get_double(settings, ST_KEY_SCALE_X) / 100.0); + _params.scale.y = static_cast(obs_data_get_double(settings, ST_KEY_SCALE_Y) / 100.0); + _params.scale.z = 1.0f; + _params.rotation_order = static_cast(obs_data_get_int(settings, ST_KEY_ROTATION_ORDER)); + _params.rotation.x = static_cast(obs_data_get_double(settings, ST_KEY_ROTATION_X) / 180.0 * S_PI); + _params.rotation.y = static_cast(obs_data_get_double(settings, ST_KEY_ROTATION_Y) / 180.0 * S_PI); + _params.rotation.z = static_cast(obs_data_get_double(settings, ST_KEY_ROTATION_Z) / 180.0 * S_PI); + _params.shear.x = static_cast(obs_data_get_double(settings, ST_KEY_SHEAR_X) / 100.0); + _params.shear.y = static_cast(obs_data_get_double(settings, ST_KEY_SHEAR_Y) / 100.0); + _params.shear.z = 0.0f; + } + { // Corners + std::pair opts[] = { + {ST_KEY_CORNERS_TOPLEFT "X", _corners.tl.x}, {ST_KEY_CORNERS_TOPLEFT "Y", _corners.tl.y}, {ST_KEY_CORNERS_TOPRIGHT "X", _corners.tr.x}, {ST_KEY_CORNERS_TOPRIGHT "Y", _corners.tr.y}, {ST_KEY_CORNERS_BOTTOMLEFT "X", _corners.bl.x}, {ST_KEY_CORNERS_BOTTOMLEFT "Y", _corners.bl.y}, {ST_KEY_CORNERS_BOTTOMRIGHT "X", _corners.br.x}, {ST_KEY_CORNERS_BOTTOMRIGHT "Y", _corners.br.y}, + }; + for (auto opt : opts) { + opt.second = static_cast(obs_data_get_double(settings, opt.first.c_str()) / 100.0); + } + } + + _mesh_dirty = true; +} + +void transform_instance::video_tick(float) +{ + // Retrieve the source's current size. + std::pair source_size; + if (auto target = _self.get_filter_target(); target) { + source_size = {target.width(), target.height()}; + } + + // If the source changed size, update a few things. + if (_mesh_dirty || (_source_size.first != source_size.first) || (_source_size.second != source_size.second)) { + // Update the stored size. + _source_size = source_size; + + // Update the cache size. + if (_mipmap_enabled) { + double_t aspect = double_t(_source_size.first) / double_t(_source_size.second); + double_t aspect2 = 1.0 / aspect; + _cache_size.first = std::clamp(uint32_t(pow(2, streamfx::util::math::get_power_of_two_exponent_ceil(_cache_size.first))), 1u, 8192u); + _cache_size.second = std::clamp(uint32_t(pow(2, streamfx::util::math::get_power_of_two_exponent_ceil(_cache_size.second))), 1u, 8192u); + + if (aspect > 1.0) { + _cache_size.second = std::clamp(streamfx::util::math::pow(2, streamfx::util::math::get_power_of_two_exponent_ceil(static_cast(_cache_size.first * aspect2))), 1u, 8192u); + } else if (aspect < 1.0) { + _cache_size.first = std::clamp(uint32_t(pow(2, streamfx::util::math::get_power_of_two_exponent_ceil(uint64_t(_cache_size.second * aspect)))), 1u, 8192u); + } // This transparently handles aspect == 1.0, since we already did the math for that above. + + // Update the number of mip levels. + _mip_levels = _mipmapper.calculate_max_mip_level(_cache_size.first, _cache_size.second); + } else { + _cache_size = source_size; + } + + // Mark mesh as dirty if not using corner pin. + if (_camera_mode != transform_mode::CORNER_PIN) { + _mesh_dirty = true; + } + } + + // Update the mesh. + if (_mesh_dirty && (_camera_mode != transform_mode::CORNER_PIN)) { + // Calculate Aspect Ratio + float aspect_ratio_x = float(_source_size.first) / float(_source_size.second); + if (_camera_mode == transform_mode::ORTHOGRAPHIC) + aspect_ratio_x = 1.0; + + // Mesh + matrix4 ident; + matrix4_identity(&ident); + switch (_params.rotation_order) { + case RotationOrder::XYZ: // XYZ + matrix4_rotate_aa4f(&ident, &ident, 1, 0, 0, _params.rotation.x); + matrix4_rotate_aa4f(&ident, &ident, 0, 1, 0, _params.rotation.y); + matrix4_rotate_aa4f(&ident, &ident, 0, 0, 1, _params.rotation.z); + break; + case RotationOrder::XZY: // XZY + matrix4_rotate_aa4f(&ident, &ident, 1, 0, 0, _params.rotation.x); + matrix4_rotate_aa4f(&ident, &ident, 0, 0, 1, _params.rotation.z); + matrix4_rotate_aa4f(&ident, &ident, 0, 1, 0, _params.rotation.y); + break; + case RotationOrder::YXZ: // YXZ + matrix4_rotate_aa4f(&ident, &ident, 0, 1, 0, _params.rotation.y); + matrix4_rotate_aa4f(&ident, &ident, 1, 0, 0, _params.rotation.x); + matrix4_rotate_aa4f(&ident, &ident, 0, 0, 1, _params.rotation.z); + break; + case RotationOrder::YZX: // YZX + matrix4_rotate_aa4f(&ident, &ident, 0, 1, 0, _params.rotation.y); + matrix4_rotate_aa4f(&ident, &ident, 0, 0, 1, _params.rotation.z); + matrix4_rotate_aa4f(&ident, &ident, 1, 0, 0, _params.rotation.x); + break; + case RotationOrder::ZXY: // ZXY + matrix4_rotate_aa4f(&ident, &ident, 0, 0, 1, _params.rotation.z); + matrix4_rotate_aa4f(&ident, &ident, 1, 0, 0, _params.rotation.x); + matrix4_rotate_aa4f(&ident, &ident, 0, 1, 0, _params.rotation.y); + break; + case RotationOrder::ZYX: // ZYX + matrix4_rotate_aa4f(&ident, &ident, 0, 0, 1, _params.rotation.z); + matrix4_rotate_aa4f(&ident, &ident, 0, 1, 0, _params.rotation.y); + matrix4_rotate_aa4f(&ident, &ident, 1, 0, 0, _params.rotation.x); + break; + } + matrix4_translate3f(&ident, &ident, _params.position.x, _params.position.y, _params.position.z); + //matrix4_scale3f(&ident, &ident, _source_size.first / 2.f, _source_size.second / 2.f, 1.f); + + /// Calculate vertex position once only. + float p_x = aspect_ratio_x * _params.scale.x; + float p_y = 1.0f * _params.scale.y; + + /// Generate mesh + { + auto vtx = _mesh->at(0); + *vtx.color = 0xFFFFFFFF; + vec4_set(vtx.uv[0], 0, 0, 0, 0); + vec3_set(vtx.position, -p_x + _params.shear.x, -p_y - _params.shear.y, 0); + vec3_transform(vtx.position, vtx.position, &ident); + } + { + auto vtx = _mesh->at(1); + *vtx.color = 0xFFFFFFFF; + vec4_set(vtx.uv[0], 1, 0, 0, 0); + vec3_set(vtx.position, p_x + _params.shear.x, -p_y + _params.shear.y, 0); + vec3_transform(vtx.position, vtx.position, &ident); + } + { + auto vtx = _mesh->at(2); + *vtx.color = 0xFFFFFFFF; + vec4_set(vtx.uv[0], 0, 1, 0, 0); + vec3_set(vtx.position, -p_x - _params.shear.x, p_y - _params.shear.y, 0); + vec3_transform(vtx.position, vtx.position, &ident); + } + { + auto vtx = _mesh->at(3); + *vtx.color = 0xFFFFFFFF; + vec4_set(vtx.uv[0], 1, 1, 0, 0); + vec3_set(vtx.position, p_x - _params.shear.x, p_y + _params.shear.y, 0); + vec3_transform(vtx.position, vtx.position, &ident); + } + + _mesh->update(true); + _mesh_dirty = false; + } + + // Flag the frame as dirty. + _is_rendered = false; +} + +void transform_instance::video_render(gs_effect_t* effect) +{ + // Resource acquisition and time + // - Render the original source into a rendertarget, acquiring [Cache]. + // - If Mip-Mapping is enabled: + // - Generate a Mip-Map of [Cache], acquiring [Mip-Map] + // - Release [Cache] + // - Transform [Cache] or [Mip-Map], depending on what's remaining, acquiring [Final] + // - Release [Cache] and [Mip-Map]. + // + // Temporary RTs: + // - [Cache] + // - [Mip-Map] + // + // Permanent RTs: + // - [Final] + + vec4 clear_color = {0, 0, 0, 0}; + gs_effect_t* default_effect = obs_get_base_effect(OBS_EFFECT_DEFAULT); + if (!effect) + effect = default_effect; + + // Test for our own sanity. + if (!(_self.get_filter_target() && _self.get_filter_parent() && effect && _source_size.first && _source_size.second && _cache_size.first && _cache_size.second)) { + // We're clearly insane, so we should not render anything. + obs_source_skip_video_filter(_self); + return; + } + + if (!_is_rendered) { + // Acquire a render target for caching. + auto cache_rt = streamfx::obs::gs::texrender::pool::instance()->acquire(GS_RGBA, GS_ZS_NONE); + { + auto op = cache_rt->render(_cache_size.first, _cache_size.second); + gs_ortho(0, static_cast(_source_size.first), 0, static_cast(_source_size.second), -1., 1.); + gs_clear(GS_CLEAR_COLOR, &clear_color, 0, 0); + if (_self.process_filter_begin(GS_RGBA, OBS_ALLOW_DIRECT_RENDERING)) { + gs_blend_state_push(); + gs_reset_blend_state(); + gs_enable_blending(false); + gs_blend_function_separate(GS_BLEND_ONE, GS_BLEND_ZERO, GS_BLEND_SRCALPHA, GS_BLEND_ZERO); + gs_enable_depth_test(false); + gs_enable_stencil_test(false); + gs_enable_stencil_write(false); + gs_enable_color(true, true, true, true); + gs_set_cull_mode(GS_NEITHER); + + _self.process_filter_end(default_effect, _source_size.first, _source_size.second); + + gs_blend_state_pop(); + } else { // Cleanly exit the render call. + op.reset(); + obs_source_skip_video_filter(_self); + return; + } + } + + // Retrieve the texture from the previous operation. + auto buffer_tex = cache_rt->get_texture(); + if (!buffer_tex) { + obs_source_skip_video_filter(_self); + return; + } + + // If requested, generate a mip-map for later rendering. + if (_mipmap_enabled) { + auto mipmap_tex = streamfx::obs::gs::texture::pool::instance()->acquire(_cache_size.first, _cache_size.second, GS_RGBA, static_cast(_mip_levels), nullptr, streamfx::obs::gs::texture_flags::None); + _mipmapper.rebuild(buffer_tex, mipmap_tex); + buffer_tex = mipmap_tex; + + // 'cache_rt' is now no longer in use, so we can release it early. + cache_rt.reset(); + } + + // Now finally apply the 3D Transform effect. + { + auto op = _final_rt->render(_source_size.first, _source_size.second); + + vec4 clear_color = {0, 0, 0, 0}; + gs_clear(GS_CLEAR_COLOR | GS_CLEAR_DEPTH, &clear_color, 0, 0); + + gs_blend_state_push(); + gs_reset_blend_state(); + gs_enable_blending(false); + gs_blend_function_separate(GS_BLEND_ONE, GS_BLEND_ZERO, GS_BLEND_ONE, GS_BLEND_ZERO); + + gs_enable_depth_test(false); + gs_enable_stencil_test(false); + gs_enable_stencil_write(false); + gs_enable_color(true, true, true, true); + gs_set_cull_mode(GS_NEITHER); + + switch (_camera_mode) { + case transform_mode::ORTHOGRAPHIC: + gs_ortho(-1., 1., -1., 1., -farZ, farZ); + break; + case transform_mode::PERSPECTIVE: + gs_perspective(_camera_fov, float(_source_size.first) / float(_source_size.second), nearZ, farZ); + gs_matrix_scale3f(1.0, 1.0, 1.0); + gs_matrix_translate3f(0., 0., -1.0); + break; + case transform_mode::CORNER_PIN: + gs_ortho(0., 1., 0., 1., -farZ, farZ); + break; + } + + if (_camera_mode != transform_mode::CORNER_PIN) { + gs_load_vertexbuffer(_mesh->update(false)); + gs_load_indexbuffer(nullptr); + if (auto v = _standard_effect.get_parameter("InputA"); v.get_type() == ::streamfx::obs::gs::effect_parameter::type::Texture) { + v.set_texture(buffer_tex); + v.set_sampler(_sampler->update()); + } + while (gs_effect_loop(_standard_effect.get_object(), "Draw")) { + gs_draw(GS_TRISTRIP, 0, _mesh->size()); + } + gs_load_vertexbuffer(nullptr); + } else { + gs_load_vertexbuffer(nullptr); + gs_load_indexbuffer(nullptr); + if (auto v = _transform_effect.get_parameter("InputA"); v.get_type() == ::streamfx::obs::gs::effect_parameter::type::Texture) { + v.set_texture(buffer_tex); + v.set_sampler(_sampler->update()); + } + if (auto v = _transform_effect.get_parameter("CornerTL"); v.get_type() == ::streamfx::obs::gs::effect_parameter::type::Float2) { + v.set_float2(_corners.tl); + } + if (auto v = _transform_effect.get_parameter("CornerTR"); v.get_type() == ::streamfx::obs::gs::effect_parameter::type::Float2) { + v.set_float2(_corners.tr); + } + if (auto v = _transform_effect.get_parameter("CornerBL"); v.get_type() == ::streamfx::obs::gs::effect_parameter::type::Float2) { + v.set_float2(_corners.bl); + } + if (auto v = _transform_effect.get_parameter("CornerBR"); v.get_type() == ::streamfx::obs::gs::effect_parameter::type::Float2) { + v.set_float2(_corners.br); + } + while (gs_effect_loop(_transform_effect.get_object(), "CornerPin")) { + _gfx_util->draw_fullscreen_triangle(); + } + } + + gs_blend_state_pop(); + } + _final_tex = _final_rt->get_texture(); + if (!_final_tex) { + obs_source_skip_video_filter(_self); + return; + } + + _is_rendered = true; + } + + { + gs_effect_set_texture(gs_effect_get_param_by_name(effect, "image"), *_final_tex); + while (gs_effect_loop(effect, "Draw")) { + gs_draw_sprite(nullptr, 0, _source_size.first, _source_size.second); + } + } +} + +transform_factory::transform_factory() +{ + _info.id = S_PREFIX "filter-transform"; + _info.type = OBS_SOURCE_TYPE_FILTER; + _info.output_flags = OBS_SOURCE_VIDEO; + + support_size(false); + finish_setup(); + register_proxy("obs-stream-effects-filter-transform"); +} + +transform_factory::~transform_factory() {} + +const char* transform_factory::get_name() +{ + return D_TRANSLATE(ST_I18N); +} + +void transform_factory::get_defaults2(obs_data_t* settings) +{ + obs_data_set_default_int(settings, ST_KEY_CAMERA_MODE, static_cast(transform_mode::CORNER_PIN)); + obs_data_set_default_double(settings, ST_KEY_CAMERA_FIELDOFVIEW, 90.0); + obs_data_set_default_double(settings, ST_KEY_POSITION_X, 0); + obs_data_set_default_double(settings, ST_KEY_POSITION_Y, 0); + obs_data_set_default_double(settings, ST_KEY_POSITION_Z, 0); + obs_data_set_default_double(settings, ST_KEY_ROTATION_X, 0); + obs_data_set_default_double(settings, ST_KEY_ROTATION_Y, 0); + obs_data_set_default_double(settings, ST_KEY_ROTATION_Z, 0); + obs_data_set_default_int(settings, ST_KEY_ROTATION_ORDER, static_cast(RotationOrder::ZXY)); + obs_data_set_default_double(settings, ST_KEY_SCALE_X, 100); + obs_data_set_default_double(settings, ST_KEY_SCALE_Y, 100); + obs_data_set_default_double(settings, ST_KEY_SHEAR_X, 0); + obs_data_set_default_double(settings, ST_KEY_SHEAR_Y, 0); + obs_data_set_default_double(settings, ST_KEY_CORNERS_TOPLEFT "X", -100.); + obs_data_set_default_double(settings, ST_KEY_CORNERS_TOPLEFT "Y", -100.); + obs_data_set_default_double(settings, ST_KEY_CORNERS_TOPRIGHT "X", 100.); + obs_data_set_default_double(settings, ST_KEY_CORNERS_TOPRIGHT "Y", -100.); + obs_data_set_default_double(settings, ST_KEY_CORNERS_BOTTOMLEFT "X", -100.); + obs_data_set_default_double(settings, ST_KEY_CORNERS_BOTTOMLEFT "Y", 100.); + obs_data_set_default_double(settings, ST_KEY_CORNERS_BOTTOMRIGHT "X", 100.); + obs_data_set_default_double(settings, ST_KEY_CORNERS_BOTTOMRIGHT "Y", 100.); + obs_data_set_default_bool(settings, ST_KEY_MIPMAPPING, false); +} + +static bool modified_camera_mode(obs_properties_t* pr, obs_property_t*, obs_data_t* d) noexcept +{ + try { + auto mode = static_cast(obs_data_get_int(d, ST_KEY_CAMERA_MODE)); + bool is_camera = mode != transform_mode::CORNER_PIN; + bool is_perspective = (mode == transform_mode::PERSPECTIVE) && is_camera; + bool is_orthographic = (mode == transform_mode::ORTHOGRAPHIC) && is_camera; + + obs_property_set_visible(obs_properties_get(pr, ST_KEY_CAMERA_FIELDOFVIEW), is_perspective); + obs_property_set_visible(obs_properties_get(pr, ST_I18N_POSITION), is_camera); + obs_property_set_visible(obs_properties_get(pr, ST_KEY_POSITION_Z), is_perspective); + obs_property_set_visible(obs_properties_get(pr, ST_I18N_ROTATION), is_camera); + obs_property_set_visible(obs_properties_get(pr, ST_I18N_SCALE), is_camera); + obs_property_set_visible(obs_properties_get(pr, ST_I18N_SHEAR), is_camera); + obs_property_set_visible(obs_properties_get(pr, ST_KEY_ROTATION_ORDER), is_camera); + obs_property_set_visible(obs_properties_get(pr, ST_I18N_CORNERS), !is_camera); + + return true; + } catch (const std::exception& ex) { + DLOG_ERROR("Unexpected exception in function '%s': %s.", __FUNCTION_NAME__, ex.what()); + return true; + } catch (...) { + DLOG_ERROR("Unexpected exception in function '%s'.", __FUNCTION_NAME__); + return true; + } +} + +obs_properties_t* transform_factory::get_properties2(transform_instance* data) +{ + obs_properties_t* pr = obs_properties_create(); + + { + obs_properties_add_button2(pr, S_MANUAL_OPEN, D_TRANSLATE(S_MANUAL_OPEN), streamfx::filter::transform::transform_factory::on_manual_open, nullptr); + } + + // Camera + { + auto grp = obs_properties_create(); + + { // Projection Mode + auto p = obs_properties_add_list(grp, ST_KEY_CAMERA_MODE, D_TRANSLATE(ST_I18N_CAMERA_MODE), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); + obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_CAMERA_MODE_CORNER_PIN), static_cast(transform_mode::CORNER_PIN)); + obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_CAMERA_MODE_ORTHOGRAPHIC), static_cast(transform_mode::ORTHOGRAPHIC)); + obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_CAMERA_MODE_PERSPECTIVE), static_cast(transform_mode::PERSPECTIVE)); + obs_property_set_modified_callback(p, modified_camera_mode); + } + { // Field Of View + auto p = obs_properties_add_float_slider(grp, ST_KEY_CAMERA_FIELDOFVIEW, D_TRANSLATE(ST_I18N_CAMERA_FIELDOFVIEW), 1.0, 179.0, 0.01); + } + + obs_properties_add_group(pr, ST_I18N_CAMERA, D_TRANSLATE(ST_I18N_CAMERA), OBS_GROUP_NORMAL, grp); + } + + { + ; // Parmametrized Mesh + + { // Position + auto grp = obs_properties_create(); + + std::pair opts[] = { + {ST_KEY_POSITION_X, "X"}, + {ST_KEY_POSITION_Y, "Y"}, + {ST_KEY_POSITION_Z, "Z"}, + }; + for (const auto& opt : opts) { + auto p = obs_properties_add_float(grp, opt.first.c_str(), D_TRANSLATE(opt.second.c_str()), std::numeric_limits::lowest(), std::numeric_limits::max(), 0.01); + } + + obs_properties_add_group(pr, ST_I18N_POSITION, D_TRANSLATE(ST_I18N_POSITION), OBS_GROUP_NORMAL, grp); + } + { // Rotation + auto grp = obs_properties_create(); + + std::pair opts[] = { + {ST_KEY_ROTATION_X, D_TRANSLATE(ST_I18N_ROTATION ".X")}, + {ST_KEY_ROTATION_Y, D_TRANSLATE(ST_I18N_ROTATION ".Y")}, + {ST_KEY_ROTATION_Z, D_TRANSLATE(ST_I18N_ROTATION ".Z")}, + }; + for (const auto& opt : opts) { + auto p = obs_properties_add_float_slider(grp, opt.first.c_str(), D_TRANSLATE(opt.second.c_str()), -180.0, 180.0, 0.01); + obs_property_float_set_suffix(p, "° Deg"); + } + + obs_properties_add_group(pr, ST_I18N_ROTATION, D_TRANSLATE(ST_I18N_ROTATION), OBS_GROUP_NORMAL, grp); + } + { // Scale + auto grp = obs_properties_create(); + + std::pair opts[] = { + {ST_KEY_SCALE_X, "X"}, + {ST_KEY_SCALE_Y, "Y"}, + }; + for (const auto& opt : opts) { + auto p = obs_properties_add_float_slider(grp, opt.first.c_str(), opt.second.c_str(), -1000, 1000, 0.01); + obs_property_float_set_suffix(p, "%"); + } + + obs_properties_add_group(pr, ST_I18N_SCALE, D_TRANSLATE(ST_I18N_SCALE), OBS_GROUP_NORMAL, grp); + } + { // Shear + auto grp = obs_properties_create(); + + std::pair opts[] = { + {ST_KEY_SHEAR_X, "X"}, + {ST_KEY_SHEAR_Y, "Y"}, + }; + for (const auto& opt : opts) { + auto p = obs_properties_add_float_slider(grp, opt.first.c_str(), opt.second.c_str(), -200.0, 200.0, 0.01); + obs_property_float_set_suffix(p, "%"); + } + + obs_properties_add_group(pr, ST_I18N_SHEAR, D_TRANSLATE(ST_I18N_SHEAR), OBS_GROUP_NORMAL, grp); + } + } + + { // Corners + + auto grp = obs_properties_create(); + { // Top Left + auto grp2 = obs_properties_create(); + + std::pair opts[] = { + {ST_KEY_CORNERS_TOPLEFT "X", "X"}, + {ST_KEY_CORNERS_TOPLEFT "Y", "Y"}, + }; + for (auto& opt : opts) { + auto p = obs_properties_add_float_slider(grp2, opt.first.c_str(), opt.second.c_str(), -200.0, 200.0, 0.01); + obs_property_float_set_suffix(p, "%"); + } + + obs_properties_add_group(grp, ST_I18N_CORNERS_TOPLEFT, D_TRANSLATE(ST_I18N_CORNERS_TOPLEFT), OBS_GROUP_NORMAL, grp2); + } + { // Top Right + auto grp2 = obs_properties_create(); + + std::pair opts[] = { + {ST_KEY_CORNERS_TOPRIGHT "X", "X"}, + {ST_KEY_CORNERS_TOPRIGHT "Y", "Y"}, + }; + for (auto& opt : opts) { + auto p = obs_properties_add_float_slider(grp2, opt.first.c_str(), opt.second.c_str(), -200.0, 200.0, 0.01); + obs_property_float_set_suffix(p, "%"); + } + + obs_properties_add_group(grp, ST_I18N_CORNERS_TOPRIGHT, D_TRANSLATE(ST_I18N_CORNERS_TOPRIGHT), OBS_GROUP_NORMAL, grp2); + } + { // Bottom Left + auto grp2 = obs_properties_create(); + + std::pair opts[] = { + {ST_KEY_CORNERS_BOTTOMLEFT "X", "X"}, + {ST_KEY_CORNERS_BOTTOMLEFT "Y", "Y"}, + }; + for (auto& opt : opts) { + auto p = obs_properties_add_float_slider(grp2, opt.first.c_str(), opt.second.c_str(), -200.0, 200.0, 0.01); + obs_property_float_set_suffix(p, "%"); + } + + obs_properties_add_group(grp, ST_I18N_CORNERS_BOTTOMLEFT, D_TRANSLATE(ST_I18N_CORNERS_BOTTOMLEFT), OBS_GROUP_NORMAL, grp2); + } + { // Bottom Right + auto grp2 = obs_properties_create(); + + std::pair opts[] = { + {ST_KEY_CORNERS_BOTTOMRIGHT "X", "X"}, + {ST_KEY_CORNERS_BOTTOMRIGHT "Y", "Y"}, + }; + for (auto& opt : opts) { + auto p = obs_properties_add_float_slider(grp2, opt.first.c_str(), opt.second.c_str(), -200.0, 200.0, 0.01); + obs_property_float_set_suffix(p, "%"); + } + + obs_properties_add_group(grp, ST_I18N_CORNERS_BOTTOMRIGHT, D_TRANSLATE(ST_I18N_CORNERS_BOTTOMRIGHT), OBS_GROUP_NORMAL, grp2); + } + + obs_properties_add_group(pr, ST_I18N_CORNERS, D_TRANSLATE(ST_I18N_CORNERS), OBS_GROUP_NORMAL, grp); + } + + { + auto grp = obs_properties_create(); + obs_properties_add_group(pr, S_ADVANCED, D_TRANSLATE(S_ADVANCED), OBS_GROUP_NORMAL, grp); + + { // Mip-mapping + auto p = obs_properties_add_bool(grp, ST_KEY_MIPMAPPING, D_TRANSLATE(ST_I18N_MIPMAPPING)); + } + + { // Order + auto p = obs_properties_add_list(grp, ST_KEY_ROTATION_ORDER, D_TRANSLATE(ST_I18N_ROTATION_ORDER), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); + obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_ROTATION_ORDER_XYZ), RotationOrder::XYZ); + obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_ROTATION_ORDER_XZY), RotationOrder::XZY); + obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_ROTATION_ORDER_YXZ), RotationOrder::YXZ); + obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_ROTATION_ORDER_YZX), RotationOrder::YZX); + obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_ROTATION_ORDER_ZXY), RotationOrder::ZXY); + obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_ROTATION_ORDER_ZYX), RotationOrder::ZYX); + } + } + + return pr; +} + +bool transform_factory::on_manual_open(obs_properties_t* props, obs_property_t* property, void* data) +{ + try { + streamfx::open_url(HELP_URL); + return false; + } catch (const std::exception& ex) { + D_LOG_ERROR("Failed to open manual due to error: %s", ex.what()); + return false; + } catch (...) { + D_LOG_ERROR("Failed to open manual due to unknown error.", ""); + return false; + } +} + +std::shared_ptr transform_factory::instance() +{ + static std::weak_ptr winst; + static std::mutex mtx; + + std::unique_lock lock(mtx); + auto instance = winst.lock(); + if (!instance) { + instance = std::shared_ptr(new transform_factory()); + winst = instance; + } + return instance; +} + +static std::shared_ptr loader_instance; + +static auto loader = streamfx::component( + "transform", + []() { // Initializer + loader_instance = transform_factory::instance(); + }, + []() { // Finalizer + loader_instance.reset(); + }, + {"core::gs::texrender", "core::gs::texture", "core::gs::sampler"}); diff --git a/components/transform/source/filter/filter-transform.hpp b/components/transform/source/filter/filter-transform.hpp new file mode 100644 index 0000000..2baf95b --- /dev/null +++ b/components/transform/source/filter/filter-transform.hpp @@ -0,0 +1,90 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "common.hpp" +#include "gfx/gfx-mipmapper.hpp" +#include "gfx/gfx-util.hpp" +#include "obs/gs/gs-texrender.hpp" +#include "obs/gs/gs-texture.hpp" +#include "obs/gs/gs-vertexbuffer.hpp" +#include "obs/obs-source-factory.hpp" + +#include "warning-disable.hpp" +#include +#include "warning-enable.hpp" + +namespace streamfx::filter::transform { + enum class transform_mode { + ORTHOGRAPHIC = 0, + PERSPECTIVE = 1, + CORNER_PIN = 2, + }; + + class transform_instance : public obs::source_instance { + std::shared_ptr _gfx_util; + + // Lifetime Data + streamfx::obs::gs::effect _standard_effect; + streamfx::obs::gs::effect _transform_effect; + std::pair _source_size; + size_t _mip_levels; + std::shared_ptr _mesh; + std::shared_ptr _sampler; + streamfx::gfx::mipmapper _mipmapper; + std::shared_ptr _final_rt; + std::shared_ptr _final_tex; + + // Frame Data + bool _mesh_dirty; + bool _is_rendered; + std::pair _cache_size; + + // Settings + transform_mode _camera_mode; + float _camera_fov; + struct { + vec3 position; + vec3 rotation; + uint32_t rotation_order; + vec3 scale; + vec3 shear; + } _params; + struct { + vec2 tl; + vec2 tr; + vec2 bl; + vec2 br; + } _corners; + bool _mipmap_enabled; + + public: + transform_instance(obs_data_t*, obs_source_t*); + virtual ~transform_instance() override; + + virtual void load(obs_data_t* settings) override; + virtual void migrate(obs_data_t* data, uint64_t version) override; + virtual void update(obs_data_t*) override; + + virtual void video_tick(float) override; + virtual void video_render(gs_effect_t*) override; + }; + + class transform_factory : public obs::source_factory { + public: + transform_factory(); + virtual ~transform_factory() override; + + virtual const char* get_name() override; + + virtual void get_defaults2(obs_data_t* data) override; + + virtual obs_properties_t* get_properties2(filter::transform::transform_instance* data) override; + + static bool on_manual_open(obs_properties_t* props, obs_property_t* property, void* data); + + public: // Singleton + static std::shared_ptr instance(); + }; +} // namespace streamfx::filter::transform diff --git a/components/upscaling/CMakeLists.txt b/components/upscaling/CMakeLists.txt new file mode 100644 index 0000000..d280eb7 --- /dev/null +++ b/components/upscaling/CMakeLists.txt @@ -0,0 +1,23 @@ +# AUTOGENERATED COPYRIGHT HEADER START +# Copyright (C) 2023 Michael Fabian 'Xaymar' Dirks +# AUTOGENERATED COPYRIGHT HEADER END + +cmake_minimum_required(VERSION 3.26) +project("Upscaling") +list(APPEND CMAKE_MESSAGE_INDENT "[${PROJECT_NAME}] ") + +streamfx_add_component("Upscaling" + RESOLVER streamfx_upscaling_resolver +) +streamfx_add_component_dependency("NVIDIA" OPTIONAL) + +function(streamfx_upscaling_resolver) + # Providers + #- NVIDIA + streamfx_enabled_component("NVIDIA" T_CHECK) + if(T_CHECK) + target_compile_definitions(${COMPONENT_TARGET} PRIVATE + ENABLE_NVIDIA + ) + endif() +endfunction() diff --git a/components/upscaling/source/filters/filter-upscaling.cpp b/components/upscaling/source/filters/filter-upscaling.cpp new file mode 100644 index 0000000..f5a6986 --- /dev/null +++ b/components/upscaling/source/filters/filter-upscaling.cpp @@ -0,0 +1,639 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2021-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#include "filter-upscaling.hpp" +#include "obs/gs/gs-helper.hpp" +#include "plugin.hpp" +#include "util/util-logging.hpp" + +#include "warning-disable.hpp" +#include +#include "warning-enable.hpp" + +#ifdef _DEBUG +#define ST_PREFIX "<%s> " +#define D_LOG_ERROR(x, ...) P_LOG_ERROR(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_WARNING(x, ...) P_LOG_WARN(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_INFO(x, ...) P_LOG_INFO(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_DEBUG(x, ...) P_LOG_DEBUG(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#else +#define ST_PREFIX " " +#define D_LOG_ERROR(...) P_LOG_ERROR(ST_PREFIX __VA_ARGS__) +#define D_LOG_WARNING(...) P_LOG_WARN(ST_PREFIX __VA_ARGS__) +#define D_LOG_INFO(...) P_LOG_INFO(ST_PREFIX __VA_ARGS__) +#define D_LOG_DEBUG(...) P_LOG_DEBUG(ST_PREFIX __VA_ARGS__) +#endif + +#define ST_I18N "Filter.Upscaling" +#define ST_KEY_PROVIDER "Provider" +#define ST_I18N_PROVIDER ST_I18N "." ST_KEY_PROVIDER +#define ST_I18N_PROVIDER_NVIDIA_SUPERRES ST_I18N_PROVIDER ".NVIDIA.SuperResolution" + +#ifdef ENABLE_NVIDIA +#define ST_KEY_NVIDIA_SUPERRES "NVIDIA.SuperRes" +#define ST_I18N_NVIDIA_SUPERRES ST_I18N "." ST_KEY_NVIDIA_SUPERRES +#define ST_KEY_NVIDIA_SUPERRES_STRENGTH "NVIDIA.SuperRes.Strength" +#define ST_I18N_NVIDIA_SUPERRES_STRENGTH ST_I18N "." ST_KEY_NVIDIA_SUPERRES_STRENGTH +#define ST_I18N_NVIDIA_SUPERRES_STRENGTH_WEAK ST_I18N_NVIDIA_SUPERRES_STRENGTH ".Weak" +#define ST_I18N_NVIDIA_SUPERRES_STRENGTH_STRONG ST_I18N_NVIDIA_SUPERRES_STRENGTH ".Strong" +#define ST_KEY_NVIDIA_SUPERRES_SCALE "NVIDIA.SuperRes.Scale" +#define ST_I18N_NVIDIA_SUPERRES_SCALE ST_I18N "." ST_KEY_NVIDIA_SUPERRES_SCALE +#endif + +using streamfx::filter::upscaling::upscaling_factory; +using streamfx::filter::upscaling::upscaling_instance; +using streamfx::filter::upscaling::upscaling_provider; + +static constexpr std::string_view HELP_URL = "https://github.com/Xaymar/obs-StreamFX/wiki/Filter-Upscaling"; + +/** Priority of providers for automatic selection if more than one is available. + * + */ +static upscaling_provider provider_priority[] = { + upscaling_provider::NVIDIA_SUPERRESOLUTION, +}; + +const char* streamfx::filter::upscaling::cstring(upscaling_provider provider) +{ + switch (provider) { + case upscaling_provider::INVALID: + return "N/A"; + case upscaling_provider::AUTOMATIC: + return D_TRANSLATE(S_STATE_AUTOMATIC); + case upscaling_provider::NVIDIA_SUPERRESOLUTION: + return D_TRANSLATE(ST_I18N_PROVIDER_NVIDIA_SUPERRES); + default: + throw std::runtime_error("Missing Conversion Entry"); + } +} + +std::string streamfx::filter::upscaling::string(upscaling_provider provider) +{ + return cstring(provider); +} + +//------------------------------------------------------------------------------ +// Instance +//------------------------------------------------------------------------------ +upscaling_instance::upscaling_instance(obs_data_t* data, obs_source_t* self) : obs::source_instance(data, self), _in_size(1, 1), _out_size(1, 1), _provider(upscaling_provider::INVALID), _provider_ui(upscaling_provider::INVALID), _provider_ready(false), _provider_lock(), _provider_task(), _input(), _output(), _dirty(false) +{ + D_LOG_DEBUG("Initializating... (Addr: 0x%" PRIuPTR ")", this); + + { + ::streamfx::obs::gs::context gctx; + + // Create the render target for the input buffering. + _input = std::make_shared<::streamfx::obs::gs::texrender>(GS_RGBA_UNORM, GS_ZS_NONE); + _input->render(1, 1); // Preallocate the RT on the driver and GPU. + _output = _input->get_texture(); + + // Load the required effect. + _standard_effect = std::make_shared<::streamfx::obs::gs::effect>(::streamfx::data_file_path("effects/standard.effect")); + } + + if (data) { + load(data); + } +} + +upscaling_instance::~upscaling_instance() +{ + D_LOG_DEBUG("Finalizing... (Addr: 0x%" PRIuPTR ")", this); + + { // Unload the underlying effect ASAP. + std::unique_lock ul(_provider_lock); + + // De-queue the underlying task. + if (_provider_task) { + streamfx::util::threadpool::threadpool::instance()->pop(_provider_task); + _provider_task->await_completion(); + _provider_task.reset(); + } + + // TODO: Make this asynchronous. + switch (_provider) { +#ifdef ENABLE_NVIDIA + case upscaling_provider::NVIDIA_SUPERRESOLUTION: + nvvfxsr_unload(); + break; +#endif + default: + break; + } + } +} + +void upscaling_instance::load(obs_data_t* data) +{ + update(data); +} + +void upscaling_instance::migrate(obs_data_t* data, uint64_t version) {} + +void upscaling_instance::update(obs_data_t* data) +{ + // Check if the user changed which Denoising provider we use. + upscaling_provider provider = static_cast(obs_data_get_int(data, ST_KEY_PROVIDER)); + if (provider == upscaling_provider::AUTOMATIC) { + provider = upscaling_factory::instance()->find_ideal_provider(); + } + + // Check if the provider was changed, and if so switch. + if (provider != _provider) { + _provider_ui = provider; + switch_provider(provider); + } + + if (_provider_ready) { + std::unique_lock ul(_provider_lock); + + switch (_provider) { +#ifdef ENABLE_NVIDIA + case upscaling_provider::NVIDIA_SUPERRESOLUTION: + nvvfxsr_update(data); + break; +#endif + default: + break; + } + } +} + +void streamfx::filter::upscaling::upscaling_instance::properties(obs_properties_t* properties) +{ + switch (_provider_ui) { +#ifdef ENABLE_NVIDIA + case upscaling_provider::NVIDIA_SUPERRESOLUTION: + nvvfxsr_properties(properties); + break; +#endif + default: + break; + } +} + +uint32_t streamfx::filter::upscaling::upscaling_instance::get_width() +{ + return std::max(_out_size.first, 1); +} + +uint32_t streamfx::filter::upscaling::upscaling_instance::get_height() +{ + return std::max(_out_size.second, 1); +} + +void upscaling_instance::video_tick(float time) +{ + auto target = obs_filter_get_target(_self); + auto width = obs_source_get_base_width(target); + auto height = obs_source_get_base_height(target); + _in_size = {width, height}; + _out_size = _in_size; + + // Allow the provider to restrict the size. + if (target && _provider_ready) { + std::unique_lock ul(_provider_lock); + + switch (_provider) { +#ifdef ENABLE_NVIDIA + case upscaling_provider::NVIDIA_SUPERRESOLUTION: + nvvfxsr_size(); + break; +#endif + default: + break; + } + } + + _dirty = true; +} + +void upscaling_instance::video_render(gs_effect_t* effect) +{ + auto parent = obs_filter_get_parent(_self); + auto target = obs_filter_get_target(_self); + auto width = obs_source_get_base_width(target); + auto height = obs_source_get_base_height(target); + vec4 blank = vec4{0, 0, 0, 0}; + + // Ensure we have the bare minimum of valid information. + target = target ? target : parent; + effect = effect ? effect : obs_get_base_effect(OBS_EFFECT_DEFAULT); + + // Skip the filter if: + // - The Provider isn't ready yet. + // - We don't have a target. + // - The width/height of the next filter in the chain is empty. + if (!_provider_ready || !target || (width == 0) || (height == 0)) { + obs_source_skip_video_filter(_self); + return; + } + +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + ::streamfx::obs::gs::debug_marker profiler0{::streamfx::obs::gs::debug_color_source, "StreamFX Upscaling"}; + ::streamfx::obs::gs::debug_marker profiler0_0{::streamfx::obs::gs::debug_color_gray, "'%s' on '%s'", obs_source_get_name(_self), obs_source_get_name(parent)}; +#endif + + if (_dirty) { + // Lock the provider from being changed. + std::unique_lock ul(_provider_lock); + + { // Capture the incoming frame. +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + ::streamfx::obs::gs::debug_marker profiler1{::streamfx::obs::gs::debug_color_capture, "Capture"}; +#endif + if (obs_source_process_filter_begin(_self, GS_RGBA, OBS_ALLOW_DIRECT_RENDERING)) { + auto op = _input->render(_in_size.first, _in_size.second); + + // Matrix + gs_matrix_push(); + gs_ortho(0., 1., 0., 1., 0., 1.); + + // Clear the buffer + gs_clear(GS_CLEAR_COLOR | GS_CLEAR_DEPTH, &blank, 0, 0); + + // Set GPU state + gs_blend_state_push(); + gs_enable_color(true, true, true, true); + gs_enable_blending(false); + gs_enable_depth_test(false); + gs_enable_stencil_test(false); + gs_set_cull_mode(GS_NEITHER); + + // Render +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + ::streamfx::obs::gs::debug_marker profiler2{::streamfx::obs::gs::debug_color_capture, "Storage"}; +#endif + obs_source_process_filter_end(_self, obs_get_base_effect(OBS_EFFECT_DEFAULT), 1, 1); + + // Reset GPU state + gs_blend_state_pop(); + gs_matrix_pop(); + } else { + obs_source_skip_video_filter(_self); + return; + } + } + + try { // Process the captured input with the provider. +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + ::streamfx::obs::gs::debug_marker profiler1{::streamfx::obs::gs::debug_color_convert, "Process"}; +#endif + switch (_provider) { +#ifdef ENABLE_NVIDIA + case upscaling_provider::NVIDIA_SUPERRESOLUTION: + nvvfxsr_process(); + break; +#endif + default: + _output.reset(); + break; + } + } catch (...) { + obs_source_skip_video_filter(_self); + return; + } + + if (!_output) { + D_LOG_ERROR("Provider '%s' did not return a result.", cstring(_provider)); + obs_source_skip_video_filter(_self); + return; + } + + _dirty = false; + } + + { // Draw the result for the next filter to use. +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + ::streamfx::obs::gs::debug_marker profiler1{::streamfx::obs::gs::debug_color_render, "Render"}; +#endif + if (_standard_effect->has_parameter("InputA", ::streamfx::obs::gs::effect_parameter::type::Texture)) { + _standard_effect->get_parameter("InputA").set_texture(_output); + } + if (_standard_effect->has_parameter("InputB", ::streamfx::obs::gs::effect_parameter::type::Texture)) { + _standard_effect->get_parameter("InputB").set_texture(_input->get_texture()); + } + while (gs_effect_loop(_standard_effect->get_object(), "RestoreAlpha")) { + gs_draw_sprite(nullptr, 0, _out_size.first, _out_size.second); + } + } +} + +struct switch_provider_data_t { + upscaling_provider provider; +}; + +void streamfx::filter::upscaling::upscaling_instance::switch_provider(upscaling_provider provider) +{ + std::unique_lock ul(_provider_lock); + + // Safeguard against calls made from unlocked memory. + if (provider == _provider) { + return; + } + + // This doesn't work correctly. + // - Need to allow multiple switches at once because OBS is weird. + // - Doesn't guarantee that the task is properly killed off. + + // Log information. + D_LOG_INFO("Instance '%s' is switching provider from '%s' to '%s'.", obs_source_get_name(_self), cstring(_provider), cstring(provider)); + + // If there is an existing task, attempt to cancel it. + if (_provider_task) { + // De-queue it. + streamfx::util::threadpool::threadpool::instance()->pop(_provider_task); + + // Await the death of the task itself. + _provider_task->await_completion(); + + // Clear any memory associated with it. + _provider_task.reset(); + } + + // Build data to pass into the task. + auto spd = std::make_shared(); + spd->provider = _provider; + _provider = provider; + + // Then spawn a new task to switch provider. + _provider_task = streamfx::util::threadpool::threadpool::instance()->push(std::bind(&upscaling_instance::task_switch_provider, this, std::placeholders::_1), spd); +} + +void streamfx::filter::upscaling::upscaling_instance::task_switch_provider(util::threadpool::task_data_t data) +{ + std::shared_ptr spd = std::static_pointer_cast(data); + + // 1. Mark the provider as no longer ready. + _provider_ready = false; + + // 2. Lock the provider from being used. + std::unique_lock ul(_provider_lock); + + try { + // 3. Unload the previous provider. + switch (spd->provider) { +#ifdef ENABLE_NVIDIA + case upscaling_provider::NVIDIA_SUPERRESOLUTION: + nvvfxsr_unload(); + break; +#endif + default: + break; + } + + // 4. Load the new provider. + switch (_provider) { +#ifdef ENABLE_NVIDIA + case upscaling_provider::NVIDIA_SUPERRESOLUTION: + nvvfxsr_load(); + { + auto data = obs_source_get_settings(_self); + nvvfxsr_update(data); + obs_data_release(data); + } + break; +#endif + default: + break; + } + + // Log information. + D_LOG_INFO("Instance '%s' switched provider from '%s' to '%s'.", obs_source_get_name(_self), cstring(spd->provider), cstring(_provider)); + + // 5. Set the new provider as valid. + _provider_ready = true; + } catch (std::exception const& ex) { + // Log information. + D_LOG_ERROR("Instance '%s' failed switching provider with error: %s", obs_source_get_name(_self), ex.what()); + } +} + +#ifdef ENABLE_NVIDIA +void streamfx::filter::upscaling::upscaling_instance::nvvfxsr_load() +{ + _nvidia_fx = std::make_shared<::streamfx::nvidia::vfx::superresolution>(); +} + +void streamfx::filter::upscaling::upscaling_instance::nvvfxsr_unload() +{ + _nvidia_fx.reset(); +} + +void streamfx::filter::upscaling::upscaling_instance::nvvfxsr_size() +{ + if (!_nvidia_fx) { + return; + } + + auto in_size = _in_size; + _nvidia_fx->size(in_size, _in_size, _out_size); +} + +void streamfx::filter::upscaling::upscaling_instance::nvvfxsr_process() +{ + if (!_nvidia_fx) { + _output = _input->get_texture(); + return; + } + + _output = _nvidia_fx->process(_input->get_texture()); +} + +void streamfx::filter::upscaling::upscaling_instance::nvvfxsr_properties(obs_properties_t* props) +{ + obs_properties_t* grp = obs_properties_create(); + obs_properties_add_group(props, ST_KEY_NVIDIA_SUPERRES, D_TRANSLATE(ST_I18N_NVIDIA_SUPERRES), OBS_GROUP_NORMAL, grp); + + { + auto p = obs_properties_add_list(grp, ST_KEY_NVIDIA_SUPERRES_STRENGTH, D_TRANSLATE(ST_I18N_NVIDIA_SUPERRES_STRENGTH), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); + obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_NVIDIA_SUPERRES_STRENGTH_WEAK), 0); + obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_NVIDIA_SUPERRES_STRENGTH_STRONG), 1); + } + + { + auto p = obs_properties_add_float_slider(grp, ST_KEY_NVIDIA_SUPERRES_SCALE, D_TRANSLATE(ST_I18N_NVIDIA_SUPERRES_SCALE), 100.00, 400.00, .01); + obs_property_float_set_suffix(p, " %"); + } +} + +void streamfx::filter::upscaling::upscaling_instance::nvvfxsr_update(obs_data_t* data) +{ + if (!_nvidia_fx) + return; + + _nvidia_fx->set_strength(static_cast(obs_data_get_int(data, ST_KEY_NVIDIA_SUPERRES_STRENGTH) == 0 ? 0. : 1.)); + _nvidia_fx->set_scale(static_cast(obs_data_get_double(data, ST_KEY_NVIDIA_SUPERRES_SCALE) / 100.)); +} + +#endif + +//------------------------------------------------------------------------------ +// Factory +//------------------------------------------------------------------------------ +upscaling_factory::~upscaling_factory() {} + +upscaling_factory::upscaling_factory() +{ + bool any_available = false; + + // 1. Try and load any configured providers. +#ifdef ENABLE_NVIDIA + try { + // Load CVImage and Video Effects SDK. + _nvcuda = ::streamfx::nvidia::cuda::obs::get(); + _nvcvi = ::streamfx::nvidia::cv::cv::get(); + _nvvfx = ::streamfx::nvidia::vfx::vfx::get(); + _nvidia_available = true; + any_available |= _nvidia_available; + } catch (const std::exception& ex) { + _nvidia_available = false; + _nvvfx.reset(); + _nvcvi.reset(); + _nvcuda.reset(); + D_LOG_WARNING("Failed to make NVIDIA Super-Resolution available due to error: %s", ex.what()); + } catch (...) { + _nvidia_available = false; + _nvvfx.reset(); + _nvcvi.reset(); + _nvcuda.reset(); + D_LOG_WARNING("Failed to make NVIDIA Super-Resolution available.", nullptr); + } +#endif + + // 2. Check if any of them managed to load at all. + if (!any_available) { + D_LOG_ERROR("All supported Super-Resolution providers failed to initialize, disabling effect.", 0); + return; + } + + // 3. In any other case, register the filter! + _info.id = S_PREFIX "filter-upscaling"; + _info.type = OBS_SOURCE_TYPE_FILTER; + _info.output_flags = OBS_SOURCE_VIDEO | OBS_SOURCE_CUSTOM_DRAW /*| OBS_SOURCE_SRGB*/; + + support_size(true); + finish_setup(); + + // Proxies + register_proxy("streamfx-filter-video-superresolution"); +} + +const char* upscaling_factory::get_name() +{ + return D_TRANSLATE(ST_I18N); +} + +void upscaling_factory::get_defaults2(obs_data_t* data) +{ + obs_data_set_default_int(data, ST_KEY_PROVIDER, static_cast(upscaling_provider::AUTOMATIC)); + +#ifdef ENABLE_NVIDIA + obs_data_set_default_double(data, ST_KEY_NVIDIA_SUPERRES_SCALE, 150.); + obs_data_set_default_double(data, ST_KEY_NVIDIA_SUPERRES_STRENGTH, 0.); +#endif +} + +static bool modified_provider(obs_properties_t* props, obs_property_t*, obs_data_t* settings) noexcept +{ + try { + return true; + } catch (const std::exception& ex) { + DLOG_ERROR("Unexpected exception in function '%s': %s.", __FUNCTION_NAME__, ex.what()); + return false; + } catch (...) { + DLOG_ERROR("Unexpected exception in function '%s'.", __FUNCTION_NAME__); + return false; + } +} + +obs_properties_t* upscaling_factory::get_properties2(upscaling_instance* data) +{ + obs_properties_t* pr = obs_properties_create(); + + { + obs_properties_add_button2(pr, S_MANUAL_OPEN, D_TRANSLATE(S_MANUAL_OPEN), upscaling_factory::on_manual_open, nullptr); + } + + if (data) { + data->properties(pr); + } + + { // Advanced Settings + auto grp = obs_properties_create(); + obs_properties_add_group(pr, S_ADVANCED, D_TRANSLATE(S_ADVANCED), OBS_GROUP_NORMAL, grp); + + { + auto p = obs_properties_add_list(grp, ST_KEY_PROVIDER, D_TRANSLATE(ST_I18N_PROVIDER), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); + obs_property_set_modified_callback(p, modified_provider); + obs_property_list_add_int(p, D_TRANSLATE(S_STATE_AUTOMATIC), static_cast(upscaling_provider::AUTOMATIC)); + obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_PROVIDER_NVIDIA_SUPERRES), static_cast(upscaling_provider::NVIDIA_SUPERRESOLUTION)); + } + } + + return pr; +} + +bool upscaling_factory::on_manual_open(obs_properties_t* props, obs_property_t* property, void* data) +{ + try { + streamfx::open_url(HELP_URL); + return false; + } catch (const std::exception& ex) { + D_LOG_ERROR("Failed to open manual due to error: %s", ex.what()); + return false; + } catch (...) { + D_LOG_ERROR("Failed to open manual due to unknown error.", ""); + return false; + } +} + +bool streamfx::filter::upscaling::upscaling_factory::is_provider_available(upscaling_provider provider) +{ + switch (provider) { +#ifdef ENABLE_NVIDIA + case upscaling_provider::NVIDIA_SUPERRESOLUTION: + return _nvidia_available; +#endif + default: + return false; + } +} + +upscaling_provider streamfx::filter::upscaling::upscaling_factory::find_ideal_provider() +{ + for (auto v : provider_priority) { + if (upscaling_factory::instance()->is_provider_available(v)) { + return v; + break; + } + } + return upscaling_provider::AUTOMATIC; +} + +std::shared_ptr upscaling_factory::instance() +{ + static std::weak_ptr winst; + static std::mutex mtx; + + std::unique_lock lock(mtx); + auto instance = winst.lock(); + if (!instance) { + instance = std::shared_ptr(new upscaling_factory()); + winst = instance; + } + return instance; +} + +static std::shared_ptr loader_instance; + +static auto loader = streamfx::component( + "upscaling", + []() { // Initializer + loader_instance = upscaling_factory::instance(); + }, + []() { // Finalizer + loader_instance.reset(); + }, + {"core::threadpool", "core::gs::texrender", "core::gs::texture", "core::gs::sampler"}); diff --git a/components/upscaling/source/filters/filter-upscaling.hpp b/components/upscaling/source/filters/filter-upscaling.hpp new file mode 100644 index 0000000..e40d2b4 --- /dev/null +++ b/components/upscaling/source/filters/filter-upscaling.hpp @@ -0,0 +1,111 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2021-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "obs/gs/gs-effect.hpp" +#include "obs/gs/gs-texrender.hpp" +#include "obs/gs/gs-texture.hpp" +#include "obs/obs-source-factory.hpp" +#include "plugin.hpp" +#include "util/util-threadpool.hpp" + +#include "warning-disable.hpp" +#include +#include +#include +#include "warning-enable.hpp" + +#ifdef ENABLE_NVIDIA +#include "nvidia/vfx/nvidia-vfx-superresolution.hpp" +#endif + +namespace streamfx::filter::upscaling { + enum class upscaling_provider { + INVALID = -1, + AUTOMATIC = 0, + NVIDIA_SUPERRESOLUTION = 1, + }; + + const char* cstring(upscaling_provider provider); + + std::string string(upscaling_provider provider); + + class upscaling_instance : public ::streamfx::obs::source_instance { + std::pair _in_size; + std::pair _out_size; + + std::atomic _provider; + upscaling_provider _provider_ui; + std::atomic _provider_ready; + std::mutex _provider_lock; + std::shared_ptr _provider_task; + + std::shared_ptr<::streamfx::obs::gs::effect> _standard_effect; + + std::shared_ptr<::streamfx::obs::gs::texrender> _input; + std::shared_ptr<::streamfx::obs::gs::texture> _output; + bool _dirty; + +#ifdef ENABLE_NVIDIA + std::shared_ptr<::streamfx::nvidia::vfx::superresolution> _nvidia_fx; +#endif + + public: + upscaling_instance(obs_data_t* data, obs_source_t* self); + ~upscaling_instance() override; + + void load(obs_data_t* data) override; + void migrate(obs_data_t* data, uint64_t version) override; + void update(obs_data_t* data) override; + void properties(obs_properties_t* properties); + + uint32_t get_width() override; + uint32_t get_height() override; + + void video_tick(float time) override; + void video_render(gs_effect_t* effect) override; + + private: + void switch_provider(upscaling_provider provider); + void task_switch_provider(util::threadpool::task_data_t data); + +#ifdef ENABLE_NVIDIA + void nvvfxsr_load(); + void nvvfxsr_unload(); + void nvvfxsr_size(); + void nvvfxsr_process(); + void nvvfxsr_properties(obs_properties_t* props); + void nvvfxsr_update(obs_data_t* data); +#endif + }; + + class upscaling_factory : public ::streamfx::obs::source_factory<::streamfx::filter::upscaling::upscaling_factory, ::streamfx::filter::upscaling::upscaling_instance> { +#ifdef ENABLE_NVIDIA + bool _nvidia_available; + std::shared_ptr<::streamfx::nvidia::cuda::obs> _nvcuda; + std::shared_ptr<::streamfx::nvidia::cv::cv> _nvcvi; + std::shared_ptr<::streamfx::nvidia::vfx::vfx> _nvvfx; +#endif + + public: + virtual ~upscaling_factory(); + upscaling_factory(); + + virtual const char* get_name() override; + + virtual void get_defaults2(obs_data_t* data) override; + virtual obs_properties_t* get_properties2(upscaling_instance* data) override; + + static bool on_manual_open(obs_properties_t* props, obs_property_t* property, void* data); + + bool is_provider_available(upscaling_provider); + upscaling_provider find_ideal_provider(); + + public: // Singleton + static void initialize(); + static void finalize(); + static std::shared_ptr<::streamfx::filter::upscaling::upscaling_factory> instance(); + }; + +} // namespace streamfx::filter::upscaling diff --git a/components/virtual-greenscreen/CMakeLists.txt b/components/virtual-greenscreen/CMakeLists.txt new file mode 100644 index 0000000..08ca26a --- /dev/null +++ b/components/virtual-greenscreen/CMakeLists.txt @@ -0,0 +1,24 @@ +# AUTOGENERATED COPYRIGHT HEADER START +# Copyright (C) 2023 Michael Fabian 'Xaymar' Dirks +# AUTOGENERATED COPYRIGHT HEADER END + +cmake_minimum_required(VERSION 3.26) +project("Virtual Greenscreen") +list(APPEND CMAKE_MESSAGE_INDENT "[${PROJECT_NAME}] ") + +streamfx_add_component(${PROJECT_NAME} + RESOLVER streamfx_virtual_greenscreen_resolver +) +streamfx_add_component_dependency("NVIDIA" OPTIONAL) + +function(streamfx_virtual_greenscreen_resolver) + # Providers + #- NVIDIA + streamfx_enabled_component("NVIDIA" T_CHECK) + if(T_CHECK) + target_compile_definitions(${COMPONENT_TARGET} PRIVATE + ENABLE_NVIDIA + ) + endif() +endfunction() + diff --git a/components/virtual-greenscreen/source/filters/filter-virtual-greenscreen.cpp b/components/virtual-greenscreen/source/filters/filter-virtual-greenscreen.cpp new file mode 100644 index 0000000..bb084e5 --- /dev/null +++ b/components/virtual-greenscreen/source/filters/filter-virtual-greenscreen.cpp @@ -0,0 +1,638 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2021-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#include "filter-virtual-greenscreen.hpp" +#include "obs/gs/gs-helper.hpp" +#include "plugin.hpp" +#include "util/util-logging.hpp" + +#include "warning-disable.hpp" +#include +#include "warning-enable.hpp" + +#ifdef _DEBUG +#define ST_PREFIX "<%s> " +#define D_LOG_ERROR(x, ...) P_LOG_ERROR(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_WARNING(x, ...) P_LOG_WARN(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_INFO(x, ...) P_LOG_INFO(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#define D_LOG_DEBUG(x, ...) P_LOG_DEBUG(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__) +#else +#define ST_PREFIX " " +#define D_LOG_ERROR(...) P_LOG_ERROR(ST_PREFIX __VA_ARGS__) +#define D_LOG_WARNING(...) P_LOG_WARN(ST_PREFIX __VA_ARGS__) +#define D_LOG_INFO(...) P_LOG_INFO(ST_PREFIX __VA_ARGS__) +#define D_LOG_DEBUG(...) P_LOG_DEBUG(ST_PREFIX __VA_ARGS__) +#endif + +#define ST_I18N "Filter.VirtualGreenscreen" +#define ST_KEY_PROVIDER "Provider" +#define ST_I18N_PROVIDER ST_I18N "." ST_KEY_PROVIDER +#define ST_I18N_PROVIDER_NVIDIA_GREENSCREEN ST_I18N_PROVIDER ".NVIDIA.Greenscreen" + +#ifdef ENABLE_NVIDIA +#define ST_KEY_NVIDIA_GREENSCREEN "NVIDIA.Greenscreen" +#define ST_I18N_NVIDIA_GREENSCREEN ST_I18N "." ST_KEY_NVIDIA_GREENSCREEN +#define ST_KEY_NVIDIA_GREENSCREEN_MODE ST_KEY_NVIDIA_GREENSCREEN ".Mode" +#define ST_I18N_NVIDIA_GREENSCREEN_MODE ST_I18N_NVIDIA_GREENSCREEN ".Mode" +#define ST_I18N_NVIDIA_GREENSCREEN_MODE_PERFORMANCE ST_I18N_NVIDIA_GREENSCREEN_MODE ".Performance" +#define ST_I18N_NVIDIA_GREENSCREEN_MODE_QUALITY ST_I18N_NVIDIA_GREENSCREEN_MODE ".Quality" +#endif + +using streamfx::filter::virtual_greenscreen::virtual_greenscreen_factory; +using streamfx::filter::virtual_greenscreen::virtual_greenscreen_instance; +using streamfx::filter::virtual_greenscreen::virtual_greenscreen_provider; + +static constexpr std::string_view HELP_URL = "https://github.com/Xaymar/obs-StreamFX/wiki/Filter-Virtual-Greenscreen"; + +/** Priority of providers for automatic selection if more than one is available. + * + */ +static virtual_greenscreen_provider provider_priority[] = { + virtual_greenscreen_provider::NVIDIA_GREENSCREEN, +}; + +const char* streamfx::filter::virtual_greenscreen::cstring(virtual_greenscreen_provider provider) +{ + switch (provider) { + case virtual_greenscreen_provider::INVALID: + return "N/A"; + case virtual_greenscreen_provider::AUTOMATIC: + return D_TRANSLATE(S_STATE_AUTOMATIC); + case virtual_greenscreen_provider::NVIDIA_GREENSCREEN: + return D_TRANSLATE(ST_I18N_PROVIDER_NVIDIA_GREENSCREEN); + default: + throw std::runtime_error("Missing Conversion Entry"); + } +} + +std::string streamfx::filter::virtual_greenscreen::string(virtual_greenscreen_provider provider) +{ + return cstring(provider); +} + +//------------------------------------------------------------------------------ +// Instance +//------------------------------------------------------------------------------ +virtual_greenscreen_instance::virtual_greenscreen_instance(obs_data_t* data, obs_source_t* self) + : obs::source_instance(data, self), + + _size(1, 1), _provider(virtual_greenscreen_provider::INVALID), _provider_ui(virtual_greenscreen_provider::INVALID), _provider_ready(false), _provider_lock(), _provider_task(), _effect(), _input(), _output_color(), _output_alpha(), _dirty(true) +{ + D_LOG_DEBUG("Initializating... (Addr: 0x%" PRIuPTR ")", this); + + { + ::streamfx::obs::gs::context gctx; + + // Create the render target for the input buffering. + _input = std::make_shared<::streamfx::obs::gs::texrender>(GS_RGBA_UNORM, GS_ZS_NONE); + _input->render(1, 1); // Preallocate the RT on the driver and GPU. + _output_color = _input->get_texture(); + _output_alpha = _input->get_texture(); + + // Load the required effect. + { + std::filesystem::path file = ::streamfx::data_file_path("effects/virtual-greenscreen.effect"); + try { + _effect = std::make_shared<::streamfx::obs::gs::effect>(file); + } catch (...) { + D_LOG_ERROR("Failed to load '%s'.", file.generic_u8string().c_str()); + } + } + } + + if (data) { + load(data); + } +} + +virtual_greenscreen_instance::~virtual_greenscreen_instance() +{ + D_LOG_DEBUG("Finalizing... (Addr: 0x%" PRIuPTR ")", this); + + { // Unload the underlying effect ASAP. + std::unique_lock ul(_provider_lock); + + // De-queue the underlying task. + if (_provider_task) { + streamfx::util::threadpool::threadpool::instance()->pop(_provider_task); + _provider_task->await_completion(); + _provider_task.reset(); + } + + // TODO: Make this asynchronous. + switch (_provider) { +#ifdef ENABLE_NVIDIA + case virtual_greenscreen_provider::NVIDIA_GREENSCREEN: + nvvfxgs_unload(); + break; +#endif + default: + break; + } + } +} + +void virtual_greenscreen_instance::load(obs_data_t* data) +{ + update(data); +} + +void virtual_greenscreen_instance::migrate(obs_data_t* data, uint64_t version) {} + +void virtual_greenscreen_instance::update(obs_data_t* data) +{ + // Check if the user changed which Denoising provider we use. + virtual_greenscreen_provider provider = static_cast(obs_data_get_int(data, ST_KEY_PROVIDER)); + if (provider == virtual_greenscreen_provider::AUTOMATIC) { + provider = virtual_greenscreen_factory::instance()->find_ideal_provider(); + } + + // Check if the provider was changed, and if so switch. + if (provider != _provider) { + _provider_ui = provider; + switch_provider(provider); + } + + if (_provider_ready) { + std::unique_lock ul(_provider_lock); + + switch (_provider) { +#ifdef ENABLE_NVIDIA + case virtual_greenscreen_provider::NVIDIA_GREENSCREEN: + nvvfxgs_update(data); + break; +#endif + default: + break; + } + } +} + +void streamfx::filter::virtual_greenscreen::virtual_greenscreen_instance::properties(obs_properties_t* properties) +{ + switch (_provider_ui) { +#ifdef ENABLE_NVIDIA + case virtual_greenscreen_provider::NVIDIA_GREENSCREEN: + nvvfxgs_properties(properties); + break; +#endif + default: + break; + } +} + +uint32_t streamfx::filter::virtual_greenscreen::virtual_greenscreen_instance::get_width() +{ + return std::max(_size.first, 1); +} + +uint32_t streamfx::filter::virtual_greenscreen::virtual_greenscreen_instance::get_height() +{ + return std::max(_size.second, 1); +} + +void virtual_greenscreen_instance::video_tick(float time) +{ + auto target = obs_filter_get_target(_self); + auto width = obs_source_get_base_width(target); + auto height = obs_source_get_base_height(target); + _size = {width, height}; + + // Allow the provider to restrict the size. + if (target && _provider_ready) { + std::unique_lock ul(_provider_lock); + + switch (_provider) { +#ifdef ENABLE_NVIDIA + case virtual_greenscreen_provider::NVIDIA_GREENSCREEN: + nvvfxgs_size(); + break; +#endif + default: + break; + } + } + + _dirty = true; +} + +void virtual_greenscreen_instance::video_render(gs_effect_t* effect) +{ + auto parent = obs_filter_get_parent(_self); + auto target = obs_filter_get_target(_self); + auto width = obs_source_get_base_width(target); + auto height = obs_source_get_base_height(target); + vec4 blank = vec4{0, 0, 0, 0}; + + // Ensure we have the bare minimum of valid information. + target = target ? target : parent; + effect = effect ? effect : obs_get_base_effect(OBS_EFFECT_DEFAULT); + + // Skip the filter if: + // - The Provider isn't ready yet. + // - We don't have a target. + // - The width/height of the next filter in the chain is empty. + if (!_provider_ready || !target || (width == 0) || (height == 0)) { + obs_source_skip_video_filter(_self); + return; + } + +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + ::streamfx::obs::gs::debug_marker profiler0{::streamfx::obs::gs::debug_color_source, "StreamFX Virtual Green-Screen"}; + ::streamfx::obs::gs::debug_marker profiler0_0{::streamfx::obs::gs::debug_color_gray, "'%s' on '%s'", obs_source_get_name(_self), obs_source_get_name(parent)}; +#endif + + if (_dirty) { + // Lock the provider from being changed. + std::unique_lock ul(_provider_lock); + + { // Capture the incoming frame. +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + ::streamfx::obs::gs::debug_marker profiler1{::streamfx::obs::gs::debug_color_capture, "Capture"}; +#endif + if (obs_source_process_filter_begin(_self, GS_RGBA, OBS_ALLOW_DIRECT_RENDERING)) { + auto op = _input->render(_size.first, _size.second); + + // Matrix + gs_matrix_push(); + gs_ortho(0., 1., 0., 1., 0., 1.); + + // Clear the buffer + gs_clear(GS_CLEAR_COLOR | GS_CLEAR_DEPTH, &blank, 0, 0); + + // Set GPU state + gs_blend_state_push(); + gs_enable_color(true, true, true, true); + gs_enable_blending(false); + gs_enable_depth_test(false); + gs_enable_stencil_test(false); + gs_set_cull_mode(GS_NEITHER); + + // Render +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + ::streamfx::obs::gs::debug_marker profiler2{::streamfx::obs::gs::debug_color_capture, "Storage"}; +#endif + obs_source_process_filter_end(_self, obs_get_base_effect(OBS_EFFECT_DEFAULT), 1, 1); + + // Reset GPU state + gs_blend_state_pop(); + gs_matrix_pop(); + } else { + obs_source_skip_video_filter(_self); + return; + } + + _output_color = _input->get_texture(); + _output_alpha = _output_color; + } + + try { // Process the captured input with the provider. +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + ::streamfx::obs::gs::debug_marker profiler1{::streamfx::obs::gs::debug_color_convert, "Process"}; +#endif + switch (_provider) { +#ifdef ENABLE_NVIDIA + case virtual_greenscreen_provider::NVIDIA_GREENSCREEN: + nvvfxgs_process(_output_color, _output_alpha); + break; +#endif + default: + break; + } + } catch (...) { + obs_source_skip_video_filter(_self); + return; + } + + _dirty = false; + } + + { // Draw the result for the next filter to use. +#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG + ::streamfx::obs::gs::debug_marker profiler1{::streamfx::obs::gs::debug_color_render, "Render"}; +#endif + if (_effect->has_parameter("InputA", ::streamfx::obs::gs::effect_parameter::type::Texture)) { + _effect->get_parameter("InputA").set_texture(_output_color); + } + if (_effect->has_parameter("InputB", ::streamfx::obs::gs::effect_parameter::type::Texture)) { + _effect->get_parameter("InputB").set_texture(_output_alpha); + } + if (_effect->has_parameter("Threshold", ::streamfx::obs::gs::effect_parameter::type::Float)) { + _effect->get_parameter("Threshold").set_float(.666667); + } + if (_effect->has_parameter("ThresholdRange", ::streamfx::obs::gs::effect_parameter::type::Float)) { + _effect->get_parameter("ThresholdRange").set_float(.333333); + } + while (gs_effect_loop(_effect->get_object(), "DrawAlphaThreshold")) { + gs_draw_sprite(nullptr, 0, _size.first, _size.second); + } + } +} + +struct switch_provider_data_t { + virtual_greenscreen_provider provider; +}; + +void streamfx::filter::virtual_greenscreen::virtual_greenscreen_instance::switch_provider(virtual_greenscreen_provider provider) +{ + std::unique_lock ul(_provider_lock); + + // Safeguard against calls made from unlocked memory. + if (provider == _provider) { + return; + } + + // This doesn't work correctly. + // - Need to allow multiple switches at once because OBS is weird. + // - Doesn't guarantee that the task is properly killed off. + + // Log information. + D_LOG_INFO("Instance '%s' is switching provider from '%s' to '%s'.", obs_source_get_name(_self), cstring(_provider), cstring(provider)); + + // If there is an existing task, attempt to cancel it. + if (_provider_task) { + // De-queue it. + streamfx::util::threadpool::threadpool::instance()->pop(_provider_task); + + // Await the death of the task itself. + _provider_task->await_completion(); + + // Clear any memory associated with it. + _provider_task.reset(); + } + + // Build data to pass into the task. + auto spd = std::make_shared(); + spd->provider = _provider; + _provider = provider; + + // Then spawn a new task to switch provider. + _provider_task = streamfx::util::threadpool::threadpool::instance()->push(std::bind(&virtual_greenscreen_instance::task_switch_provider, this, std::placeholders::_1), spd); +} + +void streamfx::filter::virtual_greenscreen::virtual_greenscreen_instance::task_switch_provider(util::threadpool::task_data_t data) +{ + std::shared_ptr spd = std::static_pointer_cast(data); + + // Mark the provider as no longer ready. + _provider_ready = false; + + // Lock the provider from being used. + std::unique_lock ul(_provider_lock); + + try { + // Unload the previous provider. + switch (spd->provider) { +#ifdef ENABLE_NVIDIA + case virtual_greenscreen_provider::NVIDIA_GREENSCREEN: + nvvfxgs_unload(); + break; +#endif + default: + break; + } + + // Load the new provider. + switch (_provider) { +#ifdef ENABLE_NVIDIA + case virtual_greenscreen_provider::NVIDIA_GREENSCREEN: + nvvfxgs_load(); + { + auto data = obs_source_get_settings(_self); + nvvfxgs_update(data); + obs_data_release(data); + } + break; +#endif + default: + break; + } + + // Log information. + D_LOG_INFO("Instance '%s' switched provider from '%s' to '%s'.", obs_source_get_name(_self), cstring(spd->provider), cstring(_provider)); + + // Set the new provider as valid. + _provider_ready = true; + } catch (std::exception const& ex) { + // Log information. + D_LOG_ERROR("Instance '%s' failed switching provider with error: %s", obs_source_get_name(_self), ex.what()); + } +} + +#ifdef ENABLE_NVIDIA +void streamfx::filter::virtual_greenscreen::virtual_greenscreen_instance::nvvfxgs_load() +{ + _nvidia_fx = std::make_shared<::streamfx::nvidia::vfx::greenscreen>(); +} + +void streamfx::filter::virtual_greenscreen::virtual_greenscreen_instance::nvvfxgs_unload() +{ + _nvidia_fx.reset(); +} + +void streamfx::filter::virtual_greenscreen::virtual_greenscreen_instance::nvvfxgs_size() +{ + if (!_nvidia_fx) { + return; + } + + _nvidia_fx->size(_size); +} + +void streamfx::filter::virtual_greenscreen::virtual_greenscreen_instance::nvvfxgs_process(std::shared_ptr<::streamfx::obs::gs::texture>& color, std::shared_ptr<::streamfx::obs::gs::texture>& alpha) +{ + if (!_nvidia_fx) { + return; + } + + alpha = _nvidia_fx->process(_input->get_texture()); + color = _nvidia_fx->get_color(); +} + +void streamfx::filter::virtual_greenscreen::virtual_greenscreen_instance::nvvfxgs_properties(obs_properties_t* props) +{ + obs_properties_t* grp = obs_properties_create(); + obs_properties_add_group(props, ST_KEY_NVIDIA_GREENSCREEN, D_TRANSLATE(ST_I18N_NVIDIA_GREENSCREEN), OBS_GROUP_NORMAL, grp); + + { + auto p = obs_properties_add_list(grp, ST_KEY_NVIDIA_GREENSCREEN_MODE, D_TRANSLATE(ST_I18N_NVIDIA_GREENSCREEN_MODE), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); + obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_NVIDIA_GREENSCREEN_MODE_PERFORMANCE), static_cast(::streamfx::nvidia::vfx::greenscreen_mode::PERFORMANCE)); + obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_NVIDIA_GREENSCREEN_MODE_QUALITY), static_cast(::streamfx::nvidia::vfx::greenscreen_mode::QUALITY)); + } +} + +void streamfx::filter::virtual_greenscreen::virtual_greenscreen_instance::nvvfxgs_update(obs_data_t* data) +{ + if (!_nvidia_fx) + return; + + _nvidia_fx->set_mode(static_cast<::streamfx::nvidia::vfx::greenscreen_mode>(obs_data_get_int(data, ST_KEY_NVIDIA_GREENSCREEN_MODE))); +} + +#endif + +//------------------------------------------------------------------------------ +// Factory +//------------------------------------------------------------------------------ +virtual_greenscreen_factory::~virtual_greenscreen_factory() {} + +virtual_greenscreen_factory::virtual_greenscreen_factory() +{ + bool any_available = false; + + // 1. Try and load any configured providers. +#ifdef ENABLE_NVIDIA + try { + // Load CVImage and Video Effects SDK. + _nvcuda = ::streamfx::nvidia::cuda::obs::get(); + _nvcvi = ::streamfx::nvidia::cv::cv::get(); + _nvvfx = ::streamfx::nvidia::vfx::vfx::get(); + _nvidia_available = true; + any_available |= _nvidia_available; + } catch (const std::exception& ex) { + _nvidia_available = false; + _nvvfx.reset(); + _nvcvi.reset(); + _nvcuda.reset(); + D_LOG_WARNING("Failed to make NVIDIA Greenscreen available due to error: %s", ex.what()); + } catch (...) { + _nvidia_available = false; + _nvvfx.reset(); + _nvcvi.reset(); + _nvcuda.reset(); + D_LOG_WARNING("Failed to make NVIDIA Greenscreen available.", nullptr); + } +#endif + + // 2. Check if any of them managed to load at all. + if (!any_available) { + D_LOG_ERROR("All supported Virtual Greenscreen providers failed to initialize, disabling effect.", 0); + return; + } + + // 3. In any other case, register the filter! + _info.id = S_PREFIX "filter-virtual-greenscreen"; + _info.type = OBS_SOURCE_TYPE_FILTER; + _info.output_flags = OBS_SOURCE_VIDEO | OBS_SOURCE_CUSTOM_DRAW /*| OBS_SOURCE_SRGB*/; + + support_size(true); + finish_setup(); +} + +const char* virtual_greenscreen_factory::get_name() +{ + return D_TRANSLATE(ST_I18N); +} + +void virtual_greenscreen_factory::get_defaults2(obs_data_t* data) +{ + obs_data_set_default_int(data, ST_KEY_PROVIDER, static_cast(virtual_greenscreen_provider::AUTOMATIC)); + +#ifdef ENABLE_NVIDIA + obs_data_set_default_int(data, ST_KEY_NVIDIA_GREENSCREEN_MODE, static_cast(::streamfx::nvidia::vfx::greenscreen_mode::QUALITY)); +#endif +} + +static bool modified_provider(obs_properties_t* props, obs_property_t*, obs_data_t* settings) noexcept +{ + try { + return true; + } catch (const std::exception& ex) { + DLOG_ERROR("Unexpected exception in function '%s': %s.", __FUNCTION_NAME__, ex.what()); + return false; + } catch (...) { + DLOG_ERROR("Unexpected exception in function '%s'.", __FUNCTION_NAME__); + return false; + } +} + +obs_properties_t* virtual_greenscreen_factory::get_properties2(virtual_greenscreen_instance* data) +{ + obs_properties_t* pr = obs_properties_create(); + + { + obs_properties_add_button2(pr, S_MANUAL_OPEN, D_TRANSLATE(S_MANUAL_OPEN), virtual_greenscreen_factory::on_manual_open, nullptr); + } + + if (data) { + data->properties(pr); + } + + { // Advanced Settings + auto grp = obs_properties_create(); + obs_properties_add_group(pr, S_ADVANCED, D_TRANSLATE(S_ADVANCED), OBS_GROUP_NORMAL, grp); + + { + auto p = obs_properties_add_list(grp, ST_KEY_PROVIDER, D_TRANSLATE(ST_I18N_PROVIDER), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); + obs_property_set_modified_callback(p, modified_provider); + obs_property_list_add_int(p, D_TRANSLATE(S_STATE_AUTOMATIC), static_cast(virtual_greenscreen_provider::AUTOMATIC)); + obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_PROVIDER_NVIDIA_GREENSCREEN), static_cast(virtual_greenscreen_provider::NVIDIA_GREENSCREEN)); + } + } + + return pr; +} + +bool virtual_greenscreen_factory::on_manual_open(obs_properties_t* props, obs_property_t* property, void* data) +{ + try { + streamfx::open_url(HELP_URL); + return false; + } catch (const std::exception& ex) { + D_LOG_ERROR("Failed to open manual due to error: %s", ex.what()); + return false; + } catch (...) { + D_LOG_ERROR("Failed to open manual due to unknown error.", ""); + return false; + } +} + +bool streamfx::filter::virtual_greenscreen::virtual_greenscreen_factory::is_provider_available(virtual_greenscreen_provider provider) +{ + switch (provider) { +#ifdef ENABLE_NVIDIA + case virtual_greenscreen_provider::NVIDIA_GREENSCREEN: + return _nvidia_available; +#endif + default: + return false; + } +} + +virtual_greenscreen_provider streamfx::filter::virtual_greenscreen::virtual_greenscreen_factory::find_ideal_provider() +{ + for (auto v : provider_priority) { + if (virtual_greenscreen_factory::instance()->is_provider_available(v)) { + return v; + break; + } + } + return virtual_greenscreen_provider::AUTOMATIC; +} + +std::shared_ptr virtual_greenscreen_factory::instance() +{ + static std::weak_ptr winst; + static std::mutex mtx; + + std::unique_lock lock(mtx); + auto instance = winst.lock(); + if (!instance) { + instance = std::shared_ptr(new virtual_greenscreen_factory()); + winst = instance; + } + return instance; +} + +static std::shared_ptr loader_instance; + +static auto loader = streamfx::component( + "virtual_greenscreen", + []() { // Initializer + loader_instance = virtual_greenscreen_factory::instance(); + }, + []() { // Finalizer + loader_instance.reset(); + }, + {"core::threadpool", "core::gs::texrender", "core::gs::texture", "core::gs::sampler"}); diff --git a/components/virtual-greenscreen/source/filters/filter-virtual-greenscreen.hpp b/components/virtual-greenscreen/source/filters/filter-virtual-greenscreen.hpp new file mode 100644 index 0000000..4bd1d9a --- /dev/null +++ b/components/virtual-greenscreen/source/filters/filter-virtual-greenscreen.hpp @@ -0,0 +1,111 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2021-2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "obs/gs/gs-effect.hpp" +#include "obs/gs/gs-texrender.hpp" +#include "obs/gs/gs-texture.hpp" +#include "obs/obs-source-factory.hpp" +#include "plugin.hpp" +#include "util/util-threadpool.hpp" + +#include "warning-disable.hpp" +#include +#include +#include +#include "warning-enable.hpp" + +#ifdef ENABLE_NVIDIA +#include "nvidia/vfx/nvidia-vfx-greenscreen.hpp" +#endif + +namespace streamfx::filter::virtual_greenscreen { + enum class virtual_greenscreen_provider { + INVALID = -1, + AUTOMATIC = 0, + NVIDIA_GREENSCREEN = 1, + }; + + const char* cstring(virtual_greenscreen_provider provider); + + std::string string(virtual_greenscreen_provider provider); + + class virtual_greenscreen_instance : public ::streamfx::obs::source_instance { + std::pair _size; + + std::atomic _provider; + virtual_greenscreen_provider _provider_ui; + std::atomic _provider_ready; + std::mutex _provider_lock; + std::shared_ptr _provider_task; + + std::shared_ptr<::streamfx::obs::gs::effect> _effect; + + std::shared_ptr<::streamfx::obs::gs::texrender> _input; + std::shared_ptr<::streamfx::obs::gs::texture> _output_color; + std::shared_ptr<::streamfx::obs::gs::texture> _output_alpha; + bool _dirty; + +#ifdef ENABLE_NVIDIA + std::shared_ptr<::streamfx::nvidia::vfx::greenscreen> _nvidia_fx; +#endif + + public: + virtual_greenscreen_instance(obs_data_t* data, obs_source_t* self); + ~virtual_greenscreen_instance() override; + + void load(obs_data_t* data) override; + void migrate(obs_data_t* data, uint64_t version) override; + void update(obs_data_t* data) override; + void properties(obs_properties_t* properties); + + uint32_t get_width() override; + uint32_t get_height() override; + + void video_tick(float time) override; + void video_render(gs_effect_t* effect) override; + + private: + void switch_provider(virtual_greenscreen_provider provider); + void task_switch_provider(util::threadpool::task_data_t data); + +#ifdef ENABLE_NVIDIA + void nvvfxgs_load(); + void nvvfxgs_unload(); + void nvvfxgs_size(); + void nvvfxgs_process(std::shared_ptr<::streamfx::obs::gs::texture>& color, std::shared_ptr<::streamfx::obs::gs::texture>& alpha); + void nvvfxgs_properties(obs_properties_t* props); + void nvvfxgs_update(obs_data_t* data); +#endif + }; + + class virtual_greenscreen_factory : public ::streamfx::obs::source_factory<::streamfx::filter::virtual_greenscreen::virtual_greenscreen_factory, ::streamfx::filter::virtual_greenscreen::virtual_greenscreen_instance> { +#ifdef ENABLE_NVIDIA + bool _nvidia_available; + std::shared_ptr<::streamfx::nvidia::cuda::obs> _nvcuda; + std::shared_ptr<::streamfx::nvidia::cv::cv> _nvcvi; + std::shared_ptr<::streamfx::nvidia::vfx::vfx> _nvvfx; +#endif + + public: + virtual ~virtual_greenscreen_factory(); + virtual_greenscreen_factory(); + + virtual const char* get_name() override; + + virtual void get_defaults2(obs_data_t* data) override; + virtual obs_properties_t* get_properties2(virtual_greenscreen_instance* data) override; + + static bool on_manual_open(obs_properties_t* props, obs_property_t* property, void* data); + + bool is_provider_available(virtual_greenscreen_provider); + virtual_greenscreen_provider find_ideal_provider(); + + public: // Singleton + static void initialize(); + static void finalize(); + static std::shared_ptr<::streamfx::filter::virtual_greenscreen::virtual_greenscreen_factory> instance(); + }; + +} // namespace streamfx::filter::virtual_greenscreen diff --git a/data/effects/mipgen.effect b/data/effects/mipgen.effect index 9afb7ab..a43a338 100644 --- a/data/effects/mipgen.effect +++ b/data/effects/mipgen.effect @@ -2,21 +2,11 @@ // Copyright (C) 2018-2023 Michael Fabian 'Xaymar' Dirks // AUTOGENERATED COPYRIGHT HEADER END -uniform float4x4 ViewProj; +#include "shared.effect" + uniform texture2d image; uniform float2 imageTexel; -uniform int level; - -sampler_state def_sampler { - Filter = Linear; - AddressU = Clamp; - AddressV = Clamp; -}; - -struct VertexData { - float4 pos : POSITION; - float2 uv : TEXCOORD0; -}; +uniform float2 separableTexel; VertexData VSDefault(VertexData vtx) { @@ -24,16 +14,104 @@ VertexData VSDefault(VertexData vtx) return vtx; } -float4 PSDefault(VertexData vtx) : TARGET +float4 Linear(VertexData vtx) : TARGET { - return image.SampleLevel(def_sampler, vtx.uv, level); + // The GPU takes care of the UVs here. + return image.Sample(LinearRepeatSampler, vtx.uv); } -technique Draw +technique Linear { pass { - vertex_shader = VSDefault(vtx); - pixel_shader = PSDefault(vtx); + vertex_shader = DefaultVertexShader(vtx); + pixel_shader = Linear(vtx); + } +} + +float4 MagicKernel2011(VertexData vtx) : TARGET { + // [0.015625] [0.046875] [0.015625] + // [0.046875] [0.140625] [0.046875] + // [0.015625] [0.046875] [0.015625] + // X weight .25 + // Y weight .25 + + // Create UVs that are "centered" on the texel. + float2 uv = (floor(vtx.uv / imageTexel) + 0.5) * imageTexel; + + float4 tl = image.Sample(LinearRepeatSampler, uv + imageTexel * float2(-.25, -.25)); + float4 tr = image.Sample(LinearRepeatSampler, uv + imageTexel * float2( .25, -.25)); + float4 bl = image.Sample(LinearRepeatSampler, uv + imageTexel * float2(-.25, .25)); + float4 br = image.Sample(LinearRepeatSampler, uv + imageTexel * float2( .25, .25)); + + return (tl + tr + bl + br) / 4.; +} + +technique MagicKernel2011 +{ + pass + { + vertex_shader = DefaultVertexShader(vtx); + pixel_shader = MagicKernel2011(vtx); + } +} + +float4 MagicKernel2011Separable(VertexData vtx) : TARGET { + // 1/8 3/4 1/8 + // Separable 3-tap, optimizable to 2-tap. Slower than Single Pass + + // Create UVs that are "centered" on the texel. + float2 uv = (floor(vtx.uv / imageTexel) + 0.5) * imageTexel; + + float4 center = image.Sample(PointRepeatSampler, uv); + float4 m1 = image.Sample(PointRepeatSampler, uv + separableTexel * -1.); + float4 p1 = image.Sample(PointRepeatSampler, uv + separableTexel * 1.); + + return ((center * 0.75) + ((m1 + p1) * 0.125)); +} + +technique MagicKernel2011Separable +{ + pass + { + vertex_shader = DefaultVertexShader(vtx); + pixel_shader = MagicKernel2011Separable(vtx); + } + pass + { + vertex_shader = DefaultVertexShader(vtx); + pixel_shader = MagicKernel2011Separable(vtx); + } +} + +float4 MagicKernelSharp2021Separable(VertexData vtx) : TARGET { + // -1 +6 -35 +204 -35 +6 -1 / 144 + // Separable 7-tap, AFAIK not optimizable due to negative weights. + + // Create UVs that are "centered" on the texel. + float2 uv = (floor(vtx.uv / imageTexel) + 0.5) * imageTexel; + + float4 center = image.Sample(PointRepeatSampler, uv); + float4 m1 = image.Sample(PointRepeatSampler, uv + separableTexel * -1.); + float4 m2 = image.Sample(PointRepeatSampler, uv + separableTexel * -2.); + float4 m3 = image.Sample(PointRepeatSampler, uv + separableTexel * -3.); + float4 p1 = image.Sample(PointRepeatSampler, uv + separableTexel * 1.); + float4 p2 = image.Sample(PointRepeatSampler, uv + separableTexel * 2.); + float4 p3 = image.Sample(PointRepeatSampler, uv + separableTexel * 3.); + + return ((center * 204.) + ((m1 + p1) * -35.) + ((m2 + p2) * 6.) + ((m3 + p3) * -1.)) / 144.; +} + +technique MagicKernelSharp2021Separable +{ + pass + { + vertex_shader = DefaultVertexShader(vtx); + pixel_shader = MagicKernelSharp2021Separable(vtx); + } + pass + { + vertex_shader = DefaultVertexShader(vtx); + pixel_shader = MagicKernelSharp2021Separable(vtx); } } diff --git a/data/effects/sdf/sdf-producer.effect b/data/effects/sdf/sdf-producer.effect index 226dc87..9fe8c4d 100644 --- a/data/effects/sdf/sdf-producer.effect +++ b/data/effects/sdf/sdf-producer.effect @@ -1,5 +1,5 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2018-2023 Michael Fabian 'Xaymar' Dirks // AUTOGENERATED COPYRIGHT HEADER END // 2D Signed Distance Field Generator diff --git a/data/effects/shared.effect b/data/effects/shared.effect index b5d36e0..120bc3f 100644 --- a/data/effects/shared.effect +++ b/data/effects/shared.effect @@ -1,5 +1,6 @@ // AUTOGENERATED COPYRIGHT HEADER START // Copyright (C) 2021-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2023 brighten // AUTOGENERATED COPYRIGHT HEADER END //------------------------------------------------------------------------------ @@ -8,7 +9,7 @@ // OBS Studio does not correctly translate all HLSL functionality to GLSL. // log10(x) is HLSL-exclusive and not translated by OBS Shader Parser. -#define m_log10(x) (log(x) / log(10)) +#define m_log10(x) (log(x) / log(10.)) //------------------------------------------------------------------------------ // Uniforms diff --git a/data/examples/shaders/filter/bulge_pinch.effect b/data/examples/shaders/filter/bulge_pinch.effect index 8ed255f..b1497c6 100644 --- a/data/examples/shaders/filter/bulge_pinch.effect +++ b/data/examples/shaders/filter/bulge_pinch.effect @@ -1,5 +1,5 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2021 Radegast-FFXIV +// Copyright (C) 2021 Radegast Stravinsky // Copyright (C) 2023 Michael Fabian 'Xaymar' Dirks // AUTOGENERATED COPYRIGHT HEADER END // Copyright 2021 Radegast Stravinsky diff --git a/data/examples/shaders/filter/pixelation.effect b/data/examples/shaders/filter/pixelation.effect index 0aead4a..ba9941b 100644 --- a/data/examples/shaders/filter/pixelation.effect +++ b/data/examples/shaders/filter/pixelation.effect @@ -1,6 +1,7 @@ // AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2020 Daniel Hodgson // Copyright (C) 2021 kilin -// Copyright (C) 2023 Michael Fabian 'Xaymar' Dirks // AUTOGENERATED COPYRIGHT HEADER END // Always provided by OBS diff --git a/data/examples/shaders/filter/wave.effect b/data/examples/shaders/filter/wave.effect index a999854..0d2bfac 100644 --- a/data/examples/shaders/filter/wave.effect +++ b/data/examples/shaders/filter/wave.effect @@ -1,5 +1,5 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2021 Radegast-FFXIV +// Copyright (C) 2021 Radegast Stravinsky // Copyright (C) 2023 Michael Fabian 'Xaymar' Dirks // AUTOGENERATED COPYRIGHT HEADER END // diff --git a/data/examples/shaders/filter/zigzag.effect b/data/examples/shaders/filter/zigzag.effect index 9204287..178bd60 100644 --- a/data/examples/shaders/filter/zigzag.effect +++ b/data/examples/shaders/filter/zigzag.effect @@ -1,5 +1,5 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2021 Radegast-FFXIV +// Copyright (C) 2021 Radegast Stravinsky // Copyright (C) 2023 Michael Fabian 'Xaymar' Dirks // AUTOGENERATED COPYRIGHT HEADER END // diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini index 6ef2d66..2083851 100644 --- a/data/locale/en-US.ini +++ b/data/locale/en-US.ini @@ -64,68 +64,6 @@ UI.Updater.Menu.Channel.Candidate="Candidate" UI.Updater.Menu.Channel.Beta="Beta" UI.Updater.Menu.Channel.Alpha="Alpha" -# Encoder/AOM-AV1 -Encoder.AOM.AV1="AOM AV1 (direct)" -Encoder.AOM.AV1.Deprecated="This encoder is deprecated and will be removed soon. Users are urged to switch to the integrated 'SVT-AV1' or 'AOM AV1' encoder as soon as possible." -Encoder.AOM.AV1.Encoder="Encoder" -Encoder.AOM.AV1.Encoder.Usage="Usage" -Encoder.AOM.AV1.Encoder.Usage.GoodQuality="Good Quality" -Encoder.AOM.AV1.Encoder.Usage.RealTime="Real Time" -Encoder.AOM.AV1.Encoder.Usage.AllIntra="All Intra-Frame" -Encoder.AOM.AV1.Encoder.CPUUsage="CPU Usage" -Encoder.AOM.AV1.Encoder.CPUUsage.0="Placebo" -Encoder.AOM.AV1.Encoder.CPUUsage.1="Very Slow" -Encoder.AOM.AV1.Encoder.CPUUsage.2="Slower" -Encoder.AOM.AV1.Encoder.CPUUsage.3="Slow" -Encoder.AOM.AV1.Encoder.CPUUsage.4="Medium" -Encoder.AOM.AV1.Encoder.CPUUsage.5="Fast" -Encoder.AOM.AV1.Encoder.CPUUsage.6="Faster" -Encoder.AOM.AV1.Encoder.CPUUsage.7="Very Fast" -Encoder.AOM.AV1.Encoder.CPUUsage.8="Super Fast" -Encoder.AOM.AV1.Encoder.CPUUsage.9="Ultra Fast" -Encoder.AOM.AV1.Encoder.CPUUsage.10="Insanely Fast" -Encoder.AOM.AV1.Encoder.Profile="Profile" -Encoder.AOM.AV1.KeyFrames="Key-Frame" -Encoder.AOM.AV1.KeyFrames.IntervalType="Interval Type" -Encoder.AOM.AV1.KeyFrames.IntervalType.Frames="Frames" -Encoder.AOM.AV1.KeyFrames.IntervalType.Seconds="Seconds" -Encoder.AOM.AV1.KeyFrames.Interval="Interval" -Encoder.AOM.AV1.RateControl="Rate Control" -Encoder.AOM.AV1.RateControl.Mode="Mode" -Encoder.AOM.AV1.RateControl.Mode.CBR="Constant Bitrate (CBR)" -Encoder.AOM.AV1.RateControl.Mode.VBR="Variable Bitrate (VBR)" -Encoder.AOM.AV1.RateControl.Mode.CQ="Constrained Quality (CQ)" -Encoder.AOM.AV1.RateControl.Mode.Q="Constant Quality (Q)" -Encoder.AOM.AV1.RateControl.LookAhead="Look-Ahead" -Encoder.AOM.AV1.RateControl.Limits="Limits" -Encoder.AOM.AV1.RateControl.Limits.Bitrate="Bitrate" -Encoder.AOM.AV1.RateControl.Limits.Bitrate.Undershoot="Bitrate Undershoot" -Encoder.AOM.AV1.RateControl.Limits.Bitrate.Overshoot="Bitrate Overshoot" -Encoder.AOM.AV1.RateControl.Limits.Quality="Quality" -Encoder.AOM.AV1.RateControl.Limits.Quantizer.Minimum="Minimum Quantizer" -Encoder.AOM.AV1.RateControl.Limits.Quantizer.Maximum="Maximum Quantizer" -Encoder.AOM.AV1.RateControl.Buffer="Buffer" -Encoder.AOM.AV1.RateControl.Buffer.Size="Size" -Encoder.AOM.AV1.RateControl.Buffer.Size.Initial="Initial Size" -Encoder.AOM.AV1.RateControl.Buffer.Size.Optimal="Optimal Size" -Encoder.AOM.AV1.Advanced="Advanced" -Encoder.AOM.AV1.Advanced.Threads="Threads" -Encoder.AOM.AV1.Advanced.RowMultiThreading="Per-Row Multi-Threading" -Encoder.AOM.AV1.Advanced.Tile.Columns="Tile Columns" -Encoder.AOM.AV1.Advanced.Tile.Rows="Tile Rows" -Encoder.AOM.AV1.Advanced.Tune="Tune" -Encoder.AOM.AV1.Advanced.Tune.Metric="Metric" -Encoder.AOM.AV1.Advanced.Tune.Metric.PSNR="PSNR" -Encoder.AOM.AV1.Advanced.Tune.Metric.SSIM="SSIM" -Encoder.AOM.AV1.Advanced.Tune.Metric.VMAF.WithPreprocessing="VMAF (with pre-processing)" -Encoder.AOM.AV1.Advanced.Tune.Metric.VMAF.WithoutPreprocessing="VMAF (without pre-processing)" -Encoder.AOM.AV1.Advanced.Tune.Metric.VMAF.MaxGain="VMAF (NegMaxGain)" -Encoder.AOM.AV1.Advanced.Tune.Metric.VMAF.NegMaxGain="VMAF (MaxGain)" -Encoder.AOM.AV1.Advanced.Tune.Metric.Butteraugli="Butteraugli" -Encoder.AOM.AV1.Advanced.Tune.Content="Content" -Encoder.AOM.AV1.Advanced.Tune.Content.Screen="Screen" -Encoder.AOM.AV1.Advanced.Tune.Content.Film="Film" - # Encoder/FFmpeg Encoder.FFmpeg="FFmpeg Options" Encoder.FFmpeg.Suffix=" (via FFmpeg)" @@ -140,34 +78,65 @@ Encoder.FFmpeg.KeyFrames.Interval="Interval" Encoder.FFmpeg.Framerate="Framerate Override" # Encoder/FFmpeg/AMF -Encoder.FFmpeg.AMF.Deprecated="This encoder is deprecated and will be removed soon. Users are urged to migrate to the integrated 'AMD HW H.264 (AVC)' or 'AMD HW H.265 (HEVC)' encoder as soon as possible." -Encoder.FFmpeg.AMF.Preset="Preset" -Encoder.FFmpeg.AMF.Preset.Speed="Speed" -Encoder.FFmpeg.AMF.Preset.Balanced="Balanced" -Encoder.FFmpeg.AMF.Preset.Quality="Quality" +Encoder.FFmpeg.AMF.Quality="Preset" +Encoder.FFmpeg.AMF.Quality.speed="Speed" +Encoder.FFmpeg.AMF.Quality.balanced="Balanced" +Encoder.FFmpeg.AMF.Quality.quality="Quality" +Encoder.FFmpeg.AMF.Quality.high_quality="High Quality" +Encoder.FFmpeg.AMF.Usage="Tune" +Encoder.FFmpeg.AMF.Usage.transcoding="Transcoding" +Encoder.FFmpeg.AMF.Usage.lowlatency="Low Latency" Encoder.FFmpeg.AMF.RateControl="Rate Control Options" Encoder.FFmpeg.AMF.RateControl.Mode="Mode" -Encoder.FFmpeg.AMF.RateControl.Mode.CQP="Constant Quantization Parameter" -Encoder.FFmpeg.AMF.RateControl.Mode.VBR_PEAK="Variable Bitrate (Peak Constrained)" -Encoder.FFmpeg.AMF.RateControl.Mode.VBR_LATENCY="Variable Bitrate (Latency Constrained)" -Encoder.FFmpeg.AMF.RateControl.Mode.CBR="Constant Bitrate" -Encoder.FFmpeg.AMF.RateControl.LookAhead="Look Ahead" -Encoder.FFmpeg.AMF.RateControl.FrameSkipping="Frame Skipping" +Encoder.FFmpeg.AMF.RateControl.Mode.constqp="Constant Quantization Parameter" +Encoder.FFmpeg.AMF.RateControl.Mode.vbr_latency="Latency Constrained Variable Bitrate" +Encoder.FFmpeg.AMF.RateControl.Mode.vbr_peak="Variable Bitrate" +Encoder.FFmpeg.AMF.RateControl.Mode.vbr="Variable Bitrate" +Encoder.FFmpeg.AMF.RateControl.Mode.cbr="Constant Bitrate" +Encoder.FFmpeg.AMF.RateControl.Mode.qvbr="Quality Constrained Variable Bitrate" +Encoder.FFmpeg.AMF.RateControl.Mode.hqvbr="High Quality Variable Bitrate" +Encoder.FFmpeg.AMF.RateControl.Mode.hqcbr="High Quality Constant Bitrate" +Encoder.FFmpeg.AMF.RateControl.PreEncode="Pre-Encode" Encoder.FFmpeg.AMF.RateControl.Limits="Limits" Encoder.FFmpeg.AMF.RateControl.Limits.BufferSize="Buffer Size" +Encoder.FFmpeg.AMF.RateControl.Limits.Quality="Target Quality" Encoder.FFmpeg.AMF.RateControl.Limits.Bitrate.Target="Target Bitrate" Encoder.FFmpeg.AMF.RateControl.Limits.Bitrate.Maximum="Maximum Bitrate" Encoder.FFmpeg.AMF.RateControl.QP="Quantization Parameters" -Encoder.FFmpeg.AMF.RateControl.QP.I="I-Frame QP" -Encoder.FFmpeg.AMF.RateControl.QP.P="P-Frame QP" -Encoder.FFmpeg.AMF.RateControl.QP.B="B-Frame QP" +Encoder.FFmpeg.AMF.RateControl.QP.I="I-Frame" +Encoder.FFmpeg.AMF.RateControl.QP.P="P-Frame" +Encoder.FFmpeg.AMF.RateControl.QP.B="B-Frame" +Encoder.FFmpeg.AMF.PreAnalysis="Pre-Analysis" +Encoder.FFmpeg.AMF.PreAnalysis.LookAhead="Look Ahead" +Encoder.FFmpeg.AMF.PreAnalysis.SceneChangeDetection="Scene Change Detection" +Encoder.FFmpeg.AMF.PreAnalysis.SceneChangeDetection.low="Low Sensitivity" +Encoder.FFmpeg.AMF.PreAnalysis.SceneChangeDetection.medium="Medium Sensitivity" +Encoder.FFmpeg.AMF.PreAnalysis.SceneChangeDetection.high="High Sensitivity" +Encoder.FFmpeg.AMF.PreAnalysis.StaticSceneDetection="Static Scene Detection" +Encoder.FFmpeg.AMF.PreAnalysis.StaticSceneDetection.low="Low Sensitivity" +Encoder.FFmpeg.AMF.PreAnalysis.StaticSceneDetection.medium="Medium Sensitivity" +Encoder.FFmpeg.AMF.PreAnalysis.StaticSceneDetection.high="High Sensitivity" +Encoder.FFmpeg.AMF.PreAnalysis.AQ="Adaptive Quantization" +Encoder.FFmpeg.AMF.PreAnalysis.AQ.Perceptual="Perceptual AQ" +Encoder.FFmpeg.AMF.PreAnalysis.AQ.Perceptual.none="Disabled" +Encoder.FFmpeg.AMF.PreAnalysis.AQ.Perceptual.caq="Content AQ" +Encoder.FFmpeg.AMF.PreAnalysis.AQ.Content="Content AQ Strength" +Encoder.FFmpeg.AMF.PreAnalysis.AQ.Content.low="Low" +Encoder.FFmpeg.AMF.PreAnalysis.AQ.Content.medium="Medium" +Encoder.FFmpeg.AMF.PreAnalysis.AQ.Content.high="High" +Encoder.FFmpeg.AMF.PreAnalysis.AQ.Temporal="Temporal AQ" +Encoder.FFmpeg.AMF.PreAnalysis.AQ.Temporal.none="Disabled" +Encoder.FFmpeg.AMF.PreAnalysis.AQ.Temporal.1="Non-Gaming Content" +Encoder.FFmpeg.AMF.PreAnalysis.AQ.Temporal.2="Gaming Content" +Encoder.FFmpeg.AMF.PreAnalysis.HighMotionQualityBoost="High Motion Quality Boost" +Encoder.FFmpeg.AMF.PreAnalysis.HighMotionQualityBoost.none="Disabled" +Encoder.FFmpeg.AMF.PreAnalysis.HighMotionQualityBoost.auto="Automatic" Encoder.FFmpeg.AMF.Other="Other Options" Encoder.FFmpeg.AMF.Other.BFrames="Maximum B-Frames" Encoder.FFmpeg.AMF.Other.BFrameReferences="B-Frame References" -Encoder.FFmpeg.AMF.Other.ReferenceFrames="Reference Frames" -Encoder.FFmpeg.AMF.Other.EnforceHRD="Enforce HRD" -Encoder.FFmpeg.AMF.Other.VBAQ="VBAQ" -Encoder.FFmpeg.AMF.Other.AccessUnitDelimiter="Access Unit Delimiter" +Encoder.FFmpeg.AMF.Other.VBAQ="Variance Based Adaptive Quantization" +Encoder.FFmpeg.AMF.Other.HighMotionQualityBoost="High Motion Quality Boost" +Encoder.FFmpeg.AMF.Other.ReferenceFrames="Maximum Reference Frames" # Encoder/FFmpeg/NVENC Encoder.FFmpeg.NVENC.Preset="Preset" @@ -214,11 +183,9 @@ Encoder.FFmpeg.NVENC.RateControl.Limits.Quality="Target Quality" Encoder.FFmpeg.NVENC.RateControl.Limits.Bitrate.Target="Target Bitrate" Encoder.FFmpeg.NVENC.RateControl.Limits.Bitrate.Maximum="Maximum Bitrate" Encoder.FFmpeg.NVENC.RateControl.QP="Quantization Parameters" -Encoder.FFmpeg.NVENC.RateControl.QP.Minimum="Minimum QP" -Encoder.FFmpeg.NVENC.RateControl.QP.Maximum="Maximum QP" -Encoder.FFmpeg.NVENC.RateControl.QP.I="I-Frame QP" -Encoder.FFmpeg.NVENC.RateControl.QP.P="P-Frame QP" -Encoder.FFmpeg.NVENC.RateControl.QP.B="B-Frame QP" +Encoder.FFmpeg.NVENC.RateControl.QP.I="I-Frame" +Encoder.FFmpeg.NVENC.RateControl.QP.P="P-Frame" +Encoder.FFmpeg.NVENC.RateControl.QP.B="B-Frame" Encoder.FFmpeg.NVENC.AQ="Adaptive Quantization" Encoder.FFmpeg.NVENC.AQ.Spatial="Spatial Adaptive Quantization" Encoder.FFmpeg.NVENC.AQ.Strength="Spatial Adaptive Quantization Strength" @@ -232,7 +199,7 @@ Encoder.FFmpeg.NVENC.Other.BFrameReferenceMode.each="Each B-Frame will be used a Encoder.FFmpeg.NVENC.Other.ZeroLatency="Zero Latency" Encoder.FFmpeg.NVENC.Other.WeightedPrediction="Weighted Prediction" Encoder.FFmpeg.NVENC.Other.NonReferencePFrames="Non-reference P-Frames" -Encoder.FFmpeg.NVENC.Other.ReferenceFrames="Reference Frames" +Encoder.FFmpeg.NVENC.Other.ReferenceFrames="Maximum Reference Frames" Encoder.FFmpeg.NVENC.Other.LowDelayKeyFrameScale="Low Delay Key-Frame Scale" # Encoder/FFmpeg/CFHD @@ -519,16 +486,25 @@ Source.Mirror.Source.Audio.Layout.FullSurround="Full Surround" # Codec: AV1 Codec.AV1="AV1" Codec.AV1.Profile="Profile" +Codec.AV1.Profile.0="Main" +Codec.AV1.Profile.main="Main" Codec.AV1.Profile.Main="Main" +Codec.AV1.Profile.1="High" +Codec.AV1.Profile.high="High" Codec.AV1.Profile.High="High" +Codec.AV1.Profile.2="Professional" +Codec.AV1.Profile.professional="Professional" Codec.AV1.Profile.Professional="Professional" +Codec.AV1.Level="Level" # Codec: H264 Codec.H264="H264" Codec.H264.Profile="Profile" Codec.H264.Profile.baseline="Baseline" +Codec.H264.Profile.constrained_baseline="Constrained Baseline" Codec.H264.Profile.main="Main" Codec.H264.Profile.high="High" +Codec.H264.Profile.constrained_high="Constrained High" Codec.H264.Profile.high444p="High 4:4:4 Predictive" Codec.H264.Level="Level" @@ -560,3 +536,16 @@ Codec.DNxHR.Profile.dnxhr_sq="DNxHR SQ (4:2:2)" Codec.DNxHR.Profile.dnxhr_hq="DNxHR HQ (4:2:2)" Codec.DNxHR.Profile.dnxhr_hqx="DNxHR HQX (4:2:2)" Codec.DNxHR.Profile.dnxhr_444="DNxHR 444 (4:4:4)" + +# Component: Spout/Sink +Source.Sink="Sink" +Source.Sink.Video="Video" +Source.Sink.Video.Source="Source Name" +Source.Sink.Audio="Audio" +Source.Sink.Audio.Source="Source Name" +Filter.Spout.AsyncVideo="Spout (Async Video)" +Filter.Spout.AsyncVideo.Name="Name" +Filter.Spout.Video="Spout (Video)" +Filter.Spout.Video.Name="Name" +Filter.Spout.Audio="Spout (Audio)" +Filter.Spout.Audio.Name="Name" diff --git a/patches/third-party/obs-studio/0001-libobs-Allow-building-with-DENABLE_UI-OFF.patch b/patches/third-party/obs-studio/0001-libobs-Allow-building-with-DENABLE_UI-OFF.patch new file mode 100644 index 0000000..4f3bdd2 --- /dev/null +++ b/patches/third-party/obs-studio/0001-libobs-Allow-building-with-DENABLE_UI-OFF.patch @@ -0,0 +1,28 @@ +From 475f1b374ab196117f09d239dca9076a33857fd3 Mon Sep 17 00:00:00 2001 +From: Michael Fabian 'Xaymar' Dirks +Date: Mon, 8 Jan 2024 09:22:16 +0100 +Subject: [PATCH] libobs: Allow building with -DENABLE_UI=OFF + +An oversight only creates the OBS::w32-pthreads target when -DENABLE_UI=ON. +--- + libobs/cmake/os-windows.cmake | 4 ++++ + 1 file changed, 4 insertions(+) + +diff --git a/libobs/cmake/os-windows.cmake b/libobs/cmake/os-windows.cmake +index 04500d1b0..5c80325e6 100644 +--- a/libobs/cmake/os-windows.cmake ++++ b/libobs/cmake/os-windows.cmake +@@ -43,6 +43,10 @@ target_compile_options(libobs PRIVATE $<$:/EHc->) + set_source_files_properties(obs-win-crash-handler.c PROPERTIES COMPILE_DEFINITIONS + OBS_VERSION="${OBS_VERSION_CANONICAL}") + ++if(NOT TARGET OBS::w32-pthreads) ++add_subdirectory("${CMAKE_SOURCE_DIR}/deps/w32-pthreads" "${CMAKE_BINARY_DIR}/deps/w32-pthreads") ++endif() ++ + target_link_libraries( + libobs + PRIVATE Avrt +-- +2.42.0.windows.1 + diff --git a/source/common.hpp b/source/common.hpp index 0e75aba..2626d4b 100644 --- a/source/common.hpp +++ b/source/common.hpp @@ -1,5 +1,5 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks // AUTOGENERATED COPYRIGHT HEADER END #pragma once diff --git a/source/configuration.cpp b/source/configuration.cpp index 37b58ee..4f97b07 100644 --- a/source/configuration.cpp +++ b/source/configuration.cpp @@ -43,7 +43,7 @@ streamfx::configuration::configuration() : _config_path(), _data(), _task_lock() if (!std::filesystem::exists(_config_path) || !std::filesystem::is_regular_file(_config_path)) { throw std::runtime_error("Configuration does not exist."); } else { - obs_data_t* data = obs_data_create_from_json_file_safe(_config_path.u8string().c_str(), path_backup_ext.data()); + obs_data_t* data = obs_data_create_from_json_file_safe(reinterpret_cast(_config_path.string().c_str()), path_backup_ext.data()); if (!data) { throw std::runtime_error("Failed to load configuration from disk."); } else { @@ -59,7 +59,7 @@ void streamfx::configuration::save() { std::lock_guard lg(_task_lock); if (!_save_task || _save_task->is_completed()) { - _save_task = streamfx::threadpool()->push([this](streamfx::util::threadpool::task_data_t) { + _save_task = streamfx::util::threadpool::threadpool::instance()->push([this](streamfx::util::threadpool::task_data_t) { // ToDo: Implement delayed tasks in ::threadpool. std::this_thread::sleep_for(std::chrono::milliseconds(100)); @@ -69,7 +69,7 @@ void streamfx::configuration::save() if (_config_path.has_parent_path()) { std::filesystem::create_directories(_config_path.parent_path()); } - if (!obs_data_save_json_safe(_data.get(), _config_path.u8string().c_str(), ".tmp", path_backup_ext.data())) { + if (!obs_data_save_json_safe(_data.get(), reinterpret_cast(_config_path.string().c_str()), ".tmp", path_backup_ext.data())) { D_LOG_ERROR("Failed to save configuration file.", nullptr); } }); @@ -108,11 +108,12 @@ std::shared_ptr streamfx::configuration::instance() static std::shared_ptr loader_instance; -static auto loader = streamfx::loader( - []() { // Initalizer +static auto loader = streamfx::component( + "core::configuration", + []() { // Initializer loader_instance = streamfx::configuration::instance(); }, []() { // Finalizer loader_instance.reset(); }, - streamfx::loader_priority::HIGHER); // Attempt to load after critical functionality. + {"core::threadpool"}); diff --git a/source/configuration.hpp b/source/configuration.hpp index 2574301..453f6b6 100644 --- a/source/configuration.hpp +++ b/source/configuration.hpp @@ -1,5 +1,5 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks // AUTOGENERATED COPYRIGHT HEADER END #pragma once diff --git a/source/gfx/gfx-mipmapper.cpp b/source/gfx/gfx-mipmapper.cpp index 6974f98..5668d0e 100644 --- a/source/gfx/gfx-mipmapper.cpp +++ b/source/gfx/gfx-mipmapper.cpp @@ -1,5 +1,6 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2022-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2022 lainon // AUTOGENERATED COPYRIGHT HEADER END #include "gfx-mipmapper.hpp" @@ -27,17 +28,17 @@ struct d3d_info { ID3D11Resource* target = nullptr; }; -void d3d_initialize(d3d_info& info, std::shared_ptr source, std::shared_ptr target) +static void d3d_initialize(d3d_info& info, std::shared_ptr source, std::shared_ptr target) { - info.target = reinterpret_cast(gs_texture_get_obj(target->get_object())); + info.target = reinterpret_cast(gs_texture_get_obj(*target)); info.device = reinterpret_cast(gs_get_device_obj()); info.device->GetImmediateContext(&info.context); } -void d3d_copy_subregion(d3d_info& info, std::shared_ptr source, uint32_t mip_level, uint32_t width, uint32_t height) +static void d3d_copy_subregion(d3d_info& info, gs_texture_t* source, uint32_t mip_level, uint32_t width, uint32_t height) { D3D11_BOX box = {0, 0, 0, width, height, 1}; - auto source_ref = reinterpret_cast(gs_texture_get_obj(source->get_object())); + auto source_ref = reinterpret_cast(gs_texture_get_obj(source)); info.context->CopySubresourceRegion(info.target, mip_level, 0, 0, 0, source_ref, 0, &box); } @@ -48,43 +49,43 @@ struct opengl_info { GLuint fbo = 0; }; -std::string opengl_translate_error(GLenum error) +static std::string opengl_translate_error(GLenum error) { #define TRANSLATE_CASE(X) \ case X: \ return #X; switch (error) { - TRANSLATE_CASE(GL_NO_ERROR); - TRANSLATE_CASE(GL_INVALID_ENUM); - TRANSLATE_CASE(GL_INVALID_VALUE); - TRANSLATE_CASE(GL_INVALID_OPERATION); - TRANSLATE_CASE(GL_STACK_OVERFLOW); - TRANSLATE_CASE(GL_STACK_UNDERFLOW); - TRANSLATE_CASE(GL_OUT_OF_MEMORY); - TRANSLATE_CASE(GL_INVALID_FRAMEBUFFER_OPERATION); + TRANSLATE_CASE(GL_NO_ERROR) break; + TRANSLATE_CASE(GL_INVALID_ENUM) break; + TRANSLATE_CASE(GL_INVALID_VALUE) break; + TRANSLATE_CASE(GL_INVALID_OPERATION) break; + TRANSLATE_CASE(GL_STACK_OVERFLOW) break; + TRANSLATE_CASE(GL_STACK_UNDERFLOW) break; + TRANSLATE_CASE(GL_OUT_OF_MEMORY) break; + TRANSLATE_CASE(GL_INVALID_FRAMEBUFFER_OPERATION) break; } return std::to_string(error); #undef TRANSLATE_CASE } -std::string opengl_translate_framebuffer_status(GLenum error) +static std::string opengl_translate_framebuffer_status(GLenum error) { #define TRANSLATE_CASE(X) \ case X: \ return #X; switch (error) { - TRANSLATE_CASE(GL_FRAMEBUFFER_COMPLETE); - TRANSLATE_CASE(GL_FRAMEBUFFER_UNDEFINED); - TRANSLATE_CASE(GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT); - TRANSLATE_CASE(GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT); - TRANSLATE_CASE(GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER); - TRANSLATE_CASE(GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER); - TRANSLATE_CASE(GL_FRAMEBUFFER_UNSUPPORTED); - TRANSLATE_CASE(GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE); - TRANSLATE_CASE(GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS); + TRANSLATE_CASE(GL_FRAMEBUFFER_COMPLETE) break; + TRANSLATE_CASE(GL_FRAMEBUFFER_UNDEFINED) break; + TRANSLATE_CASE(GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT) break; + TRANSLATE_CASE(GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT) break; + TRANSLATE_CASE(GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER) break; + TRANSLATE_CASE(GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER) break; + TRANSLATE_CASE(GL_FRAMEBUFFER_UNSUPPORTED) break; + TRANSLATE_CASE(GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE) break; + TRANSLATE_CASE(GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS) break; } return std::to_string(error); @@ -105,21 +106,21 @@ std::string opengl_translate_framebuffer_status(GLenum error) throw std::runtime_error(sstr.str()); \ } -void opengl_initialize(opengl_info& info, std::shared_ptr source, std::shared_ptr target) +static void opengl_initialize(opengl_info& info, std::shared_ptr source, std::shared_ptr target) { - info.target = *reinterpret_cast(gs_texture_get_obj(target->get_object())); + info.target = *reinterpret_cast(gs_texture_get_obj(*target)); glGenFramebuffers(1, &info.fbo); } -void opengl_finalize(opengl_info& info) +static void opengl_finalize(opengl_info& info) { glDeleteFramebuffers(1, &info.fbo); } -void opengl_copy_subregion(opengl_info& info, std::shared_ptr source, uint32_t mip_level, uint32_t width, uint32_t height) +static void opengl_copy_subregion(opengl_info& info, gs_texture_t* source, uint32_t mip_level, uint32_t width, uint32_t height) { - GLuint source_ref = *reinterpret_cast(gs_texture_get_obj(source->get_object())); + GLuint source_ref = *reinterpret_cast(gs_texture_get_obj(source)); // Source -> Texture Unit 0, Read Color Framebuffer glActiveTexture(GL_TEXTURE0); @@ -164,7 +165,6 @@ void opengl_copy_subregion(opengl_info& info, std::shared_ptr(1 + std::lroundl(floor(log2(std::max(static_cast(width), static_cast(height)))))); } -void streamfx::gfx::mipmapper::rebuild(std::shared_ptr source, std::shared_ptr target) +void streamfx::gfx::mipmapper::rebuild(std::shared_ptr source, std::shared_ptr target, mip_generator generator) { { // Validate arguments and structure. if (!source || !target) @@ -197,27 +198,41 @@ void streamfx::gfx::mipmapper::rebuild(std::shared_ptrget_width() != target->get_width()) || (source->get_height() != target->get_height())) { + if ((source->width() != target->width()) || (source->height() != target->height())) { throw std::invalid_argument("source and target must have same size"); } - // Ensure texture types match - if ((source->get_type() != target->get_type())) { - throw std::invalid_argument("source and target must have same type"); - } - // Ensure texture formats match - if ((source->get_color_format() != target->get_color_format())) { + if ((source->color_format() != target->color_format())) { throw std::invalid_argument("source and target must have same format"); } } + std::string technique = "Linear"; + bool separable_technique = false; + switch (generator) { + case mip_generator::MAGIC_KERNEL_SHARP_PLUS_2021: + separable_technique = true; + technique = "MagicKernelSharp2021Separable"; + break; + case mip_generator::MAGIC_KERNEL_2011: + separable_technique = false; + technique = "MagicKernel2011"; + break; + case mip_generator::LINEAR: + default: + break; + } + // Get a unique lock on the graphics context. auto gctx = streamfx::obs::gs::context(); - // Do we need to recreate the render target for a different format? - if ((!_rt) || (source->get_color_format() != _rt->get_color_format())) { - _rt = std::make_unique(source->get_color_format(), GS_ZS_NONE); + // Initialize some render targets, and their texture storage. + auto rt0 = streamfx::obs::gs::texrender::pool::instance()->acquire(source->color_format(), GS_ZS_NONE); + auto rt1 = streamfx::obs::gs::texrender::pool::instance()->acquire(source->color_format(), GS_ZS_NONE); + auto rt2 = rt1; + if (separable_technique) { + rt2 = streamfx::obs::gs::texrender::pool::instance()->acquire(source->color_format(), GS_ZS_NONE); } // Initialize API Handlers. @@ -233,85 +248,133 @@ void streamfx::gfx::mipmapper::rebuild(std::shared_ptrget_type() == streamfx::obs::gs::texture::type::Normal) { - uint32_t width = source->get_width(); - uint32_t height = source->get_height(); - size_t max_mip_level = calculate_max_mip_level(width, height); + uint32_t width = source->width(); + uint32_t height = source->height(); + size_t max_mip_level = calculate_max_mip_level(width, height); - { -#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG - auto cctr = streamfx::obs::gs::debug_marker(streamfx::obs::gs::debug_color_azure_radiance, "Mip Level %" PRId64 "", 0); -#endif - - // Retrieve maximum mip map level. + { // Copy mip level 0 to output. #ifdef _WIN32 - if (gs_get_device_type() == GS_DEVICE_DIRECT3D_11) { - d3d_copy_subregion(d3dinfo, source, 0, width, height); - } -#endif - if (gs_get_device_type() == GS_DEVICE_OPENGL) { - opengl_copy_subregion(oglinfo, source, 0, width, height); - } + if (gs_get_device_type() == GS_DEVICE_DIRECT3D_11) { + d3d_copy_subregion(d3dinfo, *source, 0, width, height); } - - // Set up rendering state. - gs_blend_state_push(); - gs_reset_blend_state(); - gs_enable_blending(false); - gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO); - gs_enable_color(true, true, true, true); - gs_enable_depth_test(false); - gs_enable_stencil_test(false); - gs_enable_stencil_write(false); - gs_set_cull_mode(GS_NEITHER); - - // sRGB support. - bool old_srgb = gs_framebuffer_srgb_enabled(); - gs_enable_framebuffer_srgb(gs_get_linear_srgb()); - - // Render each mip map level. - for (size_t mip = 1; mip < max_mip_level; mip++) { -#if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG - auto cctr = streamfx::obs::gs::debug_marker(streamfx::obs::gs::debug_color_azure_radiance, "Mip Level %" PRIuMAX, mip); #endif + if (gs_get_device_type() == GS_DEVICE_OPENGL) { + opengl_copy_subregion(oglinfo, *source, 0, width, height); + } + } - uint32_t cwidth = std::max(width >> mip, 1); - uint32_t cheight = std::max(height >> mip, 1); - float_t iwidth = 1.f / static_cast(cwidth); - float_t iheight = 1.f / static_cast(cheight); + // Set up rendering state. + gs_blend_state_push(); + gs_reset_blend_state(); + gs_enable_blending(false); + gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO); + gs_enable_color(true, true, true, true); + gs_enable_depth_test(false); + gs_enable_stencil_test(false); + gs_enable_stencil_write(false); + gs_set_cull_mode(GS_NEITHER); + // sRGB support. + bool was_srgb_enabled = gs_framebuffer_srgb_enabled(); + bool enable_srgb = gs_get_linear_srgb(); + gs_enable_framebuffer_srgb(enable_srgb); + + // Render each mip level. + for (size_t mip = 1; mip < max_mip_level; mip++) { + uint32_t cwidth = std::max(width >> mip, 1); + uint32_t cheight = std::max(height >> mip, 1); + float iwidth = 1.f / static_cast(cwidth); + float iheight = 1.f / static_cast(cheight); + + if (!separable_technique) { try { - auto op = _rt->render(cwidth, cheight); + auto op = rt0->render(cwidth, cheight); gs_ortho(0, 1, 0, 1, 0, 1); - _effect.get_parameter("image").set_texture(target, gs_get_linear_srgb()); + if (mip == 1) { + _effect.get_parameter("image").set_texture(source, enable_srgb); + } else { + _effect.get_parameter("image").set_texture(rt1->get_object(), enable_srgb); + } _effect.get_parameter("imageTexel").set_float2(iwidth, iheight); - _effect.get_parameter("level").set_int(int32_t(mip - 1)); - while (gs_effect_loop(_effect.get_object(), "Draw")) { + _effect.get_parameter("separableTexel").set_float2(iwidth, iheight); + while (gs_effect_loop(_effect.get_object(), technique.c_str())) { _gfx_util->draw_fullscreen_triangle(); } } catch (...) { } + } else { + for (size_t pass = 0; gs_effect_loop(_effect.get_object(), technique.c_str()); pass++) { + // Flow: + // Mip-Level 1: + // - Pass 0: + // - Swap rt0, rt2 + // - Read from source + // - Write to rt0 + // - Pass 1: + // - Swap rt0, rt2 + // - Read from rt2 + // - Write to rt0 + // - Pass 2: + // - Swap rt0, rt2 + // - Read from rt2 + // - Write to rt0 + // - Copy rt0 to target + // - Swap rt0, rt1 + // + // Mip-Level 2: + // - Pass 0: + // - Swap rt0, rt2 + // - Read from rt1 + // - Write to rt0 + // - (identical to Mip-Level 1) + // + // This works only if we swap {rt0, rt2} first, otherwise we end up only storing the data from the previous pass. - // Copy from the render target to the target mip level. -#ifdef _WIN32 - if (gs_get_device_type() == GS_DEVICE_DIRECT3D_11) { - d3d_copy_subregion(d3dinfo, _rt->get_texture(), static_cast(mip), cwidth, cheight); - } -#endif - if (gs_get_device_type() == GS_DEVICE_OPENGL) { - opengl_copy_subregion(oglinfo, _rt->get_texture(), static_cast(mip), cwidth, cheight); + // Swap rt0, rt2. + std::swap(rt0, rt2); + + if (mip == 1) { + _effect.get_parameter("image").set_texture(source, enable_srgb); + } else if (pass == 0) { + _effect.get_parameter("image").set_texture(rt1->get_object(), enable_srgb); + } else { + _effect.get_parameter("image").set_texture(rt2->get_object(), enable_srgb); + } + _effect.get_parameter("imageTexel").set_float2(iwidth, iheight); + if ((pass % 2) == 0) { + _effect.get_parameter("separableTexel").set_float2(iwidth, 0.); + } else { + _effect.get_parameter("separableTexel").set_float2(0., iheight); + } + + try { + auto op = rt0->render(cwidth, cheight); + gs_ortho(0, 1, 0, 1, 0, 1); + _gfx_util->draw_fullscreen_triangle(); + } catch (...) { + } } } - // Clean up rendering state. - gs_enable_framebuffer_srgb(old_srgb); - gs_blend_state_pop(); + // Copy from the render target to the target mip level. +#ifdef _WIN32 + if (gs_get_device_type() == GS_DEVICE_DIRECT3D_11) { + d3d_copy_subregion(d3dinfo, rt0->get_object(), static_cast(mip), cwidth, cheight); + } +#endif + if (gs_get_device_type() == GS_DEVICE_OPENGL) { + opengl_copy_subregion(oglinfo, rt0->get_object(), static_cast(mip), cwidth, cheight); + } - } else { - throw std::runtime_error("Only 2D Textures support Mip-mapping."); + // Swap the render targets. + std::swap(rt0, rt1); } + // Clean up rendering state. + gs_enable_framebuffer_srgb(was_srgb_enabled); + gs_blend_state_pop(); + // Finalize API handlers. if (gs_get_device_type() == GS_DEVICE_OPENGL) { opengl_finalize(oglinfo); diff --git a/source/gfx/gfx-mipmapper.hpp b/source/gfx/gfx-mipmapper.hpp index ec37c01..e52587a 100644 --- a/source/gfx/gfx-mipmapper.hpp +++ b/source/gfx/gfx-mipmapper.hpp @@ -1,12 +1,12 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2022-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks // AUTOGENERATED COPYRIGHT HEADER END #pragma once #include "common.hpp" #include "gfx/gfx-util.hpp" #include "obs/gs/gs-effect.hpp" -#include "obs/gs/gs-rendertarget.hpp" +#include "obs/gs/gs-texrender.hpp" #include "obs/gs/gs-texture.hpp" #include "obs/gs/gs-vertexbuffer.hpp" @@ -23,10 +23,16 @@ */ namespace streamfx::gfx { + enum class mip_generator : uint8_t { + LINEAR, + MAGIC_KERNEL_2011, + MAGIC_KERNEL_SHARP_PLUS_2021, + + }; + class mipmapper { - std::unique_ptr _rt; - streamfx::obs::gs::effect _effect; - std::shared_ptr _gfx_util; + streamfx::obs::gs::effect _effect; + std::shared_ptr _gfx_util; public: ~mipmapper(); @@ -34,6 +40,6 @@ namespace streamfx::gfx { uint32_t calculate_max_mip_level(uint32_t width, uint32_t height); - void rebuild(std::shared_ptr source, std::shared_ptr target); + void rebuild(std::shared_ptr source, std::shared_ptr target, mip_generator generator = mip_generator::LINEAR); }; } // namespace streamfx::gfx diff --git a/source/gfx/gfx-opengl.hpp b/source/gfx/gfx-opengl.hpp index e90af37..c52dfb0 100644 --- a/source/gfx/gfx-opengl.hpp +++ b/source/gfx/gfx-opengl.hpp @@ -1,5 +1,5 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2021-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks // AUTOGENERATED COPYRIGHT HEADER END #include "warning-disable.hpp" diff --git a/source/gfx/gfx-source-texture.cpp b/source/gfx/gfx-source-texture.cpp index 1b96196..1e6ab06 100644 --- a/source/gfx/gfx-source-texture.cpp +++ b/source/gfx/gfx-source-texture.cpp @@ -1,5 +1,5 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2018-2023 Michael Fabian 'Xaymar' Dirks // AUTOGENERATED COPYRIGHT HEADER END #include "gfx-source-texture.hpp" @@ -31,7 +31,7 @@ streamfx::gfx::source_texture::source_texture(streamfx::obs::source child, strea throw std::runtime_error("Child contains Parent"); } - _rt = std::make_shared(GS_RGBA, GS_ZS_NONE); + _rt = std::make_shared(GS_RGBA, GS_ZS_NONE); } obs_source_t* streamfx::gfx::source_texture::get_object() @@ -74,7 +74,7 @@ std::shared_ptr streamfx::gfx::source_texture::rende auto op = _rt->render(static_cast(width), static_cast(height)); vec4 black; vec4_zero(&black); - gs_ortho(0, static_cast(width), 0, static_cast(height), 0, 1); + gs_ortho(0, static_cast(width), 0, static_cast(height), 0, 1); gs_clear(GS_CLEAR_COLOR, &black, 0, 0); obs_source_video_render(_child.get()); } diff --git a/source/gfx/gfx-source-texture.hpp b/source/gfx/gfx-source-texture.hpp index 3d64d9c..44166db 100644 --- a/source/gfx/gfx-source-texture.hpp +++ b/source/gfx/gfx-source-texture.hpp @@ -1,10 +1,10 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2018-2023 Michael Fabian 'Xaymar' Dirks // AUTOGENERATED COPYRIGHT HEADER END #pragma once #include "common.hpp" -#include "obs/gs/gs-rendertarget.hpp" +#include "obs/gs/gs-texrender.hpp" #include "obs/gs/gs-texture.hpp" #include "obs/obs-source.hpp" #include "obs/obs-weak-source.hpp" @@ -18,7 +18,7 @@ namespace streamfx::gfx { streamfx::obs::source _parent; streamfx::obs::source _child; - std::shared_ptr _rt; + std::shared_ptr _rt; public: ~source_texture(); diff --git a/source/gfx/gfx-util.cpp b/source/gfx/gfx-util.cpp index ea78f35..3f728f1 100644 --- a/source/gfx/gfx-util.cpp +++ b/source/gfx/gfx-util.cpp @@ -1,5 +1,6 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2022-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2021-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2022 lainon // AUTOGENERATED COPYRIGHT HEADER END #include "gfx-util.hpp" @@ -67,7 +68,7 @@ void streamfx::gfx::util::draw_point(float x, float y, uint32_t color) obs::gs::context gctx{}; if (!_point_vb) { - _point_vb = std::make_shared(uint32_t{1}, uint8_t{1}); + _point_vb = std::make_shared(uint32_t{1}, uint8_t{1}); } { @@ -89,7 +90,7 @@ void streamfx::gfx::util::draw_line(float x, float y, float x2, float y2, uint32 obs::gs::context gctx{}; if (!_line_vb) { - _line_vb = std::make_shared(uint32_t{2}, uint8_t{1}); + _line_vb = std::make_shared(uint32_t{2}, uint8_t{1}); } { @@ -116,7 +117,7 @@ void streamfx::gfx::util::draw_arrow(float x, float y, float x2, float y2, float obs::gs::context gctx{}; if (!_arrow_vb) { - _arrow_vb = std::make_shared(uint32_t{5}, uint8_t{1}); + _arrow_vb = std::make_shared(uint32_t{5}, uint8_t{1}); } float dx = x2 - x; @@ -183,7 +184,7 @@ void streamfx::gfx::util::draw_rectangle(float x, float y, float w, float h, boo obs::gs::context gctx{}; if (!_quad_vb) { - _quad_vb = std::make_shared(uint32_t{5}, uint8_t{1}); + _quad_vb = std::make_shared(uint32_t{5}, uint8_t{1}); } if (frame) { @@ -253,7 +254,7 @@ void streamfx::gfx::util::draw_rectangle(float x, float y, float w, float h, boo void streamfx::gfx::util::draw_fullscreen_triangle() { if (!_fstri_vb) { - _fstri_vb = std::make_shared(uint32_t(3), uint8_t(1)); + _fstri_vb = std::make_shared(uint32_t(3), uint8_t(1)); { auto vtx = _fstri_vb->at(0); vec3_set(vtx.position, 0, 0, 0); diff --git a/source/gfx/gfx-util.hpp b/source/gfx/gfx-util.hpp index 0b19aaa..170ce14 100644 --- a/source/gfx/gfx-util.hpp +++ b/source/gfx/gfx-util.hpp @@ -1,5 +1,5 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2022-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks // AUTOGENERATED COPYRIGHT HEADER END #pragma once @@ -13,11 +13,11 @@ namespace streamfx::gfx { class util { std::shared_ptr<::streamfx::obs::gs::effect> _effect; - std::shared_ptr<::streamfx::obs::gs::vertex_buffer> _point_vb; - std::shared_ptr<::streamfx::obs::gs::vertex_buffer> _line_vb; - std::shared_ptr<::streamfx::obs::gs::vertex_buffer> _arrow_vb; - std::shared_ptr<::streamfx::obs::gs::vertex_buffer> _quad_vb; - std::shared_ptr<::streamfx::obs::gs::vertex_buffer> _fstri_vb; + std::shared_ptr<::streamfx::obs::gs::vertexbuffer> _point_vb; + std::shared_ptr<::streamfx::obs::gs::vertexbuffer> _line_vb; + std::shared_ptr<::streamfx::obs::gs::vertexbuffer> _arrow_vb; + std::shared_ptr<::streamfx::obs::gs::vertexbuffer> _quad_vb; + std::shared_ptr<::streamfx::obs::gs::vertexbuffer> _fstri_vb; public /* Singleton */: static std::shared_ptr get(); diff --git a/source/obs/gs/gs-effect-parameter.cpp b/source/obs/gs/gs-effect-parameter.cpp index 9131250..708abf1 100644 --- a/source/obs/gs/gs-effect-parameter.cpp +++ b/source/obs/gs/gs-effect-parameter.cpp @@ -216,33 +216,33 @@ void streamfx::obs::gs::effect_parameter::set_bool_array(bool v[], std::size_t s gs_effect_set_val(get(), v, sz); } -void streamfx::obs::gs::effect_parameter::set_float(float_t x) +void streamfx::obs::gs::effect_parameter::set_float(float x) { if (get_type() != type::Float) throw std::bad_cast(); gs_effect_set_float(get(), x); } -void streamfx::obs::gs::effect_parameter::get_float(float_t& x) +void streamfx::obs::gs::effect_parameter::get_float(float& x) { if (get_type() != type::Float) throw std::bad_cast(); void* ptr = gs_effect_get_val(get()); if (ptr) { - x = *reinterpret_cast(ptr); + x = *reinterpret_cast(ptr); bfree(ptr); } else { x = 0; } } -void streamfx::obs::gs::effect_parameter::get_default_float(float_t& x) +void streamfx::obs::gs::effect_parameter::get_default_float(float& x) { if (get_type() != type::Float) throw std::bad_cast(); void* ptr = gs_effect_get_default_val(get()); if (ptr) { - x = *reinterpret_cast(ptr); + x = *reinterpret_cast(ptr); bfree(ptr); } else { x = 0; @@ -266,7 +266,7 @@ void streamfx::obs::gs::effect_parameter::get_default_float2(vec2& v) get_default_float2(v.x, v.y); } -void streamfx::obs::gs::effect_parameter::set_float2(float_t x, float_t y) +void streamfx::obs::gs::effect_parameter::set_float2(float x, float y) { vec2 data; data.x = x; @@ -274,28 +274,28 @@ void streamfx::obs::gs::effect_parameter::set_float2(float_t x, float_t y) set_float2(data); } -void streamfx::obs::gs::effect_parameter::get_float2(float_t& x, float_t& y) +void streamfx::obs::gs::effect_parameter::get_float2(float& x, float& y) { if (get_type() != type::Float2) throw std::bad_cast(); uint8_t* ptr = static_cast(gs_effect_get_val(get())); if (ptr) { - x = *reinterpret_cast(ptr); - y = *reinterpret_cast(ptr + sizeof(float_t)); + x = *reinterpret_cast(ptr); + y = *reinterpret_cast(ptr + sizeof(float)); bfree(ptr); } else { x = y = 0; } } -void streamfx::obs::gs::effect_parameter::get_default_float2(float_t& x, float_t& y) +void streamfx::obs::gs::effect_parameter::get_default_float2(float& x, float& y) { if (get_type() != type::Float2) throw std::bad_cast(); uint8_t* ptr = static_cast(gs_effect_get_default_val(get())); if (ptr) { - x = *reinterpret_cast(ptr); - y = *reinterpret_cast(ptr + sizeof(float_t)); + x = *reinterpret_cast(ptr); + y = *reinterpret_cast(ptr + sizeof(float)); bfree(ptr); } else { x = y = 0; @@ -319,7 +319,7 @@ void streamfx::obs::gs::effect_parameter::get_default_float3(vec3& v) get_default_float3(v.x, v.y, v.z); } -void streamfx::obs::gs::effect_parameter::set_float3(float_t x, float_t y, float_t z) +void streamfx::obs::gs::effect_parameter::set_float3(float x, float y, float z) { if (get_type() != type::Float3) throw std::bad_cast(); @@ -327,30 +327,30 @@ void streamfx::obs::gs::effect_parameter::set_float3(float_t x, float_t y, float gs_effect_set_vec3(get(), &v); } -void streamfx::obs::gs::effect_parameter::get_float3(float_t& x, float_t& y, float_t& z) +void streamfx::obs::gs::effect_parameter::get_float3(float& x, float& y, float& z) { if (get_type() != type::Float3) throw std::bad_cast(); uint8_t* ptr = static_cast(gs_effect_get_val(get())); if (ptr) { - x = *reinterpret_cast(ptr); - y = *reinterpret_cast(ptr + sizeof(float_t)); - z = *reinterpret_cast(ptr + sizeof(float_t) * 2); + x = *reinterpret_cast(ptr); + y = *reinterpret_cast(ptr + sizeof(float)); + z = *reinterpret_cast(ptr + sizeof(float) * 2); bfree(ptr); } else { x = y = z = 0; } } -void streamfx::obs::gs::effect_parameter::get_default_float3(float_t& x, float_t& y, float_t& z) +void streamfx::obs::gs::effect_parameter::get_default_float3(float& x, float& y, float& z) { if (get_type() != type::Float3) throw std::bad_cast(); uint8_t* ptr = static_cast(gs_effect_get_default_val(get())); if (ptr) { - x = *reinterpret_cast(ptr); - y = *reinterpret_cast(ptr + sizeof(float_t)); - z = *reinterpret_cast(ptr + sizeof(float_t) * 2); + x = *reinterpret_cast(ptr); + y = *reinterpret_cast(ptr + sizeof(float)); + z = *reinterpret_cast(ptr + sizeof(float) * 2); bfree(ptr); } else { x = y = z = 0; @@ -374,7 +374,7 @@ void streamfx::obs::gs::effect_parameter::get_default_float4(vec4& v) get_default_float4(v.x, v.y, v.z, v.w); } -void streamfx::obs::gs::effect_parameter::set_float4(float_t x, float_t y, float_t z, float_t w) +void streamfx::obs::gs::effect_parameter::set_float4(float x, float y, float z, float w) { if (get_type() != type::Float4) throw std::bad_cast(); @@ -382,32 +382,32 @@ void streamfx::obs::gs::effect_parameter::set_float4(float_t x, float_t y, float gs_effect_set_vec4(get(), &v); } -void streamfx::obs::gs::effect_parameter::get_float4(float_t& x, float_t& y, float_t& z, float_t& w) +void streamfx::obs::gs::effect_parameter::get_float4(float& x, float& y, float& z, float& w) { if (get_type() != type::Float4) throw std::bad_cast(); uint8_t* ptr = static_cast(gs_effect_get_val(get())); if (ptr) { - x = *reinterpret_cast(ptr); - y = *reinterpret_cast(ptr + sizeof(float_t)); - z = *reinterpret_cast(ptr + sizeof(float_t) * 2); - w = *reinterpret_cast(ptr + sizeof(float_t) * 3); + x = *reinterpret_cast(ptr); + y = *reinterpret_cast(ptr + sizeof(float)); + z = *reinterpret_cast(ptr + sizeof(float) * 2); + w = *reinterpret_cast(ptr + sizeof(float) * 3); bfree(ptr); } else { x = y = z = w = 0; } } -void streamfx::obs::gs::effect_parameter::get_default_float4(float_t& x, float_t& y, float_t& z, float_t& w) +void streamfx::obs::gs::effect_parameter::get_default_float4(float& x, float& y, float& z, float& w) { if (get_type() != type::Float4) throw std::bad_cast(); uint8_t* ptr = static_cast(gs_effect_get_default_val(get())); if (ptr) { - x = *reinterpret_cast(ptr); - y = *reinterpret_cast(ptr + sizeof(float_t)); - z = *reinterpret_cast(ptr + sizeof(float_t) * 2); - w = *reinterpret_cast(ptr + sizeof(float_t) * 3); + x = *reinterpret_cast(ptr); + y = *reinterpret_cast(ptr + sizeof(float)); + z = *reinterpret_cast(ptr + sizeof(float) * 2); + w = *reinterpret_cast(ptr + sizeof(float) * 3); bfree(ptr); } else { x = y = z = w = 0; @@ -574,22 +574,22 @@ void streamfx::obs::gs::effect_parameter::get_matrix(matrix4& v) throw std::bad_cast(); uint8_t* ptr = static_cast(gs_effect_get_val(get())); if (ptr) { - v.x.x = *reinterpret_cast(ptr + sizeof(float_t) * 0); - v.x.y = *reinterpret_cast(ptr + sizeof(float_t) * 1); - v.x.z = *reinterpret_cast(ptr + sizeof(float_t) * 2); - v.x.w = *reinterpret_cast(ptr + sizeof(float_t) * 3); - v.y.x = *reinterpret_cast(ptr + sizeof(float_t) * 4); - v.y.y = *reinterpret_cast(ptr + sizeof(float_t) * 5); - v.y.z = *reinterpret_cast(ptr + sizeof(float_t) * 6); - v.y.w = *reinterpret_cast(ptr + sizeof(float_t) * 7); - v.z.x = *reinterpret_cast(ptr + sizeof(float_t) * 8); - v.z.y = *reinterpret_cast(ptr + sizeof(float_t) * 9); - v.z.z = *reinterpret_cast(ptr + sizeof(float_t) * 10); - v.z.w = *reinterpret_cast(ptr + sizeof(float_t) * 11); - v.t.x = *reinterpret_cast(ptr + sizeof(float_t) * 12); - v.t.y = *reinterpret_cast(ptr + sizeof(float_t) * 13); - v.t.z = *reinterpret_cast(ptr + sizeof(float_t) * 14); - v.t.w = *reinterpret_cast(ptr + sizeof(float_t) * 15); + v.x.x = *reinterpret_cast(ptr + sizeof(float) * 0); + v.x.y = *reinterpret_cast(ptr + sizeof(float) * 1); + v.x.z = *reinterpret_cast(ptr + sizeof(float) * 2); + v.x.w = *reinterpret_cast(ptr + sizeof(float) * 3); + v.y.x = *reinterpret_cast(ptr + sizeof(float) * 4); + v.y.y = *reinterpret_cast(ptr + sizeof(float) * 5); + v.y.z = *reinterpret_cast(ptr + sizeof(float) * 6); + v.y.w = *reinterpret_cast(ptr + sizeof(float) * 7); + v.z.x = *reinterpret_cast(ptr + sizeof(float) * 8); + v.z.y = *reinterpret_cast(ptr + sizeof(float) * 9); + v.z.z = *reinterpret_cast(ptr + sizeof(float) * 10); + v.z.w = *reinterpret_cast(ptr + sizeof(float) * 11); + v.t.x = *reinterpret_cast(ptr + sizeof(float) * 12); + v.t.y = *reinterpret_cast(ptr + sizeof(float) * 13); + v.t.z = *reinterpret_cast(ptr + sizeof(float) * 14); + v.t.w = *reinterpret_cast(ptr + sizeof(float) * 15); bfree(ptr); } else { v.x = vec4{}; @@ -605,22 +605,22 @@ void streamfx::obs::gs::effect_parameter::get_default_matrix(matrix4& v) throw std::bad_cast(); uint8_t* ptr = static_cast(gs_effect_get_default_val(get())); if (ptr) { - v.x.x = *reinterpret_cast(ptr + sizeof(float_t) * 0); - v.x.y = *reinterpret_cast(ptr + sizeof(float_t) * 1); - v.x.z = *reinterpret_cast(ptr + sizeof(float_t) * 2); - v.x.w = *reinterpret_cast(ptr + sizeof(float_t) * 3); - v.y.x = *reinterpret_cast(ptr + sizeof(float_t) * 4); - v.y.y = *reinterpret_cast(ptr + sizeof(float_t) * 5); - v.y.z = *reinterpret_cast(ptr + sizeof(float_t) * 6); - v.y.w = *reinterpret_cast(ptr + sizeof(float_t) * 7); - v.z.x = *reinterpret_cast(ptr + sizeof(float_t) * 8); - v.z.y = *reinterpret_cast(ptr + sizeof(float_t) * 9); - v.z.z = *reinterpret_cast(ptr + sizeof(float_t) * 10); - v.z.w = *reinterpret_cast(ptr + sizeof(float_t) * 11); - v.t.x = *reinterpret_cast(ptr + sizeof(float_t) * 12); - v.t.y = *reinterpret_cast(ptr + sizeof(float_t) * 13); - v.t.z = *reinterpret_cast(ptr + sizeof(float_t) * 14); - v.t.w = *reinterpret_cast(ptr + sizeof(float_t) * 15); + v.x.x = *reinterpret_cast(ptr + sizeof(float) * 0); + v.x.y = *reinterpret_cast(ptr + sizeof(float) * 1); + v.x.z = *reinterpret_cast(ptr + sizeof(float) * 2); + v.x.w = *reinterpret_cast(ptr + sizeof(float) * 3); + v.y.x = *reinterpret_cast(ptr + sizeof(float) * 4); + v.y.y = *reinterpret_cast(ptr + sizeof(float) * 5); + v.y.z = *reinterpret_cast(ptr + sizeof(float) * 6); + v.y.w = *reinterpret_cast(ptr + sizeof(float) * 7); + v.z.x = *reinterpret_cast(ptr + sizeof(float) * 8); + v.z.y = *reinterpret_cast(ptr + sizeof(float) * 9); + v.z.z = *reinterpret_cast(ptr + sizeof(float) * 10); + v.z.w = *reinterpret_cast(ptr + sizeof(float) * 11); + v.t.x = *reinterpret_cast(ptr + sizeof(float) * 12); + v.t.y = *reinterpret_cast(ptr + sizeof(float) * 13); + v.t.z = *reinterpret_cast(ptr + sizeof(float) * 14); + v.t.w = *reinterpret_cast(ptr + sizeof(float) * 15); bfree(ptr); } else { v.x = vec4{}; @@ -650,7 +650,7 @@ void streamfx::obs::gs::effect_parameter::set_sampler(std::shared_ptrget_object()); + gs_effect_set_next_sampler(get(), v->update()); } void streamfx::obs::gs::effect_parameter::set_sampler(gs_sampler_state* v) diff --git a/source/obs/gs/gs-effect-parameter.hpp b/source/obs/gs/gs-effect-parameter.hpp index 454058e..b28ba97 100644 --- a/source/obs/gs/gs-effect-parameter.hpp +++ b/source/obs/gs/gs-effect-parameter.hpp @@ -129,30 +129,30 @@ namespace streamfx::obs::gs { void set_bool_array(bool v[], std::size_t sz); - void set_float(float_t x); - void get_float(float_t& x); - void get_default_float(float_t& x); + void set_float(float x); + void get_float(float& x); + void get_default_float(float& x); void set_float2(vec2 const& v); void get_float2(vec2& v); void get_default_float2(vec2& v); - void set_float2(float_t x, float_t y); - void get_float2(float_t& x, float_t& y); - void get_default_float2(float_t& x, float_t& y); + void set_float2(float x, float y); + void get_float2(float& x, float& y); + void get_default_float2(float& x, float& y); void set_float3(vec3 const& v); void get_float3(vec3& v); void get_default_float3(vec3& v); - void set_float3(float_t x, float_t y, float_t z); - void get_float3(float_t& x, float_t& y, float_t& z); - void get_default_float3(float_t& x, float_t& y, float_t& z); + void set_float3(float x, float y, float z); + void get_float3(float& x, float& y, float& z); + void get_default_float3(float& x, float& y, float& z); void set_float4(vec4 const& v); void get_float4(vec4& v); void get_default_float4(vec4& v); - void set_float4(float_t x, float_t y, float_t z, float_t w); - void get_float4(float_t& x, float_t& y, float_t& z, float_t& w); - void get_default_float4(float_t& x, float_t& y, float_t& z, float_t& w); + void set_float4(float x, float y, float z, float w); + void get_float4(float& x, float& y, float& z, float& w); + void get_default_float4(float& x, float& y, float& z, float& w); void set_int(int32_t x); void get_int(int32_t& x); @@ -185,7 +185,7 @@ namespace streamfx::obs::gs { void get_default_string(std::string& v); public /* Helpers */: - inline float_t get_bool() + inline float get_bool() { bool v; get_bool(v); @@ -198,15 +198,15 @@ namespace streamfx::obs::gs { return v; }; - inline float_t get_float() + inline float get_float() { - float_t v; + float v; get_float(v); return v; }; - inline float_t get_default_float() + inline float get_default_float() { - float_t v; + float v; get_default_float(v); return v; }; diff --git a/source/obs/gs/gs-effect-pass.cpp b/source/obs/gs/gs-effect-pass.cpp index 8bf6c47..83581c4 100644 --- a/source/obs/gs/gs-effect-pass.cpp +++ b/source/obs/gs/gs-effect-pass.cpp @@ -1,5 +1,5 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks // Copyright (C) 2022 lainon // AUTOGENERATED COPYRIGHT HEADER END diff --git a/source/obs/gs/gs-effect-pass.hpp b/source/obs/gs/gs-effect-pass.hpp index 6c79e74..31130da 100644 --- a/source/obs/gs/gs-effect-pass.hpp +++ b/source/obs/gs/gs-effect-pass.hpp @@ -1,5 +1,5 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks // Copyright (C) 2022 lainon // AUTOGENERATED COPYRIGHT HEADER END diff --git a/source/obs/gs/gs-effect-technique.hpp b/source/obs/gs/gs-effect-technique.hpp index d93d1d2..350f9c6 100644 --- a/source/obs/gs/gs-effect-technique.hpp +++ b/source/obs/gs/gs-effect-technique.hpp @@ -1,5 +1,5 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks // Copyright (C) 2022 lainon // AUTOGENERATED COPYRIGHT HEADER END diff --git a/source/obs/gs/gs-effect.cpp b/source/obs/gs/gs-effect.cpp index 4894ff4..de0350f 100644 --- a/source/obs/gs/gs-effect.cpp +++ b/source/obs/gs/gs-effect.cpp @@ -1,5 +1,5 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks // Copyright (C) 2022 lainon // AUTOGENERATED COPYRIGHT HEADER END @@ -99,7 +99,7 @@ streamfx::obs::gs::effect::effect(std::string_view code, std::string_view name) reset(effect, [](gs_effect_t* ptr) { gs_effect_destroy(ptr); }); } -streamfx::obs::gs::effect::effect(std::filesystem::path file) : effect(load_file_as_code(file), streamfx::util::platform::utf8_to_native(std::filesystem::absolute(file)).generic_u8string()) {} +streamfx::obs::gs::effect::effect(std::filesystem::path file) : effect(load_file_as_code(file), std::filesystem::absolute(file).generic_string()) {} streamfx::obs::gs::effect::~effect() { diff --git a/source/obs/gs/gs-effect.hpp b/source/obs/gs/gs-effect.hpp index 50e0c46..789f601 100644 --- a/source/obs/gs/gs-effect.hpp +++ b/source/obs/gs/gs-effect.hpp @@ -1,5 +1,5 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks // Copyright (C) 2022 lainon // AUTOGENERATED COPYRIGHT HEADER END diff --git a/source/obs/gs/gs-helper.cpp b/source/obs/gs/gs-helper.cpp index e01cba3..1038c70 100644 --- a/source/obs/gs/gs-helper.cpp +++ b/source/obs/gs/gs-helper.cpp @@ -1,5 +1,5 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks // AUTOGENERATED COPYRIGHT HEADER END #include "gs-helper.hpp" diff --git a/source/obs/gs/gs-helper.hpp b/source/obs/gs/gs-helper.hpp index d802af4..d8efcb3 100644 --- a/source/obs/gs/gs-helper.hpp +++ b/source/obs/gs/gs-helper.hpp @@ -1,5 +1,5 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks // AUTOGENERATED COPYRIGHT HEADER END #pragma once @@ -26,36 +26,36 @@ namespace streamfx::obs::gs { }; #if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG - static constexpr float_t debug_color_white[4] = {1.f, 1.f, 1.f, 1.f}; - static constexpr float_t debug_color_gray[4] = {.5f, .5f, .5f, 1.f}; - static constexpr float_t debug_color_black[4] = {0.f, 0.f, 0.f, 1.f}; - static constexpr float_t debug_color_red[4] = {1.f, 0.f, 0.f, 1.f}; - static constexpr float_t debug_color_flush_orange[4] = {1.f, .5f, 0.f, 1.f}; - static constexpr float_t debug_color_yellow[4] = {1.f, 1.f, 0.f, 1.f}; - static constexpr float_t debug_color_chartreuse[4] = {.5f, 1.f, 0.f, 1.f}; - static constexpr float_t debug_color_green[4] = {0.f, 1.f, 0.f, 1.f}; - static constexpr float_t debug_color_spring_green[4] = {0.f, 1.f, .5f, 1.f}; - static constexpr float_t debug_color_teal[4] = {0.f, 1.f, 1.f, 1.f}; - static constexpr float_t debug_color_azure_radiance[4] = {0.f, .5f, 1.f, 1.f}; - static constexpr float_t debug_color_blue[4] = {0.f, 0.f, 1.f, 1.f}; - static constexpr float_t debug_color_electric_violet[4] = {.5f, 0.f, 1.f, 1.f}; - static constexpr float_t debug_color_magenta[4] = {1.f, 0.f, 1.f, 1.f}; - static constexpr float_t debug_color_rose[4] = {1.f, 0.f, .5f, 1.f}; + static constexpr float debug_color_white[4] = {1.f, 1.f, 1.f, 1.f}; + static constexpr float debug_color_gray[4] = {.5f, .5f, .5f, 1.f}; + static constexpr float debug_color_black[4] = {0.f, 0.f, 0.f, 1.f}; + static constexpr float debug_color_red[4] = {1.f, 0.f, 0.f, 1.f}; + static constexpr float debug_color_flush_orange[4] = {1.f, .5f, 0.f, 1.f}; + static constexpr float debug_color_yellow[4] = {1.f, 1.f, 0.f, 1.f}; + static constexpr float debug_color_chartreuse[4] = {.5f, 1.f, 0.f, 1.f}; + static constexpr float debug_color_green[4] = {0.f, 1.f, 0.f, 1.f}; + static constexpr float debug_color_spring_green[4] = {0.f, 1.f, .5f, 1.f}; + static constexpr float debug_color_teal[4] = {0.f, 1.f, 1.f, 1.f}; + static constexpr float debug_color_azure_radiance[4] = {0.f, .5f, 1.f, 1.f}; + static constexpr float debug_color_blue[4] = {0.f, 0.f, 1.f, 1.f}; + static constexpr float debug_color_electric_violet[4] = {.5f, 0.f, 1.f, 1.f}; + static constexpr float debug_color_magenta[4] = {1.f, 0.f, 1.f, 1.f}; + static constexpr float debug_color_rose[4] = {1.f, 0.f, .5f, 1.f}; - static const float_t* debug_color_source = debug_color_white; - static const float_t* debug_color_capture = debug_color_flush_orange; - static const float_t* debug_color_cache = debug_color_capture; - static const float_t* debug_color_convert = debug_color_electric_violet; - static const float_t* debug_color_cache_render = debug_color_convert; - static const float_t* debug_color_copy = debug_color_azure_radiance; - static const float_t* debug_color_allocate = debug_color_red; - static const float_t* debug_color_render = debug_color_teal; + static const float* debug_color_source = debug_color_white; + static const float* debug_color_capture = debug_color_flush_orange; + static const float* debug_color_cache = debug_color_capture; + static const float* debug_color_convert = debug_color_electric_violet; + static const float* debug_color_cache_render = debug_color_convert; + static const float* debug_color_copy = debug_color_azure_radiance; + static const float* debug_color_allocate = debug_color_red; + static const float* debug_color_render = debug_color_teal; class debug_marker { std::string _name; public: - inline debug_marker(const float_t color[4], const char* format, ...) + inline debug_marker(const float color[4], const char* format, ...) { std::size_t size; std::vector buffer(64); diff --git a/source/obs/gs/gs-limits.hpp b/source/obs/gs/gs-limits.hpp index 0fdccfd..d44c558 100644 --- a/source/obs/gs/gs-limits.hpp +++ b/source/obs/gs/gs-limits.hpp @@ -1,5 +1,5 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks // AUTOGENERATED COPYRIGHT HEADER END #pragma once diff --git a/source/obs/gs/gs-sampler.cpp b/source/obs/gs/gs-sampler.cpp index c5eb943..5a06e56 100644 --- a/source/obs/gs/gs-sampler.cpp +++ b/source/obs/gs/gs-sampler.cpp @@ -1,122 +1,54 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2018-2023 Michael Fabian 'Xaymar' Dirks // AUTOGENERATED COPYRIGHT HEADER END #include "gs-sampler.hpp" +#include "obs/gs/gs-helper.hpp" #include "warning-disable.hpp" #include #include "warning-enable.hpp" -streamfx::obs::gs::sampler::sampler() -{ - _dirty = true; - _sampler_info = {GS_FILTER_LINEAR, GS_ADDRESS_WRAP, GS_ADDRESS_WRAP, GS_ADDRESS_WRAP, 1, 0}; - _sampler_state = nullptr; -} - streamfx::obs::gs::sampler::~sampler() { - if (_sampler_state) - gs_samplerstate_destroy(_sampler_state); + auto gctx = streamfx::obs::gs::context(); + if (_state) { + gs_samplerstate_destroy(_state); + } } -void streamfx::obs::gs::sampler::set_filter(gs_sample_filter v) +streamfx::obs::gs::sampler::sampler() { - _dirty = true; - _sampler_info.filter = v; + auto gctx = streamfx::obs::gs::context(); + _info = {GS_FILTER_LINEAR, GS_ADDRESS_WRAP, GS_ADDRESS_WRAP, GS_ADDRESS_WRAP, 1, 0}; + _state = gs_samplerstate_create(&_info); } -gs_sample_filter streamfx::obs::gs::sampler::get_filter() +streamfx::obs::gs::sampler::sampler(gs_sampler_info si) { - return _sampler_info.filter; + auto gctx = streamfx::obs::gs::context(); + _info = si; + _state = gs_samplerstate_create(&_info); } -void streamfx::obs::gs::sampler::set_address_mode_u(gs_address_mode v) +gs_sampler_info streamfx::obs::gs::sampler::pool::as_key(streamfx::obs::gs::sampler* to_hash) { - _dirty = true; - _sampler_info.address_u = v; + return as_key(to_hash->info()); } -gs_address_mode streamfx::obs::gs::sampler::get_address_mode_u() +gs_sampler_info streamfx::obs::gs::sampler::pool::as_key(gs_sampler_info to_hash) { - return _sampler_info.address_u; + return to_hash; } -void streamfx::obs::gs::sampler::set_address_mode_v(gs_address_mode v) -{ - _dirty = true; - _sampler_info.address_v = v; -} +static std::shared_ptr loader_instance; -gs_address_mode streamfx::obs::gs::sampler::get_address_mode_v() -{ - return _sampler_info.address_v; -} - -void streamfx::obs::gs::sampler::set_address_mode_w(gs_address_mode v) -{ - _dirty = true; - _sampler_info.address_w = v; -} - -gs_address_mode streamfx::obs::gs::sampler::get_address_mode_w() -{ - return _sampler_info.address_w; -} - -void streamfx::obs::gs::sampler::set_max_anisotropy(int32_t v) -{ - _dirty = true; - _sampler_info.max_anisotropy = v; -} - -int32_t streamfx::obs::gs::sampler::get_max_anisotropy() -{ - return _sampler_info.max_anisotropy; -} - -void streamfx::obs::gs::sampler::set_border_color(uint32_t v) -{ - _dirty = true; - _sampler_info.border_color = v; -} - -void streamfx::obs::gs::sampler::set_border_color(uint8_t r, uint8_t g, uint8_t b, uint8_t a) -{ - _dirty = true; - _sampler_info.border_color = (static_cast(a) << 24) | (static_cast(r) << 16) | (static_cast(g) << 8) | static_cast(b); -} - -uint32_t streamfx::obs::gs::sampler::get_border_color() -{ - return _sampler_info.border_color; -} - -uint8_t streamfx::obs::gs::sampler::get_border_color(bool r, bool g, bool b, bool a) -{ - if (a) - return (_sampler_info.border_color >> 24) & 0xFF; - if (r) - return (_sampler_info.border_color >> 16) & 0xFF; - if (g) - return (_sampler_info.border_color >> 8) & 0xFF; - if (b) - return _sampler_info.border_color & 0xFF; - return 0; -} - -gs_sampler_state* streamfx::obs::gs::sampler::refresh() -{ - gs_samplerstate_destroy(_sampler_state); - _sampler_state = gs_samplerstate_create(&_sampler_info); - _dirty = false; - return _sampler_state; -} - -gs_sampler_state* streamfx::obs::gs::sampler::get_object() -{ - if (_dirty) - return refresh(); - return _sampler_state; -} +static auto loader = streamfx::component( + "core::gs::sampler", + []() { // Initializer + loader_instance = streamfx::obs::gs::sampler::pool::instance(); + }, + []() { // Finalizer + loader_instance.reset(); + }, + {}); diff --git a/source/obs/gs/gs-sampler.hpp b/source/obs/gs/gs-sampler.hpp index eab8825..a432a0a 100644 --- a/source/obs/gs/gs-sampler.hpp +++ b/source/obs/gs/gs-sampler.hpp @@ -1,43 +1,130 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks // AUTOGENERATED COPYRIGHT HEADER END #pragma once #include "common.hpp" +#include "util/util-pool.hpp" +#include "util/util-singleton.hpp" + +#include "warning-disable.hpp" +#include +#include +#include +#include +#include +#include +#include + +#include +#include "warning-enable.hpp" + +static bool operator<(gs_sampler_info const& lhs, gs_sampler_info const& rhs) noexcept +{ + return (lhs.address_u < rhs.address_u) || (lhs.address_v < rhs.address_v) || (lhs.address_w < rhs.address_w) || (lhs.filter < rhs.filter) || (lhs.max_anisotropy < rhs.max_anisotropy); +} + +static bool operator==(gs_sampler_info const& lhs, gs_sampler_info const& rhs) noexcept +{ + return (lhs.address_u == rhs.address_u) && (lhs.address_v == rhs.address_v) && (lhs.address_w == rhs.address_w) && (lhs.border_color == rhs.border_color) && (lhs.filter == rhs.filter) && (lhs.max_anisotropy == rhs.max_anisotropy); +} + +template<> +struct std::hash { + std::size_t operator()(gs_sampler_info si) const noexcept + { + auto hash = std::hash{}(si.address_u); + hash ^= std::hash{}(si.address_v); + hash ^= std::hash{}(si.address_w); + hash ^= std::hash{}(si.border_color); + hash ^= std::hash{}(si.filter); + hash ^= std::hash{}(si.max_anisotropy); + return hash; + } +}; namespace streamfx::obs::gs { class sampler { + gs_sampler_info _info; + gs_sampler_state* _state; + public: - sampler(); ~sampler(); - void set_filter(gs_sample_filter v); - gs_sample_filter get_filter(); + sampler(); - void set_address_mode_u(gs_address_mode v); - gs_address_mode get_address_mode_u(); + sampler(gs_sampler_info si); - void set_address_mode_v(gs_address_mode v); - gs_address_mode get_address_mode_v(); + FORCE_INLINE gs_sample_filter get_filter() const + { + return _info.filter; + } - void set_address_mode_w(gs_address_mode v); - gs_address_mode get_address_mode_w(); + FORCE_INLINE gs_address_mode get_address_mode_u() const + { + return _info.address_u; + } - void set_max_anisotropy(int32_t v); - int get_max_anisotropy(); + FORCE_INLINE gs_address_mode get_address_mode_v() const + { + return _info.address_v; + } - void set_border_color(uint32_t v); - void set_border_color(uint8_t r, uint8_t g, uint8_t b, uint8_t a); - uint32_t get_border_color(); - uint8_t get_border_color(bool r, bool g, bool b, bool a); + FORCE_INLINE gs_address_mode get_address_mode_w() const + { + return _info.address_w; + } - gs_sampler_state* refresh(); + FORCE_INLINE int get_max_anisotropy() const + { + return _info.max_anisotropy; + } - gs_sampler_state* get_object(); + FORCE_INLINE uint32_t get_border_color() const + { + return _info.border_color; + } + FORCE_INLINE uint8_t get_border_color(bool r, bool g, bool b, bool a) const + { + if (a) + return (_info.border_color >> 24) & 0xFF; + if (r) + return (_info.border_color >> 16) & 0xFF; + if (g) + return (_info.border_color >> 8) & 0xFF; + if (b) + return _info.border_color & 0xFF; + return 0; + } - private: - bool _dirty; - gs_sampler_info _sampler_info; - gs_sampler_state* _sampler_state; + FORCE_INLINE gs_sampler_info info() const + { + return _info; + } + + FORCE_INLINE gs_sampler_state* update() const + { + return _state; + } + + public: + class pool; + typedef gs_sampler_info _pool_key_t; + typedef streamfx::util::multipool _pool_t; + + class pool : public _pool_t { + friend streamfx::util::singleton; + friend _pool_t; + + public: + ~pool() {} + + protected: + pool() : _pool_t() {} + + static _pool_key_t as_key(streamfx::obs::gs::sampler* to_hash); + + static _pool_key_t as_key(gs_sampler_info to_hash); + }; }; } // namespace streamfx::obs::gs diff --git a/source/obs/gs/gs-texrender.cpp b/source/obs/gs/gs-texrender.cpp new file mode 100644 index 0000000..4ab17a1 --- /dev/null +++ b/source/obs/gs/gs-texrender.cpp @@ -0,0 +1,75 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2017-2024 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2022 lainon +// AUTOGENERATED COPYRIGHT HEADER END + +#include "gs-texrender.hpp" +#include "obs/gs/gs-helper.hpp" + +#include "warning-disable.hpp" +#include +#include "warning-enable.hpp" + +streamfx::obs::gs::texrender::~texrender() +{ + auto gctx = streamfx::obs::gs::context(); + gs_texrender_destroy(_ptr); +} + +streamfx::obs::gs::texrender::texrender(gs_color_format colorFormat, gs_zstencil_format zsFormat) : _color_format(colorFormat), _zstencil_format(zsFormat) +{ +#ifdef _DEBUG + _active = false; +#endif + auto gctx = streamfx::obs::gs::context(); + if (_ptr = gs_texrender_create(colorFormat, zsFormat); !_ptr) { + throw std::runtime_error("Failed to create render target."); + } +} + +streamfx::obs::gs::texrender::op::op(std::shared_ptr rt, uint32_t width, uint32_t height, gs_color_space cs) : parent(rt) +{ +#ifdef _DEBUG + if (parent == nullptr) + throw std::invalid_argument("rt"); + if (parent->_active) + throw std::logic_error("Can't start rendering to the same render target twice."); +#endif + + auto gctx = streamfx::obs::gs::context(); + gs_texrender_reset(parent->_ptr); + if (!gs_texrender_begin_with_color_space(parent->_ptr, width, height, cs)) { + throw std::runtime_error("Failed to begin rendering to render target."); + } + +#ifdef _DEBUG + parent->_active = true; +#endif +} + +streamfx::obs::gs::texrender::op::~op() +{ +#ifdef _DEBUG + if (parent == nullptr) + return; +#endif + + auto gctx = streamfx::obs::gs::context(); + gs_texrender_end(parent->_ptr); + +#ifdef _DEBUG + parent->_active = false; +#endif +} + +static std::shared_ptr loader_instance; + +static auto loader = streamfx::component( + "core::gs::texrender", + []() { // Initializer + loader_instance = streamfx::obs::gs::texrender::pool::instance(); + }, + []() { // Finalizer + loader_instance.reset(); + }, + {}); diff --git a/source/obs/gs/gs-texrender.hpp b/source/obs/gs/gs-texrender.hpp new file mode 100644 index 0000000..36770bb --- /dev/null +++ b/source/obs/gs/gs-texrender.hpp @@ -0,0 +1,146 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2022 lainon +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "common.hpp" +#include "gs-texture.hpp" +#include "util/util-pool.hpp" + +#include "warning-disable.hpp" +#include +#include "warning-enable.hpp" + +namespace streamfx::obs::gs { + class texrender : public std::enable_shared_from_this { + protected: +#ifdef _DEBUG + bool _active; +#endif + gs_texrender_t* _ptr; + gs_color_format _color_format; + gs_zstencil_format _zstencil_format; + + public: + class op; + friend op; + + public: + ~texrender(); + + /** Create a new texrender with a given format. + * + */ + texrender(gs_color_format format, gs_zstencil_format zstencil_format); + + FORCE_INLINE gs_color_format color_format() const + { + return _color_format; + } + + FORCE_INLINE gs_zstencil_format zstencil_format() const + { + return _zstencil_format; + } + + /** Try and reset the underlying texrender object. + * + */ + FORCE_INLINE void reset() + { + gs_texrender_reset(_ptr); // Does not require a graphic context. + // TODO: Actually clear the included texture, so that the driver knows it's not needed anymore. + // Might be significantly more difficult than expected. + } + + /** Render into the texrender. + * + */ + FORCE_INLINE std::shared_ptr render(uint32_t width, uint32_t height, gs_color_space cs = GS_CS_SRGB) + { + return std::make_shared(this->shared_from_this(), width, height, cs); + } + + FORCE_INLINE operator gs_texrender_t*() const + { + return _ptr; + } + + [[deprecated("Use typecasts instead.")]] gs_texture_t* get_object() + { + return *this; + } + + [[deprecated("Unsafe! Contained texture only lives as long as texrender object.")]] std::shared_ptr get_texture() + { + return std::make_shared(*this, false); + } + + [[deprecated("Unsafe! Contained texture only lives as long as texrender object.")]] void get_texture(streamfx::obs::gs::texture& tex) + { + tex = streamfx::obs::gs::texture(*this, false); + } + + [[deprecated("Unsafe! Contained texture only lives as long as texrender object.")]] void get_texture(std::shared_ptr& tex) + { + tex = std::make_shared(*this, false); + } + + [[deprecated("Unsafe! Contained texture only lives as long as texrender object.")]] void get_texture(std::unique_ptr& tex) + { + tex = std::make_unique(*this, false); + } + + FORCE_INLINE operator gs_texture_t*() const + { + return gs_texrender_get_texture(_ptr); + } + + class op { + std::shared_ptr parent; + + public: + ~op(); + + op(std::shared_ptr rt, uint32_t width, uint32_t height, gs_color_space cs = GS_CS_SRGB); + + // Move Constructor + FORCE_INLINE op(streamfx::obs::gs::texrender::op&& r) noexcept + { + this->parent = r.parent; + r.parent = nullptr; + } + + // Move Operator + op& operator=(const streamfx::obs::gs::texrender::op&&) = delete; + + // Copy Constructor + op(const streamfx::obs::gs::texrender::op&) = delete; + + // Copy Operator + op& operator=(const streamfx::obs::gs::texrender::op& r) = delete; + }; + + public: + class pool; + typedef streamfx::util::pool _pool_t; + + class pool : public _pool_t { + friend streamfx::util::singleton; + friend _pool_t; + + public: + virtual ~pool() {} + + protected: + pool() : _pool_t() {} + + virtual void reset(void* ptr) + { + reinterpret_cast(ptr)->reset(); + } + }; + }; + +} // namespace streamfx::obs::gs diff --git a/source/obs/gs/gs-texture.cpp b/source/obs/gs/gs-texture.cpp index 332e00f..f05c861 100644 --- a/source/obs/gs/gs-texture.cpp +++ b/source/obs/gs/gs-texture.cpp @@ -1,32 +1,30 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks // AUTOGENERATED COPYRIGHT HEADER END #include "gs-texture.hpp" -#include "obs/gs/gs-helper.hpp" -#include "warning-disable.hpp" -#include -#include -#include -#include "warning-enable.hpp" - -static uint32_t decode_flags(streamfx::obs::gs::texture::flags texture_flags) +static uint32_t decode_flags(streamfx::obs::gs::texture_flags texture_flags) { uint32_t flags = 0; - if (has(texture_flags, streamfx::obs::gs::texture::flags::Dynamic)) + if (has(texture_flags, streamfx::obs::gs::texture_flags::Dynamic)) flags |= GS_DYNAMIC; - if (has(texture_flags, streamfx::obs::gs::texture::flags::BuildMipMaps)) + if (has(texture_flags, streamfx::obs::gs::texture_flags::BuildMipMaps)) flags |= GS_BUILD_MIPMAPS; - if (has(texture_flags, streamfx::obs::gs::texture::flags::Shared)) + if (has(texture_flags, streamfx::obs::gs::texture_flags::Shared)) flags |= GS_SHARED_TEX; - if (has(texture_flags, streamfx::obs::gs::texture::flags::GlobalShared)) + if (has(texture_flags, streamfx::obs::gs::texture_flags::GlobalShared)) flags |= GS_SHARED_KM_TEX; return flags; } -streamfx::obs::gs::texture::texture(uint32_t width, uint32_t height, gs_color_format format, uint32_t mip_levels, const uint8_t** mip_data, streamfx::obs::gs::texture::flags texture_flags) +streamfx::obs::gs::texture::texture(uint32_t width, uint32_t height, gs_color_format format, uint32_t mip_levels, const uint8_t** mip_data, streamfx::obs::gs::texture_flags flags) { + _is_owner = true; + _flags = flags; + _file = std::filesystem::path(); + +#ifdef _DEBUG if (width == 0) throw std::logic_error("width must be at least 1"); if (height == 0) @@ -34,83 +32,31 @@ streamfx::obs::gs::texture::texture(uint32_t width, uint32_t height, gs_color_fo if (mip_levels == 0) throw std::logic_error("mip_levels must be at least 1"); - if (mip_levels > 1 || ((texture_flags & flags::BuildMipMaps) == flags::BuildMipMaps)) { + if (mip_levels > 1 || ((flags & texture_flags::BuildMipMaps) == texture_flags::BuildMipMaps)) { bool isPOT = streamfx::util::math::is_power_of_two(width) && streamfx::util::math::is_power_of_two(height); if (!isPOT) - throw std::logic_error("mip mapping requires power of two dimensions"); + throw std::logic_error("Texture dimensions must be a power of two."); } +#endif { auto gctx = streamfx::obs::gs::context(); - _texture = gs_texture_create(width, height, format, mip_levels, mip_data, decode_flags(texture_flags)); + _texture = gs_texture_create(width, height, format, mip_levels, mip_data, decode_flags(flags)); } if (!_texture) throw std::runtime_error("Failed to create texture."); - - _type = type::Normal; } -streamfx::obs::gs::texture::texture(uint32_t width, uint32_t height, uint32_t depth, gs_color_format format, uint32_t mip_levels, const uint8_t** mip_data, streamfx::obs::gs::texture::flags texture_flags) +streamfx::obs::gs::texture::texture(std::filesystem::path file) { - if (width == 0) - throw std::logic_error("width must be at least 1"); - if (height == 0) - throw std::logic_error("height must be at least 1"); - if (depth == 0) - throw std::logic_error("depth must be at least 1"); - if (mip_levels == 0) - throw std::logic_error("mip_levels must be at least 1"); - - if (mip_levels > 1 || ((texture_flags & flags::BuildMipMaps) == flags::BuildMipMaps)) { - bool isPOT = (streamfx::util::math::is_equal(pow(2, static_cast(floor(log(width) / log(2)))), width) && streamfx::util::math::is_equal(pow(2, static_cast(floor(log(height) / log(2)))), height) && streamfx::util::math::is_equal(pow(2, static_cast(floor(log(depth) / log(2)))), depth)); - if (!isPOT) - throw std::logic_error("mip mapping requires power of two dimensions"); - } - - { - auto gctx = streamfx::obs::gs::context(); - _texture = gs_voltexture_create(width, height, depth, format, mip_levels, mip_data, decode_flags(texture_flags)); - } - - if (!_texture) - throw std::runtime_error("Failed to create texture."); - - _type = type::Volume; -} - -streamfx::obs::gs::texture::texture(uint32_t size, gs_color_format format, uint32_t mip_levels, const uint8_t** mip_data, streamfx::obs::gs::texture::flags texture_flags) -{ - if (size == 0) - throw std::logic_error("size must be at least 1"); - if (mip_levels == 0) - throw std::logic_error("mip_levels must be at least 1"); - - if (mip_levels > 1 || ((texture_flags & flags::BuildMipMaps) == flags::BuildMipMaps)) { - bool isPOT = streamfx::util::math::is_equal(pow(2, static_cast(floor(log(size) / log(2)))), size); - if (!isPOT) - throw std::logic_error("mip mapping requires power of two dimensions"); - } - - { - auto gctx = streamfx::obs::gs::context(); - _texture = gs_cubetexture_create(size, format, mip_levels, mip_data, decode_flags(texture_flags)); - } - - if (!_texture) - throw std::runtime_error("Failed to create texture."); - - _type = type::Cube; -} - -streamfx::obs::gs::texture::texture(std::string file) -{ - struct stat st; - if (os_stat(file.c_str(), &st) != 0) - throw std::ios_base::failure(file); + _is_owner = true; + _file = file; + _flags = texture_flags::None; + _file = std::filesystem::path(); auto gctx = streamfx::obs::gs::context(); - _texture = gs_texture_create_from_file(file.c_str()); + _texture = gs_texture_create_from_file(file.generic_string().c_str()); if (!_texture) throw std::runtime_error("Failed to load texture."); @@ -120,77 +66,19 @@ streamfx::obs::gs::texture::~texture() { if (_is_owner && _texture) { auto gctx = streamfx::obs::gs::context(); - switch (gs_get_texture_type(_texture)) { - case GS_TEXTURE_2D: - gs_texture_destroy(_texture); - break; - case GS_TEXTURE_3D: - gs_voltexture_destroy(_texture); - break; - case GS_TEXTURE_CUBE: - gs_cubetexture_destroy(_texture); - break; - } + gs_texture_destroy(_texture); } _texture = nullptr; } -void streamfx::obs::gs::texture::load(int32_t unit) -{ - auto gctx = streamfx::obs::gs::context(); - gs_load_texture(_texture, unit); -} +static std::shared_ptr loader_instance; -gs_texture_t* streamfx::obs::gs::texture::get_object() -{ - return _texture; -} - -uint32_t streamfx::obs::gs::texture::get_width() -{ - switch (_type) { - case type::Normal: - return gs_texture_get_width(_texture); - case type::Volume: - return gs_voltexture_get_width(_texture); - case type::Cube: - return gs_cubetexture_get_size(_texture); - } - return 0; -} - -uint32_t streamfx::obs::gs::texture::get_height() -{ - switch (_type) { - case type::Normal: - return gs_texture_get_height(_texture); - case type::Volume: - return gs_voltexture_get_height(_texture); - case type::Cube: - return gs_cubetexture_get_size(_texture); - } - return 0; -} - -uint32_t streamfx::obs::gs::texture::get_depth() -{ - switch (_type) { - case type::Normal: - return 1; - case type::Volume: - return gs_voltexture_get_depth(_texture); - case type::Cube: - return 6; - } - return 0; -} - -streamfx::obs::gs::texture::type streamfx::obs::gs::texture::get_type() -{ - return _type; -} - -gs_color_format streamfx::obs::gs::texture::get_color_format() -{ - return gs_texture_get_color_format(_texture); -} +static auto loader = streamfx::component( + "core::gs::texture", + []() { // Initializer + loader_instance = streamfx::obs::gs::texture::pool::instance(); + }, + []() { // Finalizer + loader_instance.reset(); + }, + {}); diff --git a/source/obs/gs/gs-texture.hpp b/source/obs/gs/gs-texture.hpp index aba7b6b..7a67ea8 100644 --- a/source/obs/gs/gs-texture.hpp +++ b/source/obs/gs/gs-texture.hpp @@ -1,27 +1,33 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks // AUTOGENERATED COPYRIGHT HEADER END #pragma once #include "common.hpp" +#include "obs/gs/gs-helper.hpp" +#include "util/util-pool.hpp" + +#include "warning-disable.hpp" +#include +#include +#include "warning-enable.hpp" namespace streamfx::obs::gs { + enum class texture_flags : uint8_t { + None, + Dynamic = 1 << 0, + BuildMipMaps = 1 << 1, + Shared = 1 << 2, + GlobalShared = 1 << 3, + }; + class texture { public: - enum class type : uint8_t { Normal, Volume, Cube }; - - enum class flags : uint8_t { - None, - Dynamic, - BuildMipMaps, - Shared, - GlobalShared, - }; - protected: - gs_texture_t* _texture; - bool _is_owner = true; - type _type = type::Normal; + bool _is_owner; + gs_texture_t* _texture; + texture_flags _flags; + std::filesystem::path _file; public: ~texture(); @@ -33,34 +39,10 @@ namespace streamfx::obs::gs { * \param height Height of the 2D Texture * \param format Color Format to use * \param mip_levels Number of Mip Levels available - * \param mip_data Texture data including mipmaps - * \param texture_flags Texture Flags + * \param data Texture data including mipmaps + * \param flags Texture Flags */ - texture(uint32_t width, uint32_t height, gs_color_format format, uint32_t mip_levels, const uint8_t** mip_data, streamfx::obs::gs::texture::flags texture_flags); - - /*! - * \brief Create a 3D Texture - * - * \param width Width of the 3D Texture - * \param height Height of the 3D Texture - * \param depth Depth of the 3D Texture - * \param format Color Format to use - * \param mip_levels Number of Mip Levels available - * \param mip_data Texture data including mipmaps - * \param texture_flags Texture Flags - */ - texture(uint32_t width, uint32_t height, uint32_t depth, gs_color_format format, uint32_t mip_levels, const uint8_t** mip_data, streamfx::obs::gs::texture::flags texture_flags); - - /*! - * \brief Create a Cube Texture - * - * \param size Size of each Cube Maps face - * \param format Color Format to use - * \param mip_levels Number of Mip Levels available - * \param mip_data Texture data including mipmaps - * \param texture_flags Texture Flags - */ - texture(uint32_t size, gs_color_format format, uint32_t mip_levels, const uint8_t** mip_data, streamfx::obs::gs::texture::flags texture_flags); + texture(uint32_t width, uint32_t height, gs_color_format format, uint32_t mip_levels, const uint8_t** data, texture_flags flags); /*! * \brief Load a texture from a file @@ -72,27 +54,118 @@ namespace streamfx::obs::gs { * * \param file File to create the texture from. */ - texture(std::string file); + texture(std::filesystem::path file); /*! * \brief Create a texture from an existing gs_texture_t object. */ - texture(gs_texture_t* tex, bool takeOwnership = false) : _texture(tex), _is_owner(takeOwnership) {} + texture(gs_texture_t* tex, bool takeOwnership = false) + { + _is_owner = takeOwnership; + _texture = tex; + _flags = texture_flags::None; + _file = std::filesystem::path(); + } - void load(int32_t unit); + [[deprecated("Use typecast instead.")]] gs_texture_t* get_object() + { + return _texture; + } - gs_texture_t* get_object(); + /** Width of the contained texture. + * + */ + FORCE_INLINE uint32_t width() const + { + return gs_texture_get_width(_texture); + } - uint32_t get_width(); + /** Height of the contained texture. + * + */ + FORCE_INLINE uint32_t height() const + { + return gs_texture_get_height(_texture); + } - uint32_t get_height(); + /** Format of the texture. + * + */ + FORCE_INLINE gs_color_format color_format() const + { + return gs_texture_get_color_format(_texture); + } - uint32_t get_depth(); + /** Flags used for the texture + * + */ + FORCE_INLINE texture_flags flags() const + { + return _flags; + } - streamfx::obs::gs::texture::type get_type(); + /** The file from which this texture originates from. + * + * Will be an empty string if not created from a file. + */ + FORCE_INLINE std::filesystem::path file() const + { + return _file; + } - gs_color_format get_color_format(); + /** Load the texture into a texture unit slot. + * + */ + FORCE_INLINE void load(int32_t unit) + { + auto gctx = streamfx::obs::gs::context(); + gs_load_texture(_texture, unit); + } + + FORCE_INLINE operator gs_texture_t*() const + { + return _texture; + } + + public: + class pool; + typedef std::tuple _pool_key_t; + typedef streamfx::util::multipool _pool_t; + + class pool : public _pool_t { + friend streamfx::util::singleton; + friend _pool_t; + + public: + virtual ~pool() {} + + protected: + pool() : _pool_t() {} + + static _pool_key_t as_key(streamfx::obs::gs::texture* ptr) + { + if (auto file = ptr->file(); !file.empty()) { + return _pool_key_t{0, file}; + } else { + uint64_t left = (uint64_t(ptr->width()) << 48) | (uint64_t(ptr->height()) << 32) | (uint64_t(ptr->color_format()) << 16) | (uint64_t(ptr->flags()) << 0); + return _pool_key_t{left, std::string()}; + } + } + + static _pool_key_t as_key(uint32_t width, uint32_t height, gs_color_format format, uint32_t mip_levels, const uint8_t**, texture_flags flags) + { + uint64_t left = (uint64_t(width) << 48) | (uint64_t(height) << 32) | (uint64_t(format) << 16) | (uint64_t(flags) << 0); + return _pool_key_t{left, std::string()}; + } + + static _pool_key_t as_key(std::string file) + { + return _pool_key_t{0, file}; + } + + virtual void reset(void* ptr) {} + }; }; } // namespace streamfx::obs::gs -P_ENABLE_BITMASK_OPERATORS(streamfx::obs::gs::texture::flags) +P_ENABLE_BITMASK_OPERATORS(streamfx::obs::gs::texture_flags) diff --git a/source/obs/gs/gs-vertex.cpp b/source/obs/gs/gs-vertex.cpp index 851e0e3..8f4bff3 100644 --- a/source/obs/gs/gs-vertex.cpp +++ b/source/obs/gs/gs-vertex.cpp @@ -1,5 +1,5 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks // AUTOGENERATED COPYRIGHT HEADER END #include "gs-vertex.hpp" @@ -10,7 +10,7 @@ streamfx::obs::gs::vertex::vertex() : position(nullptr), normal(nullptr), tangent(nullptr), color(nullptr), _has_store(true), _store(nullptr) { - _store = streamfx::util::malloc_aligned(16, sizeof(vec3) * 3 + sizeof(uint32_t) + sizeof(vec4) * MAXIMUM_UVW_LAYERS); + _store = streamfx::util::memory::malloc_aligned(16, sizeof(vec3) * 3 + sizeof(uint32_t) + sizeof(vec4) * MAXIMUM_UVW_LAYERS); std::size_t offset = 0; @@ -35,7 +35,7 @@ streamfx::obs::gs::vertex::vertex() : position(nullptr), normal(nullptr), tangen streamfx::obs::gs::vertex::~vertex() { if (_has_store) { - streamfx::util::free_aligned(_store); + streamfx::util::memory::free_aligned(_store); } } diff --git a/source/obs/gs/gs-vertex.hpp b/source/obs/gs/gs-vertex.hpp index e6ca003..61dfda8 100644 --- a/source/obs/gs/gs-vertex.hpp +++ b/source/obs/gs/gs-vertex.hpp @@ -1,5 +1,5 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks // AUTOGENERATED COPYRIGHT HEADER END #pragma once diff --git a/source/obs/gs/gs-vertexbuffer.cpp b/source/obs/gs/gs-vertexbuffer.cpp index 0fb5e5a..8f77dd9 100644 --- a/source/obs/gs/gs-vertexbuffer.cpp +++ b/source/obs/gs/gs-vertexbuffer.cpp @@ -1,5 +1,5 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks // Copyright (C) 2022 lainon // AUTOGENERATED COPYRIGHT HEADER END @@ -10,7 +10,7 @@ #include #include "warning-enable.hpp" -void streamfx::obs::gs::vertex_buffer::initialize(uint32_t capacity, uint8_t layers) +void streamfx::obs::gs::vertexbuffer::initialize(uint32_t capacity, uint8_t layers) { finalize(); @@ -21,45 +21,45 @@ void streamfx::obs::gs::vertex_buffer::initialize(uint32_t capacity, uint8_t lay throw std::out_of_range("layers"); } - // Allocate memory for data. - _data = std::make_shared(); - _data->num = _capacity; - _data->num_tex = _layers; - _data->points = _positions = static_cast(streamfx::util::malloc_aligned(16, sizeof(vec3) * _capacity)); - _data->normals = _normals = static_cast(streamfx::util::malloc_aligned(16, sizeof(vec3) * _capacity)); - _data->tangents = _tangents = static_cast(streamfx::util::malloc_aligned(16, sizeof(vec3) * _capacity)); - _data->colors = _colors = static_cast(streamfx::util::malloc_aligned(16, sizeof(uint32_t) * _capacity)); + // Allocate necessary memory for storing information. + _positions = decltype(_positions)(static_cast(streamfx::util::memory::malloc_aligned(16, sizeof(decltype(_positions)::element_type) * _capacity)), [](decltype(_positions)::element_type* v) { streamfx::util::memory::free_aligned(v); }); + _normals = decltype(_normals)(static_cast(streamfx::util::memory::malloc_aligned(16, sizeof(vec3) * _capacity)), [](vec3* v) { streamfx::util::memory::free_aligned(v); }); + _tangents = decltype(_tangents)(static_cast(streamfx::util::memory::malloc_aligned(16, sizeof(vec3) * _capacity)), [](vec3* v) { streamfx::util::memory::free_aligned(v); }); + _colors = decltype(_colors)(static_cast(streamfx::util::memory::malloc_aligned(16, sizeof(decltype(_colors)::element_type) * _capacity)), [](decltype(_colors)::element_type* v) { streamfx::util::memory::free_aligned(v); }); + if (_layers) { + for (auto i = 0; i < _layers; i++) { + using tn = std::remove_all_extents::type; + _uvs[i] = tn(static_cast(streamfx::util::memory::malloc_aligned(16, sizeof(tn::element_type) * _capacity)), [](tn::element_type* v) { streamfx::util::memory::free_aligned(v); }); + } + } - // Clear the allocated memory of any data. - memset(_positions, 0, sizeof(vec3) * _capacity); - memset(_normals, 0, sizeof(vec3) * _capacity); - memset(_tangents, 0, sizeof(vec3) * _capacity); - memset(_colors, 0, sizeof(uint32_t) * _capacity); + // Allocate memory for data. + _data = std::make_unique(); + _data->num = _capacity; + _data->num_tex = _layers; + _data->points = _positions.get(); + _data->normals = _normals.get(); + _data->tangents = _tangents.get(); + _data->colors = _colors.get(); if (_layers == 0) { _data->tvarray = nullptr; } else { - _data->tvarray = _uv_layers = static_cast(streamfx::util::malloc_aligned(16, sizeof(gs_tvertarray) * _layers)); - for (uint8_t n = 0; n < _layers; n++) { - _uv_layers[n].array = _uvs[n] = static_cast(streamfx::util::malloc_aligned(16, sizeof(vec4) * _capacity)); - _uv_layers[n].width = 4; - memset(_uvs[n], 0, sizeof(vec4) * _capacity); + _uv_layers = decltype(_uv_layers)(static_cast(streamfx::util::memory::malloc_aligned(16, sizeof(decltype(_uv_layers)::element_type) * _capacity)), [](decltype(_uv_layers)::element_type* v) { streamfx::util::memory::free_aligned(v); }); + _data->tvarray = _uv_layers.get(); + for (auto i = 0; i < _layers; i++) { + // This insanity is sponsored by C. It works, get over it. + _uv_layers.get()[i].array = _uvs[i].get(); + _uv_layers.get()[i].width = 4; } } // Allocate actual GPU vertex buffer. { auto gctx = streamfx::obs::gs::context(); - _buffer = decltype(_buffer)(gs_vertexbuffer_create(_data.get(), GS_DYNAMIC | GS_DUP_BUFFER), [this](gs_vertbuffer_t* v) { - try { - auto gctx = streamfx::obs::gs::context(); - gs_vertexbuffer_destroy(v); - } catch (...) { - if (obs_get_version() < MAKE_SEMANTIC_VERSION(26, 0, 0)) { - // Fixes a memory leak with OBS Studio versions older than 26.x. - gs_vbdata_destroy(_obs_data); - } - } + _buffer = decltype(_buffer)(gs_vertexbuffer_create(_data.get(), GS_DYNAMIC | GS_DUP_BUFFER), [](gs_vertbuffer_t* v) { + auto gctx = streamfx::obs::gs::context(); + gs_vertexbuffer_destroy(v); }); _obs_data = gs_vertexbuffer_get_data(_buffer.get()); } @@ -69,28 +69,28 @@ void streamfx::obs::gs::vertex_buffer::initialize(uint32_t capacity, uint8_t lay } } -void streamfx::obs::gs::vertex_buffer::finalize() +void streamfx::obs::gs::vertexbuffer::finalize() { // Free data - streamfx::util::free_aligned(_positions); - streamfx::util::free_aligned(_normals); - streamfx::util::free_aligned(_tangents); - streamfx::util::free_aligned(_colors); - streamfx::util::free_aligned(_uv_layers); + _positions.reset(); + _normals.reset(); + _tangents.reset(); + _colors.reset(); + _uv_layers.reset(); for (std::size_t n = 0; n < _layers; n++) { - streamfx::util::free_aligned(_uvs[n]); + _uvs[n].reset(); } _buffer.reset(); _data.reset(); } -streamfx::obs::gs::vertex_buffer::~vertex_buffer() +streamfx::obs::gs::vertexbuffer::~vertexbuffer() { finalize(); } -streamfx::obs::gs::vertex_buffer::vertex_buffer(uint32_t size, uint8_t layers) +streamfx::obs::gs::vertexbuffer::vertexbuffer(uint32_t size, uint8_t layers) : _capacity(size), _size(size), _layers(layers), _buffer(nullptr), _data(nullptr), @@ -102,7 +102,7 @@ streamfx::obs::gs::vertex_buffer::vertex_buffer(uint32_t size, uint8_t layers) initialize(_size, _layers); } -streamfx::obs::gs::vertex_buffer::vertex_buffer(gs_vertbuffer_t* vb) +streamfx::obs::gs::vertexbuffer::vertexbuffer(gs_vertbuffer_t* vb) : _capacity(0), _size(0), _layers(0), _buffer(nullptr), _data(nullptr), @@ -119,23 +119,23 @@ streamfx::obs::gs::vertex_buffer::vertex_buffer(gs_vertbuffer_t* vb) initialize(static_cast(vbd->num), static_cast(vbd->num_tex)); if (_positions && vbd->points) - memcpy(_positions, vbd->points, vbd->num * sizeof(vec3)); + memcpy(_positions.get(), vbd->points, vbd->num * sizeof(vec3)); if (_normals && vbd->normals) - memcpy(_normals, vbd->normals, vbd->num * sizeof(vec3)); + memcpy(_normals.get(), vbd->normals, vbd->num * sizeof(vec3)); if (_tangents && vbd->tangents) - memcpy(_tangents, vbd->tangents, vbd->num * sizeof(vec3)); + memcpy(_tangents.get(), vbd->tangents, vbd->num * sizeof(vec3)); if (_colors && vbd->colors) - memcpy(_colors, vbd->colors, vbd->num * sizeof(uint32_t)); + memcpy(_colors.get(), vbd->colors, vbd->num * sizeof(uint32_t)); if (vbd->tvarray != nullptr) { for (std::size_t n = 0; n < vbd->num_tex; n++) { if (vbd->tvarray[n].array != nullptr && vbd->tvarray[n].width <= 4 && vbd->tvarray[n].width > 0) { if (vbd->tvarray[n].width == 4) { - memcpy(_uvs[n], vbd->tvarray[n].array, vbd->num * sizeof(vec4)); + memcpy(_uvs[n].get(), vbd->tvarray[n].array, vbd->num * sizeof(vec4)); } else if (vbd->tvarray[n].width < 4) { for (std::size_t idx = 0; idx < _capacity; idx++) { float* mem = reinterpret_cast(vbd->tvarray[n].array) + (idx * vbd->tvarray[n].width); - memset(&_uvs[n][idx], 0, sizeof(vec4)); - memcpy(&_uvs[n][idx], mem, vbd->tvarray[n].width); + memset(&_uvs[n].get()[idx], 0, sizeof(vec4)); + memcpy(&_uvs[n].get()[idx], mem, vbd->tvarray[n].width); } } } @@ -143,34 +143,34 @@ streamfx::obs::gs::vertex_buffer::vertex_buffer(gs_vertbuffer_t* vb) } } -streamfx::obs::gs::vertex_buffer::vertex_buffer(vertex_buffer const& other) : vertex_buffer(other._capacity, other._layers) +streamfx::obs::gs::vertexbuffer::vertexbuffer(vertexbuffer const& other) : vertexbuffer(other._capacity, other._layers) { // Copy Constructor - memcpy(_positions, other._positions, _capacity * sizeof(vec3)); - memcpy(_normals, other._normals, _capacity * sizeof(vec3)); - memcpy(_tangents, other._tangents, _capacity * sizeof(vec3)); - memcpy(_colors, other._colors, _capacity * sizeof(vec3)); + memcpy(_positions.get(), other._positions.get(), _capacity * sizeof(vec3)); + memcpy(_normals.get(), other._normals.get(), _capacity * sizeof(vec3)); + memcpy(_tangents.get(), other._tangents.get(), _capacity * sizeof(vec3)); + memcpy(_colors.get(), other._colors.get(), _capacity * sizeof(vec3)); for (std::size_t n = 0; n < other._layers; n++) { - memcpy(_uvs[n], other._uvs[n], _capacity * sizeof(vec4)); + memcpy(_uvs[n].get(), other._uvs[n].get(), _capacity * sizeof(vec4)); } } -void streamfx::obs::gs::vertex_buffer::operator=(vertex_buffer const& other) +void streamfx::obs::gs::vertexbuffer::operator=(vertexbuffer const& other) { // Copy operator initialize(other._capacity, other._layers); _size = other._size; // Copy actual data over. - memcpy(_positions, other._positions, other._capacity * sizeof(vec3)); - memcpy(_normals, other._normals, other._capacity * sizeof(vec3)); - memcpy(_tangents, other._tangents, other._capacity * sizeof(vec3)); - memcpy(_colors, other._colors, other._capacity * sizeof(uint32_t)); - memcpy(_uv_layers, other._uv_layers, sizeof(gs_tvertarray)); + memcpy(_positions.get(), other._positions.get(), other._capacity * sizeof(vec3)); + memcpy(_normals.get(), other._normals.get(), other._capacity * sizeof(vec3)); + memcpy(_tangents.get(), other._tangents.get(), other._capacity * sizeof(vec3)); + memcpy(_colors.get(), other._colors.get(), other._capacity * sizeof(uint32_t)); + memcpy(_uv_layers.get(), other._uv_layers.get(), sizeof(gs_tvertarray)); for (std::size_t n = 0; n < other._layers; n++) { - memcpy(_uvs[n], other._uvs[n], _capacity * sizeof(vec4)); + memcpy(_uvs[n].get(), other._uvs[n].get(), _capacity * sizeof(vec4)); } } -streamfx::obs::gs::vertex_buffer::vertex_buffer(vertex_buffer const&& other) noexcept +streamfx::obs::gs::vertexbuffer::vertexbuffer(vertexbuffer const&& other) noexcept { // Move Constructor _capacity = other._capacity; _size = other._size; @@ -188,7 +188,7 @@ streamfx::obs::gs::vertex_buffer::vertex_buffer(vertex_buffer const&& other) noe _obs_data = other._obs_data; } -void streamfx::obs::gs::vertex_buffer::operator=(vertex_buffer const&& other) noexcept +void streamfx::obs::gs::vertexbuffer::operator=(vertexbuffer const&& other) noexcept { // Move Assignment finalize(); @@ -208,7 +208,7 @@ void streamfx::obs::gs::vertex_buffer::operator=(vertex_buffer const&& other) no _obs_data = other._obs_data; } -void streamfx::obs::gs::vertex_buffer::resize(uint32_t size) +void streamfx::obs::gs::vertexbuffer::resize(uint32_t size) { if (size > _capacity) { throw std::out_of_range("size larger than capacity"); @@ -216,78 +216,78 @@ void streamfx::obs::gs::vertex_buffer::resize(uint32_t size) _size = size; } -uint32_t streamfx::obs::gs::vertex_buffer::size() +uint32_t streamfx::obs::gs::vertexbuffer::size() { return _size; } -uint32_t streamfx::obs::gs::vertex_buffer::capacity() +uint32_t streamfx::obs::gs::vertexbuffer::capacity() { return _capacity; } -bool streamfx::obs::gs::vertex_buffer::empty() +bool streamfx::obs::gs::vertexbuffer::empty() { return _size == 0; } -const streamfx::obs::gs::vertex streamfx::obs::gs::vertex_buffer::at(uint32_t idx) +const streamfx::obs::gs::vertex streamfx::obs::gs::vertexbuffer::at(uint32_t idx) { if (idx >= _size) { throw std::out_of_range("idx out of range"); } - streamfx::obs::gs::vertex vtx(&_positions[idx], &_normals[idx], &_tangents[idx], &_colors[idx], nullptr); + streamfx::obs::gs::vertex vtx(&_positions.get()[idx], &_normals.get()[idx], &_tangents.get()[idx], &_colors.get()[idx], nullptr); for (std::size_t n = 0; n < _layers; n++) { - vtx.uv[n] = &_uvs[n][idx]; + vtx.uv[n] = &_uvs[n].get()[idx]; } return vtx; } -const streamfx::obs::gs::vertex streamfx::obs::gs::vertex_buffer::operator[](uint32_t const pos) +const streamfx::obs::gs::vertex streamfx::obs::gs::vertexbuffer::operator[](uint32_t const pos) { return at(pos); } -void streamfx::obs::gs::vertex_buffer::set_uv_layers(uint8_t layers) +void streamfx::obs::gs::vertexbuffer::set_uv_layers(uint8_t layers) { _layers = layers; } -uint8_t streamfx::obs::gs::vertex_buffer::get_uv_layers() +uint8_t streamfx::obs::gs::vertexbuffer::get_uv_layers() { return _layers; } -vec3* streamfx::obs::gs::vertex_buffer::get_positions() +vec3* streamfx::obs::gs::vertexbuffer::get_positions() { - return _positions; + return _positions.get(); } -vec3* streamfx::obs::gs::vertex_buffer::get_normals() +vec3* streamfx::obs::gs::vertexbuffer::get_normals() { - return _normals; + return _normals.get(); } -vec3* streamfx::obs::gs::vertex_buffer::get_tangents() +vec3* streamfx::obs::gs::vertexbuffer::get_tangents() { - return _tangents; + return _tangents.get(); } -uint32_t* streamfx::obs::gs::vertex_buffer::get_colors() +uint32_t* streamfx::obs::gs::vertexbuffer::get_colors() { - return _colors; + return _colors.get(); } -vec4* streamfx::obs::gs::vertex_buffer::get_uv_layer(uint8_t idx) +vec4* streamfx::obs::gs::vertexbuffer::get_uv_layer(uint8_t idx) { if (idx >= _layers) { throw std::out_of_range("idx out of range"); } - return _uvs[idx]; + return _uvs[idx].get(); } -gs_vertbuffer_t* streamfx::obs::gs::vertex_buffer::update(bool refreshGPU) +gs_vertbuffer_t* streamfx::obs::gs::vertexbuffer::update(bool refreshGPU) { if (refreshGPU) { auto gctx = streamfx::obs::gs::context(); @@ -297,7 +297,80 @@ gs_vertbuffer_t* streamfx::obs::gs::vertex_buffer::update(bool refreshGPU) return _buffer.get(); } -gs_vertbuffer_t* streamfx::obs::gs::vertex_buffer::update() +gs_vertbuffer_t* streamfx::obs::gs::vertexbuffer::update() { return update(true); } + +streamfx::obs::gs::vertexbuffer::pool::pool() {} + +streamfx::obs::gs::vertexbuffer::pool::~pool() {} + +void streamfx::obs::gs::vertexbuffer::pool::release(_underlying* value) +{ + cleanup(); + + std::unique_lock lock{_lock}; + auto key = _key{value->capacity(), value->get_uv_layers()}; + auto rvalue = _value{_tracked{value}, std::chrono::high_resolution_clock::now()}; + if (auto kv = _pool.find(key); kv != _pool.end()) { + kv->second.push_front(rvalue); + } else { + _pool.emplace(key, std::list{rvalue}); + } +} + +streamfx::obs::gs::vertexbuffer::pool::_tracked streamfx::obs::gs::vertexbuffer::pool::acquire(uint32_t capacity, uint8_t layers) +{ + cleanup(); + + std::unique_lock lock{_lock}; + _underlying* ptr = nullptr; + if (auto kv = _pool.find(_key{capacity, layers}); kv != _pool.end()) { + // Found an existing list and item. + auto value = kv->second.front(); + kv->second.pop_front(); + ptr = new streamfx::obs::gs::vertexbuffer(std::move(*value.first.get())); // Move Construction should do the trick + } else { + ptr = new streamfx::obs::gs::vertexbuffer(capacity, layers); + } + + return _tracked(ptr, [](_underlying* v) { streamfx::obs::gs::vertexbuffer::pool::instance()->release(v); }); +} + +void streamfx::obs::gs::vertexbuffer::pool::cleanup() +{ + auto time = std::chrono::high_resolution_clock::now(); + + std::unique_lock lock{_lock}; + for (auto kv : _pool) { + std::erase_if(kv.second, [&time](const auto& value) { return ((time - value.second) > std::chrono::seconds(1)); }); + } + std::erase_if(_pool, [](const auto& value) { return value.second.empty(); }); +} + +std::shared_ptr streamfx::obs::gs::vertexbuffer::pool::instance() +{ + static std::weak_ptr winst; + static std::mutex mtx; + + std::unique_lock lock(mtx); + auto instance = winst.lock(); + if (!instance) { + instance = std::shared_ptr(new streamfx::obs::gs::vertexbuffer::pool()); + winst = instance; + } + return instance; +} + +static std::shared_ptr loader_instance; + +static auto loader = streamfx::component( + "core::gs::vertexbuffer", + []() { // Initializer + loader_instance = streamfx::obs::gs::vertexbuffer::pool::instance(); + }, + []() { // Finalizer + loader_instance.reset(); + }, + {}); diff --git a/source/obs/gs/gs-vertexbuffer.hpp b/source/obs/gs/gs-vertexbuffer.hpp index 3127915..b1d8e9b 100644 --- a/source/obs/gs/gs-vertexbuffer.hpp +++ b/source/obs/gs/gs-vertexbuffer.hpp @@ -1,5 +1,5 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks // Copyright (C) 2022 lainon // AUTOGENERATED COPYRIGHT HEADER END @@ -8,23 +8,31 @@ #include "gs-limits.hpp" #include "gs-vertex.hpp" +#include "warning-disable.hpp" +#include +#include +#include +#include +#include +#include "warning-enable.hpp" + namespace streamfx::obs::gs { - class vertex_buffer { + class vertexbuffer { uint32_t _capacity; uint32_t _size; uint8_t _layers; - // OBS GS Data - std::shared_ptr _buffer; - std::shared_ptr _data; + // libOBS Data + std::shared_ptr _buffer; + std::shared_ptr _data; + std::shared_ptr _uv_layers; - // Memory Storage - vec3* _positions; - vec3* _normals; - vec3* _tangents; - uint32_t* _colors; - gs_tvertarray* _uv_layers; - vec4* _uvs[MAXIMUM_UVW_LAYERS]; + // Our Data + std::shared_ptr _positions; + std::shared_ptr _normals; + std::shared_ptr _tangents; + std::shared_ptr _colors; + std::shared_ptr _uvs[MAXIMUM_UVW_LAYERS]; // OBS compatability gs_vb_data* _obs_data; @@ -33,19 +41,19 @@ namespace streamfx::obs::gs { void finalize(); public: - virtual ~vertex_buffer(); + virtual ~vertexbuffer(); /*! * \brief Create a Vertex Buffer with the default number of Vertices. */ - vertex_buffer() : vertex_buffer(MAXIMUM_VERTICES, MAXIMUM_UVW_LAYERS) {} + vertexbuffer() : vertexbuffer(MAXIMUM_VERTICES, MAXIMUM_UVW_LAYERS) {} /*! * \brief Create a Vertex Buffer with a specific number of Vertices. * * \param vertices Number of vertices to store. */ - vertex_buffer(uint32_t vertices) : vertex_buffer(vertices, MAXIMUM_UVW_LAYERS) {} + vertexbuffer(uint32_t vertices) : vertexbuffer(vertices, MAXIMUM_UVW_LAYERS) {} /*! * \brief Create a Vertex Buffer with a specific number of Vertices and uv layers. @@ -53,7 +61,7 @@ namespace streamfx::obs::gs { * \param vertices Number of vertices to store. * \param layers Number of uv layers to store. */ - vertex_buffer(uint32_t vertices, uint8_t layers); + vertexbuffer(uint32_t vertices, uint8_t layers); /*! * \brief Create a copy of a Vertex Buffer @@ -61,7 +69,7 @@ namespace streamfx::obs::gs { * * \param other The Vertex Buffer to copy */ - vertex_buffer(gs_vertbuffer_t* other); + vertexbuffer(gs_vertbuffer_t* other); // Copy Constructor & Assignments @@ -71,7 +79,7 @@ namespace streamfx::obs::gs { * * \param other */ - vertex_buffer(vertex_buffer const& other); + vertexbuffer(vertexbuffer const& other); /*! * \brief Copy Assignment @@ -79,7 +87,7 @@ namespace streamfx::obs::gs { * * \param other */ - void operator=(vertex_buffer const& other); + void operator=(vertexbuffer const& other); // Move Constructor & Assignments @@ -89,7 +97,7 @@ namespace streamfx::obs::gs { * * \param other */ - vertex_buffer(vertex_buffer const&& other) noexcept; + vertexbuffer(vertexbuffer const&& other) noexcept; /*! * \brief Move Assignment @@ -97,7 +105,7 @@ namespace streamfx::obs::gs { * * \param other */ - void operator=(vertex_buffer const&& other) noexcept; + void operator=(vertexbuffer const&& other) noexcept; void resize(uint32_t new_size); @@ -158,5 +166,31 @@ namespace streamfx::obs::gs { gs_vertbuffer_t* update(); gs_vertbuffer_t* update(bool refreshGPU); + + public: + class pool { + typedef streamfx::obs::gs::vertexbuffer _underlying; + typedef std::shared_ptr<_underlying> _tracked; + typedef std::pair _key; + typedef std::pair<_tracked, std::chrono::high_resolution_clock::time_point> _value; + + std::map<_key, std::list<_value>> _pool; + std::mutex _lock; + + private: + pool(); + + void release(_underlying* value); + + public: + ~pool(); + + _tracked acquire(uint32_t capacity = MAXIMUM_VERTICES, uint8_t layers = MAXIMUM_UVW_LAYERS); + + void cleanup(); + + public: + static std::shared_ptr instance(); + }; }; } // namespace streamfx::obs::gs diff --git a/source/obs/obs-encoder-factory.cpp b/source/obs/obs-encoder-factory.cpp index 91a3438..d0f3866 100644 --- a/source/obs/obs-encoder-factory.cpp +++ b/source/obs/obs-encoder-factory.cpp @@ -1,5 +1,5 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks // AUTOGENERATED COPYRIGHT HEADER END #include "obs-encoder-factory.hpp" diff --git a/source/obs/obs-signal-handler.cpp b/source/obs/obs-signal-handler.cpp index 73f9aca..432101d 100644 --- a/source/obs/obs-signal-handler.cpp +++ b/source/obs/obs-signal-handler.cpp @@ -1,5 +1,5 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks // AUTOGENERATED COPYRIGHT HEADER END #include "obs-signal-handler.hpp" diff --git a/source/obs/obs-source-active-child.cpp b/source/obs/obs-source-active-child.cpp index 469b6e4..f318f46 100644 --- a/source/obs/obs-source-active-child.cpp +++ b/source/obs/obs-source-active-child.cpp @@ -1,5 +1,5 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2022-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks // AUTOGENERATED COPYRIGHT HEADER END #include "obs-source-active-child.hpp" diff --git a/source/obs/obs-source-active-reference.cpp b/source/obs/obs-source-active-reference.cpp index fcf393a..71a5fce 100644 --- a/source/obs/obs-source-active-reference.cpp +++ b/source/obs/obs-source-active-reference.cpp @@ -1,5 +1,5 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2022-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks // AUTOGENERATED COPYRIGHT HEADER END #include "obs-source-active-reference.hpp" diff --git a/source/obs/obs-source-active-reference.hpp b/source/obs/obs-source-active-reference.hpp index d7faa32..41f5154 100644 --- a/source/obs/obs-source-active-reference.hpp +++ b/source/obs/obs-source-active-reference.hpp @@ -1,5 +1,5 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2022-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks // AUTOGENERATED COPYRIGHT HEADER END #pragma once diff --git a/source/obs/obs-source-factory.cpp b/source/obs/obs-source-factory.cpp index c7f43e2..7a686be 100644 --- a/source/obs/obs-source-factory.cpp +++ b/source/obs/obs-source-factory.cpp @@ -1,5 +1,5 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks // AUTOGENERATED COPYRIGHT HEADER END #include "obs-source-factory.hpp" diff --git a/source/obs/obs-source-factory.hpp b/source/obs/obs-source-factory.hpp index 91f83a8..f25e3c3 100644 --- a/source/obs/obs-source-factory.hpp +++ b/source/obs/obs-source-factory.hpp @@ -850,7 +850,7 @@ namespace streamfx::obs { return GS_CS_SRGB; } - virtual void video_tick(float_t seconds) {} + virtual void video_tick(float seconds) {} virtual void video_render(gs_effect_t* effect) {} diff --git a/source/obs/obs-source-showing-reference.cpp b/source/obs/obs-source-showing-reference.cpp index bfd9e4b..9d3c36a 100644 --- a/source/obs/obs-source-showing-reference.cpp +++ b/source/obs/obs-source-showing-reference.cpp @@ -1,5 +1,5 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2022-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks // AUTOGENERATED COPYRIGHT HEADER END #include "obs-source-showing-reference.hpp" diff --git a/source/obs/obs-source-showing-reference.hpp b/source/obs/obs-source-showing-reference.hpp index c28f82e..eadf3d1 100644 --- a/source/obs/obs-source-showing-reference.hpp +++ b/source/obs/obs-source-showing-reference.hpp @@ -1,5 +1,5 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2022-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks // AUTOGENERATED COPYRIGHT HEADER END #pragma once diff --git a/source/obs/obs-source-tracker.cpp b/source/obs/obs-source-tracker.cpp index 4a010b7..427f0e5 100644 --- a/source/obs/obs-source-tracker.cpp +++ b/source/obs/obs-source-tracker.cpp @@ -252,11 +252,12 @@ std::shared_ptr streamfx::obs::source_tracker::in static std::shared_ptr loader_instance; -static auto loader = streamfx::loader( - []() { // Initalizer +static auto loader = streamfx::component( + "core::source_tracker", + []() { // Initializer loader_instance = streamfx::obs::source_tracker::instance(); }, []() { // Finalizer loader_instance.reset(); }, - streamfx::loader_priority::HIGHEST); // Does not rely on other critical functionality. + {"core::threadpool"}); diff --git a/source/obs/obs-source.cpp b/source/obs/obs-source.cpp index 51ea0ce..2cad4ed 100644 --- a/source/obs/obs-source.cpp +++ b/source/obs/obs-source.cpp @@ -1,5 +1,5 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2018-2023 Michael Fabian 'Xaymar' Dirks // AUTOGENERATED COPYRIGHT HEADER END #include "obs-source.hpp" diff --git a/source/obs/obs-source.hpp b/source/obs/obs-source.hpp index defdc68..d600279 100644 --- a/source/obs/obs-source.hpp +++ b/source/obs/obs-source.hpp @@ -1,5 +1,5 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks // AUTOGENERATED COPYRIGHT HEADER END #pragma once @@ -496,7 +496,7 @@ namespace streamfx::obs { */ FORCE_INLINE ::streamfx::obs::source get_filter_parent() { - return obs_filter_get_parent(_ref); + return {obs_filter_get_parent(_ref), false, false}; }; /** @@ -505,7 +505,7 @@ namespace streamfx::obs { */ FORCE_INLINE ::streamfx::obs::source get_filter_target() { - return obs_filter_get_target(_ref); + return {obs_filter_get_target(_ref), false, false}; }; /** @@ -601,6 +601,16 @@ namespace streamfx::obs { obs_source_dec_showing(_ref); } + public /* Video, Audio */: + /** + * + * EXPORT enum gs_color_space obs_source_get_color_space(obs_source_t *source, size_t count, const enum gs_color_space *preferred_spaces); + */ + gs_color_space color_space(size_t count, gs_color_space preferred_spaces[]) + { + return obs_source_get_color_space(_ref, count, preferred_spaces); + } + public /* ToDo */: /** * diff --git a/source/obs/obs-tools.cpp b/source/obs/obs-tools.cpp index a38834a..134c8cf 100644 --- a/source/obs/obs-tools.cpp +++ b/source/obs/obs-tools.cpp @@ -1,5 +1,5 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2018-2023 Michael Fabian 'Xaymar' Dirks // AUTOGENERATED COPYRIGHT HEADER END #include "obs-tools.hpp" diff --git a/source/obs/obs-tools.hpp b/source/obs/obs-tools.hpp index 71ea270..6260c3c 100644 --- a/source/obs/obs-tools.hpp +++ b/source/obs/obs-tools.hpp @@ -1,5 +1,5 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks // AUTOGENERATED COPYRIGHT HEADER END #pragma once diff --git a/source/obs/obs-weak-source.cpp b/source/obs/obs-weak-source.cpp index cc5ff26..e5fe792 100644 --- a/source/obs/obs-weak-source.cpp +++ b/source/obs/obs-weak-source.cpp @@ -1,5 +1,5 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2022-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks // AUTOGENERATED COPYRIGHT HEADER END #include "obs-weak-source.hpp" diff --git a/source/plugin.cpp b/source/plugin.cpp index ea304a2..2430de5 100644 --- a/source/plugin.cpp +++ b/source/plugin.cpp @@ -3,22 +3,13 @@ // AUTOGENERATED COPYRIGHT HEADER END #include "plugin.hpp" -#include "configuration.hpp" #include "gfx/gfx-opengl.hpp" #include "obs/gs/gs-helper.hpp" #include "obs/gs/gs-vertexbuffer.hpp" -#ifdef ENABLE_NVIDIA_CUDA -#include "nvidia/cuda/nvidia-cuda-obs.hpp" -#endif - -#ifdef ENABLE_FRONTEND +#include "configuration.hpp" #include "ui/ui.hpp" -#endif - -#ifdef ENABLE_UPDATER #include "updater.hpp" -#endif #include "warning-disable.hpp" #include @@ -30,39 +21,34 @@ static std::shared_ptr _streamfx_gfx_opengl; namespace streamfx { - typedef std::list loader_list_t; - typedef std::map loader_map_t; + struct component_info_t { + std::string name; + std::set dependencies; + loader_priority_t priority; + loader_function_t initializer; + loader_function_t finalizer; - loader_map_t& get_initializers() - { - static loader_map_t initializers; - return initializers; - } + typedef std::list component_list_t; - loader_map_t& get_finalizers() - { - static loader_map_t finalizers; - return finalizers; - } - - loader::loader(loader_function_t initializer, loader_function_t finalizer, loader_priority_t priority) - { - auto init_kv = get_initializers().find(priority); - if (init_kv != get_initializers().end()) { - init_kv->second.push_back(initializer); - } else { - get_initializers().emplace(priority, loader_list_t{initializer}); + static component_list_t& get() + { + static component_list_t list; + return list; } + }; - // Invert the order for finalizers. - auto ipriority = priority ^ static_cast(0xFFFFFFFFFFFFFFFF); - auto fina_kv = get_finalizers().find(ipriority); - if (fina_kv != get_finalizers().end()) { - fina_kv->second.push_back(finalizer); - } else { - get_finalizers().emplace(ipriority, loader_list_t{finalizer}); - } + component::component(std::string_view name, loader_function_t initializer, loader_function_t finalizer, std::set dependencies, loader_priority_t priority /*= loader_priority::DEFAULT*/) + { + component_info_t ld; + ld.name = name; + ld.dependencies = dependencies; + ld.priority = priority; + ld.initializer = initializer; + ld.finalizer = finalizer; + + component_info_t::get().push_back(ld); } + } // namespace streamfx MODULE_EXPORT bool obs_module_load(void) @@ -78,25 +64,63 @@ MODULE_EXPORT bool obs_module_load(void) } } -#ifdef ENABLE_NVIDIA_CUDA - // Initialize CUDA if features requested it. - std::shared_ptr<::streamfx::nvidia::cuda::obs> cuda; - try { - cuda = ::streamfx::nvidia::cuda::obs::get(); - } catch (...) { - // If CUDA failed to load, it is considered safe to ignore. - } -#endif + { // Run component initializers. + // ToDo: This can be optimized - // Run all initializers. - for (auto kv : streamfx::get_initializers()) { - for (auto init : kv.second) { - try { - init(); - } catch (const std::exception& ex) { - DLOG_ERROR("Initializer threw exception: %s", ex.what()); - } catch (...) { - DLOG_ERROR("Initializer threw unknown exception."); + // Retrieve the list + auto& components = streamfx::component_info_t::get(); + + // Sort by priority. + components.sort([](const streamfx::component_info_t::component_list_t::value_type& a, const streamfx::component_info_t::component_list_t::value_type& b) { return (a.priority < b.priority); }); + + // Create a copy of the list and clear the original. + auto unsorted_components = components; + components.clear(); + + std::set resolved; + while (resolved.size() < unsorted_components.size()) { + bool has_loaded_anything = false; + + for (auto ld : unsorted_components) { + // Skip entries that have already been loaded. + if (resolved.contains(ld.name)) { + continue; + } + + // Check if all dependencies are met. + bool have_dependencies = true; + for (auto dep : ld.dependencies) { + if (!resolved.contains(dep)) { + have_dependencies = false; + break; + } + } + if (!have_dependencies) { + continue; + } + + // Call the initializer + try { + ld.initializer(); + has_loaded_anything = true; + + // Add back into the original list. + components.push_back(ld); + + // And mark as loaded. + resolved.emplace(ld.name); + + DLOG_INFO("Component %s loaded.", ld.name.c_str()); + } catch (const std::exception& ex) { + DLOG_ERROR("Initializer threw exception: %s", ex.what()); + } catch (...) { + DLOG_ERROR("Initializer threw unknown exception."); + } + } + + // Fatal error if loading stalled. + if (!has_loaded_anything) { + throw std::runtime_error("Loading components stalled, this is a fatal error."); } } } @@ -112,21 +136,22 @@ MODULE_EXPORT bool obs_module_load(void) } } +MODULE_EXPORT void obs_module_post_load(void) {} + MODULE_EXPORT void obs_module_unload(void) { try { DLOG_INFO("Unloading Version %s", STREAMFX_VERSION_STRING); - // Run all finalizers. - for (auto kv : streamfx::get_finalizers()) { - for (auto init : kv.second) { - try { - init(); - } catch (const std::exception& ex) { - DLOG_ERROR("Finalizer threw exception: %s", ex.what()); - } catch (...) { - DLOG_ERROR("Finalizer threw unknown exception."); - } + // Run component finalizers. + auto& components = streamfx::component_info_t::get(); + for (auto itr = components.rbegin(); itr != components.crend(); ++itr) { + try { + itr->finalizer(); + } catch (const std::exception& ex) { + DLOG_ERROR("Finalizer threw exception: %s", ex.what()); + } catch (...) { + DLOG_ERROR("Finalizer threw unknown exception."); } } @@ -144,16 +169,11 @@ MODULE_EXPORT void obs_module_unload(void) } } -std::shared_ptr streamfx::threadpool() -{ - return streamfx::util::threadpool::threadpool::instance(); -} - std::filesystem::path streamfx::data_file_path(std::string_view file) { - const char* root_path = obs_get_module_data_path(obs_current_module()); + const char8_t* root_path = reinterpret_cast(obs_get_module_data_path(obs_current_module())); if (root_path) { - auto ret = std::filesystem::u8path(root_path); + auto ret = std::filesystem::path(root_path); ret.append(file.data()); return ret; } else { @@ -163,9 +183,9 @@ std::filesystem::path streamfx::data_file_path(std::string_view file) std::filesystem::path streamfx::config_file_path(std::string_view file) { - char* root_path = obs_module_get_config_path(obs_current_module(), file.data()); + char8_t* root_path = reinterpret_cast(obs_module_get_config_path(obs_current_module(), file.data())); if (root_path) { - auto ret = std::filesystem::u8path(root_path); + auto ret = std::filesystem::path(root_path); bfree(root_path); return ret; } else { @@ -173,10 +193,16 @@ std::filesystem::path streamfx::config_file_path(std::string_view file) } } -#ifdef ENABLE_FRONTEND bool streamfx::open_url(std::string_view url) { QUrl qurl = QString::fromUtf8(url.data()); return QDesktopServices::openUrl(qurl); } -#endif + +const char* streamfx::translate(const char* key, const char* fallback) +{ + const char* out = nullptr; + if (obs_module_get_string(key, &out)) + return out; + return fallback; +} diff --git a/source/plugin.hpp b/source/plugin.hpp index ef843a2..da79ca0 100644 --- a/source/plugin.hpp +++ b/source/plugin.hpp @@ -1,12 +1,17 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks // AUTOGENERATED COPYRIGHT HEADER END #pragma once #include "common.hpp" #include "warning-disable.hpp" +#include #include +#include +#include +#include +#include #include "warning-enable.hpp" namespace streamfx { @@ -21,26 +26,21 @@ namespace streamfx { HIGH = INT32_MIN / 4 * 2, ABOVE = INT32_MIN / 4, NORMAL = 0, + DEFAULT = NORMAL, BELOW = INT32_MAX / 4, LOW = INT32_MAX / 4 * 2, LOWER = INT32_MAX / 4 * 3, LOWEST = INT32_MAX, }; - struct loader { - loader(loader_function_t initializer, loader_function_t finalizer, loader_priority_t priority); - - // Usage: - // auto loader = streamfx::loader([]() { ... }, []() { ... }, 0); + struct component { + component(std::string_view name, loader_function_t initializer, loader_function_t finalizer, std::set dependencies, loader_priority_t priority = loader_priority::DEFAULT); }; - // Threadpool - std::shared_ptr threadpool(); - std::filesystem::path data_file_path(std::string_view file); std::filesystem::path config_file_path(std::string_view file); -#ifdef ENABLE_FRONTEND bool open_url(std::string_view url); -#endif + + const char* translate(const char* key, const char* fallback); } // namespace streamfx diff --git a/source/strings.hpp b/source/strings.hpp index 403692d..7977e46 100644 --- a/source/strings.hpp +++ b/source/strings.hpp @@ -1,5 +1,5 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks // AUTOGENERATED COPYRIGHT HEADER END #pragma once @@ -9,6 +9,7 @@ #define S_PREFIX "streamfx-" #define D_TRANSLATE(x) obs_module_text(x) +#define D_TRANSLATE_DEFAULT(x, y) streamfx::translate(x, y) #define S_MANUAL_OPEN "Manual.Open" diff --git a/source/ui/ui-about-entry.hpp b/source/ui/ui-about-entry.hpp index 84ce97a..17fce27 100644 --- a/source/ui/ui-about-entry.hpp +++ b/source/ui/ui-about-entry.hpp @@ -1,5 +1,5 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks // AUTOGENERATED COPYRIGHT HEADER END #pragma once diff --git a/source/ui/ui-common.hpp b/source/ui/ui-common.hpp index de69ce1..6e0160f 100644 --- a/source/ui/ui-common.hpp +++ b/source/ui/ui-common.hpp @@ -1,5 +1,5 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks // AUTOGENERATED COPYRIGHT HEADER END #pragma once diff --git a/source/ui/ui-obs-browser-widget.cpp b/source/ui/ui-obs-browser-widget.cpp index 1a37124..0b9be4d 100644 --- a/source/ui/ui-obs-browser-widget.cpp +++ b/source/ui/ui-obs-browser-widget.cpp @@ -36,7 +36,7 @@ streamfx::ui::obs_browser_cef::obs_browser_cef() _cef->wait_for_browser_init(); // Create a generic Cookie manager for widgets. - _cookie = _cef->create_cookie_manager(streamfx::config_file_path("cookies").u8string(), false); + _cookie = _cef->create_cookie_manager(streamfx::config_file_path("cookies").string(), false); } streamfx::ui::obs_browser_cef::~obs_browser_cef() diff --git a/source/ui/ui.cpp b/source/ui/ui.cpp index 2443c7b..25f1f40 100644 --- a/source/ui/ui.cpp +++ b/source/ui/ui.cpp @@ -72,11 +72,7 @@ streamfx::ui::handler::handler() _about_action(), _about_dialog(), - _translator() -#ifdef ENABLE_UPDATER - , - _updater() -#endif + _translator(), _updater() { obs_frontend_add_event_callback(frontend_event_handler, this); } @@ -168,9 +164,7 @@ void streamfx::ui::handler::on_obs_loaded() } // Create the updater. -#ifdef ENABLE_UPDATER _updater = streamfx::ui::updater::instance(_menu); -#endif _menu->addSeparator(); @@ -204,10 +198,7 @@ void streamfx::ui::handler::on_obs_loaded() } // Let the Updater start its work. - -#ifdef ENABLE_UPDATER this->_updater->obs_ready(); -#endif } void streamfx::ui::handler::on_obs_exit() @@ -260,7 +251,7 @@ void streamfx::ui::handler::on_action_about(bool checked) std::shared_ptr streamfx::ui::handler::instance() { static std::weak_ptr winst; - static std::mutex mtx; + static std::mutex mtx; std::unique_lock lock(mtx); auto instance = winst.lock(); @@ -294,11 +285,12 @@ QString streamfx::ui::translator::translate(const char* context, const char* sou static std::shared_ptr loader_instance; -static auto loader = streamfx::loader( - []() { // Initalizer +static auto loader = streamfx::component( + "ui", + []() { // Initializer loader_instance = streamfx::ui::handler::instance(); }, []() { // Finalizer loader_instance.reset(); }, - streamfx::loader_priority::LOWEST); // Must be loaded after all other functionality. + {"core::threadpool", "core::configuration"}); diff --git a/source/ui/ui.hpp b/source/ui/ui.hpp index b1da769..13d5e02 100644 --- a/source/ui/ui.hpp +++ b/source/ui/ui.hpp @@ -5,10 +5,7 @@ #pragma once #include "ui-common.hpp" #include "ui-about.hpp" - -#ifdef ENABLE_UPDATER #include "ui-updater.hpp" -#endif namespace streamfx::ui { class handler : public QObject { @@ -32,9 +29,7 @@ namespace streamfx::ui { QTranslator* _translator; -#ifdef ENABLE_UPDATER std::shared_ptr _updater; -#endif private: handler(); diff --git a/source/updater.cpp b/source/updater.cpp index 7114a80..6b641b9 100644 --- a/source/updater.cpp +++ b/source/updater.cpp @@ -485,7 +485,7 @@ void streamfx::updater::refresh() save(); // Spawn a new task. - _task = streamfx::threadpool()->push(std::bind(&streamfx::updater::task, this, std::placeholders::_1), nullptr); + _task = streamfx::util::threadpool::threadpool::instance()->push(std::bind(&streamfx::updater::task, this, std::placeholders::_1), nullptr); } else { events.refreshed(*this); } diff --git a/source/util/util-event.hpp b/source/util/util-event.hpp index c90f6cc..cbbbc5f 100644 --- a/source/util/util-event.hpp +++ b/source/util/util-event.hpp @@ -1,5 +1,5 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks // Copyright (C) 2022 lainon // AUTOGENERATED COPYRIGHT HEADER END diff --git a/source/util/util-library.cpp b/source/util/util-library.cpp index 7c733e9..51c5642 100644 --- a/source/util/util-library.cpp +++ b/source/util/util-library.cpp @@ -25,7 +25,7 @@ streamfx::util::library::library(std::filesystem::path file) : _library(nullptr) { #if defined(ST_WINDOWS) SetLastError(ERROR_SUCCESS); - auto wfile = ::streamfx::util::platform::utf8_to_native(file.u8string()); + auto wfile = file.wstring(); if (file.is_absolute()) { _library = reinterpret_cast(LoadLibraryExW(wfile.c_str(), nullptr, LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR)); } else { @@ -46,7 +46,7 @@ streamfx::util::library::library(std::filesystem::path file) : _library(nullptr) throw std::runtime_error(ex); } #elif defined(ST_UNIX) - _library = dlopen(file.u8string().c_str(), RTLD_LAZY); + _library = dlopen(file.generic_string().c_str(), RTLD_LAZY); if (!_library) { if (char* error = dlerror(); error) throw std::runtime_error(error); @@ -78,7 +78,7 @@ void* streamfx::util::library::load_symbol(std::string_view name) #endif } -static std::unordered_map> libraries; +static std::unordered_map> libraries; std::shared_ptr<::streamfx::util::library> streamfx::util::library::load(std::filesystem::path file) { @@ -97,13 +97,17 @@ std::shared_ptr<::streamfx::util::library> streamfx::util::library::load(std::fi std::shared_ptr<::streamfx::util::library> streamfx::util::library::load(std::string_view name) { - return load(std::filesystem::u8path(name)); + return load(std::filesystem::path(name)); } std::shared_ptr<::streamfx::util::library> streamfx::util::library::load(obs_module_t* instance) { + if (!instance) { + throw std::runtime_error("Can't load nullptr as a library."); + } + // Get an absolute path to the module. - auto path = std::filesystem::absolute(std::filesystem::u8path(obs_get_module_binary_path(instance))); + auto path = std::filesystem::absolute(std::filesystem::path(std::u8string(reinterpret_cast(obs_get_module_binary_path(instance))))); // Find by absolute path. auto kv = libraries.find(path.u8string()); diff --git a/source/util/util-logging.cpp b/source/util/util-logging.cpp index 5152807..2c27b95 100644 --- a/source/util/util-logging.cpp +++ b/source/util/util-logging.cpp @@ -1,5 +1,5 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks // Copyright (C) 2021 William Pettersson // AUTOGENERATED COPYRIGHT HEADER END diff --git a/source/util/util-pool.hpp b/source/util/util-pool.hpp new file mode 100644 index 0000000..629f248 --- /dev/null +++ b/source/util/util-pool.hpp @@ -0,0 +1,208 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "util/util-singleton.hpp" + +#include "warning-disable.hpp" +#include +#include +#include +#include +#include +#include "warning-enable.hpp" + +namespace streamfx::util { + class poolbase { + public: + virtual ~poolbase(){}; + + protected: + virtual void reset(void* ptr){}; + }; + + /** Simple pool for objects of a single type. + * + */ + template + class pool : public poolbase, public streamfx::util::singleton<_self> { + typedef std::chrono::high_resolution_clock::time_point _time; + typedef _type* _ptr; + typedef std::shared_ptr<_type> _tracked; + + friend _tracked; + friend streamfx::util::singleton<_self>; + + std::list> _pool; + std::mutex _lock; + + public: + ~pool() + { + std::unique_lock lock(_lock); + for (auto kv : _pool) { + delete kv.second; + } + } + + protected: + pool(void) {} + + void release(_ptr ptr) + { + std::unique_lock lock(_lock); + + _time now = std::chrono::high_resolution_clock::now(); + + // Insert the released object into the pool. + _pool.emplace_front(std::pair<_time, _ptr>{now, ptr}); + reset(ptr); + + // Clean up any stragglers that exceeded their time limit. + for (auto pit = _pool.begin(); pit != _pool.end();) { + if ((pit->first - now) >= std::chrono::milliseconds(_lifetime)) { + delete pit->second; // Internal object is untracked. + pit = _pool.erase(pit); + } else { + pit++; + } + } + } + + public: + template + _tracked acquire(_valuetypes&&... _values) + { + std::unique_lock lock(_lock); + + // Try and find a free object. + _ptr ptr = nullptr; + if (_pool.size() > 0) { + ptr = _pool.front().second; + _pool.pop_front(); + } + + // If there was no usable object, create a new one. + if (!ptr) { + ptr = new _type(std::forward<_valuetypes>(_values)...); + } + + // Generate a shared pointer which automatically releases the object back to the pool after the end of use. + auto self = this->shared_from_this(); + return std::shared_ptr<_type>(ptr, [self](_type* v) { self->release(v); }); + } + }; + + /** Generic template for resource pooling with lifetimes. + * + * Declaration / Definition: + * class example { + * ... + * public: + * class pool; + * typedef example_type_t _pool_key_t; + * typedef streamfx::util::pool _pool_t; + * class pool : public _pool_t { + * friend streamfx::util::singleton; + * protected: + * pool() : _pool_t() {} + * + * static _pool_key_t as_key(example*) {...} + * static _pool_key_t as_key(example_type_t) {...} + * }; + * }; + * + * Usage: + * auto inst = example::pool::instance()->acquire(...); + * + * The returned pointer will keep a reference to the pool, and automatically release back into it when needed. + */ + template + class multipool : public poolbase, public streamfx::util::singleton<_self> { + typedef std::chrono::high_resolution_clock::time_point _time; + typedef _type* _ptr; + typedef std::shared_ptr<_type> _tracked; + + friend _tracked; + friend streamfx::util::singleton<_self>; + + std::map<_key, std::list>> _pool; + std::mutex _lock; + + public: + ~multipool() + { + std::unique_lock lock(_lock); + for (auto kv : _pool) { + for (auto lv : kv.second) { + delete lv.second; + } + } + } + + protected: + multipool(void) : _pool(), _lock() {} + + void release(_ptr ptr) + { + std::unique_lock lock(_lock); + + _time now = std::chrono::high_resolution_clock::now(); + + // Re-insert the released object into the pool(s). + auto hash = _self::as_key(ptr); + if (auto kv = _pool.find(hash); kv != _pool.end()) { + kv->second.emplace_front(now, ptr); + } else { + _pool.emplace(hash, std::list>{{now, ptr}}); + } + reset(ptr); + + // Clean up any stragglers that exceeded their time limit. + for (auto pit = _pool.begin(); pit != _pool.end();) { + for (auto lit = pit->second.begin(); lit != pit->second.end();) { + if ((lit->first - now) >= std::chrono::milliseconds(_lifetime)) { + delete lit->second; // Internal object is untracked. + lit = pit->second.erase(lit); + } else { + lit++; + } + } + + // Delete any empty lists from memory. + if (pit->second.empty()) { + pit = _pool.erase(pit); + } else { + pit++; + } + } + } + + public: + template + _tracked acquire(_valuetypes&&... _values) + { + std::unique_lock lock(_lock); + + // Try and find an existing pool, and a free object. + _ptr ptr = nullptr; + auto hash = _self::as_key(std::forward<_valuetypes>(_values)...); + if (auto kv = _pool.find(hash); kv != _pool.cend()) { + if (kv->second.size() > 0) { + ptr = kv->second.front().second; + kv->second.pop_front(); + } + } + + // If there was no usable object, create a new one. + if (!ptr) { + ptr = new _type(std::forward<_valuetypes>(_values)...); + } + + // Generate a shared pointer which automatically releases the object back to the pool after the end of use. + auto self = this->shared_from_this(); + return std::shared_ptr<_type>(ptr, [self](_type* v) { self->release(v); }); + } + }; +} // namespace streamfx::util diff --git a/source/util/util-singleton.hpp b/source/util/util-singleton.hpp new file mode 100644 index 0000000..c0ceee2 --- /dev/null +++ b/source/util/util-singleton.hpp @@ -0,0 +1,34 @@ +// AUTOGENERATED COPYRIGHT HEADER START +// Copyright (C) 2023 Michael Fabian 'Xaymar' Dirks +// AUTOGENERATED COPYRIGHT HEADER END + +#pragma once +#include "warning-disable.hpp" +#include +#include +#include "warning-enable.hpp" + +namespace streamfx::util { + /** Reference counted Singleton implementation + * + * Call _classname::instance() to get a reference, which will self-delete when not referenced anymore. + * + */ + template + class singleton : public std::enable_shared_from_this<_type> { + public: + static std::shared_ptr<_type> instance() + { + static std::weak_ptr<_type> winst; + static std::mutex mtx; + std::unique_lock lock(mtx); + + auto ptr = winst.lock(); + if (!ptr) { + ptr = std::shared_ptr<_type>(new _type()); + winst = ptr; + } + return ptr; + } + }; +} // namespace streamfx::util diff --git a/source/util/util-threadpool.cpp b/source/util/util-threadpool.cpp index e54f8a1..3f1fd73 100644 --- a/source/util/util-threadpool.cpp +++ b/source/util/util-threadpool.cpp @@ -253,11 +253,12 @@ std::shared_ptr streamfx::util::threadpo static std::shared_ptr loader_instance; -static auto loader = streamfx::loader( - []() { // Initalizer +static auto loader = streamfx::component( + "core::threadpool", + []() { // Initializer loader_instance = streamfx::util::threadpool::threadpool::instance(); }, []() { // Finalizer loader_instance.reset(); }, - streamfx::loader_priority::HIGHEST); + {}); diff --git a/source/util/util-threadpool.hpp b/source/util/util-threadpool.hpp index 853f528..fe7a4bf 100644 --- a/source/util/util-threadpool.hpp +++ b/source/util/util-threadpool.hpp @@ -1,5 +1,5 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks // AUTOGENERATED COPYRIGHT HEADER END #pragma once diff --git a/source/util/utility.cpp b/source/util/utility.cpp index e59b7c1..eca4e39 100644 --- a/source/util/utility.cpp +++ b/source/util/utility.cpp @@ -1,5 +1,5 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks // Copyright (C) 2022 lainon // AUTOGENERATED COPYRIGHT HEADER END @@ -22,66 +22,6 @@ obs_property_t* streamfx::util::obs_properties_add_tristate(obs_properties_t* pr return p; } -void* streamfx::util::vec2a::operator new(std::size_t count) -{ - return streamfx::util::malloc_aligned(16, count); -} - -void* streamfx::util::vec2a::operator new[](std::size_t count) -{ - return streamfx::util::malloc_aligned(16, count); -} - -void streamfx::util::vec2a::operator delete(void* p) -{ - streamfx::util::free_aligned(p); -} - -void streamfx::util::vec2a::operator delete[](void* p) -{ - streamfx::util::free_aligned(p); -} - -void* streamfx::util::vec3a::operator new(std::size_t count) -{ - return streamfx::util::malloc_aligned(16, count); -} - -void* streamfx::util::vec3a::operator new[](std::size_t count) -{ - return streamfx::util::malloc_aligned(16, count); -} - -void streamfx::util::vec3a::operator delete(void* p) -{ - streamfx::util::free_aligned(p); -} - -void streamfx::util::vec3a::operator delete[](void* p) -{ - streamfx::util::free_aligned(p); -} - -void* streamfx::util::vec4a::operator new(std::size_t count) -{ - return streamfx::util::malloc_aligned(16, count); -} - -void* streamfx::util::vec4a::operator new[](std::size_t count) -{ - return streamfx::util::malloc_aligned(16, count); -} - -void streamfx::util::vec4a::operator delete(void* p) -{ - streamfx::util::free_aligned(p); -} - -void streamfx::util::vec4a::operator delete[](void* p) -{ - streamfx::util::free_aligned(p); -} - std::pair streamfx::util::size_from_string(std::string_view text, bool allowSquare) { int64_t width, height; @@ -122,18 +62,19 @@ std::pair streamfx::util::size_from_string(std::string_view te return {width, height}; } -void* streamfx::util::malloc_aligned(std::size_t align, std::size_t size) +void* streamfx::util::memory::malloc_aligned(std::size_t align, std::size_t size) { + void* ptr = nullptr; #ifdef USE_MSC_ALLOC - return _aligned_malloc(size, align); + ptr = _aligned_malloc(size, align); #elif defined(USE_STD_ALLOC) - return aligned_alloc(size, align); + ptr = aligned_alloc(size, align); #else // Ensure that we have space for the pointer and the data. std::size_t asize = aligned_offset(align, size + (sizeof(void*) * 2)); // Allocate memory and store integer representation of pointer. - void* ptr = malloc(asize); + ptr = malloc(asize); // Calculate actual aligned position intptr_t ptr_off = static_cast(aligned_offset(align, reinterpret_cast(ptr) + sizeof(void*))); @@ -142,11 +83,22 @@ void* streamfx::util::malloc_aligned(std::size_t align, std::size_t size) *reinterpret_cast(ptr_off - sizeof(void*)) = reinterpret_cast(ptr); // Return aligned pointer - return reinterpret_cast(ptr_off); + ptr = reinterpret_cast(ptr_off); #endif + if (!ptr) { + throw std::bad_alloc(); + } else { + #ifndef D_PLATFORM_WINDOWS + // For user-space allocations, Windows automatically zeroes out memory no questions asked. We can ask + // for this not to happen, but usually we want to do it anyway. So on other platforms, this ensures + // the memory region zeroed by default. + memset(ptr, 0, size); + #endif + } + return ptr; } -void streamfx::util::free_aligned(void* mem) +void streamfx::util::memory::free_aligned(void* mem) { #ifdef USE_MSC_ALLOC _aligned_free(mem); diff --git a/source/util/utility.hpp b/source/util/utility.hpp index 0b63c26..254f7d0 100644 --- a/source/util/utility.hpp +++ b/source/util/utility.hpp @@ -1,10 +1,11 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2018-2023 Michael Fabian 'Xaymar' Dirks // Copyright (C) 2022 lainon // AUTOGENERATED COPYRIGHT HEADER END #pragma once #include "warning-disable.hpp" +#include #include #include #include @@ -56,39 +57,13 @@ namespace streamfx::util { return tristate == -1; } - struct vec2a : public vec2 { - // 16-byte Aligned version of vec2 - static void* operator new(std::size_t count); - static void* operator new[](std::size_t count); - static void operator delete(void* p); - static void operator delete[](void* p); - }; - -#ifdef _MSC_VER - __declspec(align(16)) -#endif - struct vec3a : public vec3 { - // 16-byte Aligned version of vec3 - static void* operator new(std::size_t count); - static void* operator new[](std::size_t count); - static void operator delete(void* p); - static void operator delete[](void* p); - }; - -#ifdef _MSC_VER - __declspec(align(16)) -#endif - struct vec4a : public vec4 { - // 16-byte Aligned version of vec4 - static void* operator new(std::size_t count); - static void* operator new[](std::size_t count); - static void operator delete(void* p); - static void operator delete[](void* p); - }; - std::pair size_from_string(std::string_view text, bool allowSquare = true); namespace math { + /** Integer exponentiation by squaring. + * Complexity: O(log(exp) + * Has fall-backs to the normal methods for non-integer types. + */ template inline T pow(T base, T exp) { @@ -102,19 +77,55 @@ namespace streamfx::util { return res; } - // Proven by tests to be the fastest implementation on Intel and AMD CPUs. - // Ranking: log10, loop < bitscan < pow - // loop and log10 trade blows, usually almost identical. - // loop is used for integers, log10 for anything else. + template<> + inline float pow(float base, float exp) + { + return ::powf(base, exp); + } + + template<> + inline double pow(double base, double exp) + { + return ::pow(base, exp); + } + + template<> + inline long double pow(long double base, long double exp) + { + return ::powl(base, exp); + } + + /** Fast PoT testing + * + * This was tested and verified to be the fastest implementation on + * Intel and AMD x86 CPUs, in both 32bit and 64bit modes. It is + * possible to make it even faster with a bit of SIMD magic (see + * json-simd), but it would no longer be generic - and not worth it. + * + * Ranking: popcnt < log10, loop < bitscan < pow + * - log10 version is useful for floating point. + * - popcnt and loop are fixed point and integers. + * - Pretty clear solution here. + * + */ template inline bool is_power_of_two(T v) { - return T(1ull << uint64_t(floor(log10(T(v)) / log10(2.0)))) == v; - } + static_assert(std::is_integral::value, "Input must be an integral type."); - template - inline bool is_power_of_two_loop(T v) - { +#if 1 // Optimizes to popcount if available. + return std::bitset(v).count() == 1; + +#elif 0 // Loop Variant 1, uses bit shifts. + bool bit = false; + for (std::size_t index = 0; index < (sizeof(T) * 8) || (v == 0); index++) { + if (bit && (v & 1)) + return false; + bit = (v & 1); + v >>= 1; + } + return true; +#elif 0 // Loop Variant 2, optimized by compiler to the above. bool have_bit = false; for (std::size_t index = 0; index < (sizeof(T) * 8); index++) { bool cur = (v & (static_cast(1ull) << index)) != 0; @@ -125,48 +136,61 @@ namespace streamfx::util { } } return true; +#endif } -#pragma push_macro("P_IS_POWER_OF_TWO_AS_LOOP") -#define P_IS_POWER_OF_TWO_AS_LOOP(x) \ - template<> \ - inline bool is_power_of_two(x v) \ - { \ - return is_power_of_two_loop(v); \ - } - P_IS_POWER_OF_TWO_AS_LOOP(int8_t) - P_IS_POWER_OF_TWO_AS_LOOP(uint8_t) - P_IS_POWER_OF_TWO_AS_LOOP(int16_t) - P_IS_POWER_OF_TWO_AS_LOOP(uint16_t) - P_IS_POWER_OF_TWO_AS_LOOP(int32_t) - P_IS_POWER_OF_TWO_AS_LOOP(uint32_t) - P_IS_POWER_OF_TWO_AS_LOOP(int64_t) - P_IS_POWER_OF_TWO_AS_LOOP(uint64_t) -#undef P_IS_POWER_OF_TWO_AS_LOOP -#pragma pop_macro("P_IS_POWER_OF_TWO_AS_LOOP") + template + inline bool is_power_of_two(float v) + { + return T(1ull << uint64_t(floor(log10(v) / log10(2.0f)))) == v; + } + template + inline bool is_power_of_two(double v) + { + return T(1ull << uint64_t(floor(log10(v) / log10(2.0)))) == v; + } + + template + inline bool is_power_of_two(long double v) + { + return T(1ull << uint64_t(floor(log10(v) / log10(2.0l)))) == v; + } + + template + inline bool is_power_of_two_loop(T v) + { + return is_power_of_two(v); + } + + /** Retrieves the lower bound on the exponent for a texture that would fit the given size. + * + */ template inline uint64_t get_power_of_two_exponent_floor(T v) { - return uint64_t(floor(log10(T(v)) / log10(2.0))); + return uint64_t(floor(log10(T(v)) / log10(2.0l))); } + /** Retrieves the upper bound on the exponent for a texture that would fit the given size. + * + */ template inline uint64_t get_power_of_two_exponent_ceil(T v) { - return uint64_t(ceil(log10(T(v)) / log10(2.0))); + return uint64_t(ceil(log10(T(v)) / log10(2.0l))); } template - inline bool is_equal(T target, C value) + inline bool is_close_epsilon(T target, C value) { return (target > (value - std::numeric_limits::epsilon())) && (target < (value + std::numeric_limits::epsilon())); } template - inline bool is_close(T target, T value, T delta) + inline bool is_close(T target, T value, T epsilon) { - return (target > (value - delta)) && (target < (value + delta)); + return (target > (value - epsilon)) && (target < (value + epsilon)); } template @@ -189,7 +213,7 @@ namespace streamfx::util { //static const double_t two_pi = pi * 2.; static const double_t two_pi_sqroot = 2.506628274631000502415765284811; //sqrt(two_pi); - if (is_equal(0, o)) { + if (is_close_epsilon(0, o)) { return T(std::numeric_limits::infinity()); } @@ -197,7 +221,7 @@ namespace streamfx::util { double_t left_e = 1. / (o * two_pi_sqroot); double_t mid_right_e = ((x /* - u*/) / o); double_t right_e = -0.5 * mid_right_e * mid_right_e; - double_t final = left_e * exp(right_e); + double final = left_e * exp(right_e); return T(final); } @@ -237,10 +261,12 @@ namespace streamfx::util { }; } // namespace math - inline std::size_t aligned_offset(std::size_t align, std::size_t pos) - { - return ((pos / align) + 1) * align; - } - void* malloc_aligned(std::size_t align, std::size_t size); - void free_aligned(void* mem); + namespace memory { + inline std::size_t aligned_offset(std::size_t align, std::size_t pos) + { + return ((pos / align) + 1) * align; + } + void* malloc_aligned(std::size_t align, std::size_t size); + void free_aligned(void* mem); + } // namespace memory } // namespace streamfx::util diff --git a/source/windll.cpp b/source/windll.cpp index 0d561d4..28b39e8 100644 --- a/source/windll.cpp +++ b/source/windll.cpp @@ -1,5 +1,5 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks // AUTOGENERATED COPYRIGHT HEADER END #include "warning-disable.hpp" diff --git a/templates/config.hpp.in b/templates/config.hpp.in index edec16d..8a16f24 100644 --- a/templates/config.hpp.in +++ b/templates/config.hpp.in @@ -1,7 +1,6 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks // AUTOGENERATED COPYRIGHT HEADER END -// Copyright (c) 2020 Michael Fabian Dirks // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal diff --git a/templates/macos/installer.pkgproj.in b/templates/macos/installer.pkgproj.in index 3c4f4ed..2a79c8f 100644 --- a/templates/macos/installer.pkgproj.in +++ b/templates/macos/installer.pkgproj.in @@ -1,4 +1,5 @@ + @@ -963,7 +964,7 @@ NAME - @PACKAGE_NAME@-@_PACKAGE_SUFFIX_OVERRIDE@ + @PACKAGE_NAME@@_PACKAGE_SUFFIX_OVERRIDE@ PAYLOAD_ONLY REFERENCE_FOLDER_PATH diff --git a/templates/module.cpp.in b/templates/module.cpp.in index 3598f1f..444726e 100644 --- a/templates/module.cpp.in +++ b/templates/module.cpp.in @@ -1,5 +1,5 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2018-2023 Michael Fabian 'Xaymar' Dirks // AUTOGENERATED COPYRIGHT HEADER END #include "warning-disable.hpp" diff --git a/templates/version.hpp.in b/templates/version.hpp.in index 436a2f3..4e58cb6 100644 --- a/templates/version.hpp.in +++ b/templates/version.hpp.in @@ -1,25 +1,6 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks // AUTOGENERATED COPYRIGHT HEADER END -// Copyright (c) 2017 Michael Fabian Dirks -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. #pragma once #include "warning-disable.hpp" diff --git a/templates/windows/installer.iss.in b/templates/windows/installer.iss.in index 582149c..fd020c7 100644 --- a/templates/windows/installer.iss.in +++ b/templates/windows/installer.iss.in @@ -1,8 +1,10 @@ -; Copyright (C) 2023 XoXFaby +; AUTOGENERATED COPYRIGHT HEADER START +; Copyright (C) 2018-2024 Michael Fabian 'Xaymar' Dirks +; AUTOGENERATED COPYRIGHT HEADER END #define MyAppName "@PROJECT_TITLE@" #define MyAppVersion "@PROJECT_VERSION@" -#define MyAppVersionText "@_VERSION@" +#define MyAppVersionText "@_VERSION_THIN@" #define MyAppPublisher "Xaymars Technology Workshop" #define MyAppURL "https://xaymar.com/" #define MyAppCopyright "@PROJECT_COPYRIGHT@" @@ -68,7 +70,7 @@ ChangesEnvironment=yes AllowNoIcons=yes LicenseFile="@ISS_SOURCE_DIR@/LICENSE" OutputDir="@ISS_PACKAGE_DIR@" -OutputBaseFilename=@PACKAGE_NAME@ +OutputBaseFilename=@PACKAGE_NAME@@_PACKAGE_SUFFIX_OVERRIDE@ Compression=lzma2/ultra64 SolidCompression=yes LZMAAlgorithm=1 diff --git a/templates/windows/version.rc.in b/templates/windows/version.rc.in index 13ed322..8834ffc 100644 --- a/templates/windows/version.rc.in +++ b/templates/windows/version.rc.in @@ -1,5 +1,5 @@ // AUTOGENERATED COPYRIGHT HEADER START -// Copyright (C) 2022-2023 Michael Fabian 'Xaymar' Dirks +// Copyright (C) 2018-2023 Michael Fabian 'Xaymar' Dirks // AUTOGENERATED COPYRIGHT HEADER END #pragma code_page(65001) diff --git a/tools/build.sh b/tools/build.sh new file mode 100644 index 0000000..43b4ed1 --- /dev/null +++ b/tools/build.sh @@ -0,0 +1,386 @@ +#!/usr/bin/env bash +shopt -s nocasematch + +# Variables +opt_generator="Ninja Multi-config" +opt_build_dir="${PWD}/build" +opt_install_dir="${PWD}/build/install" +opt_package_dir="${PWD}/build/package" +opt_package_name="" + +# Aliases +shopt -s expand_aliases +alias check_err='_r=$?; if [[ $_r != 0 ]]; then return $_r; fi' + +# Figure out the platform we are on. +platform="unknown" +if [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "msys" ]]; then + platform="windows" + suffix=x64 +elif [[ "$OSTYPE" == "freebsd"* ]] || [[ "$OSTYPE" == "linux"* ]]; then + platform="linux" + suffix=x86_64 +else + platform="mac" + suffix=universal +fi + +function group_start { + if [[ "${GITHUB_ACTIONS}" ]]; then echo "##[group]$1"; fi +} +function group_end { + if [[ "${GITHUB_ACTIONS}" ]]; then echo "##[endgroup]"; fi +} + +function if_value { + if [[ ${1} ]]; then echo ${2}; fi +} + +function task_prerequisites { + if [[ "$platform" == "mac" ]]; then + brew install cmake + check_err + elif [[ "$platform" == "linux" ]]; then + sudo apt-get -qq update + check_err + # Build Tools + sudo apt-get -y install \ + build-essential \ + cmake \ + git \ + ninja-build \ + pkg-config + check_err + # FFmpeg + sudo apt-get -y install \ + libavcodec-dev \ + libavdevice-dev \ + libavfilter-dev \ + libavformat-dev \ + libavutil-dev \ + libswresample-dev \ + libswscale-dev + check_err + # Qt6 + sudo apt-get -y install \ + qt6-base-dev \ + qt6-base-private-dev \ + qt6-image-formats-plugins \ + qt6-wayland \ + libqt6svg6-dev \ + libglx-dev \ + libgl1-mesa-dev + check_err + # curl + sudo apt-get -y install \ + libcurl4-openssl-dev + check_err + fi +} + +function task_fetch { + #git fetch --all --tags + git submodule update --init --force --recursive + return 0 +} + +function task_clean { + git clean -xfd + git submodule foreach --recursive git clean -xfd + git reset --hard + git submodule foreach --recursive git reset --hard + return 0 +} + +function task_patch { + function find_apply_patches { + patch_dir=$1 + target_dir=$2 + + if [[ ! -d "${patch_dir}" ]]; then + return 1 + fi + + echo "Searching tree '${patch_dir}'..." + for p in "${patch_dir}"/*; do + pf=$(basename "$p") + + # Skip platform specific directories + if [[ "${pf}" == "windows" ]] || [[ "${pf}" == "linux" ]] || [[ "${pf}" == "mac" ]] || [[ "${pf}" == "unknown" ]]; then + continue + fi + + if [ -f "${p}" ]; then + # This is a file, so apply it. + echo "Applying patch '${p}'..." + $(cd "${target_dir}/" && git apply "${p}") + elif [ -d "${p}" ]; then + find_apply_patches "${patch_dir}/${pf}" "${target_dir}/${pf}" + fi + done + + return 0 + } + + find_apply_patches "${PWD}/patches" "${PWD}" + find_apply_patches "${PWD}/patches/${platform}" "${PWD}" + + return 0 +} + +function task_libobs { + if [[ "$platform" == "linux" ]]; then + group_start "Install Pre-requisites" + sudo apt-get -y install \ + build-essential \ + pkg-config \ + cmake \ + git \ + libavcodec-dev \ + libavdevice-dev \ + libavfilter-dev \ + libavformat-dev \ + libavutil-dev \ + libswresample-dev \ + libswscale-dev \ + libcurl4-openssl-dev \ + libmbedtls-dev \ + libjansson-dev \ + libgl1-mesa-dev \ + libx11-dev \ + libxcb-randr0-dev \ + libxcb-shm0-dev \ + libxcb-xinerama0-dev \ + libxcb-composite0-dev \ + libxcomposite-dev \ + libxinerama-dev \ + libxcb1-dev \ + libx11-xcb-dev \ + libxcb-xfixes0-dev \ + libxss-dev \ + libglvnd-dev \ + libgles2-mesa \ + libgles2-mesa-dev \ + libwayland-dev \ + libasound2-dev \ + libjack-jackd2-dev \ + libpulse-dev \ + libsndio-dev \ + libasio-dev \ + libasound2-dev \ + libfontconfig-dev \ + libjack-jackd2-dev \ + libpulse-dev \ + libsndio-dev \ + libudev-dev \ + libv4l-dev \ + libva-dev \ + libvpl-dev \ + libdrm-dev \ + libasio-dev + check_err + group_end + elif [[ "$platform" == "mac" ]]; then + group_start "Install Pre-requisites" + brew install git + check_err + brew install cmake + check_err + group_end + fi + + group_start "Configure" + if [[ "$platform" == "linux" ]]; then + cmake \ + -S "${PWD}/third-party/obs-studio/" \ + --preset linux-x86_64 \ + -G "Unix Makefiles" \ + -B "${PWD}/third-party/obs-studio/build" \ + -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + -DENABLE_UI=OFF \ + -DENABLE_PLUGINS=OFF \ + -DENABLE_SCRIPTING=OFF \ + -DENABLE_BROWSER=ON \ + -DOBS_VERSION_OVERRIDE=$(cd "${PWD}/third-party/obs-studio/" && git describe --tags HEAD) + check_err + elif [[ "$platform" == "mac" ]]; then + cmake \ + -S "${PWD}/third-party/obs-studio/" \ + --preset macos \ + -G "Xcode" \ + -B "${PWD}/third-party/obs-studio/build" \ + -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + -DENABLE_UI=OFF \ + -DENABLE_PLUGINS=OFF \ + -DENABLE_SCRIPTING=OFF \ + -DENABLE_BROWSER=ON \ + -DOBS_VERSION_OVERRIDE=$(cd "${PWD}/third-party/obs-studio/" && git describe --tags HEAD) \ + -DCMAKE_OSX_DEPLOYMENT_TARGET=10.15 \ + -DCMAKE_OSX_ARCHITECTURES="x86_64;arm64" + check_err + else + cmake \ + -S "${PWD}/third-party/obs-studio/" \ + --preset windows-x64 \ + -B "${PWD}/third-party/obs-studio/build" \ + -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + -DENABLE_UI=OFF \ + -DENABLE_PLUGINS=OFF \ + -DENABLE_SCRIPTING=OFF \ + -DENABLE_BROWSER=ON \ + -DOBS_VERSION_OVERRIDE=$(cd "${PWD}/third-party/obs-studio/" && git describe --tags HEAD) + check_err + fi + check_err + group_end + + group_start "Build" + cmake \ + --build "${PWD}/third-party/obs-studio/build" \ + --parallel \ + --config RelWithDebInfo \ + --target obs-frontend-api + check_err + group_end + + group_start "Install" + if [[ "$platform" == "linux" ]]; then + cmake \ + --install "${PWD}/third-party/obs-studio/build" \ + --prefix "${PWD}/third-party/obs-studio/build/install" \ + --config RelWithDebInfo \ + --component obs_libraries + check_err + else + cmake \ + --install "${PWD}/third-party/obs-studio/build" \ + --prefix "${PWD}/third-party/obs-studio/build/install" \ + --config RelWithDebInfo \ + --component Development + check_err + fi + group_end +} + +function task_configure { + # Figure out where things are + libobs_dir=(${PWD}/third-party/obs-studio/build/install/) + ffmpeg_dir=(${PWD}/third-party/obs-studio/.deps/obs-deps-*-${suffix}/) + curl_dir=(${PWD}/third-party/obs-studio/.deps/obs-deps-*-${suffix}/) + qt_dir=(${PWD}/third-party/obs-studio/.deps/obs-deps-qt6-*-${suffix}/) + + if [[ "$platform" == "mac" ]]; then + # obs-deps/qt6's AutoMOC has an unpatched bug: https://bugreports.qt.io/browse/QTBUG-117765 + group_start "Fix unpatched Qt6.5.3 bug in upstream obs-deps" + + brew install qt@6 + check_err + rm "${qt_dir}/libexec/moc" + check_err + ln -s "$(brew ls -q qt | grep libexec/moc)" "${qt_dir}/libexec/moc" + check_err + + group_end + fi + + # Apply default Package name + if [[ "$opt_package_name" == "" ]]; then + OBS_VERSION=$(cd third-party/obs-studio/ && git describe --tags --long --abbrev=8 HEAD) + opt_package_name="StreamFX ${platform} (for OBS Studio v${OBS_VERSION}) v" + fi + + group_start "Configure" + if [[ "$platform" == "linux" ]]; then + cmake \ + -G "Ninja Multi-Config" \ + -S ${PWD} \ + -B ${opt_build_dir} \ + -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON \ + -DCMAKE_INSTALL_PREFIX="${opt_install_dir}" \ + -DPACKAGE_NAME="${opt_package_name}" \ + -DPACKAGE_PREFIX="${opt_package_dir}" \ + -Dlibobs_DIR="${PWD}/third-party/obs-studio/build/install" \ + -DFFmpeg_DIR="${ffmpeg_dir}" \ + -DCURL_DIR="${curl_dir}" \ + -DQt6_DIR="${qt_dir}" + check_err + elif [[ "$platform" == "mac" ]]; then + cmake \ + -G "Xcode" \ + -S ${PWD} \ + -B ${opt_build_dir} \ + -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON \ + -DCMAKE_INSTALL_PREFIX="${opt_install_dir}" \ + -DPACKAGE_NAME="${opt_package_name}" \ + -DPACKAGE_PREFIX="${opt_package_dir}" \ + -Dlibobs_DIR="${PWD}/third-party/obs-studio/build/install" \ + -DFFmpeg_DIR="${ffmpeg_dir}" \ + -DCURL_DIR="${curl_dir}" \ + -DQt6_DIR="${qt_dir}" \ + -DCMAKE_OSX_DEPLOYMENT_TARGET=10.15 \ + -DCMAKE_OSX_ARCHITECTURES="x86_64;arm64" + check_err + else + cmake \ + -S ${PWD} \ + -B ${opt_build_dir} \ + -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON \ + -DCMAKE_INSTALL_PREFIX="${opt_install_dir}" \ + -DPACKAGE_NAME="${opt_package_name}" \ + -DPACKAGE_PREFIX="${opt_package_dir}" \ + -Dlibobs_DIR="${PWD}/third-party/obs-studio/build/install" \ + -DFFmpeg_DIR="${ffmpeg_dir}" \ + -DCURL_DIR="${curl_dir}" \ + -DQt6_DIR="${qt_dir}" + check_err + fi + group_end +} + +function task_build { + group_start "Build" + cmake \ + --build ${opt_build_dir} \ + --parallel \ + --config RelWithDebInfo \ + --target StreamFX + check_err + group_end +} + +function task_install { + group_start "Install" + cmake \ + --install ${opt_build_dir} \ + --config RelWithDebInfo + check_err + group_end +} + +while [[ "$1" != "" ]]; do + if [[ $(type -t "task_$1") == "function" ]]; then + task="$1" + elif [[ ("$1" == "--build") || ("$1" == "-b") ]]; then + opt_build_dir="$2" + shift + elif [[ ("$1" == "--install") || ("$1" == "-i") ]]; then + opt_install_dir="$2" + shift + elif [[ ("$1" == "--package") || ("$1" == "-p") ]]; then + opt_package_dir="$2" + shift + elif [[ ("$1" == "--package-name") || ("$1" == "-n") ]]; then + opt_package_name="$2" + shift + fi + shift +done + +if [[ $(type -t "task_${task}") == "function" ]]; then + task_${task} $* + exit $? +else + echo "Usage: '$0' " + echo " Unknown command: $1" + exit 1 +fi diff --git a/tools/copyright.js b/tools/copyright.js index 66e2dc1..2beab25 100644 --- a/tools/copyright.js +++ b/tools/copyright.js @@ -133,8 +133,7 @@ async function git_retrieveAuthors(file) { let proc = CHILD_PROCESS.spawn("git", [ "--no-pager", "log", - "--date-order", - "--reverse", + "--follow", "--format=format:%aI|%aN <%aE>", "--", file @@ -190,7 +189,11 @@ async function git_retrieveAuthors(file) { let author = authors.get(name); if (author) { - author.to = new Date(date) + let dt = new Date(date) + if (author.from > dt) + author.from = dt; + if (author.to < dt) + author.to = dt; } else { authors.set(name, { from: new Date(date), @@ -203,6 +206,14 @@ async function git_retrieveAuthors(file) { async function generateCopyright(file) { let authors = await git_retrieveAuthors(file) + authors = new Map([...authors].sort((a, b) => { + if (a[1].from < b[1].from) { + return -1; + } else if (a[1].from > b[1].from) { + return 1; + } + return 0; + })); let lines = []; for (let entry of authors) { let from = entry[1].from.getUTCFullYear();